本文為作者原創,轉載請注明出處:https://www.cnblogs.com/leisure_chn/p/10307089.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源碼分析”系列文章如下:
[1]. ffplay源碼分析1-概述
[2]. ffplay源碼分析2-數據結構
[3]. ffplay源碼分析3-代碼框架
[4]. ffplay源碼分析4-音視頻同步
[5]. ffplay源碼分析5-圖像格式轉換
[6]. ffplay源碼分析6-音頻重采樣
[7]. ffplay源碼分析7-播放控制
4. 音視頻同步
音視頻同步的目的是為了使播放的聲音和顯示的畫面保持一致。視頻按幀播放,圖像顯示設備每次顯示一幀畫面,視頻播放速度由幀率確定,幀率指示每秒顯示多少幀;音頻按采樣點播放,聲音播放設備每次播放一個采樣點,聲音播放速度由采樣率確定,采樣率指示每秒播放多少個采樣點。如果僅僅是視頻按幀率播放,音頻按采樣率播放,二者沒有同步機制,即使最初音視頻是基本同步的,隨着時間的流逝,音視頻會逐漸失去同步,並且不同步的現象會越來越嚴重。這是因為:一、播放時間難以精確控制,二、異常及誤差會隨時間累積。所以,必須要采用一定的同步策略,不斷對音視頻的時間差作校正,使圖像顯示與聲音播放總體保持一致。
我們以一個44.1KHz的AAC音頻流和25FPS的H264視頻流為例,來看一下理想情況下音視頻的同步過程:
一個AAC音頻frame每個聲道包含1024個采樣點(也可能是2048,參“FFmpeg關於nb_smples,frame_size以及profile的解釋”),則一個frame的播放時長(duration)為:(1024/44100)×1000ms = 23.22ms;一個H264視頻frame播放時長(duration)為:1000ms/25 = 40ms。聲卡雖然是以音頻采樣點為播放單位,但通常我們每次往聲卡緩沖區送一個音頻frame,每送一個音頻frame更新一下音頻的播放時刻,即每隔一個音頻frame時長更新一下音頻時鍾,實際上ffplay就是這么做的。我們暫且把一個音頻時鍾更新點記作其播放點,理想情況下,音視頻完全同步,音視頻播放過程如下圖所示:

