转自: https://zhuanlan.zhihu.com/p/43672062
ffplay中有一个线程专门处理数据读取,即read_thread
。
read_thread主要按以下步骤执行:
- 准备阶段:打开文件,检测Stream信息,打开解码器
- 主循环读数据,解封装:读取Packet,存入PacketQueue
read_thread的函数比较长,这里不贴完整代码,直接根据其功能分步分析。
准备阶段
准备阶段,主要包括以下步骤:
- avformat_open_input
- avformat_find_stream_info
- av_find_best_stream
- 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.\n");
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\n", 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函数传参设定,格式可以有很多种,参考官方文档:https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1
如果用户没有指定流,或指定部分流,或指定流不存在,则主要由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'\n", forced_codec_name);
else av_log(NULL, AV_LOG_WARNING,
"No codec could be found with id %d\n", 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\n", 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_frame
和packet_queue_put
,av_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\n", 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成功,需要:
- 清除PacketQueue的缓存,并放入一个flush_pkt。放入的flush_pkt可以让PacketQueue的serial增1,以区分seek前后的数据(PacketQueue函数的分析,可以参考:https://zhuanlan.zhihu.com/p/43295650)
- 同步外部时钟。在后续音视频同步的文章中再具体分析。
最后清理一些变量,并:
- 设置queue_attachments_req以显示attachment画面
- 如果当前是暂停状态,就跳到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;
}
缓冲区满有两种可能:
- audioq,videoq,subtitleq三个PacketQueue的总字节数达到了MAX_QUEUE_SIZE(15M)
- 音频、视频、字幕流都已有够用的包(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的前提下:
有这么几种情况包是够用的:
- 流没有打开(stream_id < 0)
- 有退出请求(queue->abort_request)
- 配置了AV_DISPOSITION_ATTACHED_PIC?(这个还不理解,后续分析attachement时回头看看)
- 队列内包个数大于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”,需要满足:
- 不在暂停状态
- 音频未打开,或者打开了,但是解码已解码完毕,serial等于PacketQueue的serial,并且PacketQueue中没有节点了
- 视频未打开,或者打开了,但是解码已解码完毕,serial等于PacketQueue的serial,并且PacketQueue中没有节点了
在确认已结束的情况下,用户有两个变量可以控制播放器行为:
- loop: 控制播放次数(当前这次也算在内,也就是最小就是1次了),0表示无限次
- 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);
}
看起来很长,实际比上述各种特殊流程的处理都直白,主要为:
- av_read_frame读取一个包(AVPacket)
- 返回值处理
- pkt_in_play_range计算
- 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 ts,AVFormatContext* ic,int 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的代码缺点吧:
- 很多长函数,对于本来就对播放器一知半解的人要读懂是不小的挑战
- 过度简化的条件判断,导致即使理解这段代码,过一段时间还得再分析一次
其实很多不必要的简化,和对函数拆分对效率影响的担忧,编译器都可以优化,甚至比手戳的代码更搞笑。