前言
之前陸續(xù)學(xué)習(xí)了視頻渲染相關(guān)技術(shù)opengl es,視頻編解碼相關(guān)技術(shù)(基于ffmpeg封裝接口的使用)甥捺,雖然擁有了這些基礎(chǔ)知識(shí)抢蚀,但是離寫(xiě)出一個(gè)功能完善的播放器還有一段距離,我覺(jué)得可以先學(xué)學(xué)ffplay.c镰禾,ijkplayer等等開(kāi)源播放器他們是如何實(shí)現(xiàn)的皿曲,然后學(xué)以致用。所以從今天開(kāi)始將逐步學(xué)習(xí)ffplay.c的實(shí)現(xiàn)方式吴侦。我認(rèn)為閱讀源碼的方式如果帶著問(wèn)題舉一反三的思維去理解代碼思路屋休,這樣可能印象更加深刻。首先拋出問(wèn)題”如果讓我實(shí)現(xiàn)我會(huì)怎么做备韧,開(kāi)源是怎么實(shí)現(xiàn)的劫樟,它為什么這么做,這么做的優(yōu)缺點(diǎn)是什么?“ 所以我讀ffplay.c的源碼也會(huì)按照這樣的思路來(lái)
拋出問(wèn)題
在實(shí)現(xiàn)一個(gè)播放器時(shí)织堂,會(huì)涉及到如下三個(gè)部分
拉流:從本地或者遠(yuǎn)程讀取壓縮音視頻數(shù)據(jù)
解碼:將讀取到的音視頻數(shù)據(jù)進(jìn)行解碼得到未壓縮音視頻數(shù)據(jù)
渲染:將解碼得到的未壓縮視頻渲染到屏幕叠艳、未壓縮音頻通過(guò)揚(yáng)聲器播放
那么應(yīng)該創(chuàng)建幾個(gè)線(xiàn)程來(lái)做這些事情?首先拉流肯定要單獨(dú)的線(xiàn)程易阳,解碼和渲染是否應(yīng)該放到一個(gè)線(xiàn)程里面呢附较,我覺(jué)得應(yīng)該分開(kāi),因?yàn)椴环珠_(kāi)的話(huà)流程就是潦俺,解碼成功->渲染-->解碼下一幀-->渲染 一直循環(huán)拒课。對(duì)于I、B事示、P幀解碼所花時(shí)間會(huì)不一樣早像,那么渲染時(shí)間非等間隔的就會(huì)導(dǎo)致播放不那么流暢。綜上很魂,應(yīng)該有拉流一個(gè)線(xiàn)程(音視頻共用)扎酷,解碼兩個(gè)(音視頻獨(dú)立),渲染兩個(gè)(音視頻獨(dú)立)
音視頻要分開(kāi)的原因是音視頻渲染間隔是是不同的遏匆。
今天先從渲染部分開(kāi)始閱讀和學(xué)習(xí)
對(duì)于線(xiàn)程的創(chuàng)建實(shí)際上ffplay.c也是這樣實(shí)現(xiàn)的法挨,它的整個(gè)架構(gòu)設(shè)計(jì)圖如下:
截止到ffmpeg 4.2版本,ffplay.c大概有近四千行代碼幅聘。整體的流程圖架構(gòu)設(shè)計(jì)如下:
ffplay.c的實(shí)現(xiàn)
實(shí)際上ffplay.c也是按照這樣
- 視頻渲染線(xiàn)程的代碼
這里只貼出關(guān)鍵代碼凡纳。這段代碼的主要工作流程如下:
1、取視頻幀帝蒿;沒(méi)有可渲染視頻幀就返回睡眠荐糜,有則進(jìn)入步驟2
2、取出上一次已渲染視頻幀和當(dāng)前待渲染視頻幀
3、根據(jù)音視頻同步規(guī)則決定當(dāng)前待渲染視頻幀是否立即渲染暴氏,即:如果本幀的播放時(shí)刻(即上一幀的播放時(shí)刻+上一幀的時(shí)長(zhǎng))大于當(dāng)前時(shí)刻延塑,代表本幀的播放時(shí)刻還未到來(lái),渲染時(shí)間未到來(lái)則繼續(xù)播放上一幀(由如下goto語(yǔ)句進(jìn)行跳轉(zhuǎn))答渔,然后將remain_time賦值為本幀的播放時(shí)刻與當(dāng)前時(shí)刻的時(shí)間差值(睡眠remain_time時(shí)間后)下一個(gè)流程再渲染此幀关带;否則進(jìn)入步驟4
4、更新視頻幀F(xiàn)rameQueue隊(duì)列相關(guān)數(shù)據(jù)沼撕,然后渲染本幀(真正執(zhí)行視頻渲染工作的代碼在video_display()函數(shù)中)
步驟1-4循環(huán)調(diào)用
/** 1宋雏、用于控制視頻的顯示
* 2、如果音視頻同步方式為視頻同步到音頻务豺,則這塊邏輯在此實(shí)現(xiàn)
*/
static void video_refresh(void *opaque, double *remaining_time)
{
VideoState *is = opaque;
double time;
Frame *sp, *sp2;
....省略代碼.....
// rtsp 等實(shí)時(shí)流則is->realtime的值為1磨总,本地文件的播放則為0
if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
check_external_clock_speed(is);
if (is->video_st) {
retry:
if (frame_queue_nb_remaining(&is->pictq) == 0) { // 步驟1、取視頻幀笼沥;沒(méi)有可渲染視頻幀就返回睡眠蚪燕,
// nothing to do, no picture to display in the queue
} else {
double last_duration, duration, delay;
Frame *vp, *lastvp;
/* dequeue the picture */
// 步驟2、取出上一次已渲染視頻幀和當(dāng)前待渲染視頻幀
// 首次進(jìn)入此方法敬拓,由于f->rindex_shown是默認(rèn)值邻薯,所以得到的lastvp和vp是同一個(gè)Frame
lastvp = frame_queue_peek_last(&is->pictq);
vp = frame_queue_peek(&is->pictq);
if (vp->serial != is->videoq.serial) {
frame_queue_next(&is->pictq);
goto retry;
}
if (lastvp->serial != vp->serial)
is->frame_timer = av_gettime_relative() / 1000000.0;
if (is->paused)
goto display;
/* compute nominal last_duration */
last_duration = vp_duration(is, lastvp, vp);
/** 這里實(shí)現(xiàn)了視頻同步音頻或者同步系統(tǒng)時(shí)鐘的關(guān)鍵代碼
* 本幀視頻是否能夠播放的條件為 is->frame_timer (上一幀視頻的播放時(shí)間) + delay(本幀視頻的播放延遲) >= 當(dāng)前系統(tǒng)時(shí)間
* delay是基于音頻時(shí)鐘或者系統(tǒng)時(shí)鐘計(jì)算出來(lái)的播放延遲時(shí)間(它的理論值就是本幀pts-上幀pts)
* delay>=0 值越小代表視頻播的太慢了,本幀越需要盡快播放乘凸,越大則代表視頻播的太快了,本幀需要延后播放 為0 則視頻有可能慢了音頻至少一個(gè)幀
*/
delay = compute_target_delay(last_duration, is);
// frame_timer表示上一幀的播放時(shí)刻(這個(gè)時(shí)刻比非實(shí)際顯示到屏幕的時(shí)刻提前一點(diǎn)點(diǎn)時(shí)間)
time= av_gettime_relative()/1000000.0;
if (time < is->frame_timer + delay) { // 步驟3累榜、 如果本幀的播放時(shí)刻(即上一幀的播放時(shí)刻+上一幀的時(shí)長(zhǎng))大于當(dāng)前時(shí)刻营勤,代表本幀的播放時(shí)刻還未到來(lái)
// 渲染時(shí)間未到來(lái)則繼續(xù)播放上一幀(由如下goto語(yǔ)句進(jìn)行跳轉(zhuǎn)),然后將remain_time賦值為本幀的播放時(shí)刻與當(dāng)前時(shí)刻的時(shí)間差值
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display; //繼續(xù)播放上一幀
}
// 同步上一幀的播放時(shí)間
/** 疑問(wèn):這里is->frame_timer += delay;為什么是這樣寫(xiě)的壹罚,而不是直接is->frame_timer = time;呢葛作?
* 分析:如果直接用is->frame_timer = time;進(jìn)行賦值,那么視頻幀因?yàn)槟撤N原因累積了很多幀未播放時(shí)猖凛,那么會(huì)導(dǎo)致多出來(lái)的視頻幀無(wú)法丟棄
* 實(shí)際上ffplay有兩條時(shí)鐘赂蠢,一條時(shí)鐘音視頻時(shí)鐘,用于音視頻同步用辨泳,即計(jì)算這里的delay值虱岂,另一條時(shí)鐘frame_timer用來(lái)記錄上一幀的播放時(shí)間
* 同時(shí)用于計(jì)算是否滿(mǎn)足丟幀的條件
*/
is->frame_timer += delay;
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) // 處理首幀播放和音視頻幀同時(shí)出現(xiàn)解碼時(shí)間過(guò)程導(dǎo)致的抖動(dòng),這里就需要重新更新上一幀播放時(shí)間為當(dāng)前時(shí)間了
is->frame_timer = time;
// 步驟4菠红、以下都是
/** ffplay.c里面有三個(gè)時(shí)間
* 1第岖、frame_timer:保存在VideoState結(jié)構(gòu)體里面,用以記錄視頻播放的時(shí)間點(diǎn)试溯,該時(shí)間點(diǎn)基于系統(tǒng)時(shí)鐘
* 2蔑滓、pts:保存在視頻Clock結(jié)構(gòu)體里面,等同于視頻幀的pts
* 3、pts_drift:視頻幀的pts與視頻播放時(shí)刻的時(shí)間差
*/
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
update_video_pts(is, vp->pts, vp->pos, vp->serial);
SDL_UnlockMutex(is->pictq.mutex);
// 如果本幀的pts+duration < time(當(dāng)前時(shí)間)則丟棄該幀(說(shuō)明隊(duì)列中有大量還未渲染的視頻幀键袱,必須得丟掉一些了)
if (frame_queue_nb_remaining(&is->pictq) > 1) {
Frame *nextvp = frame_queue_peek_next(&is->pictq);
duration = vp_duration(is, vp, nextvp);
if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
is->frame_drops_late++;
frame_queue_next(&is->pictq);
goto retry;
}
}
// FrameQueue隊(duì)列的當(dāng)前讀取指針rindex的值+1(即指向本幀的索引),并且刪除上一幀的Frame數(shù)據(jù)(因?yàn)橐呀?jīng)不需要了)
frame_queue_next(&is->pictq);
is->force_refresh = 1;
if (is->step && !is->paused)
stream_toggle_pause(is);
}
display:
/* display picture */
// 執(zhí)行渲染本幀的工作
if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
video_display(is);
}
is->force_refresh = 0;
}
....省略代碼.....
}
/* display the current picture, if any */
static void video_display(VideoState *is)
{
if (!is->width)
video_open(is);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO)
video_audio_display(is);
else if (is->video_st)
video_image_display(is);
SDL_RenderPresent(renderer);
}
還有一個(gè)很重要的就是如何保證前面的video_refresh()函數(shù)循環(huán)調(diào)用呢燎窘,通過(guò)如下的refresh_loop_wait_event()函數(shù),它通過(guò)SDL庫(kù)檢測(cè)是否有鼠標(biāo)和鍵盤(pán)等事件蹄咖,如果沒(méi)有則一直循環(huán)荠耽,有則退出循環(huán)去處理事件。處理事件又在event_loop()函數(shù)中比藻,它是在main()函數(shù)中啟動(dòng)的铝量。
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
double remaining_time = 0.0;
SDL_PumpEvents();
while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) { // 沒(méi)有捕捉到事件就去渲染視頻,捕捉到了視頻則先處理事件
if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
SDL_ShowCursor(0);
cursor_hidden = 1;
}
if (remaining_time > 0.0)
av_usleep((int64_t)(remaining_time * 1000000.0));
remaining_time = REFRESH_RATE;
if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
video_refresh(is, &remaining_time);
SDL_PumpEvents();
}
}
static void event_loop(VideoState *cur_stream)
{
SDL_Event event;
double incr, pos, frac;
/** 學(xué)習(xí):線(xiàn)程的事件循環(huán)隊(duì)列
* 分析:時(shí)間循環(huán)隊(duì)列的組成一定是 for/while循環(huán)+usleep()休眠+事件捕捉器組成银亲,如果沒(méi)有usleep()休眠那么會(huì)導(dǎo)致cpu空轉(zhuǎn)慢叨,造成大量浪費(fèi)
* 這里休眠時(shí)間由播放視頻的幀率(間隔)根據(jù)一定的規(guī)則計(jì)算而來(lái),然后如果捕捉到事件則優(yōu)先處理事件务蝠,接著再去播放視頻
*/
for (;;) {
double x;
refresh_loop_wait_event(cur_stream, &event);
switch (event.type) {
case SDL_KEYDOWN:
.......各種鍵盤(pán)和鼠標(biāo)事件....省略
.....
}
}
學(xué)習(xí):線(xiàn)程的事件循環(huán)隊(duì)列
分析:事件循環(huán)隊(duì)列的組成一定是 for/while循環(huán)+usleep()休眠+事件捕捉器組成拍谐,如果沒(méi)有usleep()休眠那么會(huì)導(dǎo)致cpu空轉(zhuǎn),造成大量浪費(fèi)這里休眠時(shí)間由播放視頻的幀率(間隔)根據(jù)一定的規(guī)則計(jì)算而來(lái)馏段,然后如果捕捉到事件則優(yōu)先處理事件轩拨,接著再去播放視頻
這一套事件循環(huán)隊(duì)列機(jī)制就保證了視頻持續(xù)播放又能響應(yīng)用戶(hù)鍵盤(pán)和鼠標(biāo)事件。以上就是視頻渲染線(xiàn)程的工作流程和機(jī)制
- 音頻渲染線(xiàn)程的代碼
音頻渲染線(xiàn)程院喜,它是通過(guò)SDL內(nèi)部自驅(qū)動(dòng)的一個(gè)回調(diào)函數(shù)亡蓉,被周期性的回調(diào),只需要不停的往里面填充音頻即可進(jìn)行音頻的渲染了喷舀。每一次調(diào)用稱(chēng)為一個(gè)音頻渲染周期
len:代表需要填充的音頻數(shù)據(jù)長(zhǎng)度;stream代表填充音頻的buffer地址
/* prepare a new audio buffer */
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
VideoState *is = opaque;
int audio_size, len1;
audio_callback_time = av_gettime_relative();
/** 學(xué)習(xí):音頻渲染線(xiàn)程砍濒,它是通過(guò)SDL內(nèi)部自驅(qū)動(dòng)的一個(gè)回調(diào)函數(shù),被周期性的回調(diào)硫麻,只需要不停的往里面填充音頻即可進(jìn)行音頻的渲染了爸邢。每一次調(diào)用稱(chēng)為一個(gè)音頻渲染周期
* len:代表需要填充的音頻數(shù)據(jù)長(zhǎng)度;stream代表填充音頻的buffer地址
*
* is->audio_buf_index:表示當(dāng)前渲染周期內(nèi)已拷貝的音頻數(shù)據(jù)字節(jié)的索引,即下一塊音頻數(shù)據(jù)放入stream+is->audio_buf_index的位置
* is->audio_buf_size:表示當(dāng)前音頻Frame的字節(jié)數(shù)
*/
while (len > 0) {
if (is->audio_buf_index >= is->audio_buf_size) {
audio_size = audio_decode_frame(is);
if (audio_size < 0) {
/* if error, just output silence */
is->audio_buf = NULL;
is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
} else {
if (is->show_mode != SHOW_MODE_VIDEO)
update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
is->audio_buf_size = audio_size;
}
is->audio_buf_index = 0;
}
len1 = is->audio_buf_size - is->audio_buf_index;
if (len1 > len)
len1 = len;
if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
else {
memset(stream, 0, len1);
if (!is->muted && is->audio_buf)
SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume);
}
len -= len1;
stream += len1;
is->audio_buf_index += len1;
}
is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
/* Let's assume the audio driver that is used by SDL has two periods. */
if (!isnan(is->audio_clock)) {
set_clock_at(&is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, audio_callback_time / 1000000.0);
// 如果音視頻同步方式為同步外部時(shí)鐘拿愧,則調(diào)用此方法會(huì)有用
sync_clock_to_slave(&is->extclk, &is->audclk);
}
}
該函數(shù)的開(kāi)啟過(guò)程是隨著拉流線(xiàn)程開(kāi)啟的杠河,在拉流線(xiàn)程內(nèi)通過(guò)stream_component_open()開(kāi)啟音頻流處理
static int read_thread(void *arg){
.....省略代碼.....
/* open the streams */
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
}
.....省略代碼.....
}
接下來(lái)在音頻流處理流程中開(kāi)啟音頻渲染相關(guān)代碼
static int stream_component_open(VideoState *is, int stream_index){
.....省略代碼.....
/* prepare audio output */
if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
goto fail;
....省略代碼......
}
然后就是初始化SDL音頻渲染相關(guān),在這里指定音頻渲染回調(diào)
static int stream_component_open(VideoState *is, int stream_index)
{
....省略代碼......
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.silence = 0;
wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC));
wanted_spec.callback = sdl_audio_callback;
wanted_spec.userdata = opaque;
....省略代碼......
}
- 字幕渲染線(xiàn)程的代碼
實(shí)際上字幕渲染沒(méi)有單獨(dú)的線(xiàn)程浇辜,它與視頻共用一個(gè)線(xiàn)程券敌,可以看到這段代碼和視頻渲染在同一個(gè)函數(shù)中的,前面說(shuō)道奢赂,視頻渲染流程到了步驟4之后陪白,就代表著即將渲染本幀視頻,而字幕的渲染則是在本幀視頻渲染之前進(jìn)行渲染膳灶,即下面這段代碼是在視頻幀渲染之前執(zhí)行
static void video_refresh(void *opaque, double *remaining_time){
.....省略代碼.....
if (is->subtitle_st) {
// 步驟1咱士、字幕FrameQueue隊(duì)列中是否有字幕幀立由,沒(méi)有則退出循環(huán)
while (frame_queue_nb_remaining(&is->subpq) > 0) {
// 步驟2、獲取當(dāng)前待渲染字幕幀sp以及下一個(gè)待渲染字幕幀sp2(如果有的話(huà))
sp = frame_queue_peek(&is->subpq);
if (frame_queue_nb_remaining(&is->subpq) > 1)
sp2 = frame_queue_peek_next(&is->subpq);
else
sp2 = NULL;
// 步驟3序厉、決定當(dāng)前字幕幀是否需要被渲染锐膜。一幀字幕開(kāi)始顯示時(shí)間=pts+start_display_time,結(jié)束顯示時(shí)間=pts+end_display_time
/** 學(xué)習(xí):視頻和字幕同步
* 分析:視頻幀和字幕幀的同步主要以視頻的時(shí)鐘為準(zhǔn)進(jìn)行同步,這里is->vidclk.pts表示即將要渲染的視頻幀的時(shí)間,當(dāng)它大于(晚于)當(dāng)前要渲染字
* 幕幀結(jié)束時(shí)間或者下一個(gè)要渲染字幕幀開(kāi)始時(shí)間表示字幕顯示已經(jīng)落后于視頻了弛房,趕緊渲染當(dāng)前字幕幀道盏;否則就退出字幕幀渲染循環(huán)
*/
// sp->serial != is->subtitleq.serial 用于首幀字幕渲染
/** 疑問(wèn):既然當(dāng)前字幕幀都落后于即將要渲染的字幕幀了直接丟棄不就好了么?為撒要渲染上去呢文捶?
* 分析:知悉分析就發(fā)現(xiàn)荷逞,下面這個(gè)if語(yǔ)句寫(xiě)法保證字幕幀在其顯示時(shí)間內(nèi)只被渲染一次。這樣有利于效率提升
*/
if (sp->serial != is->subtitleq.serial
|| (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
|| (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
{
if (sp->uploaded) {
int i;
for (i = 0; i < sp->sub.num_rects; i++) {
AVSubtitleRect *sub_rect = sp->sub.rects[i];
uint8_t *pixels;
int pitch, j;
if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)&pixels, &pitch)) {
for (j = 0; j < sub_rect->h; j++, pixels += pitch)
memset(pixels, 0, sub_rect->w << 2);
SDL_UnlockTexture(is->sub_texture);
}
}
}
frame_queue_next(&is->subpq);
} else {
break;
}
}
}
.....省略代碼.....
}