本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-ykoqdepd-go.htmlhtml
ffplay是FFmpeg工程自帶的簡單播放器,使用FFmpeg提供的解碼器和SDL庫進行視頻播放。本文基於FFmpeg工程4.1版本進行分析,其中ffplay源碼清單以下:
https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.cgit
在嘗試分析源碼前,可先閱讀以下參考文章做爲鋪墊:
[1]. 雷霄驊,視音頻編解碼技術零基礎學習方法
[2]. 視頻編解碼基礎概念
[3]. 色彩空間與像素格式
[4]. 音頻參數解析
[5]. FFmpeg基礎概念github
「ffplay源碼分析」系列文章以下:
[1]. ffplay源碼分析1-概述
[2]. ffplay源碼分析2-數據結構
[3]. ffplay源碼分析3-代碼框架
[4]. ffplay源碼分析4-音視頻同步
[5]. ffplay源碼分析5-圖像格式轉換
[6]. ffplay源碼分析6-音頻重採樣
[7]. ffplay源碼分析7-播放控制緩存
暫停/繼續狀態的切換是由用戶按空格鍵實現的,每按一次空格鍵,暫停/繼續的狀態翻轉一次。數據結構
函數調用關係以下:框架
main() --> event_loop() --> toggle_pause() --> stream_toggle_pause()
stream_toggle_pause()實現狀態翻轉:ide
/* pause or resume the video */ static void stream_toggle_pause(VideoState *is) { if (is->paused) { // 這裏表示當前是暫停狀態,將切換到繼續播放狀態。在繼續播放以前,先將暫停期間流逝的時間加到frame_timer中 is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated; if (is->read_pause_return != AVERROR(ENOSYS)) { is->vidclk.paused = 0; } set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial); } set_clock(&is->extclk, get_clock(&is->extclk), is->extclk.serial); is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused; }
在video_refresh()函數中有以下代碼:函數
/* called to display each frame */ static void video_refresh(void *opaque, double *remaining_time) { ...... // 視頻播放 if (is->video_st) { ...... // 暫停處理:不停播放上一幀圖像 if (is->paused) goto display; ...... } ...... }
在暫停狀態下,實際就是不停播放上一幀(最後一幀)圖像。畫面不更新。oop
逐幀播放是用戶每按一次s鍵,播放器播放一幀畫現。
逐幀播放實現的方法是:每次按了s鍵,就將狀態切換爲播放,播放一幀畫面後,將狀態切換爲暫停。
函數調用關係以下:源碼分析
main() --> event_loop() --> step_to_next_frame() --> stream_toggle_pause()
實現代碼比較簡單,以下:
static void step_to_next_frame(VideoState *is) { /* if the stream is paused unpause it, then step */ if (is->paused) stream_toggle_pause(is); // 確保切換到播放狀態,播放一幀畫面 is->step = 1; }
/* called to display each frame */ static void video_refresh(void *opaque, double *remaining_time) { ...... // 視頻播放 if (is->video_st) { ...... if (is->step && !is->paused) stream_toggle_pause(is); // 逐幀播放模式下,播放一幀畫面後暫停 ...... } ...... }
待補充
SEEK操做就是由用戶干預而改變播放進度的實現方式,好比鼠標拖動播放進度條。
相關數據變量定義以下:
typedef struct VideoState { ...... int seek_req; // 標識一次SEEK請求 int seek_flags; // SEEK標誌,諸如AVSEEK_FLAG_BYTE等 int64_t seek_pos; // SEEK的目標位置(當前位置+增量) int64_t seek_rel; // 本次SEEK的位置增量 ...... } VideoState;
「VideoState.seek_flags」表示SEEK標誌。SEEK標誌的類型定義以下:
#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward #define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes #define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes #define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number
SEEK目標播放點(後文簡稱SEEK點)的肯定,根據SEEK標誌的不一樣,分爲以下幾種狀況:
[1]. AVSEEK_FLAG_BYTE
:SEEK點對應文件中的位置(字節表示)。有些解複用器可能不支持這種狀況。
[2]. AVSEEK_FLAG_FRAME
:SEEK點對應stream中frame序號(?frame序號仍是frame 的PTS?),stream由stream_index指定。有些解複用器可能不支持這種狀況。
[3]. 若是不含上述兩種標誌且stream_index有效:SEEK點對應時間戳,單位是stream中的timebase,stream由stream_index指定。SEEK點的值由「目標frame中的pts(秒) × stream中的timebase」獲得。
[4]. 若是不含上述兩種標誌且stream_index是-1:SEEK點對應時間戳,單位是AV_TIME_BASE。SEEK點的值由「目標frame中的pts(秒) × AV_TIME_BASE」獲得。
[5]. AVSEEK_FLAG_ANY
:SEEK點對應幀序號(待肯定),播放點可停留在任意幀(包括非關鍵幀)。有些解複用器可能不支持這種狀況。
[6]. AVSEEK_FLAG_BACKWARD
:忽略。
其中AV_TIME_BASE
是FFmpeg內部使用的時間基,定義以下:
/** * Internal time base represented as integer */ #define AV_TIME_BASE 1000000
AV_TIME_BASE表示1000000us。
當用戶按下「PAGEUP」,「PAGEDOWN」,「UP」,「DOWN」,「LEFT」,「RHIGHT」按鍵以及用鼠標拖動進度條時,引發播放進度變化,會觸發SEEK操做。
在event_loop()
函數進行的SDL消息處理中有以下代碼片斷:
case SDLK_LEFT: incr = seek_interval ? -seek_interval : -10.0; goto do_seek; case SDLK_RIGHT: incr = seek_interval ? seek_interval : 10.0; goto do_seek; case SDLK_UP: incr = 60.0; goto do_seek; case SDLK_DOWN: incr = -60.0; do_seek: if (seek_by_bytes) { pos = -1; if (pos < 0 && cur_stream->video_stream >= 0) pos = frame_queue_last_pos(&cur_stream->pictq); if (pos < 0 && cur_stream->audio_stream >= 0) pos = frame_queue_last_pos(&cur_stream->sampq); if (pos < 0) pos = avio_tell(cur_stream->ic->pb); if (cur_stream->ic->bit_rate) incr *= cur_stream->ic->bit_rate / 8.0; else incr *= 180000.0; pos += incr; stream_seek(cur_stream, pos, incr, 1); } else { pos = get_master_clock(cur_stream); if (isnan(pos)) pos = (double)cur_stream->seek_pos / AV_TIME_BASE; pos += incr; if (cur_stream->ic->start_time != AV_NOPTS_VALUE && pos < cur_stream->ic->start_time / (double)AV_TIME_BASE) pos = cur_stream->ic->start_time / (double)AV_TIME_BASE; stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0); } break;
seek_by_bytes生效(對應AVSEEK_FLAG_BYTE標誌)時,SEEK點對應文件中的位置,上述代碼中設置了對應1秒數據量的播放增量;不生效時,SEEK點對應於播放時刻。咱們暫不考慮seek_by_bytes生效這種狀況。
此函數實現以下功能:
[1]. 首先肯定SEEK操做的播放進度增量(SEEK增量)和目標播放點(SEEK點),seek_by_bytes不生效時,將增量設爲選定值,如10.0秒(用戶按「RHIGHT」鍵的狀況)。
[2]. 將同步主時鐘加上進度增量,便可獲得SEEK點。先將相關數值記錄下來,供後續SEEK操做時使用。stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);
就是記錄目標播放點和播放進度增量兩個參數的,精確到微秒。調用這個函數的前提是,咱們只考慮8.1節中的第[4]種狀況。
再看一下stream_seak()
函數的實現,僅僅是變量賦值:
/* seek in the stream */ static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes) { if (!is->seek_req) { is->seek_pos = pos; is->seek_rel = rel; is->seek_flags &= ~AVSEEK_FLAG_BYTE; if (seek_by_bytes) is->seek_flags |= AVSEEK_FLAG_BYTE; is->seek_req = 1; SDL_CondSignal(is->continue_read_thread); } }
在解複用線程主循環中處理了SEEK操做。
static int read_thread(void *arg) { ...... for (;;) { if (is->seek_req) { int64_t seek_target = is->seek_pos; int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN; int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX; // FIXME the +-2 is due to rounding being not done in the correct direction in generation // of the seek_pos/seek_rel variables ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "%s: error while seeking\n", is->ic->url); } else { if (is->audio_stream >= 0) { packet_queue_flush(&is->audioq); packet_queue_put(&is->audioq, &flush_pkt); } if (is->subtitle_stream >= 0) { packet_queue_flush(&is->subtitleq); packet_queue_put(&is->subtitleq, &flush_pkt); } if (is->video_stream >= 0) { packet_queue_flush(&is->videoq); packet_queue_put(&is->videoq, &flush_pkt); } if (is->seek_flags & AVSEEK_FLAG_BYTE) { set_clock(&is->extclk, NAN, 0); } else { set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0); } } is->seek_req = 0; is->queue_attachments_req = 1; is->eof = 0; if (is->paused) step_to_next_frame(is); } } ...... }
上述代碼中的SEEK操做執行以下步驟:
[1]. 調用avformat_seek_file()
完成解複用器中的SEEK點切換操做
// 函數原型 int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags); // 調用代碼 ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
這個函數會等待SEEK操做完成才返回。實際的播放點力求最接近參數ts
,並確保在[min_ts, max_ts]區間內,之因此播放點不必定在ts
位置,是由於ts
位置未必能正常播放。
函數與SEEK點相關的三個參數(實參「seek_min」,「seek_target」,「seek_max」)取值方式與SEEK標誌有關(實參「is->seek_flags」),此處「is->seek_flags」值爲0,對應7.4.1節中的第[4]中狀況。
[2]. 沖洗各解碼器緩存幀,使當前播放序列中的幀播放完成,而後再開始新的播放序列(播放序列由各數據結構中的「serial」變量標誌,此處不展開)。代碼以下:
if (is->video_stream >= 0) { packet_queue_flush(&is->videoq); packet_queue_put(&is->videoq, &flush_pkt); }
[3]. 清除本次SEEK請求標誌is->seek_req = 0;