音視頻同步的方式基本是確定一個時鍾(音頻時鍾、視頻時鍾、外部時鍾)作為主時鍾,非主時鍾的音頻或視頻時鍾為從時鍾。在播放過程中,主時鍾作為同步基准,不斷判斷從時鍾與主時鍾的差異,調節從時鍾,使從時鍾追趕(落后時)或等待(超前時)主時鍾。按照主時鍾的不同種類,可以將音視頻同步模式分為如下三種:
音頻同步到視頻,視頻時鍾作為主時鍾。
視頻同步到音頻,音頻時鍾作為主時鍾。
音視頻同步到外部時鍾,外部時鍾作為主時鍾。
ffplay中同步模式的定義如下:
enum {
AV_SYNC_AUDIO_MASTER, /* default choice */
AV_SYNC_VIDEO_MASTER,
AV_SYNC_EXTERNAL_CLOCK, /* synchronize to an external clock */
};
4.1 time_base
time_base是PTS和DTS的時間單位,也稱時間基。不同的封裝格式time_base不一樣,轉碼過程中的不同階段time_base也不一樣。以mpegts封裝格式為例,假設視頻幀率為25FPS。編碼數據包packet(數據結構AVPacket)的time_base為AVRational{1,90000},這個是容器層的time_base,定義在AVStream結構體中。原始數據幀frame(數據結構AVFrame)的time_base為AVRational{1,25},這個是視頻層的time_base,是幀率的倒數,定義在AVCodecContext結構體中。time_base的類型是AVRational,表示一個分數,例如AVRational{1,25}表示值為1/25(單位是秒)。
typedef struct AVStream {
......
/**
* This is the fundamental unit of time (in seconds) in terms
* of which frame timestamps are represented.
*
* decoding: set by libavformat
* encoding: May be set by the caller before avformat_write_header() to
* provide a hint to the muxer about the desired timebase. In
* avformat_write_header(), the muxer will overwrite this field
* with the timebase that will actually be used for the timestamps
* written into the file (which may or may not be related to the
* user-provided one, depending on the format).
*/
AVRational time_base;
......
}
typedef struct AVCodecContext {
......
/**
* This is the fundamental unit of time (in seconds) in terms
* of which frame timestamps are represented. For fixed-fps content,
* timebase should be 1/framerate and timestamp increments should be
* identically 1.
* This often, but not always is the inverse of the frame rate or field rate
* for video. 1/time_base is not the average frame rate if the frame rate is not
* constant.
*
* Like containers, elementary streams also can store timestamps, 1/time_base
* is the unit in which these timestamps are specified.
* As example of such codec time base see ISO/IEC 14496-2:2001(E)
* vop_time_increment_resolution and fixed_vop_rate
* (fixed_vop_rate == 0 implies that it is different from the framerate)
*
* - encoding: MUST be set by user.
* - decoding: the use of this field for decoding is deprecated.
* Use framerate instead.
*/
AVRational time_base;
......
}
/**
* Rational number (pair of numerator and denominator).
*/
typedef struct AVRational{
int num; ///< Numerator
int den; ///< Denominator
} AVRational;
time_base是一個分數,av_q2d(time_base)則可將分數轉換為對應的double類型數。因此有如下計算:
AVStream *st;
double duration_of_stream = st->duration * av_q2d(st->time_base); // 視頻流播放時長
double pts_of_frame = frame->pts * av_q2d(st->time_base); // 視頻幀顯示時間戳
4.2 PTS/DTS/解碼過程
DTS(Decoding Time Stamp, 解碼時間戳),表示壓縮幀的解碼時間。
PTS(Presentation Time Stamp, 顯示時間戳),表示將壓縮幀解碼后得到的原始幀的顯示時間。
音頻中DTS和PTS是相同的。視頻中由於B幀需要雙向預測,B幀依賴於其前和其后的幀,因此含B幀的視頻解碼順序與顯示順序不同,即DTS與PTS不同。當然,不含B幀的視頻,其DTS和PTS是相同的。下圖以一個開放式GOP示意圖為例,說明視頻流的解碼順序和顯示順序

