ffplay中有一個線程專門處理數據讀取,即read_thread

read_thread主要按以下步驟執行:

  1. 準備階段:打開文件,檢測Stream信息,打開解碼器
  2. 主循環讀數據,解封裝:讀取Packet,存入PacketQueue

read_thread的函數比較長,這裡不貼完整代碼,直接根據其功能分步分析。

準備階段

準備階段,主要包括以下步驟:

  1. avformat_open_input
  2. avformat_find_stream_info
  3. av_find_best_stream
  4. stream_component_open

avformat_open_input用於打開輸入文件(對於網路流也是一樣,在ffmpeg內部都抽象為URLProtocol,這裡描述為文件是為了方便與後續提到的AVStream的流作區分),讀取視頻文件的基本信息。

avformat_open_input聲明如下:

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

具體說明參考源碼注釋。需要提到的兩個參數是fmt和options。通過fmt可以強制指定視頻文件的封裝,options可以傳遞額外參數給封裝(AVInputFormat).

看下步驟1的主要代碼:

//創建一個以默認值初始化的AVFormatContext
ic = avformat_alloc_context();
if (!ic) {
av_log(NULL, AV_LOG_FATAL, "Could not allocate context.
");
ret = AVERROR(ENOMEM);
goto fail;
}

//設置interrupt_callback
ic->interrupt_callback.callback = decode_interrupt_cb;
ic->interrupt_callback.opaque = is;

//特定選項處理
if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {
av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);
scan_all_pmts_set = 1;
}

//執行avformat_open_input
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
if (err < 0) {
print_error(is->filename, err);
ret = -1;
goto fail;
}

根據注釋不難看懂代碼。avformat_alloc_context主要malloc了一個AVFormatContext,並填充了默認值;interrupt_callback用於ffmpeg內部在執行耗時操作時檢查是否有退出請求,並提前中斷,避免用戶退出請求沒有及時響應;scan_all_pmts是mpegts的一個選項,這裡在沒有設定該選項的時候,強制設為1。最後執行avformat_open_input。

scan_all_pmts的特殊處理為基於ffplay開發的軟體提供了一個「很好的錯誤示範」,導致經常看到針對特定編碼或封裝的特殊選項、特殊處理充滿了read_thread,影響代碼可讀性!

在打開了文件後,就可以從AVFormatContext中讀取流信息了。一般調用avformat_find_stream_info獲取完整的流信息。為什麼在調用了avformat_open_input後,仍然需要調用avformat_find_stream_info才能獲取正確的流信息呢?看下注釋:

Read packets of a media file to get stream information. This
is useful for file formats with no headers such as MPEG. This
function also computes the real framerate in case of MPEG-2 repeat
rame mode.
The logical file position is not changed by this function;
examined packets may be buffered for later processing.

該函數是通過讀取媒體文件的部分數據來分析流信息。在一些缺少頭信息的封裝下特別有用,比如說MPEG(我猜測這裡應該說ts更準確)。而被讀取用以分析流信息的數據可能被緩存,總之文件偏移位置相比調用前是沒有變化的,對使用者透明。

接下來就可以選取用於播放的視頻流、音頻流和字幕流了。實際操作中,選擇的策略很多,一般根據具體需求來定——比如可以是選擇最高清的視頻流;選擇本地語言的音頻流;直接選擇第一條視頻、音頻軌道;等等。

ffplay主要是通過av_find_best_stream來選擇:

//根據用戶指定來查找流
for (i = 0; i < ic->nb_streams; i++) {
AVStream *st = ic->streams[i];
enum AVMediaType type = st->codecpar->codec_type;
st->discard = AVDISCARD_ALL;
if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
st_index[type] = i;
}
for (i = 0; i < AVMEDIA_TYPE_NB; i++) {
if (wanted_stream_spec[i] && st_index[i] == -1) {
av_log(NULL, AV_LOG_ERROR, "Stream specifier %s does not match any %s stream
", wanted_stream_spec[i], av_get_media_type_string(i));
st_index[i] = INT_MAX;
}
}

//利用av_find_best_stream選擇流
if (!video_disable)
st_index[AVMEDIA_TYPE_VIDEO] =
av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
if (!audio_disable)//參考視頻流選擇
st_index[AVMEDIA_TYPE_AUDIO] =
av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
st_index[AVMEDIA_TYPE_AUDIO],
st_index[AVMEDIA_TYPE_VIDEO],
NULL, 0);
if (!video_disable && !subtitle_disable)//除指定字幕流外,優先參考音頻流選擇
st_index[AVMEDIA_TYPE_SUBTITLE] =
av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,
st_index[AVMEDIA_TYPE_SUBTITLE],
(st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?
st_index[AVMEDIA_TYPE_AUDIO] :
st_index[AVMEDIA_TYPE_VIDEO]),
NULL, 0);

