“ffplay源码分析”系列文章如下:
[1]. ffplay源码分析1-概述
[2]. ffplay源码分析2-数据结构
[3]. ffplay源码分析3-代码框架
[4]. ffplay源码分析4-音视频同步
[5]. ffplay源码分析5-图像格式转换
[6]. ffplay源码分析6-音频重采样
[7]. ffplay源码分析7-播放控制
3. 代码框架
本节简单梳理ffplay.c代码框架。一些关键问题及细节问题在后续章节探讨。
3.1 流程图
3.2 主线程
主线程主要实现三项功能:视频播放(音视频同步)、字幕播放、SDL消息处理。
主线程在进行一些必要的初始化工作、创建解复用线程后,即进入event_loop()主循环,处理视频播放和SDL消息事件:
main() -->
static void event_loop(VideoState *cur_stream)
{
SDL_Event event;
......
for (;;) {
// SDL event队列为空,则在while循环中播放视频帧。否则从队列头部取一个event,退出当前函数,在上级函数中处理event
refresh_loop_wait_event(cur_stream, &event);
// SDL事件处理
switch (event.type) {
case SDL_KEYDOWN:
switch (event.key.keysym.sym) {
case SDLK_f:
// f键:强制刷新
......
break;
case SDLK_p:
// p键
case SDLK_SPACE:
// 空格键:暂停
......
case SDLK_s:
// s键:逐帧播放
......
break;
......
......
}
}
}
3.2.1 视频播放
主要代码在refresh_loop_wait_event()函数中,如下:
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
double remaining_time = 0.0;
SDL_PumpEvents();
while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
SDL_ShowCursor(0);
cursor_hidden = 1;
}
if (remaining_time > 0.0)
av_usleep((int64_t)(remaining_time * 1000000.0));
remaining_time = REFRESH_RATE;
if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
// 立即显示当前帧,或延时remaining_time后再显示
video_refresh(is, &remaining_time);
SDL_PumpEvents();
}
}
while()语句表示如果SDL event队列为空,则在while循环中播放视频帧;否则从队列头部取一个event,退出当前函数,在上级函数中处理event。
refresh_loop_wait_event()中调用了非常关键的函数video_refresh(),video_refresh()函数实现音视频的同步及视频帧的显示,是ffplay.c中最核心函数之一,在“4.3节 视频同步到音频”中详细分析。
3.2.2 SDL消息处理
处理各种SDL消息,比如暂停、强制刷新等按键事件。比较简单。
main() -->
static void event_loop(VideoState *cur_stream)
{
SDL_Event event;
......
for (;;) {
// SDL event队列为空,则在while循环中播放视频帧。否则从队列头部取一个event,退出当前函数,在上级函数中处理event
refresh_loop_wait_event(cur_stream, &event);
// SDL事件处理
switch (event.type) {
case SDL_KEYDOWN:
switch (event.key.keysym.sym) {
case SDLK_f:
// f键:强制刷新
......
break;
case SDLK_p:
// p键
case SDLK_SPACE:
// 空格键:暂停
......
break;
......
......
}
}
}
3.3 解复用线程
解复用线程读取视频文件,将取到的packet根据类型(音频、视频、字幕)存入不同是packet队列中。
为节省篇幅,如下源码中非关键内容的源码使用“......”替代。代码流程参考注释。
/* this thread gets the stream from the disk or the network */
static int read_thread(void *arg)
{
VideoState *is = arg;
AVFormatContext *ic = NULL;
int st_index[AVMEDIA_TYPE_NB];
......
......
// 中断回调机制。为底层I/O层提供一个处理接口,比如中止IO操作。
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;
}
// 1. 构建AVFormatContext
// 1.1 打开视频文件:读取文件头,将文件格式信息存储在"fmt context"中
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
......
if (find_stream_info) {
......
// 1.2 搜索流信息:读取一段视频文件数据,尝试解码,将取到的流信息填入ic->streams
//
ic->streams是一个指针数组,数组大小是ic->nb_streams
err = avformat_find_stream_info(ic, opts);
......
}
......
// 2. 查找用于解码处理的流
// 2.1 将对应的stream_index存入st_index[]数组
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);
is->show_mode = show_mode;
// 2.2 从待处理流中获取相关参数,设置显示窗口的宽度、高度及宽高比
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];
AVCodecParameters *codecpar = st->codecpar;
// 根据流和帧宽高比猜测帧的样本宽高比。
// 由于帧宽高比由解码器设置,但流宽高比由解复用器设置,因此这两者可能不相等。此函数会尝试返回待显示帧应当使用的宽高比值。
// 基本逻辑是优先使用流宽高比(前提是值是合理的),其次使用帧宽高比。这样,流宽高比(容器设置,易于修改)可以覆盖帧宽高比。
AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);
if (codecpar->width)
// 设置显示窗口的大小和宽高比
set_default_window_size(codecpar->width, codecpar->height, sar);
}
// 3. 创建对应流的解码线程
/* open the streams */
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
// 3.1 创建音频解码线程
stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
}
ret = -1;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
// 3.2 创建视频解码线程
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) {
// 3.3 创建字幕解码线程
stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
}
......
// 4. 解复用处理
for (;;) {
// 停止
......
// 暂停/继续
......
// seek操作
......
......
// 4.1 从输入文件中读取一个packet
ret = av_read_frame(ic, pkt);
if (ret < 0) {
if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
// 输入文件已读完,则往packet队列中发送NULL packet,以冲洗(flush)解码器,否则解码器中缓存的帧取不出来
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;
}
// 4.2 判断当前packet是否在播放范围内,是则入列,否则丢弃
/* check if packet is in play range specified by user, then queue, otherwise discard */
stream_start_time = ic->streams[pkt->stream_index]->start_time; // 第一个显示帧的pts
pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
// 简化一下"||"后那个长长的表达式:
// [pkt_pts] - [stream_start_time] - [start_time]
<= [duration]
// [当前帧pts] - [第一帧pts]
- [当前播放序列第一帧(seek起始点)pts] <= [duration]
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);
// 4.3 根据当前packet类型(音频、视频、字幕),将其存入对应的packet队列
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);
}
}
ret = 0;
fail:
......
return 0;
}