采集順序指圖像傳感器采集原始信號得到圖像幀的順序。
編碼順序指編碼器編碼后圖像幀的順序。存儲到磁盤的本地視頻文件中圖像幀的順序與編碼順序相同。
傳輸順序指編碼后的流在網絡中傳輸過程中圖像幀的順序。
解碼順序指解碼器解碼圖像幀的順序。
顯示順序指圖像幀在顯示器上顯示的順序。
采集順序與顯示順序相同。編碼順序、傳輸順序和解碼順序相同。
以圖中“B[1]”幀為例進行說明,“B[1]”幀解碼時需要參考“I[0]”幀和“P[3]”幀,因此“P[3]”幀必須比“B[1]”幀先解碼。這就導致了解碼順序和顯示順序的不一致,后顯示的幀需要先解碼。
上述內容可參考“視頻編解碼基礎概念”。
理解了含B幀視頻流解碼順序與顯示順序的不同,才容易理解解碼函數decoder_decode_frame()中對視頻解碼的處理:
avcodec_send_packet()按解碼順序發送packet。
avcodec_receive_frame()按顯示順序輸出frame。
這個過程由解碼器處理,不需要用戶程序費心。
decoder_decode_frame()是非常核心的一個函數,代碼本身並不難理解。decoder_decode_frame()是一個通用函數,可以解碼音頻幀、視頻幀和字幕幀,本節着重關注視頻幀解碼過程。音頻幀解碼過程在注釋中。
// 從packet_queue中取一個packet,解碼生成frame
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
AVPacket pkt;
// 本函數被各解碼線程(音頻、視頻、字幕)首次調用時,d->pkt_serial等於-1,d->queue->serial等於1
if (d->queue->serial == d->pkt_serial) {
do {
if (d->queue->abort_request)
return -1;
// 3. 從解碼器接收frame
switch (d->avctx->codec_type) {
case AVMEDIA_TYPE_VIDEO:
// 3.1 一個視頻packet含一個視頻frame
// 解碼器緩存一定數量的packet后,才有解碼后的frame輸出
// frame輸出順序是按pts的順序,如IBBPBBP
// frame->pkt_pos變量是此frame對應的packet在視頻文件中的偏移地址,值同pkt.pos
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
if (decoder_reorder_pts == -1) {
frame->pts = frame->best_effort_timestamp;
} else if (!decoder_reorder_pts) {
frame->pts = frame->pkt_dts;
}
}
break;
case AVMEDIA_TYPE_AUDIO:
// 3.2 一個音頻packet含多個音頻frame,每次avcodec_receive_frame()返回一個frame,此函數返回。
// 下次進來此函數,繼續獲取一個frame,直到avcodec_receive_frame()返回AVERROR(EAGAIN),
// 表示解碼器需要填入新的音頻packet
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
AVRational tb = (AVRational){1, frame->sample_rate};
if (frame->pts != AV_NOPTS_VALUE)
frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
else if (d->next_pts != AV_NOPTS_VALUE)
frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
if (frame->pts != AV_NOPTS_VALUE) {
d->next_pts = frame->pts + frame->nb_samples;
d->next_pts_tb = tb;
}
}
break;
}
if (ret == AVERROR_EOF) {
d->finished = d->pkt_serial;
avcodec_flush_buffers(d->avctx);
return 0;
}
if (ret >= 0)
return 1; // 成功解碼得到一個視頻幀或一個音頻幀,則返回
} while (ret != AVERROR(EAGAIN));
}
do {
if (d->queue->nb_packets == 0) // packet_queue為空則等待
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) { // 有未處理的packet則先處理
av_packet_move_ref(&pkt, &d->pkt);
d->packet_pending = 0;
} else {
// 1. 取出一個packet。使用pkt對應的serial賦值給d->pkt_serial
if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
return -1;
}
} while (d->queue->serial != d->pkt_serial);
// packet_queue中第一個總是flush_pkt。每次seek操作會插入flush_pkt,更新serial,開啟新的播放序列
if (pkt.data == flush_pkt.data) {
// 復位解碼器內部狀態/刷新內部緩沖區。當seek操作或切換流時應調用此函數。
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
} else {
if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
int got_frame = 0;
ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &pkt);
if (ret < 0) {
ret = AVERROR(EAGAIN);
} else {
if (got_frame && !pkt.data) {
d->packet_pending = 1;
av_packet_move_ref(&d->pkt, &pkt);
}
ret = got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVERROR_EOF);
}
} else {
// 2. 將packet發送給解碼器
// 發送packet的順序是按dts遞增的順序,如IPBBPBB
// pkt.pos變量可以標識當前packet在視頻文件中的地址偏移
if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {
av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
d->packet_pending = 1;
av_packet_move_ref(&d->pkt, &pkt);
}
}
av_packet_unref(&pkt);
}
}
}
本函數實現如下功能:
[1]. 從視頻packet隊列中取一個packet
[2]. 將取得的packet發送給解碼器
[3]. 從解碼器接收解碼后的frame,此frame作為函數的輸出參數供上級函數處理
注意如下幾點:
[1]. 含B幀的視頻文件,其視頻幀存儲順序與顯示順序不同
[2]. 解碼器的輸入是packet隊列,視頻幀解碼順序與存儲順序相同,是按dts遞增的順序。dts是解碼時間戳,因此存儲順序解碼順序都是dts遞增的順序。avcodec_send_packet()就是將視頻文件中的packet序列依次發送給解碼器。發送packet的順序如IPBBPBB。
[3]. 解碼器的輸出是frame隊列,frame輸出順序是按pts遞增的順序。pts是解碼時間戳。pts與dts不一致的問題由解碼器進行了處理,用戶程序不必關心。從解碼器接收frame的順序如IBBPBBP。
[4]. 解碼器中會緩存一定數量的幀,一個新的解碼動作啟動后,向解碼器送入好幾個packet解碼器才會輸出第一個packet,這比較容易理解,因為解碼時幀之間有信賴關系,例如IPB三個幀被送入解碼器后,B幀解碼需要依賴I幀和P幀,所在在B幀輸出前,I幀和P幀必須存在於解碼器中而不能刪除。理解了這一點,后面視頻frame隊列中對視頻幀的顯示和刪除機制才容易理解。
[5]. 解碼器中緩存的幀可以通過沖洗(flush)解碼器取出。沖洗(flush)解碼器的方法就是調用avcodec_send_packet(..., NULL),然后多次調用avcodec_receive_frame()將緩存幀取盡。緩存幀取完后,avcodec_receive_frame()返回AVERROR_EOF。ffplay中,是通過向解碼器發送flush_pkt(實際為NULL),每次seek操作都會向解碼器發送flush_pkt。
如何確定解碼器的輸出frame與輸入packet的對應關系呢?可以對比frame->pkt_pos和pkt.pos的值,這兩個值表示packet在視頻文件中的偏移地址,如果這兩個變量值相等,表示此frame來自此packet。調試跟蹤這兩個變量值,即能發現解碼器輸入幀與輸出幀的關系。為簡便,就不貼圖了。
4.3 視頻同步到音頻
視頻同步到音頻是ffplay的默認同步方式。在視頻播放線程中實現。視頻播放函數video_refresh()實現了視頻顯示(包含同步控制),是非常核心的一個函數,理解起來也有些難度。這個函數的調用過程如下:
main() -->
event_loop() -->
refresh_loop_wait_event() -->
video_refresh()
函數實現如下:
/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
VideoState *is = opaque;
double time;
Frame *sp, *sp2;
if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
check_external_clock_speed(is);
// 音頻波形圖顯示
if (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
time = av_gettime_relative() / 1000000.0;
if (is->force_refresh || is->last_vis_time + rdftspeed < time) {
video_display(is);
is->last_vis_time = time;
}
*remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);
}
// 視頻播放
if (is->video_st) {
retry:
if (frame_queue_nb_remaining(&is->pictq) == 0) { // 所有幀已顯示
// nothing to do, no picture to display in the queue
} else { // 有未顯示幀
double last_duration, duration, delay;
Frame *vp, *lastvp;
/* dequeue the picture */
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;
}
// lastvp和vp不是同一播放序列(一個seek會開始一個新播放序列),將frame_timer更新為當前時間
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); // 上一幀播放時長:vp->pts - lastvp->pts
delay = compute_target_delay(last_duration, is); // 根據視頻時鍾和同步時鍾的差值,計算delay值
time= av_gettime_relative()/1000000.0;
// 當前幀播放時刻(is->frame_timer+delay)大於當前時刻(time),表示播放時刻未到
if (time < is->frame_timer + delay) {
// 播放時刻未到,則更新刷新時間remaining_time為當前時刻到下一播放時刻的時間差
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
// 播放時刻未到,則不更新rindex,把上一幀再lastvp再播放一遍
goto display;
}
// 更新frame_timer值
is->frame_timer += delay;
// 校正frame_timer值:若frame_timer落后於當前系統時間太久(超過最大同步域值),則更新為當前系統時間
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
is->frame_timer = time;
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
update_video_pts(is, vp->pts, vp->pos, vp->serial); // 更新視頻時鍾:時間戳、時鍾時間
SDL_UnlockMutex(is->pictq.mutex);
// 是否要丟棄未能及時播放的視頻幀
if (frame_queue_nb_remaining(&is->pictq) > 1) { // 隊列中未顯示幀數>1(只有一幀則不考慮丟幀)
Frame *nextvp = frame_queue_peek_next(&is->pictq); // 下一幀:下一待顯示的幀
duration = vp_duration(is, vp, nextvp); // 當前幀vp播放時長 = nextvp->pts - vp->pts
// 1. 非步進模式;2. 丟幀策略生效;3. 當前幀vp未能及時播放,即下一幀播放時刻(is->frame_timer+duration)小於當前系統時刻(time)
if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
is->frame_drops_late++; // framedrop丟幀處理有兩處:1) packet入隊列前,2) frame未及時顯示(此處)
frame_queue_next(&is->pictq); // 刪除上一幀已顯示幀,即刪除lastvp,讀指針加1(從lastvp更新到vp)
goto retry;
}
}
// 字幕播放
......
// 刪除當前讀指針元素,讀指針+1。若未丟幀,讀指針從lastvp更新到vp;若有丟幀,讀指針從vp更新到nextvp
frame_queue_next(&is->pictq);
is->force_refresh = 1;
if (is->step && !is->paused)
stream_toggle_pause(is);
}
display:
/* display picture */
if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
video_display(is); // 取出當前幀vp(若有丟幀是nextvp)進行播放
}
is->force_refresh = 0;
if (show_status) { // 更新顯示播放狀態
......
}
}
視頻同步到音頻的基本方法是:如果視頻超前音頻,則不進行播放,以等待音頻;如果視頻落后音頻,則丟棄當前幀直接播放下一幀,以追趕音頻。
此函數執行流程參考如下流程圖:

