ffplay是FFmpeg工程自帶的簡單播放器,使用FFmpeg提供的解碼器和SDL庫進行視頻播放。本文基於FFmpeg工程4.1版本進行分析,其中ffplay源碼清單以下:
https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.chtml
在嘗試分析源碼前,可先閱讀以下參考文章做爲鋪墊:
[1]. 雷霄驊,視音頻編解碼技術零基礎學習方法
[2]. 視頻編解碼基礎概念
[3]. 色彩空間與像素格式
[4]. 音頻參數解析
[5]. FFmpeg基礎概念git
「ffplay源碼分析」系列文章以下:
[1]. ffplay源碼分析1-概述
[2]. ffplay源碼分析2-數據結構
[3]. ffplay源碼分析3-代碼框架
[4]. ffplay源碼分析4-音視頻同步
[5]. ffplay源碼分析5-圖像格式轉換
[6]. ffplay源碼分析6-音頻重採樣
[7]. ffplay源碼分析7-播放控制github
本節簡單梳理ffplay.c代碼框架。一些關鍵問題及細節問題在後續章節探討。數組
主線程主要實現三項功能:視頻播放(音視頻同步)、字幕播放、SDL消息處理。緩存
主線程在進行一些必要的初始化工做、建立解複用線程後,即進入event_loop()主循環,處理視頻播放和SDL消息事件:數據結構
main() --> static void event_loop(VideoState *cur_stream) { SDL_Event event; ...... for (;;) { // SDL event隊列爲空,則在while循環中播放視頻幀。不然從隊列頭部取一個event,退出當前函數,在上級函數中處理event refresh_loop_wait_event(cur_stream, &event); // SDL事件處理 switch (event.type) { case SDL_KEYDOWN: switch (event.key.keysym.sym) { case SDLK_f: // f鍵:強制刷新 ...... break; case SDLK_p: // p鍵 case SDLK_SPACE: // 空格鍵:暫停 ...... case SDLK_s: // s鍵:逐幀播放 ...... break; ...... ...... } } }
主要代碼在refresh_loop_wait_event()函數中,以下:框架
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)) { 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)) // 當即顯示當前幀,或延時remaining_time後再顯示 video_refresh(is, &remaining_time); SDL_PumpEvents(); } }
while()
語句表示若是SDL event隊列爲空,則在while循環中播放視頻幀;不然從隊列頭部取一個event,退出當前函數,在上級函數中處理event。
refresh_loop_wait_event()
中調用了很是關鍵的函數video_refresh()
,video_refresh()
函數實現音視頻的同步及視頻幀的顯示,是ffplay.c中最核心函數之一,在「4.3節 視頻同步到音頻」中詳細分析。ide
處理各類SDL消息,好比暫停、強制刷新等按鍵事件。比較簡單。函數
main() --> static void event_loop(VideoState *cur_stream) { SDL_Event event; ...... for (;;) { // SDL event隊列爲空,則在while循環中播放視頻幀。不然從隊列頭部取一個event,退出當前函數,在上級函數中處理event refresh_loop_wait_event(cur_stream, &event); // SDL事件處理 switch (event.type) { case SDL_KEYDOWN: switch (event.key.keysym.sym) { case SDLK_f: // f鍵:強制刷新 ...... break; case SDLK_p: // p鍵 case SDLK_SPACE: // 空格鍵:暫停 ...... break; ...... ...... } } }
解複用線程讀取視頻文件,將取到的packet根據類型(音頻、視頻、字幕)存入不一樣是packet隊列中。
爲節省篇幅,以下源碼中非關鍵內容的源碼使用「......」替代。代碼流程參考註釋。oop
/* this thread gets the stream from the disk or the network */ static int read_thread(void *arg) { VideoState *is = arg; AVFormatContext *ic = NULL; int st_index[AVMEDIA_TYPE_NB]; ...... ...... // 中斷回調機制。爲底層I/O層提供一個處理接口,好比停止IO操做。 ic->interrupt_callback.callback = decode_interrupt_cb; ic->interrupt_callback.opaque = is; if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) { av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE); scan_all_pmts_set = 1; } // 1. 構建AVFormatContext // 1.1 打開視頻文件:讀取文件頭,將文件格式信息存儲在"fmt context"中 err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts); ...... if (find_stream_info) { ...... // 1.2 搜索流信息:讀取一段視頻文件數據,嘗試解碼,將取到的流信息填入ic->streams // ic->streams是一個指針數組,數組大小是ic->nb_streams err = avformat_find_stream_info(ic, opts); ...... } ...... // 2. 查找用於解碼處理的流 // 2.1 將對應的stream_index存入st_index[]數組 if (!video_disable) st_index[AVMEDIA_TYPE_VIDEO] = // 視頻流 av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0); if (!audio_disable) st_index[AVMEDIA_TYPE_AUDIO] = // 音頻流 av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, st_index[AVMEDIA_TYPE_AUDIO], st_index[AVMEDIA_TYPE_VIDEO], NULL, 0); if (!video_disable && !subtitle_disable) st_index[AVMEDIA_TYPE_SUBTITLE] = // 字幕流 av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE, st_index[AVMEDIA_TYPE_SUBTITLE], (st_index[AVMEDIA_TYPE_AUDIO] >= 0 ? st_index[AVMEDIA_TYPE_AUDIO] : st_index[AVMEDIA_TYPE_VIDEO]), NULL, 0); is->show_mode = show_mode; // 2.2 從待處理流中獲取相關參數,設置顯示窗口的寬度、高度及寬高比 if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]]; AVCodecParameters *codecpar = st->codecpar; // 根據流和幀寬高比猜想幀的樣本寬高比。 // 因爲幀寬高比由解碼器設置,但流寬高比由解複用器設置,所以這二者可能不相等。此函數會嘗試返回待顯示幀應當使用的寬高比值。 // 基本邏輯是優先使用流寬高比(前提是值是合理的),其次使用幀寬高比。這樣,流寬高比(容器設置,易於修改)能夠覆蓋幀寬高比。 AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL); if (codecpar->width) // 設置顯示窗口的大小和寬高比 set_default_window_size(codecpar->width, codecpar->height, sar); } // 3. 建立對應流的解碼線程 /* open the streams */ if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) { // 3.1 建立音頻解碼線程 stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]); } ret = -1; if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { // 3.2 建立視頻解碼線程 ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]); } if (is->show_mode == SHOW_MODE_NONE) is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT; if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) { // 3.3 建立字幕解碼線程 stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]); } ...... // 4. 解複用處理 for (;;) { // 中止 ...... // 暫停/繼續 ...... // seek操做 ...... ...... // 4.1 從輸入文件中讀取一個packet ret = av_read_frame(ic, pkt); if (ret < 0) { if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) { // 輸入文件已讀完,則往packet隊列中發送NULL packet,以沖洗(flush)解碼器,不然解碼器中緩存的幀取不出來 if (is->video_stream >= 0) packet_queue_put_nullpacket(&is->videoq, is->video_stream); if (is->audio_stream >= 0) packet_queue_put_nullpacket(&is->audioq, is->audio_stream); if (is->subtitle_stream >= 0) packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream); is->eof = 1; } if (ic->pb && ic->pb->error) // 出錯則退出當前線程 break; SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); SDL_UnlockMutex(wait_mutex); continue; } else { is->eof = 0; } // 4.2 判斷當前packet是否在播放範圍內,是則入列,不然丟棄 /* check if packet is in play range specified by user, then queue, otherwise discard */ stream_start_time = ic->streams[pkt->stream_index]->start_time; // 第一個顯示幀的pts pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts; // 簡化一下"||"後那個長長的表達式: // [pkt_pts] - [stream_start_time] - [start_time] <= [duration] // [當前幀pts] - [第一幀pts] - [當前播放序列第一幀(seek起始點)pts] <= [duration] pkt_in_play_range = duration == AV_NOPTS_VALUE || (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) * av_q2d(ic->streams[pkt->stream_index]->time_base) - (double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000 <= ((double)duration / 1000000); // 4.3 根據當前packet類型(音頻、視頻、字幕),將其存入對應的packet隊列 if (pkt->stream_index == is->audio_stream && pkt_in_play_range) { packet_queue_put(&is->audioq, pkt); } else if (pkt->stream_index == is->video_stream && pkt_in_play_range && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) { packet_queue_put(&is->videoq, pkt); } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) { packet_queue_put(&is->subtitleq, pkt); } else { av_packet_unref(pkt); } } ret = 0; fail: ...... return 0; }
解複用線程實現以下功能:
[1]. 建立音頻、視頻、字幕解碼線程
[2]. 從輸入文件讀取packet,根據packet類型(音頻、視頻、字幕)將這放入不一樣packet隊列
視頻解碼線程從視頻packet隊列中取數據,解碼後存入視頻frame隊列。
視頻解碼線程將解碼後的幀放入frame隊列中。爲節省篇幅,以下源碼中刪除了濾鏡filter相關代碼。
// 視頻解碼線程:從視頻packet_queue中取數據,解碼後放入視頻frame_queue static int video_thread(void *arg) { VideoState *is = arg; AVFrame *frame = av_frame_alloc(); double pts; double duration; int ret; AVRational tb = is->video_st->time_base; AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL); if (!frame) { return AVERROR(ENOMEM); } for (;;) { ret = get_video_frame(is, frame); if (ret < 0) goto the_end; if (!ret) continue; // 當前幀播放時長 duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0); // 當前幀顯示時間戳 pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb); // 將當前幀壓入frame_queue ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial); av_frame_unref(frame); if (ret < 0) goto the_end; } the_end: av_frame_free(&frame); return 0; }
從packet隊列中取一個packet解碼獲得一個frame,並判斷是否要根據framedrop機制丟棄失去同步的視頻幀。參考源碼中註釋:
static int get_video_frame(VideoState *is, AVFrame *frame) { int got_picture; if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0) return -1; if (got_picture) { double dpts = NAN; if (frame->pts != AV_NOPTS_VALUE) dpts = av_q2d(is->video_st->time_base) * frame->pts; frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame); // ffplay文檔中對"-framedrop"選項的說明: // Drop video frames if video is out of sync.Enabled by default if the master clock is not set to video. // Use this option to enable frame dropping for all master clock sources, use - noframedrop to disable it. // "-framedrop"選項用於設置當視頻幀失去同步時,是否丟棄視頻幀。"-framedrop"選項以bool方式改變變量framedrop值。 // 音視頻同步方式有三種:A同步到視頻,B同步到音頻,C同步到外部時鐘。 // 1) 當命令行不帶"-framedrop"選項或"-noframedrop"時,framedrop值爲默認值-1,若同步方式是"同步到視頻" // 則不丟棄失去同步的視頻幀,不然將丟棄失去同步的視頻幀。 // 2) 當命令行帶"-framedrop"選項時,framedrop值爲1,不管何種同步方式,均丟棄失去同步的視頻幀。 // 3) 當命令行帶"-noframedrop"選項時,framedrop值爲0,不管何種同步方式,均不丟棄失去同步的視頻幀。 if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) { if (frame->pts != AV_NOPTS_VALUE) { double diff = dpts - get_master_clock(is); if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD && diff - is->frame_last_filter_delay < 0 && is->viddec.pkt_serial == is->vidclk.serial && is->videoq.nb_packets) { is->frame_drops_early++; av_frame_unref(frame); // 視頻幀失去同步則直接扔掉 got_picture = 0; } } } } return got_picture; }
ffplay中framedrop處理有兩種,一處是此處解碼後獲得的frame還沒有存入frame隊列前,以is->frame_drops_early++爲標記;另外一處是frame隊列中讀取frame進行顯示的時候,以is->frame_drops_late++爲標記。
本處framedrop操做涉及的變量is->frame_last_filter_delay屬於濾鏡filter操做相關,ffplay中默認是關閉濾鏡的,本文不考慮濾鏡相關操做。
這個函數是很核心的一個函數,能夠解碼視頻幀和音頻幀。視頻解碼線程中,視頻幀實際的解碼操做就在此函數中進行。分析過程參考3.2節。
音頻解碼線程從音頻packet隊列中取數據,解碼後存入音頻frame隊列
音頻設備的打開實際是在解複用線程中實現的。解複用線程中先打開音頻設備(設定音頻回調函數供SDL音頻播放線程回調),而後再建立音頻解碼線程。調用鏈以下:
main() --> stream_open() --> read_thread() --> stream_component_open() --> audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt); decoder_start(&is->auddec, audio_thread, is);
audio_open()函數填入指望的音頻參數,打開音頻設備後,將實際的音頻參數存入輸出參數is->audio_tgt中,後面音頻播放線程用會用到此參數。
音頻格式的各參數與重採樣強相關,audio_open()的詳細實如今後面第5節講述。
從音頻packet_queue中取數據,解碼後放入音頻frame_queue:
// 音頻解碼線程:從音頻packet_queue中取數據,解碼後放入音頻frame_queue static int audio_thread(void *arg) { VideoState *is = arg; AVFrame *frame = av_frame_alloc(); Frame *af; int got_frame = 0; AVRational tb; int ret = 0; if (!frame) return AVERROR(ENOMEM); do { if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0) goto the_end; if (got_frame) { tb = (AVRational){1, frame->sample_rate}; if (!(af = frame_queue_peek_writable(&is->sampq))) goto the_end; af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb); af->pos = frame->pkt_pos; af->serial = is->auddec.pkt_serial; // 當前幀包含的(單個聲道)採樣數/採樣率就是當前幀的播放時長 af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate}); // 將frame數據拷入af->frame,af->frame指向音頻frame隊列尾部 av_frame_move_ref(af->frame, frame); // 更新音頻frame隊列大小及寫指針 frame_queue_push(&is->sampq); } } while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF); the_end: av_frame_free(&frame); return ret; }
此函數既能夠解碼音頻幀,也能夠解碼視頻幀,函數分析參考3.2節。
音頻播放線程是SDL內建的線程,經過回調的方式調用用戶提供的回調函數。
回調函數在SDL_OpenAudio()時指定。
暫停/繼續回調過程由SDL_PauseAudio()控制。
音頻回調函數以下:
// 音頻處理回調函數。讀隊列獲取音頻包,解碼,播放 // 此函數被SDL按需調用,此函數不在用戶主線程中,所以數據須要保護 // \param[in] opaque 用戶在註冊回調函數時指定的參數 // \param[out] stream 音頻數據緩衝區地址,將解碼後的音頻數據填入此緩衝區 // \param[out] len 音頻數據緩衝區大小,單位字節 // 回調函數返回後,stream指向的音頻緩衝區將變爲無效 // 雙聲道採樣點的順序爲LRLRLR /* 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(); while (len > 0) { // 輸入參數len等於is->audio_hw_buf_size,是audio_open()中申請到的SDL音頻緩衝區大小 if (is->audio_buf_index >= is->audio_buf_size) { // 1. 從音頻frame隊列中取出一個frame,轉換爲音頻設備支持的格式,返回值是重採樣音頻幀的大小 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; } // 引入is->audio_buf_index的做用:防止一幀音頻數據大小超過SDL音頻緩衝區大小,這樣一幀數據須要通過屢次拷貝 // 用is->audio_buf_index標識重採樣幀中已拷入SDL音頻緩衝區的數據位置索引,len1表示本次拷貝的數據量 len1 = is->audio_buf_size - is->audio_buf_index; if (len1 > len) len1 = len; // 2. 將轉換後的音頻數據拷貝到音頻緩衝區stream中,以後的播放就是音頻設備驅動程序的工做了 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是本幀中還沒有拷入SDL音頻緩衝區的數據量 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. */ // 3. 更新時鐘 if (!isnan(is->audio_clock)) { // 更新音頻時鐘,更新時刻:每次往聲卡緩衝區拷入數據後 // 前面audio_decode_frame中更新的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); // 使用音頻時鐘更新外部時鐘 sync_clock_to_slave(&is->extclk, &is->audclk); } }
audio_decode_frame()
主要是進行音頻重採樣,從音頻frame隊列中取出一個frame,此frame的格式是輸入文件中的音頻格式,音頻設備不必定支持這些參數,因此要將frame轉換爲音頻設備支持的格式。
audio_decode_frame()
的實如今後面第5節講述。
實現細節略。之後有機會研究字幕時,再做補充。