1 ffplay 简介
FFplay是使用FFmpeg库和SDL库的非常精简且可移植的开源媒体播放器。整个播放器源码才三千多行, 且性能稳定功能齐全,被广大开发者学习引用。比如向ijkplayer(哔哩哔哩)就是基于ffplay 做二次开发的。我们可以顺着如下六个函数,去阅读ffplay 源码:
1)read_thread() : 读取读取音频,视频,字幕packet 并将将其放入对应的packet_queue 中去。
2)audio_thread(): 音频解码,取音频packet_queue 内容,解码,并将解码后的数据放入音频packet_queue 中去
3)video_thread(): 视频解码,取视频packet_queue 内容,解码,并将解码后的数据放入视频packet_queue 中去
4)subtitle_thread():字幕解码,取音频packet_queue 内容,解码,并将解码后的数据放入字幕packet_queue 中去
5)event_loop(): 主线程,处理视频,字幕显示逻辑,接收并处理用户传递的命令,另外音视频同步也是在这里做的。
6)sdl_audio_callback(): auido 输出函数, 由sdl 发起callback 被动调用。ffplay 源码整体流程图如下所示:
关于ffplay 源码介绍如下几篇博客介绍的比较仔细,本文也是在参考如下几篇博客基础上写的笔记。
ffplay源码分析(五):ffplay video显示线程分析
ffplay源码分析(六):ffplay audio输出线程分析
ffplay源码分析(七):ffplay subtitle显示线程分析
2 read_thread()
read_thread() 线程分为两个阶段:1)准备阶段,打开文件,检测Stream信息,打开解码器, 二)demux 阶段,主循环读数据,解封装:读取Packet,存入PacketQueue
1)准备阶段
在read_thread() 线程中会去调用avformat_open_input() 去初始化media 的相关参数信息
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;
}
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
...
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
...
if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
打开audio ,video,subtitle 解码器,比create 对应解码线程. 在stream_component_open() 里面会去调用decoder_start() 建立各自线程。
然后调用av_read_frame() 函数去读取packet.
ret = av_read_frame(ic, pkt);//读取packet
...
packet_queue_put(&is->audioq, pkt);//将读取的packet 放入queue
以下关于read_thread() 几个细节地方:
一) 设置帧的最大duration, 主要是避免不连续的pts,否则会出现画面静止的情况 。 这里设置的是10s, 当两帧pts 超过10s 时, 当前帧的pts 会drop 掉,采用前一帧的pts
is->max_frame_duration = (ic->iformat->flags & AVFMT_TS_DISCONT) ? 10.0 : 3600.0;
二) 设置起点去播,起播前, 直接调用avformat_seek_file()
timestamp = start_time;
/* add the stream start time */
if (ic->start_time != AV_NOPTS_VALUE)
timestamp += ic->start_time;
ret = avformat_seek_file(ic, -1, INT64_MIN, timestamp, INT64_MAX, 0);
三) 选择合适的流去播,比如用户指定的汉语,英语音轨等等。如果没有指定,就选第一个。
if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
四) 如果用seek 请求,第一步调用avformat_seek_file() 跳到对应点,然后flush queue, 并put 一个刷新包,最后设置外部时钟
ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
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);
}
三 audio_thread()
audio 解码线程完成的任务是: 音频解码,取音频packet_queue 内容,解码,并将解码后的数据放入音频packet_queue 中去 。
通过decoder_decode_frame() 去解码,得到audio data,并开启滤镜功能,对audio 进行相应的转化。
ret = configure_audio_filters(is, afilters, 1); //根据用户指令,去配置audio filters
...
ret = av_buffersrc_add_frame(is->in_audio_filter, frame); // 将frame 添加到滤镜
...
//得到经过滤镜处理的数据,注意这里有个while, 直到audio filter 里面的数据完全获取才退出
while ((ret = av_buffersink_get_frame_flags(is->out_audio_filter, frame, 0)) >= 0)
...
frame_queue_push(&is->sampq); // 将audio 数据push 到audio frame queue
四 video_thread()
video 解码线程完成的任务是:视频解码,取视频packet_queue 内容,解码,并将解码后的数据放入视频packet_queue 中去。
解码是在get_video_frame()函数中通过调用decoder_decode_frame() 完成的。 当用户使能framedrop,且解码当前的pts diff 很大时就会drop 掉这一帧。
if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
if (frame->pts != AV_NOPTS_VALUE) {
double diff = dpts - get_master_clock(is);
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD &&
diff - is->frame_last_filter_delay < 0 &&
is->viddec.pkt_serial == is->vidclk.serial &&
is->videoq.nb_packets) {
is->frame_drops_early++;
av_frame_unref(frame);
got_picture = 0;
}
}
}
并且在video_thread() 中还有关于video filter 的动作, 当width,height,format ,帧不连续时就会启用filter 功能,调整画面输出。
最后通过queue_picture()将frame 存放到队列中去。
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
五 subtitle_thread()
subtitle_thread() 线程比较简单,直接贴源码。
static int subtitle_thread(void *arg)
{
VideoState *is = arg;
Frame *sp;
int got_subtitle;
double pts;
for (;;) {
if (!(sp = frame_queue_peek_writable(&is->subpq)))
return 0;
if ((got_subtitle = decoder_decode_frame(&is->subdec, NULL, &sp->sub)) < 0)//解码,得到一帧subtitle 数据
break;
pts = 0;
if (got_subtitle && sp->sub.format == 0) {
if (sp->sub.pts != AV_NOPTS_VALUE)
pts = sp->sub.pts / (double)AV_TIME_BASE;
sp->pts = pts;
sp->serial = is->subdec.pkt_serial;
sp->width = is->subdec.avctx->width;
sp->height = is->subdec.avctx->height;
sp->uploaded = 0;//uploaded = 0 ,表面这帧字幕没有被显示
/* now we can update the picture count */
frame_queue_push(&is->subpq);//将字幕数据推到frame queue
} else if (got_subtitle) {
avsubtitle_free(&sp->sub);
}
}
return 0;
}