ffplay是ffmpeg源碼中一個自帶的開源播放器實例,同時支持本地視頻文件的播放以及在線流媒體播放,功能非常強大。
FFplay: FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs.
ffplay中的代碼充分調用了ffmpeg中的函數庫,因此,想學習ffmpeg的使用,或基於ffmpeg開發一個自己的播放器,ffplay都是一個很好的切入點。
由於ffmpeg本身的開發文檔比較少,且ffplay播放器源碼的實現相對復雜,除了基礎的ffmpeg組件調用外,還包含視頻幀的渲染、音頻幀的播放、音視頻同步策略及線程調度等問題。
因此,這里我們以ffmpeg官網推薦的一個ffplay播放器簡化版本的開發例程為基礎,在此基礎上循序漸進由淺入深,最終探討實現一個視頻播放器的完整邏輯。
在上篇文章中,我們討論了一個播放器的基礎架構,梳理了組成播放器的基本組件及后台數據隊列,並對代碼結構進行了調整。本文在上篇文章的基礎上,討論音視頻同步的相關內容,首先介紹與音視頻同步相關的時間戳概念,然后介紹音視頻同步涉及的原理及策略,最后重點講述關鍵代碼的實現過程。
公眾號:斷點實驗室 音視頻開發系列文章
ffmpeg源碼編譯環境搭建
ffplay源碼編譯
ffmpeg播放器實現詳解 - 框架搭建
ffmpeg播放器實現詳解 - 視頻顯示
ffmpeg播放器實現詳解 - 音頻播放
ffmpeg播放器實現詳解 - 創建線程
ffmpeg播放器實現詳解 - 視頻同步控制
ffmpeg播放器實現詳解 - 音頻同步控制
ffmpeg播放器實現詳解 - 快進快退控制
1、時間戳
時間戳的概念貫穿音視頻開發始終,重要性不言而喻。時間戳告訴我們在什么時候,用多快的速度去播哪一幀,其中,DTS(decoding timestamp)告訴我們何時解碼,PTS(presentation timestamp)告訴我們何時播放。
那么,為什么要有DTS和PTS?首先需要理解編碼后的數據是如何存儲的。
1.1 幀分組(Group of picture)
視頻幀(這里主要以H.264編碼為例)以序列為單位進行組織,一個序列是一段圖像編碼后的數據流,從關鍵幀I幀開始,包含了與組內I幀相關聯的P幀和B幀,到下一個I幀結束,一個序列為一個幀組(group of picture),如下圖所示。
一個序列的長短,和序列呈現的視頻內容有關。當前后幀視頻畫面變化較小時,一個序列可以很長,因為畫面變化小意味着不需要太多的數據,即可對后續的畫面進行描述,僅需提供首幀畫面(I幀)及針對該畫面的的變化內容(預測結果),即可恢復一個幀組的全部畫面。
I幀具有以下特點:
- 解碼時僅用I幀的數據就可重構完整圖像,I幀不需要參考其他畫面而生成
- I幀是P幀和B幀的參考幀(其質量直接影響到同組中以后各幀的質量)
- 在一組中只有一個I幀,I幀所占數據的信息量比較大
- I幀不需要考慮運動矢量
P幀為自己與幀組內的I幀或其他P幀的殘差圖編碼形成,解碼時需要用之前緩存的畫面,疊加上本幀殘差畫面生成最終畫面。
P幀具有以下特點:
- P幀是I幀后面相隔1~2幀的編碼幀
- P幀采用運動補償的方法傳送它與前面的I或P幀的差值及運動矢量(預測誤差)
- P幀屬於前向預測的幀間編碼,它只參考前面最靠近它的I幀或P幀
B幀是雙向預測內插編碼幀,B幀記錄的是本幀與前后幀的差別,要解碼B幀,不僅要取得之前的緩存畫面,還要本幀之后的畫面,通過前后畫面與本幀數據的差異取得最終的畫面。
B幀壓縮率高,但因為解碼時需要前后幀的共同配合,因此可能需要等待更長的解碼時間,因為此時后面的幀可能還未接收或解析成功。因為B幀的這個特點,在一些實時性要求高的流媒體傳輸協議中,會要求編碼器去除B幀數據編碼,只保留I幀和P幀編碼,保證解碼的實時性要求。
B幀具有以下特點:
- B幀是雙向預測編碼幀,B幀是由前面的I或P幀和后面的P幀來進行預測的
- B幀傳送的是它與前面的I或P幀和后面的P幀之間的預測誤差及運動矢量
- B幀壓縮比最高(可達到50倍的壓縮率)
幀分組圖中的箭頭連線,反應了I/P/B幀之間的依賴關系,可以看出,P幀僅依賴I幀,B幀依賴它前后的I幀與P幀
1.2 PTS和DTS
我們繼續在幀組概念基礎上討論時間戳,以下圖為例,在幀組中因為有P/B幀的存在,使得視頻幀的解碼順序和播放順序存在不一致的情況,先解碼的幀可能后播放,而后解碼的幀也可能先播放。因此,我們采用解碼時間戳DTS和顯示時間戳PTS來解決解碼時序與播放時序不一致的問題。
圖中第一行為視頻流的接收時序,其中每個幀都攜帶了DTS和PTS,ffmpeg會根據這些信息,將解碼出來的音視頻原始數據,按照最終顯示的時序進行重組后,交給調用者顯示播放。
這里需要提一下,圖中的DTS與PTS值僅用來說明時間戳的工作原理,並不是一個真實的取值,在ffmpeg的返回值中,DTS與PTS的取值也是一個相對值,可以看作每幀圖像在時間軸上的投影相對位置,並不直接對應現實世界中的絕對時間,因此,在音視頻同步過程中,還需要對時間戳的值進行換算,才能得到每幀圖像確切的播放時間。
2、視頻同步策略分析
有了時間戳的基礎,我們就能確定在什么時候,用多快的速度去播哪一幀了,這樣就能完成整個視頻的播放了嗎?可能仍然不行,原因在於,即使我們能夠得到每一個視頻及音頻幀的顯示播放時間,並依次按照指定的時間播放,在理想情況下能夠達到音視頻同步的目的,但實際上,這種同步狀態很難維持太長時間。
有太多的因素,如關鍵幀接收延遲,線程調度,每幀解碼的時間不同等,影響着音視頻的同步,在毫秒級別上,這種延遲會隨着播放的進行被逐漸放大,進而完全失去同步。因此,我們需要一個機制,能夠動態的調節每幀數據的播放,約束音視頻幀間的時差,達到一種動態同步的結果。
既然要將原本不同步的音視頻幀協調到一起,那么我們需要一個同步的基准,類似於彈鋼琴時用到的節拍器一樣,左右手都根據同一個節拍有條不紊的彈着。
類似的,在音視頻同步中,也需要這樣一個時間基准,有3種時間可以作為同步的時間基准,它們分別是系統時間,音頻時間和視頻時間。我們可選擇將視頻同步到音頻上,將音頻同步到視頻上,或者將音視頻都同步到系統時間上,每種方式的效果會有所不同。
本次內容先介紹將音頻同步到視頻上的方法,后續的內容會繼續介紹其他兩種同步方式,並給出最終的方案。
2.1 音頻時間戳計算
既然我們選擇以音頻時間作為同步的時間基准,那么先來看看如何計算音頻時間戳。音頻時間戳的計算方式和視頻時間戳有所差別,通常每個音頻幀占用的緩存空間比較小,一般情況下會將多幀音頻數據打包到一起發送,如通過rtp等流媒體協議,將多個aac音頻幀打包到一起由推流端發送到接收端。
打包好的音頻幀通過解封裝解析到AVPacket后,會得到一個音頻幀組,而這一組音頻幀只對應一個時間戳(pkt->pts),這種情況下在解碼后,就需要根據音頻的采樣頻率,聲道數以及每聲道字節數等信息,來估算每個解碼后音頻采樣數據對應的時間了。
另一方面,音頻數據的播放是通過回調函數的方式,周期性的將一組音頻數據送入聲卡中播放的,回憶下我們之前在[ffmpeg播放器實現詳解 - 音頻播放]中討論過的內容,每次送入聲卡的緩存長度,和解碼后音頻數據的緩存長度是不一致的,因此需要根據每個packet對應的時間戳,以及采樣頻率,聲道數等信息估算出每次送到聲卡中緩存片段的時間值。
下面我們來看下具體的實現。首先看下在VideoState中新增的幾個相關字段。
typedef struct VideoState {
...
//video/audio_clock save pts of last decoded frame/predicted pts of next decoded frame
double video_clock;//keep track of how much time has passed according to the video
double audio_clock;
double frame_timer;//視頻播放到當前幀時的累計已播放時間
double frame_last_pts;//上一幀圖像的顯示時間戳,用於在video_refersh_timer中保存上一幀的pts值
double frame_last_delay;//上一幀圖像的動態刷新延遲時間
} VideoState;// Since we only have one decoding thread, the Big Struct can be global in case we need it.
audio_clock & video_clock 分別用於追蹤音頻和視頻播放時間的時間戳位置,后面會多次用到。我們在音頻解碼函數中,增加了時間戳相關的內容,並在解碼函數返回前,根據pkt->pts及返回的數據長度,推算出這段音頻數據相對於pkt->pts的時間,更新到音頻時鍾audio_clock上。
從下圖中可以看到,解碼函數,音頻時鍾獲取函數,解碼緩存及時間戳的關系。於此同時,隨着解碼緩存長度的遞增,播放時間呈現出同比線性增長趨勢。圖中的紅線可以看作由pcm采樣數據還原出的音頻信號。
int audio_decode_frame(VideoState *is, double *pts_ptr) {
...
double pts;//音頻播放時間戳
...
pts=is->audio_clock;//用每次更新的音頻播放時間更新音頻PTS
*pts_ptr=pts;
/*---------------------
* 當一個packet中包含多個音頻幀時
* 通過[解碼后音頻原始數據長度]及[采樣率]來推算一個packet中其他音頻幀的播放時間戳pts
* 采樣頻率44.1kHz,量化位數16位,意味着每秒采集數據44.1k個,每個數據占2字節
--------------------*/
pcm_bytes=2*is->audio_st->codec->channels;//計算每組音頻采樣數據的字節數=每個聲道音頻采樣字節數*聲道數
/*----更新audio_clock---
* 一個pkt包含多個音頻frame,同時一個pkt對應一個pts(pkt->pts)
* 因此,該pkt中包含的多個音頻幀的時間戳由以下公式推斷得出
* bytes_per_sec=pcm_bytes*is->audio_st->codec->sample_rate
* 從pkt中不斷的解碼,推斷(一個pkt中)每幀數據的pts並累加到音頻播放時鍾
--------------------*/
is->audio_clock+=(double)data_size/(double)(pcm_bytes*is->audio_st->codec->sample_rate);
// We have data, return it and come back for more later
return data_size;//返回解碼數據原始數據長度
}
...
// If update, update the audio clock w/pts
if (pkt->pts != AV_NOPTS_VALUE) {//檢查音頻播放時間戳
//獲得一個新的packet的時候,更新audio_clock,用packet中的pts更新audio_clock(一個pkt對應一個pts)
is->audio_clock=pkt->pts*av_q2d(is->audio_st->time_base);//更新音頻已經播的時間
}
}
}
下面是計算音頻時間戳的代碼,注釋的已經很詳細了,原理見上面的分析。
double get_audio_clock(VideoState *is) {
double pts=is->audio_clock;//Maintained in the audio thread,取得解碼操作完成時的當前播放時間戳
//還未(送入聲卡)播放的剩余原始音頻數據長度,等於解碼后的多幀原始音頻數據長度-累計送入聲卡的長度
int hw_buf_size=is->audio_buf_size-is->audio_buf_index;//計算當前音頻解碼數據緩存索引位置
int bytes_per_sec=0;//每秒的原始音頻字節數
int pcm_bytes=is->audio_st->codec->channels*2;//每組原始音頻數據字節數=聲道數*每聲道數據字節數
if (is->audio_st) {
bytes_per_sec=is->audio_st->codec->sample_rate*pcm_bytes;//計算每秒的原始音頻字節數
}
if (bytes_per_sec) {//檢查每秒的原始音頻字節數是否有效
pts-=(double)hw_buf_size/bytes_per_sec;//根據送入聲卡緩存的索引位置,往前倒推計算當前時刻的音頻播放時間戳pts
}
return pts;//返回當前正在播放的音頻時間戳
}
2.2 視頻同步實現
有了音頻時間戳,也就有了音視頻同步的基准,下面我們來討論下音視頻同步的策略。
因為聲音播放的速度通常恆定,試想下如果把一段一分鍾的錄音在半分鍾內播放完,而且前快后慢,你聽到的聲音可能就不那么自然了。而視頻的播放速度只要維持fps在25幀左右,多一幀或少一幀人眼一般感知不出來,因此,通過將視頻同步到音頻的方式,可以較好的達到音視頻同步的效果,在視頻播放超前於音頻時,增加視頻幀顯示的時間,當視頻播放滯后於音頻時,減少視頻幀顯示時間,加速視頻的刷新速度,通俗的解釋就是面多了加水,水多了加面,這就是音視頻同步的策略。
本次內容的代碼仍以上一篇文章中的代碼框架為基礎,代碼架構及運行時序見[ffmpeg播放器實現詳解 - 創建線程]。
video_refresh_timer是視頻同步的核心函數,上篇內容中的video_refresh_timer函數實現比較簡單,僅以40ms的固定周期對畫面進行刷新,本篇中的實現則復雜的多。下面將代碼的核心部分貼出來,可以看到,這里比較當前幀與主時鍾的時差,並用該時差與一個閾值進行比較,慢了delay設為0盡快顯示,快了加倍延遲。
然后將每次計算出的實際延遲時間delay,即下一幀的刷新間隔時間更新到frame_timer上,並根據該值確定下一幀更新的絕對時間(delay本身是一個相對時間)。frame_timer本身是以系統時間為基准的,因此這里要減去此時的系統時間。
ffmpeg的時間戳和同步機制相對復雜,大家可以根據代碼的注釋,加入調試信息把程序跑一遍,體會其中的原理和細節。
void video_refresh_timer(void *userdata) {
...
// Update delay to sync to audio,取得聲音播放時間戳(作為視頻同步的參考時間)
if (is->av_sync_type != AV_SYNC_VIDEO_MASTER) {//檢查主同步時鍾源
ref_clock = get_master_clock(is);//根據主時鍾來判斷Video播放的快慢,以主時鍾為基准時間
diff = vp->pts - ref_clock;//計算圖像幀顯示與主時鍾的時間差
//根據時間差調整播放下一幀的延遲時間,以實現同步 Skip or repeat the frame,Take delay into account
sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
//判斷音視頻不同步條件,即[畫面-聲音]時間差&[畫面-畫面]時間差<10ms閾值,若>該閾值則為快進模式,不存在音視頻同步問題
if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
if (diff <= -sync_threshold) {//慢了delay設為0盡快顯示
//下一幀畫面顯示的時間和當前的聲音很近的話加快顯示下一幀(即后面video_display顯示完當前幀后開啟定時器很快去顯示下一幀
delay=0;
} else if (diff>=sync_threshold) {//快了加倍延遲
delay=2*delay;
}
}//如果diff(明顯)大於AV_NOSYNC_THRESHOLD,即快進的模式了,畫面跳動太大,不存在音視頻同步的問題了
}
//更新視頻播放到當前幀時的已播放時間值(所有圖像幀動態播放累計時間值-真實值),frame_timer一直累加在播放過程中我們計算的延時
is->frame_timer+=delay;
//每次計算frame_timer與系統時間的差值(以系統時間為基准時間),將frame_timer與系統時間(絕對時間)相關聯的目的
actual_delay=is->frame_timer-(av_gettime()/1000000.0);//Computer the REAL delay
if (actual_delay < 0.010) {//檢查絕對時間范圍
actual_delay = 0.010;// Really it should skip the picture instead
}
schedule_refresh(is,(int)(actual_delay*1000+0.5));//用絕對時間開定時器去動態顯示刷新下一幀
...
}
3、源碼編譯驗證
源碼的編譯方法和之前的例程完全相同,源碼可采用如下Makefile腳本進行編譯
tutorial05: tutorial05.c
gcc -o tutorial04 -g3 tutorial05.c -I${FFMPEG_INCLUDE} -I${SDL_INCLUDE} \
-L${FFMPEG_LIB} -lavutil -lavformat -lavcodec -lswscale -lswresample -lz -lm \
`sdl-config --cflags --libs`
clean:
rm -rf tutorial05
執行make命令開始編譯,編譯完成后,可在源碼目錄生成名為[tutorial05]的可執行文件。
與ffplay的使用方法類似,執行[tutorial05 url]命令,url可以選擇本地視頻文件,或媒體流地址。
./tutorial05 ./xxx.mp4
輸入Ctrl+C結束程序運行
4、源碼清單
源碼在上篇內容基礎上,主要增加了音頻時間戳推算的代碼,按照慣例,源碼幾乎每行都有注釋,方便大家調試理解
// tutorial05.c
// A pedagogical video player that really works!
//
// This tutorial was written by Stephen Dranger (dranger@gmail.com).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard,
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
//
//
// Updates tested on:
// Mac OS X 10.11.6
// Apple LLVM version 8.0.0 (clang-800.0.38)
//
// Use
//
// $ gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
//
// to build (assuming libavutil/libavformat/libavcodec/libswscale are correctly installed your system).
//
// Run using
//
// $ tutorial05 myvideofile.mpg
//
// to play the video.
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libswscale/swscale.h>
#include <libavutil/avstring.h>
#include <libavutil/time.h>
#include <SDL.h>
#include <SDL_thread.h>
#ifdef __MINGW32__
#undef main // Prevents SDL from overriding main().
#endif
#include <stdio.h>
#include <math.h>
#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000
#define MAX_AUDIOQ_SIZE (5 * 16 * 1024)
#define MAX_VIDEOQ_SIZE (5 * 256 * 1024)
#define AV_SYNC_THRESHOLD 0.01//前后兩幀間的顯示時間間隔的最小值0.01s
#define AV_NOSYNC_THRESHOLD 10.0//最小刷新間隔時間10ms
#define FF_ALLOC_EVENT (SDL_USEREVENT)
#define FF_REFRESH_EVENT (SDL_USEREVENT + 1)
#define FF_QUIT_EVENT (SDL_USEREVENT + 2)
#define VIDEO_PICTURE_QUEUE_SIZE 1
SDL_Surface *screen;//SDL繪圖表面,A structure that contains a collection of pixels used in software blitting
uint64_t global_video_pkt_pts = AV_NOPTS_VALUE;
/*-------鏈表節點結構體--------
typedef struct AVPacketList {
AVPacket pkt;//鏈表數據
struct AVPacketList *next;//鏈表后繼節點
} AVPacketList;
---------------------------*/
//編碼數據包隊列(鏈表)結構體
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;//隊列首尾節點指針
int nb_packets;//隊列長度
int size;//保存編碼數據的緩存長度,size=packet->size
SDL_mutex *qlock;//隊列互斥量,保護隊列數據
SDL_cond *qready;//隊列就緒條件變量
} PacketQueue;
//圖像幀結構體
typedef struct VideoPicture {
SDL_Overlay *bmp;//SDL畫布overlay
int width, height;//Source height & width
int allocated;//是否分配內存空間,視頻幀轉換為SDL overlay標識
double pts;//當前圖像幀的絕對顯示時間戳
} VideoPicture;
typedef struct VideoState {
AVFormatContext *pFormatCtx;//保存文件容器封裝信息及碼流參數的結構體
AVPacket audio_pkt;//保存從隊列中提取的數據包
AVFrame audio_frame;//保存從數據包中解碼的音頻數據
AVStream *video_st;//視頻流信息結構體
AVStream *audio_st;//音頻流信息結構體
struct SwsContext *sws_ctx;//描述轉換器參數的結構體
AVIOContext *io_context;
PacketQueue videoq;//視頻編碼數據包隊列(編碼數據隊列,以鏈表方式實現)
//解碼后的圖像幀隊列(解碼數據隊列,以數組方式實現),渲染邏輯就會從pictq獲取數據,同時解碼邏輯又會往pictq寫入數據
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];
int pictq_size, pictq_rindex, pictq_windex;//隊列長度,讀/寫位置索引
SDL_mutex *pictq_lock;//隊列讀寫鎖對象,保護圖像幀隊列數據
SDL_cond *pictq_ready;//隊列就緒條件變量
PacketQueue audioq;//音頻編碼數據包隊列(編碼數據隊列,以鏈表方式實現)
uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2];//保存解碼一個packet后的多幀原始音頻數據(解碼數據隊列,以數組方式實現)
unsigned int audio_buf_size;//解碼后的多幀音頻數據長度
unsigned int audio_buf_index;//累計送入聲卡的長度
uint8_t *audio_pkt_data;//編碼數據緩存指針位置
int audio_pkt_size;//緩存中剩余的編碼數據長度(是否已完成一個完整的pakcet包的解碼,一個數據包中可能包含多個音頻編碼幀)
int audio_hw_buf_size;
int videoStream, audioStream;//音視頻流類型標號
SDL_Thread *parse_tid;//編碼數據包解析線程id
SDL_Thread *video_tid;//解碼線程id
char filename[1024];//輸入文件完整路徑名
int quit;//全局退出進程標識,在界面上點了退出后,告訴線程退出
//video/audio_clock save pts of last decoded frame/predicted pts of next decoded frame
double video_clock;//keep track of how much time has passed according to the video
double audio_clock;
double frame_timer;//視頻播放到當前幀時的累計已播放時間
double frame_last_pts;//上一幀圖像的顯示時間戳,用於在video_refersh_timer中保存上一幀的pts值
double frame_last_delay;//上一幀圖像的動態刷新延遲時間
} VideoState;// Since we only have one decoding thread, the Big Struct can be global in case we need it.
VideoState *global_video_state;
//數據包隊列初始化函數
void packet_queue_init(PacketQueue *q) {
memset(q, 0, sizeof(PacketQueue));//全零初始化隊列結構體對象
q->qlock = SDL_CreateMutex();//創建互斥量對象
q->qready = SDL_CreateCond();//創建條件變量對象
}
//向隊列中插入數據包
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
/*-------准備隊列(鏈表)節點對象------*/
AVPacketList *pktlist=av_malloc(sizeof(AVPacketList));//在堆上創建鏈表節點對象
if (!pktlist) {//檢查鏈表節點對象是否創建成功
return -1;
}
pktlist->pkt = *pkt;//將輸入數據包賦值給新建鏈表節點對象中的數據包對象
pktlist->next = NULL;//鏈表后繼指針為空
// if (av_packet_ref(pkt, pkt) < 0) {
// return -1;
// }
/*---------將新建節點插入隊列-------*/
SDL_LockMutex(q->qlock);//隊列互斥量加鎖,保護隊列數據
if (!q->last_pkt) {//檢查隊列尾節點是否存在(檢查隊列是否為空)
q->first_pkt = pktlist;//若不存在(隊列尾空),則將當前節點作隊列為首節點
}else {
q->last_pkt->next = pktlist;//若已存在尾節點,則將當前節點掛到尾節點的后繼指針上,並作為新的尾節點
}
q->last_pkt = pktlist;//將當前節點作為新的尾節點
q->nb_packets++;//隊列長度+1
q->size += pktlist->pkt.size;//更新隊列編碼數據的緩存長度
SDL_CondSignal(q->qready);//給等待線程發出消息,通知隊列已就緒
SDL_UnlockMutex(q->qlock);//釋放互斥量
return 0;
}
//從隊列中提取數據包,並將提取的數據包出隊列
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
AVPacketList *pktlist;//臨時鏈表節點對象指針
int ret;//操作結果
SDL_LockMutex(q->qlock);//隊列互斥量加鎖,保護隊列數據
for (;;) {
if (global_video_state->quit) {//檢查退出進程標識
ret = -1;
break;
}//end for if
pktlist = q->first_pkt;//傳遞將隊列首個數據包指針
if (pktlist) {//檢查數據包是否為空(隊列是否有數據)
q->first_pkt = pktlist->next;//隊列首節點指針后移
if (!q->first_pkt) {//檢查首節點的后繼節點是否存在
q->last_pkt = NULL;//若不存在,則將尾節點指針置空
}
q->nb_packets--;//隊列長度-1
q->size -= pktlist->pkt.size;//更新隊列編碼數據的緩存長度
*pkt = pktlist->pkt;//將隊列首節點數據返回
av_free(pktlist);//清空臨時節點數據(清空首節點數據,首節點出隊列)
ret = 1;//操作成功
break;
} else if (!block) {
ret = 0;
break;
} else {//隊列處於未就緒狀態,此時通過SDL_CondWait函數等待qready就緒信號,並暫時對互斥量解鎖
/*---------------------
* 等待隊列就緒信號qready,並對互斥量暫時解鎖
* 此時線程處於阻塞狀態,並置於等待條件就緒的線程列表上
* 使得該線程只在臨界區資源就緒后才被喚醒,而不至於線程被頻繁切換
* 該函數返回時,互斥量再次被鎖住,並執行后續操作
--------------------*/
SDL_CondWait(q->qready, q->qlock);//暫時解鎖互斥量並將自己阻塞,等待臨界區資源就緒(等待SDL_CondSignal發出臨界區資源就緒的信號)
}
}
SDL_UnlockMutex(q->qlock);//釋放互斥量
return ret;
}
//音頻解碼函數,從緩存隊列中提取數據包、解碼,並返回解碼后的數據長度(對一個完整的packet解碼,將解碼數據寫入audio_buf緩存,並返回多幀解碼數據的總長度)
int audio_decode_frame(VideoState *is, double *pts_ptr) {
int coded_consumed_size, data_size=0,pcm_bytes;//每次消耗的編碼數據長度[input](len1),輸出原始音頻數據的緩存長度[output],每組音頻采樣數據的字節數
AVPacket *pkt = &is->audio_pkt;//保存從隊列中提取的數據包
double pts;//音頻播放時間戳
for (;;) {
/*--2、從數據包pkt中不斷的解碼音頻數據,直到剩余的編碼數據長度<=0---*/
while (is->audio_pkt_size>0) {//檢查緩存中剩余的編碼數據長度(是否已完成一個完整的pakcet包的解碼,一個數據包中可能包含多個音頻編碼幀)
int got_frame;//解碼操作成功標識,成功返回非零值
//解碼一幀音頻數據,並返回消耗的編碼數據長度
coded_consumed_size = avcodec_decode_audio4(is->audio_st->codec, &is->audio_frame, &got_frame, pkt);
if (coded_consumed_size < 0) {//檢查是否執行了解碼操作
// If error, skip frame.
is->audio_pkt_size = 0;//更新編碼數據緩存長度
break;
}
if (got_frame) {//檢查解碼操作是否成功
//計算解碼后音頻原始數據長度[output]
data_size = av_samples_get_buffer_size(NULL, is->audio_st->codec->channels,
is->audio_frame.nb_samples, is->audio_st->codec->sample_fmt, 1);
memcpy(is->audio_buf, is->audio_frame.data[0], data_size);//將解碼數據復制到輸出緩存
}
is->audio_pkt_data += coded_consumed_size;//更新編碼數據緩存指針位置
is->audio_pkt_size -= coded_consumed_size;//更新緩存中剩余的編碼數據長度
if (data_size<=0) {//檢查輸出解碼數據緩存長度
// No data yet, get more frames
continue;
}
pts=is->audio_clock;//用每次更新的音頻播放時間更新音頻PTS
*pts_ptr=pts;
/*---------------------
* 當一個packet中包含多個音頻幀時
* 通過[解碼后音頻原始數據長度]及[采樣率]來推算一個packet中其他音頻幀的播放時間戳pts
* 采樣頻率44.1kHz,量化位數16位,意味着每秒采集數據44.1k個,每個數據占2字節
--------------------*/
pcm_bytes=2*is->audio_st->codec->channels;//計算每組音頻采樣數據的字節數=每個聲道音頻采樣字節數*聲道數
/*----更新audio_clock---
* 一個pkt包含多個音頻frame,同時一個pkt對應一個pts(pkt->pts)
* 因此,該pkt中包含的多個音頻幀的時間戳由以下公式推斷得出
* bytes_per_sec=pcm_bytes*is->audio_st->codec->sample_rate
* 從pkt中不斷的解碼,推斷(一個pkt中)每幀數據的pts並累加到音頻播放時鍾
--------------------*/
is->audio_clock+=(double)data_size/(double)(pcm_bytes*is->audio_st->codec->sample_rate);
// We have data, return it and come back for more later
return data_size;//返回解碼數據原始數據長度
}
/*-----------------1、從緩存隊列中提取數據包------------------*/
if (pkt->data) {//檢查數據包是殘留編碼數據
av_packet_unref(pkt);//釋放pkt中保存的編碼數據
}
if (is->quit) {//檢查退出進程標識
return -1;
}
// Next packet,從隊列中提取數據包到pkt
if (packet_queue_get(&is->audioq, pkt, 1) < 0) {
return -1;
}
is->audio_pkt_data = pkt->data;//傳遞編碼數據緩存指針
is->audio_pkt_size = pkt->size;//傳遞編碼數據緩存長度
// If update, update the audio clock w/pts
if (pkt->pts != AV_NOPTS_VALUE) {//檢查音頻播放時間戳
//獲得一個新的packet的時候,更新audio_clock,用packet中的pts更新audio_clock(一個pkt對應一個pts)
is->audio_clock=pkt->pts*av_q2d(is->audio_st->time_base);//更新音頻已經播的時間
}
}
}
/*------Audio Callback-------
* 音頻輸出回調函數,系統通過該回調函數將解碼后的pcm數據送入聲卡播放,
* 系統通常一次會准備一組緩存pcm數據(減少低速系統i/o次數),通過該回調送入聲卡,聲卡根據音頻pts依次播放pcm數據
* 待送入緩存的pcm數據完成播放后,再載入一組新的pcm緩存數據(每次音頻輸出緩存為空時,系統就調用此函數填充音頻輸出緩存,並送入聲卡播放)
* When we begin playing audio, SDL will continually call this callback function
* and ask it to fill the audio buffer with a certain number of bytes
* The audio function callback takes the following parameters:
* stream: A pointer to the audio buffer to be filled,輸出音頻數據到聲卡緩存
* len: The length (in bytes) of the audio buffer,緩存長度wanted_spec.samples=SDL_AUDIO_BUFFER_SIZE(1024)
--------------------------*/
void audio_callback(void *userdata, Uint8 *stream, int len) {
VideoState *is = (VideoState *) userdata;//傳遞用戶數據
int wt_stream_len, audio_size;//每次送入聲卡的數據長度,解碼后的數據長度
double pts;//音頻時間戳
while (len > 0) {//檢查音頻緩存的剩余長度
if (is->audio_buf_index >= is->audio_buf_size) {//檢查是否需要執行解碼操作
// We have already sent all our data; get more,從緩存隊列中提取數據包、解碼,並返回解碼后的數據長度,audio_buf緩存中可能包含多幀解碼后的音頻數據
audio_size = audio_decode_frame(is, &pts);
if (audio_size < 0) {//檢查解碼操作是否成功
// If error, output silence.
is->audio_buf_size = 1024;
memset(is->audio_buf, 0, is->audio_buf_size);//全零重置緩沖區
} else {
is->audio_buf_size = audio_size;//返回packet中包含的原始音頻數據長度(多幀)
}
is->audio_buf_index = 0;//初始化累計寫入緩存長度
}//end for if
wt_stream_len = is->audio_buf_size - is->audio_buf_index;//計算解碼緩存剩余長度
if (wt_stream_len > len) {//檢查每次寫入緩存的數據長度是否超過指定長度(1024)
wt_stream_len = len;//指定長度從解碼的緩存中取數據
}
//每次從解碼的緩存數據中以指定長度抽取數據並送入聲卡傳遞給聲卡
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, wt_stream_len);
len -= wt_stream_len;//更新解碼音頻緩存的剩余長度
stream += wt_stream_len;//更新緩存寫入位置
is->audio_buf_index += wt_stream_len;//更新累計寫入緩存數據長度
}//end for while
}
//視頻(圖像)幀渲染
void video_display(VideoState *is) {
SDL_Rect rect;//SDL矩形對象
VideoPicture *vp;//圖像幀結構體指針
float aspect_ratio;//寬度/高度比
int w, h, x, y;//窗口尺寸及起始位置
vp = &is->pictq[is->pictq_rindex];//從圖像幀隊列(數組)中提取圖像幀結構對象
if (vp->bmp) {//檢查像素數據指針是否有效
if (is->video_st->codec->sample_aspect_ratio.num == 0) {
aspect_ratio = 0;
} else {
aspect_ratio = av_q2d(is->video_st->codec->sample_aspect_ratio) * is->video_st->codec->width / is->video_st->codec->height;
}
if (aspect_ratio <= 0.0) {
aspect_ratio = (float) is->video_st->codec->width / (float) is->video_st->codec->height;
}
h = screen->h;
w = ((int) rint(h * aspect_ratio)) & -3;
if (w > screen->w) {
w = screen->w;
h = ((int)rint(w / aspect_ratio)) & -3;
}
x = (screen->w - w) / 2;
y = (screen->h - h) / 2;
//設置矩形顯示區域
rect.x = x;
rect.y = y;
rect.w = w;
rect.h = h;
SDL_DisplayYUVOverlay(vp->bmp, &rect);//圖像渲染
}
}
//創建/重置圖像幀,為圖像幀分配內存空間
void alloc_picture(void *userdata) {
VideoState *is = (VideoState *) userdata;//傳遞用戶數據
VideoPicture *vp=&is->pictq[is->pictq_windex];//從圖像幀隊列(數組)中提取圖像幀結構對象
if (vp->bmp) {//檢查圖像幀是否已存在
// We already have one make another, bigger/smaller.
SDL_FreeYUVOverlay(vp->bmp);//釋放當前overlay緩存
}
// Allocate a place to put our YUV image on that screen,根據指定尺寸及像素格式重新創建像素緩存區
vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width, is->video_st->codec->height, SDL_YV12_OVERLAY, screen);
vp->width = is->video_st->codec->width;//設置圖像幀寬度
vp->height = is->video_st->codec->height;//設置圖像幀高度
SDL_LockMutex(is->pictq_lock);//鎖定互斥量,保護畫布的像素數據
vp->allocated = 1;//圖像幀像素緩沖區已分配內存
SDL_CondSignal(is->pictq_ready);//給等待線程發出消息,通知隊列已就緒
SDL_UnlockMutex(is->pictq_lock);//釋放互斥量
}
/*---------------------------
* queue_picture:圖像幀插入隊列等待渲染
* @is:全局狀態參數集
* @pFrame:保存圖像解碼數據的結構體
* @pts:當前圖像幀的絕對顯示時間戳
* 1、首先檢查圖像幀隊列(數組)是否存在空間插入新的圖像,若沒有足夠的空間插入圖像則使當前線程休眠等待
* 2、在初始化的條件下,隊列(數組)中VideoPicture的bmp對象(YUV overlay)尚未分配空間,通過FF_ALLOC_EVENT事件的方法調用alloc_picture分配空間
* 3、當隊列(數組)中所有VideoPicture的bmp對象(YUV overlay)均已分配空間的情況下,直接跳過步驟2向bmp對象拷貝像素數據,像素數據在進行格式轉換后執行拷貝操作
---------------------------*/
int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {
/*-------1、檢查隊列是否有插入空間------*/
// Wait until we have space for a new pic
SDL_LockMutex(is->pictq_lock);//鎖定互斥量,保護圖像幀隊列
while (is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !is->quit) {//檢查隊列當前長度
SDL_CondWait(is->pictq_ready, is->pictq_lock);//線程休眠等待pictq_ready信號
}
SDL_UnlockMutex(is->pictq_lock);//釋放互斥量
if (is->quit) {//檢查進程退出標識
return -1;
}
/*------2、初始化/重置YUV overlay------*/
// Windex is set to 0 initially.
VideoPicture *vp = &is->pictq[is->pictq_windex];//從圖像幀隊列中抽取圖像幀對象
// Allocate or resize the buffer,檢查YUV overlay是否已存在,否則初始化YUV overlay,分配像素緩存空間
if (!vp->bmp || vp->width != is->video_st->codec->width || vp->height != is->video_st->codec->height) {
SDL_Event event;//SDL事件對象
vp->allocated = 0;//圖像幀未分配空間
// We have to do it in the main thread.
event.type = FF_ALLOC_EVENT;//指定分配圖像幀內存事件
event.user.data1 = is;//傳遞用戶數據
SDL_PushEvent(&event);//發送SDL事件
// Wait until we have a picture allocated.
SDL_LockMutex(is->pictq_lock);//鎖定互斥量,保護圖像幀隊列
while (!vp->allocated && !is->quit) {//檢查當前圖像幀是否已初始化(為SDL overlay)
SDL_CondWait(is->pictq_ready, is->pictq_lock);//線程休眠等待alloc_picture發送pictq_ready信號喚醒當前線程
}
SDL_UnlockMutex(is->pictq_lock);//釋放互斥量
if (is->quit) {//檢查進程退出標識
return -1;
}
}
/*-------3、拷貝視頻幀到YUV overlay------*/
// We have a place to put our picture on the queue.
// If we are skipping a frame, do we set this to null but still return vp->allocated = 1?
AVFrame pict;//臨時保存轉換后的圖像幀像素,與隊列中的元素相關聯
if (vp->bmp) {//檢查像素數據指針是否有效
SDL_LockYUVOverlay(vp->bmp);//locks the overlay for direct access to pixel data,原子操作,保護像素緩沖區,避免非法修改
// Point pict at the queue.
pict.data[0] = vp->bmp->pixels[0];//將轉碼后的圖像與畫布的像素緩沖器關聯
pict.data[1] = vp->bmp->pixels[2];
pict.data[2] = vp->bmp->pixels[1];
pict.linesize[0] = vp->bmp->pitches[0];//將轉碼后的圖像掃描行長度與畫布像素緩沖區的掃描行長度相關聯
pict.linesize[1] = vp->bmp->pitches[2];//linesize-Size, in bytes, of the data for each picture/channel plane
pict.linesize[2] = vp->bmp->pitches[1];//For audio, only linesize[0] may be set
// Convert the image into YUV format that SDL uses,將解碼后的圖像幀轉換為AV_PIX_FMT_YUV420P格式,並拷貝到圖像幀隊列
sws_scale(is->sws_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, is->video_st->codec->height, pict.data, pict.linesize);
SDL_UnlockYUVOverlay(vp->bmp);//Unlocks a previously locked overlay. An overlay must be unlocked before it can be displayed
vp->pts = pts;//傳遞當前圖像幀的絕對顯示時間戳
// Now we inform our display thread that we have a pic ready.
if (++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {//更新並檢查當前圖像幀隊列寫入位置
is->pictq_windex = 0;//重置圖像幀隊列寫入位置
}
SDL_LockMutex(is->pictq_lock);//鎖定隊列讀寫鎖,保護隊列數據
is->pictq_size++;//更新圖像幀隊列長度
SDL_UnlockMutex(is->pictq_lock);//釋放隊列讀寫鎖
}
return 0;
}
//修改FFmpeg內部退出回調對應的函數
int decode_interrupt_cb(void *opaque) {
return (global_video_state && global_video_state->quit);
}
/*---------------------------
* parse_thread:編碼數據包解析線程函數(從視頻文件中解析出音視頻編碼數據單元,一個AVPacket的data通常對應一個NAL)
* 1、直接識別文件格式和間接識別媒體格式
* 2、打開解碼器並啟動解碼線程
* 3、分離音視頻媒體包並掛接到相應隊列
---------------------------*/
int parse_thread(void *arg) {
VideoState *is = (VideoState *) arg;//傳遞用戶參數
global_video_state = is;//傳遞全局狀態參量結構體
AVFormatContext *pFormatCtx = NULL;//保存文件容器封裝信息及碼流參數的結構體
AVPacket pkt, *packet = &pkt;//在棧上創建臨時數據包對象並關聯指針
// Find the first video/audio stream.
is->videoStream=-1;//視頻流類型標號初始化為-1
is->audioStream=-1;//音頻流類型標號初始化為-1
int video_index = -1;//視頻流類型標號初始化為-1
int audio_index = -1;//音頻流類型標號初始化為-1
int i;//循環變量
AVDictionary *io_dict = NULL;
AVIOInterruptCB callback;
// Will interrupt blocking functions if we quit!
callback.callback = decode_interrupt_cb;
callback.opaque = is;
if (avio_open2(&is->io_context, is->filename, 0, &callback, &io_dict)) {
fprintf(stderr, "Unable to open I/O for %s\n", is->filename);
return -1;
}
// Open video file,打開視頻文件,取得文件容器的封裝信息及碼流參數
if (avformat_open_input(&pFormatCtx, is->filename, NULL, NULL) != 0) {
return -1; // Couldn't open file.
}
is->pFormatCtx = pFormatCtx;//傳遞文件容器封裝信息及碼流參數
// Retrieve stream information,取得文件中保存的碼流信息
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
return -1; // Couldn't find stream information.
}
// Dump information about file onto standard error,打印pFormatCtx中的碼流信息
av_dump_format(pFormatCtx, 0, is->filename, 0);
// Find the first video stream.
for (i = 0; i < pFormatCtx->nb_streams; i++) {//遍歷文件中包含的所有流媒體類型(視頻流、音頻流、字幕流等)
if (pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO && video_index < 0) {//若文件中包含有視頻流
video_index=i;//用視頻流類型的標號修改標識,使之不為-1
}
if (pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO && audio_index < 0) {//若文件中包含有音頻流
audio_index=i;//用音頻流類型的標號修改標識,使之不為-1
}
}
if (audio_index >= 0) {//檢查文件中是否存在音頻流
stream_component_open(is, audio_index);//根據指定類型打開音頻流
}
if (video_index >= 0) {//檢查文件中是否存在視頻流
stream_component_open(is, video_index);//根據指定類型打開視頻流
}
if (is->videoStream < 0 || is->audioStream < 0) {//檢查文件中是否存在音視頻流
fprintf(stderr, "%s: could not open codecs\n", is->filename);
goto fail;
}
// Main decode loop.
for (;;) {
if (is->quit) {//檢查退出進程標識
break;
}
// Seek stuff goes here,檢查音視頻編碼數據包隊列長度是否溢出
if (is->audioq.size > MAX_AUDIOQ_SIZE ||
is->videoq.size > MAX_VIDEOQ_SIZE) {
SDL_Delay(10);
continue;
}
//從文件中依次讀取每個圖像編碼數據包,並存儲在AVPacket數據結構中
if (av_read_frame(is->pFormatCtx, packet) < 0) {
if (is->pFormatCtx->pb->error == 0) {
SDL_Delay(100); // No error; wait for user input.
continue;
} else {
break;
}
}
// Is this a packet from the video stream?
if (packet->stream_index == is->videoStream) {//檢查數據包是否為視頻類型
packet_queue_put(&is->videoq, packet);//向隊列中插入數據包
} else if (packet->stream_index == is->audioStream) {//檢查數據包是否為音頻類型
packet_queue_put(&is->audioq, packet);//向隊列中插入數據包
} else {//檢查數據包是否為字幕類型
av_packet_unref(packet);//釋放packet中保存的(字幕)編碼數據
}
}
// All done - wait for it.
while (!is->quit) {
SDL_Delay(100);
}
fail://異常處理
{
SDL_Event event;//SDL事件對象
event.type = FF_QUIT_EVENT;//指定退出事件類型
event.user.data1 = is;//傳遞用戶數據
SDL_PushEvent(&event);//將該事件對象壓入SDL后台事件隊列
}
return 0;
}
/*---------------------------
* 更新內部視頻播放計時器(記錄視頻已經播時間(video_clock))
* @is:全局狀態參數集
* @src_frame:當前(輸入的)(待更新的)圖像幀對象
* @pts:當前圖像幀的顯示時間戳
* update the PTS to be in sync
---------------------------*/
double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {
/*----------檢查顯示時間戳----------*/
if (pts != 0) {//檢查顯示時間戳是否有效
// If we have pts, set video clock to it.
is->video_clock = pts;//用顯示時間戳更新已播放時間
} else {//若獲取不到顯示時間戳
// If we aren't given a pts, set it to the clock.
pts = is->video_clock;//用已播放時間更新顯示時間戳
}
/*--------更新視頻已經播時間--------*/
// Update the video clock,若該幀要重復顯示(取決於repeat_pict),則全局視頻播放時序video_clock應加上重復顯示的數量*幀率
double frame_delay = av_q2d(is->video_st->codec->time_base);//該幀顯示完將要花費的時間
// If we are repeating a frame, adjust clock accordingly,若存在重復幀,則在正常播放的前后兩幀圖像間安排渲染重復幀
frame_delay += src_frame->repeat_pict*(frame_delay*0.5);//計算渲染重復幀的時值(類似於音符時值)
is->video_clock += frame_delay;//更新視頻播放時間
// printf("repeat_pict=%d \n",src_frame->repeat_pict);
return pts;//此時返回的值即為下一幀將要開始顯示的時間戳
}
//視頻解碼線程函數
int decode_thread(void *arg) {
VideoState *is=(VideoState*)arg;//傳遞用戶數據
AVPacket pkt, *packet=&pkt;//在棧上創建臨時數據包對象並關聯指針
int frameFinished;//解碼操作是否成功標識,解碼幀結束標志frameFinished
// Allocate video frame,為解碼后的視頻信息結構體分配空間並完成初始化操作(結構體中的圖像緩存按照下面兩步手動安裝)
AVFrame *pFrame = av_frame_alloc();
double pts;//當前楨在整個視頻中的(絕對)時間位置
for (;;) {
if (packet_queue_get(&is->videoq,packet,1)<0) {//從隊列中提取數據包到packet,並將提取的數據包出隊列
// Means we quit getting packets
break;
}
pts = 0;//(絕對)顯示時間戳初始化
// global_video_pkt_pts = packet->pts;// Save global pts to be stored in pFrame in first call.
/*-------------------------
* Decode video frame,從封裝視頻文件中解碼完整的一幀數據,並將frameFinished設置為true
* 可能無法通過只解碼一個packet就獲得一個完整的視頻幀frame,可能需要讀取多個packet才行,avcodec_decode_video2()會在解碼到完整的一幀時設置frameFinished為真
* avcodec_decode_video2會按照dts指定的順序解碼,然后按照正確的顯示順序輸出圖像幀,並附帶圖像幀的pts
* 若解碼順序與顯示順序存在不一致,則avcodec_decode_video2會先對當前幀進行緩存(此時frameFinished=0),然后按照正確的順序輸出圖像幀
* The decoder bufferers a few frames for multithreaded efficiency
* It is also absolutely required to delay decoding in the case of B frames
* where the decode order may not be the same as the display order.
* Takes input raw video data from frame and writes the next output packet,
* if available, to avpkt. The output packet does not necessarily contain data for the most recent frame,
* as encoders can delay and reorder input frames internally as needed.
-------------------------*/
avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet);
//取得編碼數據包中的顯示時間戳PTS(int64_t),並暫時保存在pts(double)中
// if (packet->dts==AV_NOPTS_VALUE && pFrame->opaque && *(uint64_t*)pFrame->opaque!=AV_NOPTS_VALUE) {
// pts = *(uint64_t *)pFrame->opaque;
// } else if (packet->dts != AV_NOPTS_VALUE) {
// pts = packet->dts;
// } else {
// pts = 0;
// }
pts=av_frame_get_best_effort_timestamp(pFrame);//取得編碼數據包中的圖像幀顯示序號PTS(int64_t),並暫時保存在pts(double)中
/*-------------------------
* 在解碼線程函數中計算當前圖像幀的顯示時間戳
* 1、取得編碼數據包中的圖像幀顯示序號PTS(int64_t),並暫時保存在pts(double)中
* 2、根據PTS*time_base來計算當前楨在整個視頻中的顯示時間戳,即PTS*(1/framerate)
* av_q2d把AVRatioal結構轉換成double的函數,
* 用於計算視頻源每個圖像幀顯示的間隔時間(1/framerate),即返回(time_base->num/time_base->den)
-------------------------*/
//根據pts=PTS*time_base={numerator=1,denominator=25}計算當前楨在整個視頻中的顯示時間戳
pts*=av_q2d(is->video_st->time_base);//time_base為AVRational有理數結構體{num=1,den=25},記錄了視頻源每個圖像幀顯示的間隔時間
// Did we get a video frame,檢查是否解碼出完整一幀圖像
if (frameFinished) {
pts = synchronize_video(is, pFrame, pts);//檢查當前幀的顯示時間戳pts並更新內部視頻播放計時器(記錄視頻已經播時間(video_clock))
if (queue_picture(is, pFrame, pts) < 0) {//將解碼完成的圖像幀添加到圖像幀隊列,並記錄圖像幀的絕對顯示時間戳pts
break;
}
}
av_packet_unref(packet);//釋放pkt中保存的編碼數據
}
av_free(pFrame);//清除pFrame中的內存空間
return 0;
}
// These are called whenever we allocate a frame buffer. We use this to store the global_pts in a frame at the time it is allocated.
int our_get_buffer(struct AVCodecContext *c, AVFrame *pic, int flags) {
int ret = avcodec_default_get_buffer2(c, pic, 0);
uint64_t *pts = av_malloc(sizeof(uint64_t));
*pts = global_video_pkt_pts;
pic->opaque = pts;
return ret;
}
//根據指定類型打開流,找到對應的解碼器、創建對應的音頻配置、保存關鍵信息到 VideoState、啟動音頻和視頻線程
int stream_component_open(VideoState *is, int stream_index) {
AVFormatContext *pFormatCtx = is->pFormatCtx;//傳遞文件容器的封裝信息及碼流參數
AVCodecContext *codecCtx = NULL;//解碼器上下文對象,解碼器依賴的相關環境、狀態、資源以及參數集的接口指針
AVCodec *codec = NULL;//保存編解碼器信息的結構體,提供編碼與解碼的公共接口,可以看作是編碼器與解碼器的一個全局變量
SDL_AudioSpec wanted_spec, spec;//wanted_spec是進程請求SDL庫的音頻參數,spec是SDL庫返回給進程它能支持的音頻參數,如果請求超過SDL支持返回,則返回最相近參數
AVDictionary *optionsDict = NULL;
//檢查輸入的流類型是否在合理范圍內
if (stream_index<0 || stream_index>=pFormatCtx->nb_streams) {
return -1;
}
// Get a pointer to the codec context for the video stream.
codecCtx = pFormatCtx->streams[stream_index]->codec;//取得解碼器上下文
if (codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {//檢查解碼器類型是否為音頻解碼器
// Set audio settings from codec info,SDL_AudioSpec a structure that contains the audio output format
// 創建SDL_AudioSpec結構體,設置音頻播放參數
wanted_spec.freq = codecCtx->sample_rate;//采樣頻率 DSP frequency -- samples per second
wanted_spec.format = AUDIO_S16SYS;//采樣格式 Audio data format
wanted_spec.channels = codecCtx->channels;//聲道數 Number of channels: 1 mono, 2 stereo
wanted_spec.silence = 0;//無輸出時是否靜音
//默認每次讀音頻緩存的大小,推薦值為 512~8192,ffplay使用的是1024 specifies a unit of audio data refers to the size of the audio buffer in sample frames
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;//設置讀取音頻數據的回調接口函數 the function to call when the audio device needs more data
wanted_spec.userdata = is;//傳遞用戶數據
/*---------------------------
* 以指定參數打開音頻設備,並返回與指定參數最為接近的參數,該參數為設備實際支持的音頻參數
* Opens the audio device with the desired parameters(wanted_spec)
* return another specs we actually be using
* and not guaranteed to get what we asked for
--------------------------*/
if (SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
is->audio_hw_buf_size = spec.size;
}
/*-----------------------
* Find the decoder for the video stream,根據視頻流對應的解碼器上下文查找對應的解碼器,返回對應的解碼器(信息結構體)
* The stream's information about the codec is in what we call the "codec context.
* This contains all the information about the codec that the stream is using
-----------------------*/
codec = avcodec_find_decoder(codecCtx->codec_id);
if (!codec || (avcodec_open2(codecCtx, codec, &optionsDict) < 0)) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
//檢查解碼器類型
switch (codecCtx->codec_type) {
case AVMEDIA_TYPE_AUDIO://音頻解碼器
is->audioStream = stream_index;//音頻流類型標號初始化
is->audio_st = pFormatCtx->streams[stream_index];
is->audio_buf_size = 0;//解碼后的多幀音頻數據長度
is->audio_buf_index = 0;//累計送入聲卡的長度
memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
packet_queue_init(&is->audioq);//音頻數據包隊列初始化
SDL_PauseAudio(0);//audio callback starts running again,開啟音頻設備,如果這時候沒有獲得數據那么它就靜音
break;
case AVMEDIA_TYPE_VIDEO://視頻解碼器
is->videoStream = stream_index;//視頻流類型標號初始化
is->video_st = pFormatCtx->streams[stream_index];
//以系統時間為基准,初始化播放到當前幀的已播放時間值,該值為真實時間值、動態時間值、絕對時間值
is->frame_timer=(double)av_gettime()/1000000.0;
is->frame_last_delay = 40e-3;//初始化上一幀圖像的動態刷新延遲時間
packet_queue_init(&is->videoq);//視頻數據包隊列初始化
is->video_tid = SDL_CreateThread(decode_thread, is);//創建解碼線程
// Initialize SWS context for software scaling,設置圖像轉換像素格式為AV_PIX_FMT_YUV420P
is->sws_ctx = sws_getContext(is->video_st->codec->width, is->video_st->codec->height,
is->video_st->codec->pix_fmt, is->video_st->codec->width, is->video_st->codec->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL);
// codecCtx->get_buffer2=our_get_buffer;
break;
default:
break;
}
return 0;
}
/*------取得當前播放音頻數據的pts------
* 音視頻同步的原理是根據音頻的pts來控制視頻的播放
* 也就是說在視頻解碼一幀后,是否顯示以及顯示多長時間,是通過該幀的PTS與同時正在播放的音頻的PTS比較而來的
* 如果音頻的PTS較大,則視頻准備完畢立即刷新,否則等待
*
* 因為pcm數據采用audio_callback回調方式進行播放
* 對於音頻播放我們只能得到寫入回調函數前緩存音頻幀的pts,而無法得到當前播放幀的pts(需要采用當前播放音頻幀的pts作為參考時鍾)
* 考慮到音頻的大小與播放時間成正比(相同采樣率),那么當前時刻正在播放的音頻幀pts(位於回調函數緩存中)
* 就可以根據已送入聲卡的pcm數據長度、緩存中剩余pcm數據長度,緩存長度及采樣率進行推算了
--------------------------------*/
double get_audio_clock(VideoState *is) {
double pts=is->audio_clock;//Maintained in the audio thread,取得解碼操作完成時的當前播放時間戳
//還未(送入聲卡)播放的剩余原始音頻數據長度,等於解碼后的多幀原始音頻數據長度-累計送入聲卡的長度
int hw_buf_size=is->audio_buf_size-is->audio_buf_index;//計算當前音頻解碼數據緩存索引位置
int bytes_per_sec=0;//每秒的原始音頻字節數
int pcm_bytes=is->audio_st->codec->channels*2;//每組原始音頻數據字節數=聲道數*每聲道數據字節數
if (is->audio_st) {
bytes_per_sec=is->audio_st->codec->sample_rate*pcm_bytes;//計算每秒的原始音頻字節數
}
if (bytes_per_sec) {//檢查每秒的原始音頻字節數
pts-=(double)hw_buf_size/bytes_per_sec;//根據送入聲卡緩存的索引位置,往前倒推計算當前時刻的音頻播放時間戳pts
}
return pts;//返回當前正在播放的音頻時間戳
}
//定時器觸發的回調函數
static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
SDL_Event event;//SDL事件對象
event.type = FF_REFRESH_EVENT;//視頻顯示刷新事件
event.user.data1 = opaque;//傳遞用戶數據
SDL_PushEvent(&event);//發送事件
return 0; // 0 means stop timer
}
/*---------------------------
* Schedule a video refresh in 'delay' ms.
* 設置一下幀播放的延遲
* 告訴系統在指定的延時后來推送一個FF_REFRESH_EVENT事件,起到類似於節拍器的作用
* 這個事件將在事件隊列里觸發sdl_refresh_timer_cb函數的調用
* @delay用於在圖像幀的解碼順序與渲染順序不一致的情況下,調節下一幀的渲染時機
* 從而盡可能的使所有圖像幀按照固定的幀率渲染刷新
--------------------------*/
static void schedule_refresh(VideoState *is, int delay) {
SDL_AddTimer(delay, sdl_refresh_timer_cb, is);//在指定的時間(ms)后回調用戶指定的函數
}
/*---------------------------
* 顯示刷新函數(FF_REFRESH_EVENT響應函數)
* 將視頻同步到音頻上(聲音是連續播放的,因此用畫面去同步聲音),計算下一幀的延遲時間
* 使用當前幀的PTS和上一幀的PTS差來估計播放下一幀的延遲時間,並根據video的播放速度來調整這個延遲時間
---------------------------*/
void video_refresh_timer(void *userdata) {
VideoState *is=(VideoState*)userdata;//傳遞用戶數據
VideoPicture *vp;//圖像幀對象
//delay-前后幀間的顯示時間間隔,diff-圖像幀顯示與音頻幀播放間的時間差
//sync_threshold-前后幀間的最小時間差,actual_delay-當前幀-下已幀的顯示時間間隔(動態時間、真實時間、絕對時間)
double delay,diff,sync_threshold,actual_delay,ref_clock;//ref_clock-音頻時間戳
if (is->video_st) {//檢查全局狀態參數集中的視頻流信息結構體是否有效(是否已加載視頻文件)
if (is->pictq_size == 0) {//檢查圖像幀隊列中是否有等待顯示刷新的圖像
schedule_refresh(is, 1);//若隊列為空,則發送顯示刷新事件並再次進入video_refresh_timer函數
} else {
vp = &is->pictq[is->pictq_rindex];//從顯示隊列中取得等待顯示的圖像幀
//計算當前幀和前一幀顯示(pts)的間隔時間(顯示時間戳的差值)
delay = vp->pts - is->frame_last_pts;//The pts from last time,前后幀間的時間差
if (delay <= 0 || delay >= 1.0) {//檢查時間間隔是否在合理范圍
// If incorrect delay, use previous one
delay = is->frame_last_delay;//沿用之前的動態刷新間隔時間
}
// Save for next time
is->frame_last_delay = delay;//保存上一幀圖像的動態刷新延遲時間
is->frame_last_pts = vp->pts;//保存上一幀圖像的顯示時間戳
// Update delay to sync to audio,取得聲音播放時間戳(作為視頻同步的參考時間)
ref_clock=get_audio_clock(is);//根據Audio clock來判斷Video播放的快慢,獲取當前播放聲音的時間戳
//也就是說在diff這段時間中聲音是勻速發生的,但是在delay這段時間frame的顯示可能就會有快慢的區別
diff=vp->pts-ref_clock;//計算圖像幀顯示與音頻幀播放間的時間差
//根據時間差調整播放下一幀的延遲時間,以實現同步 Skip or repeat the frame,Take delay into account
sync_threshold=(delay>AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;//比較前后兩幀間的顯示時間間隔與最小時間間隔
//判斷音視頻不同步條件,即音視頻間的時間差 & 前后幀間的時間差<10ms閾值,若>該閾值則為快進模式,不存在音視頻同步問題
if (fabs(diff)<AV_NOSYNC_THRESHOLD) {
if (diff<=-sync_threshold) {//比較前一幀&當前幀[畫面-聲音]間的時間間隔與前一幀 & 當前幀[畫面-畫面]間的時間間隔,慢了,delay設為0
//下一幀畫面顯示的時間和當前的聲音很近的話加快顯示下一幀(即后面video_display顯示完當前幀后開啟定時器很快去顯示下一幀
delay=0;
} else if (diff>=sync_threshold) {//比較兩幀畫面間的顯示時間與兩幀畫面間聲音的播放時間,快了,加倍delay
delay=2*delay;
}
}//如果diff(明顯)大於AV_NOSYNC_THRESHOLD,即快進的模式了,畫面跳動太大,不存在音視頻同步的問題了
//更新視頻播放到當前幀時的已播放時間值(所有圖像幀動態播放累計時間值-真實值),frame_timer一直累加在播放過程中我們計算的延時
is->frame_timer+=delay;
//每次計算frame_timer與系統時間的差值(以系統時間為基准時間),將frame_timer與系統時間(絕對時間)相關聯的目的
actual_delay=is->frame_timer-(av_gettime()/1000000.0);//Computer the REAL delay
if (actual_delay < 0.010) {//檢查絕對時間范圍
actual_delay = 0.010;// Really it should skip the picture instead
}
schedule_refresh(is,(int)(actual_delay*1000+0.5));//用絕對時間開定時器去動態顯示刷新下一幀
video_display(is);//刷新當前圖像,Show the picture
// Update queue for next picture!
if (++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {//更新圖像幀隊列讀索引位置
is->pictq_rindex = 0;//若讀索引抵達隊列尾,則重置讀索引位置
}
SDL_LockMutex(is->pictq_lock);//鎖定互斥量,保護畫布的像素數據
is->pictq_size--;//更新圖像幀隊列長度
SDL_CondSignal(is->pictq_ready);//發送隊列就緒信號
SDL_UnlockMutex(is->pictq_lock);//釋放互斥量
}
} else {//若視頻信息獲取失敗,則經過指定延時(100ms)后重新嘗試刷新視圖
schedule_refresh(is, 100);
}
}
//入口函數,初始化SDL庫,注冊SDL消息事件,啟動文件解析線程,進入消息循環
int main(int argc, char *argv[]) {
if (argc<2) {//檢查輸入參數個數是否正確
fprintf(stderr, "Usage: test <file>\n");
exit(1);
}
av_register_all();// Register all formats and codecs,注冊所有多媒體格式及編解碼器
VideoState *is=av_mallocz(sizeof(VideoState));//創建全局狀態對象
av_strlcpy(is->filename, argv[1], 1024);//復制視頻文件路徑名
is->pictq_lock = SDL_CreateMutex();//創建編碼數據包隊列互斥鎖對象
is->pictq_ready = SDL_CreateCond();//創建編碼數據包隊列就緒條件對象
//SDL_Init initialize the Event Handling, File I/O, and Threading subsystems,初始化SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());//initialize the video audio & timer subsystem
exit(1);
}
// Make a screen to put our video,在SDL2.0中SDL_SetVideoMode及SDL_Overlay已經棄用,改為SDL_CreateWindow及SDL_CreateRenderer創建窗口及着色器
#ifndef __DARWIN__
screen = SDL_SetVideoMode(640, 480, 0, 0);//創建SDL窗口及繪圖表面,並指定圖像尺寸及像素格式
#else
screen = SDL_SetVideoMode(640, 480, 24, 0);//創建SDL窗口及繪圖表面,並指定圖像尺寸及像素格式
#endif
if (!screen) {//檢查SDL(繪圖表面)窗口是否創建成功(SDL用繪圖表面對象操作窗口)
fprintf(stderr, "SDL: could not set video mode - exiting\n");
exit(1);
}
schedule_refresh(is, 40);//在指定的時間(40ms)后回調用戶指定的函數,進行圖像幀的顯示更新
is->parse_tid = SDL_CreateThread(parse_thread, is);//創建編碼數據包解析線程
if (!is->parse_tid) {//檢查線程是否創建成功
av_free(is);
return -1;
}
/*-----------------------
* SDL事件(消息)循環
* 播放器通過消息循環機制,不斷循環往復的觸發(驅動)圖像幀渲染操作,完成整個視頻文件的渲染
* 整個過程類似於按照指定節拍彈奏鋼琴,消息循環機制保證了視頻按照固定的節拍(6/8)播放
* 因為存在解碼順序與渲染順序不一致的情況(解碼B幀的情況),視頻同步機制保證了在圖像在解碼后都盡量按照固定節拍播放
* 在每次的消息響應函數video_refresh_timer中,重新計算下一幀的顯示時間
* 並通過schedule_refresh指定時間(類似於節拍器作用),觸發下一輪的圖像幀顯示
-----------------------*/
SDL_Event event;//SDL事件(消息)對象
for (;;) {
SDL_WaitEvent(&event);//Use this function to wait indefinitely for the next available event,主線程阻塞,等待事件到來
switch (event.type) {//事件到來后喚醒主線程后,檢查事件類型,執行相應操作
case FF_QUIT_EVENT:
case SDL_QUIT://退出進程事件
is->quit = 1;
// If the video has finished playing, then both the picture and audio queues are waiting for more data.
// Make them stop waiting and terminate normally..
SDL_CondSignal(is->audioq.qready);//發出隊列就緒信號避免死鎖
SDL_CondSignal(is->videoq.qready);
SDL_Quit();
exit(0);
break;
case FF_ALLOC_EVENT://分配overlay事件
alloc_picture(event.user.data1);//分配overlay事件響應函數
break;
case FF_REFRESH_EVENT://視頻顯示刷新事件
video_refresh_timer(event.user.data1);//視頻顯示刷新事件響應函數
break;
default:
break;
}
}
return 0;
}
// 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
// 公眾號:斷點實驗室
// 掃描二維碼,關注更多優質原創,內容包括:音視頻開發、圖像處理、網絡、
// Linux,Windows、Android、嵌入式開發等
