作者: 葉餘 來源: https://www.cnblogs.com/leisure_chn/p/10316225.html ffplay是FFmpeg工程自帶的簡單點傳播放器,使用FFmpeg提供的解碼器和SDL庫進行視訊播放。本文基于FFmpeg工程4.1版本進行分析,其中ffplay源碼清單如下: https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.c
在嘗試分析源碼前,可先閱讀如下參考文章作為鋪墊:
[1].
雷霄骅,視音頻編解碼技術零基礎學習方法 [2]. 視訊編解碼基礎概念 [3]. 色彩空間與像素格式 [4]. 音頻參數解析 [5]. FFmpeg基礎概念 “ffplay源碼分析”系列文章如下: ffplay源碼分析1-概述 ffplay源碼分析2-資料結構 ffplay源碼分析3-代碼架構 ffplay源碼分析4-音視訊同步 ffplay源碼分析5-圖像格式轉換 [6]. ffplay源碼分析6-音頻重采樣 [7]. ffplay源碼分析7-播放控制7. 播放控制
7.1. 暫停/繼續
暫停/繼續狀态的切換是由使用者按空格鍵實作的,每按一次空格鍵,暫停/繼續的狀态翻轉一次。
7.1.1 暫停/繼續狀态切換
函數調用關系如下:
main() -->
event_loop() -->
toggle_pause() -->
stream_toggle_pause()
stream_toggle_pause()實作狀态翻轉:
/* 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;
}
7.1.2 暫停狀态下的視訊播放
在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;
......
}
......
}
在暫停狀态下,實際就是不停播放上一幀(最後一幀)圖像。畫面不更新。
7.2 逐幀播放
逐幀播放是使用者每按一次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); // 逐幀播放模式下,播放一幀畫面後暫停
......
}
......
}
7.3 播放速度控制
待補充
7.4. SEEK操作
SEEK操作就是由使用者幹預而改變播放進度的實作方式,比如滑鼠拖動播放進度條。
7.4.1 資料結構及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标志的不同,分為如下幾種情況:
AVSEEK_FLAG_BYTE
:SEEK點對應檔案中的位置(位元組表示)。有些解複用器可能不支援這種情況。
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”得到。
AVSEEK_FLAG_ANY
:SEEK點對應幀序号(待确定),播放點可停留在任意幀(包括非關鍵幀)。有些解複用器可能不支援這種情況。
AVSEEK_FLAG_BACKWARD
:忽略。
其中
AV_TIME_BASE
是FFmpeg内部使用的時間基,定義如下:
/**
* Internal time base represented as integer
*/
#define AV_TIME_BASE 1000000
AV_TIME_BASE表示1000000us。
7.4.2 SEEK的觸發方式
當使用者按下“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);
}
}
7.4.3 SEEK操作的實作
在解複用線程主循環中處理了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;
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