wanted_stream_spec通過main函數傳參設定,格式可以有很多種,參考官方文檔:ffmpeg.org/ffmpeg.html#

如果用戶沒有指定流,或指定部分流,或指定流不存在,則主要由av_find_best_stream發揮作用。

int av_find_best_stream(AVFormatContext *ic,
enum AVMediaType type,//要選擇的流類型
int wanted_stream_nb,//目標流索引
int related_stream,//參考流索引
AVCodec **decoder_ret,
int flags);

fffplay主要通過上述注釋中的3個參數找到「最佳流」。

如果指定了正確的wanted_stream_nb,一般情況都是直接返回該指定流,即用戶選擇的流。如果指定了參考流,且未指定目標流的情況,會在參考流的同一個節目中查找所需類型的流,但一般結果,都是返回該類型第一個流。

經過以上步驟,文件打開成功,且獲取了流的基本信息,並選擇音頻流、視頻流、字幕流。接下來就可以所選流對應的解碼器了。

if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
}

ret = -1;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
}
if (is->show_mode == SHOW_MODE_NONE)
is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;//選擇怎麼顯示,如果視頻打開成功,就顯示視頻畫面,否則,顯示音頻對應的頻譜圖

if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
}

看下stream_component_open.函數也比較長,逐步分析:

//static int stream_component_open(VideoState *is, int stream_index)

avctx = avcodec_alloc_context3(NULL);
if (!avctx)
return AVERROR(ENOMEM);

ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
if (ret < 0)
goto fail;
av_codec_set_pkt_timebase(avctx, ic->streams[stream_index]->time_base);

先是通過avcodec_alloc_context3分配了解碼器上下文AVCodecContex,然後通過avcodec_parameters_to_context把所選流的解碼參數賦給avctx,最後設了time_base.

codec = avcodec_find_decoder(avctx->codec_id);

