本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-nbmsavzf-ha.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-播放控制緩存
音視頻同步的目的是爲了使播放的聲音和顯示的畫面保持一致。視頻按幀播放,圖像顯示設備每次顯示一幀畫面,視頻播放速度由幀率肯定,幀率指示每秒顯示多少幀;音頻按採樣點播放,聲音播放設備每次播放一個採樣點,聲音播放速度由採樣率肯定,採樣率指示每秒播放多少個採樣點。若是僅僅是視頻按幀率播放,音頻按採樣率播放,兩者沒有同步機制,即便最初音視頻是基本同步的,隨着時間的流逝,音視頻會逐漸失去同步,而且不一樣步的現象會愈來愈嚴重。這是由於:1、播放時間難以精確控制,2、異常及偏差會隨時間累積。因此,必需要採用必定的同步策略,不斷對音視頻的時間差做校訂,使圖像顯示與聲音播放整體保持一致。網絡
咱們以一個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 */ };
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(單位是秒)。ide
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); // 視頻幀顯示時間戳
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]」幀先解碼。這就致使瞭解碼順序和顯示順序的不一致,後顯示的幀須要先解碼。oop
上述內容可參考「視頻編解碼基礎概念」。
理解了含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。調試跟蹤這兩個變量值,即能發現解碼器輸入幀與輸出幀的關係。爲簡便,就不貼圖了。
視頻同步到音頻是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()中的策略來控制播放過程。
音頻同步到視頻的方式,在音頻播放線程中,實現代碼在audio_decode_frame()及synchronize_audio()中。
函數調用關係以下:
sdl_audio_callback() --> audio_decode_frame() --> synchronize_audio()
之後有時間再補充分析過程。
略