作者: 葉餘 來源: https://www.cnblogs.com/leisure_chn/p/10307089.html fplay是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-播放控制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 音視訊同步到外部時鐘
略
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。