步驟如下:
[1] 根據上一幀lastvp的播放時長duration,校正等到delay值,duration是上一幀理想播放時長,delay是上一幀實際播放時長,根據delay值可以計算得到當前幀的播放時刻
[2] 如果當前幀vp播放時刻未到,則繼續顯示上一幀lastvp,並將延時值remaining_time作為輸出參數供上級調用函數處理
[3] 如果當前幀vp播放時刻已到,則立即顯示當前幀,並更新讀指針
在video_refresh()函數中,調用了compute_target_delay()來根據視頻時鍾與主時鍾的差異來調節delay值,從而調節視頻幀播放的時刻。
// 根據視頻時鍾與同步時鍾(如音頻時鍾)的差值,校正delay值,使視頻時鍾追趕或等待同步時鍾
// 輸入參數delay是上一幀播放時長,即上一幀播放后應延時多長時間后再播放當前幀,通過調節此值來調節當前幀播放快慢
// 返回值delay是將輸入參數delay經校正后得到的值
static double compute_target_delay(double delay, VideoState *is)
{
double sync_threshold, diff = 0;
/* update delay to follow master synchronisation source */
if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
/* if video is slave, we try to correct big delays by
duplicating or deleting a frame */
// 視頻時鍾與同步時鍾(如音頻時鍾)的差異,時鍾值是上一幀pts值(實為:上一幀pts + 上一幀至今流逝的時間差)
diff = get_clock(&is->vidclk) - get_master_clock(is);
// delay是上一幀播放時長:當前幀(待播放的幀)播放時間與上一幀播放時間差理論值
// diff是視頻時鍾與同步時鍾的差值
/* skip or repeat frame. We take into account the
delay to compute the threshold. I still don't know
if it is the best guess */
// 若delay < AV_SYNC_THRESHOLD_MIN,則同步域值為AV_SYNC_THRESHOLD_MIN
// 若delay > AV_SYNC_THRESHOLD_MAX,則同步域值為AV_SYNC_THRESHOLD_MAX
// 若AV_SYNC_THRESHOLD_MIN < delay < AV_SYNC_THRESHOLD_MAX,則同步域值為delay
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {
if (diff <= -sync_threshold) // 視頻時鍾落后於同步時鍾,且超過同步域值
delay = FFMAX(0, delay + diff); // 當前幀播放時刻落后於同步時鍾(delay+diff<0)則delay=0(視頻追趕,立即播放),否則delay=delay+diff
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) // 視頻時鍾超前於同步時鍾,且超過同步域值,但上一幀播放時長超長
delay = delay + diff; // 僅僅校正為delay=delay+diff,主要是AV_SYNC_FRAMEDUP_THRESHOLD參數的作用,不作同步補償
else if (diff >= sync_threshold) // 視頻時鍾超前於同步時鍾,且超過同步域值
delay = 2 * delay; // 視頻播放要放慢腳步,delay擴大至2倍
}
}
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
delay, -diff);
return delay;
}
compute_target_delay()的輸入參數delay是上一幀理想播放時長duration,返回值delay是經校正后的上一幀實際播放時長。為方便描述,下面我們將輸入參數記作duration(對應函數的輸入參數delay),返回值記作delay(對應函數返回值delay)。
本函數實現功能如下:
[1] 計算視頻時鍾與音頻時鍾(主時鍾)的偏差diff,實際就是視頻上一幀pts減去音頻上一幀pts。所謂上一幀,就是已經播放的最后一幀,上一幀的pts可以標識視頻流/音頻流的播放時刻(進度)。
[2] 計算同步域值sync_threshold,同步域值的作用是:若視頻時鍾與音頻時鍾差異值小於同步域值,則認為音視頻是同步的,不校正delay;若差異值大於同步域值,則認為音視頻不同步,需要校正delay值。
同步域值的計算方法如下:
若duration < AV_SYNC_THRESHOLD_MIN,則同步域值為AV_SYNC_THRESHOLD_MIN
若duration > AV_SYNC_THRESHOLD_MAX,則同步域值為AV_SYNC_THRESHOLD_MAX
若AV_SYNC_THRESHOLD_MIN < duration < AV_SYNC_THRESHOLD_MAX,則同步域值為duration
[3] delay校正策略如下:
a) 視頻時鍾落后於同步時鍾且落后值超過同步域值:
a1) 若當前幀播放時刻落后於同步時鍾(delay+diff<0)則delay=0(視頻追趕,立即播放);
a2) 否則delay=duration+diff
b) 視頻時鍾超前於同步時鍾且超過同步域值:
b1) 上一幀播放時長過長(超過最大值),僅校正為delay=duration+diff;
b2) 否則delay=duration×2,視頻播放放慢腳步,等待音頻
c) 視頻時鍾與音頻時鍾的差異在同步域值內,表明音視頻處於同步狀態,不校正delay,則delay=duration
對上述視頻同步到音頻的過程作一個總結,參考下圖:

圖中,小黑圓圈是代表幀的實際播放時刻,小紅圓圈代表幀的理論播放時刻,小綠方塊表示當前系統時間(當前時刻),小紅方塊表示位於不同區間的時間點,則當前時刻處於不同區間時,視頻同步策略為:
[1] 當前時刻在T0位置,則重復播放上一幀,延時remaining_time后再播放當前幀
[2] 當前時刻在T1位置,則立即播放當前幀
[3] 當前時刻在T2位置,則忽略當前幀,立即顯示下一幀,加速視頻追趕
上述內容是為了方便理解進行的簡單而形象的描述。實際過程要計算相關值,根據compute_target_delay()和video_refresh()中的策略來控制播放過程。
4.4 音頻同步到視頻
音頻同步到視頻的方式,在音頻播放線程中,實現代碼在audio_decode_frame()及synchronize_audio()中。
函數調用關系如下:
sdl_audio_callback() -->
audio_decode_frame() -->
synchronize_audio()
以后有時間再補充分析過程。
4.5 音視頻同步到外部時鍾
略