switch(avctx->codec_type){
case AVMEDIA_TYPE_AUDIO : is->last_audio_stream = stream_index; forced_codec_name = audio_codec_name; break;
case AVMEDIA_TYPE_SUBTITLE: is->last_subtitle_stream = stream_index; forced_codec_name = subtitle_codec_name; break;
case AVMEDIA_TYPE_VIDEO : is->last_video_stream = stream_index; forced_codec_name = video_codec_name; break;
}
if (forced_codec_name)
codec = avcodec_find_decoder_by_name(forced_codec_name);
if (!codec) {
if (forced_codec_name) av_log(NULL, AV_LOG_WARNING,
"No codec could be found with name %s
", forced_codec_name);
else av_log(NULL, AV_LOG_WARNING,
"No codec could be found with id %d
", avctx->codec_id);
ret = AVERROR(EINVAL);
goto fail;
}

這段主要是通過avcodec_find_decoder找到所需解碼器(AVCodec)。如果用戶有指定解碼器,則設置forced_codec_name,並通過avcodec_find_decoder_by_name查找解碼器。找到解碼器後,就可以通過avcodec_open2打開解碼器了。(avcodec_open2附近還有一些選項設置,基本是舊API的兼容,略過不看)

最後,是一個大的switch-case:

switch (avctx->codec_type) {
case AVMEDIA_TYPE_AUDIO:
……
case AVMEDIA_TYPE_VIDEO:
……
case AVMEDIA_TYPE_SUBTITLE:
……
default:
break;
}

即根據具體的流類型,作特定的初始化。但不論哪種流,基本步驟都包括類似以下代碼(節選自AVMEDIA_TYPE_VIDEO分支):

decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
if ((ret = decoder_start(&is->viddec, video_thread, is)) < 0)
goto out;

這兩個函數定義如下:

static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) {
memset(d, 0, sizeof(Decoder));
d->avctx = avctx;
d->queue = queue;
d->empty_queue_cond = empty_queue_cond;
d->start_pts = AV_NOPTS_VALUE;
}

static int decoder_start(Decoder *d, int (*fn)(void *), void *arg)
{
packet_queue_start(d->queue);
d->decoder_tid = SDL_CreateThread(fn, "decoder", arg);
if (!d->decoder_tid) {
av_log(NULL, AV_LOG_ERROR, "SDL_CreateThread(): %s
", SDL_GetError());
return AVERROR(ENOMEM);
}
return 0;
}

decoder_init比較簡單,看decoder_start。decoder_start中「啟動」了PacketQueue,並創建了一個名為"decoder"的線程專門用於解碼,具體的解碼流程由傳入參數fn決定。比如對於視頻,是video_thread

除了decoder_init和decoder_start,對於AVMEDIA_TYPE_AUDIO,還有一個重要工作是audio_open,我們將在分析音頻輸出時再看。

至此,該準備的都準備好了,接下來就可以真正讀取並解碼媒體文件了。

以上流程雖然在read_thread中,但基本處於「準備階段」,還未進入讀線程的主循環。所以在IjkPlayer項目中,將上述流程封裝到了prepare階段,對應Android MediaPlayer的準備階段。

主循環讀數據

主循環的代碼一如既往的長,先看簡化後的偽代碼:

for (;;) {
if (is->abort_request)
break;//處理退出請求
if (is->paused != is->last_paused) {
//處理暫停/恢復
}

if (is->seek_req) {
//處理seek請求
ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
}

//控制緩衝區大小
if (infinite_buffer<1 &&
(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
|| (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
/* wait 10 ms */
continue;
}

//播放完成,循環播放
if (!is->paused &&
(!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&
(!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 0))) {
if (loop != 1 && (!loop || --loop)) {
stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);
} else if (autoexit) {
ret = AVERROR_EOF;
goto fail;
}
}

//讀取一個Packet(解封裝後的數據)
ret = av_read_frame(ic, pkt);

//放入PacketQueue
packet_queue_put();
}

主要的代碼就av_read_framepacket_queue_putav_read_frame從文件中讀取視頻數據,並獲取一個AVPacket,packet_queue_put把它放入到對應的PacketQueue中。

當然,讀取過程還會有seek、pause、resume、abort等可能,所以有專門的分支處理這些請求。

PacketQueue默認情況下會有大小限制,達到這個大小後,就需要等待10ms,以讓消費者——解碼線程能有時間消耗。

播放完成後,會根據loop的設置決定是否循環。

接下來,我們從簡化後的流程出發,詳細看下具體代碼。

暫停/恢復的處理:

if (is->paused != is->last_paused) {//如果paused變數改變,說明暫停狀態改變
is->last_paused = is->paused;
if (is->paused)//如果暫停調用av_read_pause
is->read_pause_return = av_read_pause(ic);
else//如果恢復播放調用av_read_play
av_read_play(ic);
}

ffmpeg有專門針對暫停和恢復的函數,所以直接調用就可以了。

av_read_pause和av_read_play對於URLProtocol,會調用其url_read_pause,通過參數區分是要暫停還是恢復。對於AVInputFormat會調用其read_pause和read_play.

一般情況下URLProtocol和AVInputFormat都不需要專門處理暫停和恢復,但對於像rtsp/rtmp這種在通訊協議上支持(需要)暫停、恢復的就特別有用了。

對於seek的處理,會比暫停/恢復略微複雜一些:

if (is->seek_req) {
……

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
", is->ic->filename);
} 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操作通過avformat_seek_file完成。根據avformat_seek_file的返回值,如果seek成功,需要:

  1. 清除PacketQueue的緩存,並放入一個flush_pkt。放入的flush_pkt可以讓PacketQueue的serial增1,以區分seek前後的數據(PacketQueue函數的分析,可以參考:zhuanlan.zhihu.com/p/43
  2. 同步外部時鐘。在後續音視頻同步的文章中再具體分析。

最後清理一些變數,並:

  1. 設置queue_attachments_req以顯示attachment畫面
  2. 如果當前是暫停狀態,就跳到seek後的下一幀,以直觀體現seek成功了

關於attachment後面有研究了再分析。這裡看下step_to_next_frame。

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;
}

原代碼的注釋比較清晰了——先取消暫停,然後執行step。當設置step為1後,顯示線程會顯示出一幀畫面,然後再次進入暫停:

//in video_refresh
if (is->step && !is->paused)
stream_toggle_pause(is);

這樣seek的處理就完成了。

前面seek、暫停、恢復都可以通過調用ffmpeg的函數,輔助一些流程式控制制完成封裝。

而讀取緩衝區的控制可以說是ffplay原生的特性了。

是否需要控制緩衝區大小由變數infinite_buffer決定。infinite_buffer為1表示當前buffer無限大,不需要使用緩衝區限制策略。

infinite_buffer是可選選項,但在文件是實時協議時,且用戶未指定時,這個值會被強製為1:

static int is_realtime(AVFormatContext *s)
{
if( !strcmp(s->iformat->name, "rtp")
|| !strcmp(s->iformat->name, "rtsp")
|| !strcmp(s->iformat->name, "sdp")
)
return 1;

if(s->pb && ( !strncmp(s->url, "rtp:", 4)
|| !strncmp(s->url, "udp:", 4)
)
)
return 1;
return 0;
}

……
is->realtime = is_realtime(ic);
……
if (infinite_buffer < 0 && is->realtime)
infinite_buffer = 1;

我們看下需控制緩衝區大小的情況:

if (infinite_buffer<1 &&
(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
|| (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
/* wait 10 ms */
SDL_LockMutex(wait_mutex);
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
}

緩衝區滿有兩種可能:

  1. audioq,videoq,subtitleq三個PacketQueue的總位元組數達到了MAX_QUEUE_SIZE(15M)
  2. 音頻、視頻、字幕流都已有夠用的包(stream_has_enough_packets)

第一種好理解,看下第二種中的stream_has_enough_packets:

static int stream_has_enough_packets(AVStream *st, int stream_id, PacketQueue *queue) {
return stream_id < 0 ||
queue->abort_request ||
(st->disposition & AV_DISPOSITION_ATTACHED_PIC) ||
queue->nb_packets > MIN_FRAMES && (!queue->duration || av_q2d(st->time_base) * queue->duration > 1.0);
}

在滿足PacketQueue總時長為0,或總時長超過1s的前提下:

有這麼幾種情況包是夠用的:

  1. 流沒有打開(stream_id < 0)
  2. 有退出請求(queue->abort_request)
  3. 配置了AV_DISPOSITION_ATTACHED_PIC?(這個還不理解,後續分析attachement時回頭看看)
  4. 隊列內包個數大於MIN_FRAMES(=25)

挺饒地,沒有深刻體會其設計用意,不評論。

上述的幾種處理都還是在正常播放流程內,接下來是對播放已完成情況的處理。

if (!is->paused &&
(!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&
(!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 0))) {
if (loop != 1 && (!loop || --loop)) {
stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);
} else if (autoexit) {
ret = AVERROR_EOF;
goto fail;
}
}

這裡判斷播放已完成的條件依然很「ffplay」,需要滿足:

  1. 不在暫停狀態
  2. 音頻未打開,或者打開了,但是解碼已解碼完畢,serial等於PacketQueue的serial,並且PacketQueue中沒有節點了
  3. 視頻未打開,或者打開了,但是解碼已解碼完畢,serial等於PacketQueue的serial,並且PacketQueue中沒有節點了

在確認已結束的情況下,用戶有兩個變數可以控制播放器行為:

  1. loop: 控制播放次數(當前這次也算在內,也就是最小就是1次了),0表示無限次
  2. autoexit:自動退出,也就是播放完成後自動退出。

loop條件簡化的非常不友好,其意思是:如果loop==1,那麼已經播了1次了,無需再seek重新播放;如果loop不是1,==0,隨意,無限次循環;減1後還大於0(--loop),也允許循環。也就是:

static int allow_loop() {
if (loop == 1)
return 0;

if (loop == 0)
return 1;

--loop;
if (loop > 0)
return 1;

return 0;
}

前面講了很多讀線程主循環內的處理,比如暫停、seek、結束loop處理等,接下來就看看真正讀的代碼:

ret = av_read_frame(ic, pkt);
if (ret < 0) {
//文件讀取完了,調用packet_queue_put_nullpacket通知解碼線程
if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
if (is->video_stream >= 0)
packet_queue_put_nullpacket(&is->videoq, is->video_stream);
if (is->audio_stream >= 0)
packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
if (is->subtitle_stream >= 0)
packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
is->eof = 1;
}
//發生錯誤了,退出主循環
if (ic->pb && ic->pb->error)
break;

//如果都不是,可能只是要等一等
SDL_LockMutex(wait_mutex);
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
} else {
is->eof = 0;
}

/* check if packet is in play range specified by user, then queue, otherwise discard */
stream_start_time = ic->streams[pkt->stream_index]->start_time;
pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
pkt_in_play_range = duration == AV_NOPTS_VALUE ||
(pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
av_q2d(ic->streams[pkt->stream_index]->time_base) -
(double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
<= ((double)duration / 1000000);

//如果在時間範圍內,那麼根據stream_index,放入到視頻、音頻、會字幕的PacketQueue中
if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
packet_queue_put(&is->audioq, pkt);
} else if (pkt->stream_index == is->video_stream && pkt_in_play_range
&& !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
packet_queue_put(&is->videoq, pkt);
} else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
packet_queue_put(&is->subtitleq, pkt);
} else {
av_packet_unref(pkt);
}

看起來很長,實際比上述各種特殊流程的處理都直白,主要為:

  1. av_read_frame讀取一個包(AVPacket)
  2. 返回值處理
  3. pkt_in_play_range計算
  4. packet_queue_put放入各自隊列,或者丟棄

步驟1、步驟2、步驟4,都比較直接,看注釋即可。

這裡看下pkt_in_play_range的計算,我們把以上代碼分解下:

int64_t get_stream_start_time(AVFormatContext* ic, int index) {
int64_t stream_start_time = ic->streams[index]->start_time;
return stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0;
}

int64_t get_pkt_ts(AVPacket* pkt) {//ts: timestamp(時間戳)的縮寫
return pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
}

double ts_as_second(int64_t tsAVFormatContext* icint index) {
return ts * av_q2d(ic->streams[index]->time_base);
}

double get_ic_start_time(AVFormatContext* ic) {//ic中的時間單位是us
return (start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000;
}

有了這些函數,就可以計算pkt_in_play_range了:

int is_pkt_in_play_range(AVFormatContext* ic, AVPacket* pkt) {
if (duration == AV_NOPTS_VALUE) //如果當前流無法計算總時長,按無限時長處理
return 1;

//計算pkt相對stream位置
int64_t stream_ts = get_pkt_ts(pkt) - get_stream_start_time(ic, pkt->stream_index);
double stream_ts_s = ts_as_second(stream_ts, ic, pkt->stream_index);

//計算pkt相對ic位置
double ic_ts = stream_ts_s - get_ic_start_time(ic);

//是否在時間範圍內
return ic_ts <= ((double)duration / 1000000);
}

相信上述代碼對於pkt_in_play_range的計算表達的很清楚了,那作者為何不按這種小函數的方式組織代碼呢,估計是閑麻煩、啰嗦吧(在我看來,不贊同ffplay的代碼風格。在一個幾千行的文件里,還是可讀性比較重要)。

至此,讀線程的大部分代碼都分析完成了。文章開頭和每小節開頭都是很好的總結了,這裡不再重複。就吐槽下ffplay的代碼缺點吧:

  1. 很多長函數,對於本來就對播放器一知半解的人要讀懂是不小的挑戰
  2. 過度簡化的條件判斷,導致即使理解這段代碼,過一段時間還得再分析一次

其實很多不必要的簡化,和對函數拆分對效率影響的擔憂,編譯器都可以優化,甚至比手戳的代碼更搞笑。

推薦閱讀:

相关文章