FFmpeg 代码 version 3.3:
ffplay线程模型-视频为例.png
ffplay.c 中线程模型简单命令。主要是有如下几个线程:
1. 渲染的线程-主线程
简单的理解,来说就是main方法运行所在的线程。
实际上是SDL_CreateWindow
调用所在的线程。以Android为例(笔者比较熟悉),创建的是OpenGL
的Surface
。也就是EGLContext
所在的线程了。
在EventLoop中,将会负责下面两个功能
SDL_UpdateTexture
/SDL_RenderCopy
/SDL_RenderPresent
,更新纹理数据,送显。2. 读取线程-read_thread 在main方法中会启动的读取的线程。
av_read_frame
方法,读取解码前的数据packet。packet
队列(视频/音频/字幕都对应视频流自己的队列)
3. 对应流的解码线程-decode thread
在读取线程中,对AVFormatContext
进行初始化,获取AVStream
信息后,对应不同的码流会开启对应的解码线程Decode Thread。
ffplay
中这里包括了3种流。视频流。音频流和字幕流。avcodec_decode_video2
(旧的API)进行解码。AVFrame
,并确定对应的pts
。
最后然后其再次送入队列当中。整体的流程就是这样简单。
调用av_register_all
和avformat_network_init
。
如果有AVDevice
和AVFilter
也会对其进行初始化。
/* register all codecs, demux and protocols */
#if CONFIG_AVDEVICE
avdevice_register_all();
#endif
#if CONFIG_AVFILTER
avfilter_register_all();
#endif
av_register_all();
avformat_network_init();
parse_options(NULL, argc, argv, options, opt_input_file);
这个方法就是去解析我们传入的参数的。具体的方法写在cmdutils.h
中。
ffplay支持的参数类型,可以在定义的地方看到。
static const OptionDef options[] //这个数组中,有许多选项,不是重点,暂时不做详细的介绍了。
SDL会先初始化SDL_init 进行初始化。
flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
if (audio_disable)
flags &= ~SDL_INIT_AUDIO;
else {
/* Try to work around an occasional ALSA buffer underflow issue when the
* period size is NPOT due to ALSA resampling by forcing the buffer size. */
if (!SDL_getenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE"))
SDL_setenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE","1", 1);
}
if (display_disable)
flags &= ~SDL_INIT_VIDEO;
if (SDL_Init (flags)) {
av_log(NULL, AV_LOG_FATAL, "Could not initialize SDL - %s\n", SDL_GetError());
av_log(NULL, AV_LOG_FATAL, "(Did you set the DISPLAY variable?)\n");
exit(1);
}
后续的创建SDL_Window/SDL_Renderer/SDL_texture 的部分,会在后续初始化。
stream_open
函数,开启read_thread
读取线程 is = stream_open(input_filename, file_iformat);
通过 stream_open函数,会对VideoState
进行初始化,包括解码数据队列(PacketQueue
)和解码后数据队列(FrameQueue
)和时间钟(Clock
)等的初始化,并且开启read_thread
。
FrameQueue
/Decoder
/ AVStream
/PacketQueue
,以及对应码流各自的信息。Render
的一些变量。如SDL_Texture,窗口的位置参数等read_thread
和SDL_cond *continue_read_thread
条件锁// 视频状态结构
typedef struct VideoState {
SDL_Thread *read_tid; // 读取线程
AVInputFormat *iformat; // 输入格式
int abort_request; // 请求取消
int force_refresh; // 强制刷新
int paused; // 停止
int last_paused; // 最后停止
int queue_attachments_req; // 队列附件请求
int seek_req; // 查找请求
int seek_flags; // 查找标志
int64_t seek_pos; // 查找位置
int64_t seek_rel; //
int read_pause_return; // 读停止返回
AVFormatContext *ic; // 解码格式上下文
int realtime; // 是否实时码流
Clock audclk; // 音频时钟
Clock vidclk; // 视频时钟
Clock extclk; // 外部时钟
FrameQueue pictq; // 视频队列
FrameQueue subpq; // 字幕队列
FrameQueue sampq; // 音频队列
Decoder auddec; // 音频解码器
Decoder viddec; // 视频解码器
Decoder subdec; // 字幕解码器
int audio_stream; // 音频码流Id
int av_sync_type; // 同步类型
double audio_clock; // 音频时钟
int audio_clock_serial; // 音频时钟序列
double audio_diff_cum; // 用于音频差分计算 /* used for AV difference average computation */
double audio_diff_avg_coef; //
double audio_diff_threshold; // 音频差分阈值
int audio_diff_avg_count; // 平均差分数量
AVStream *audio_st; // 音频码流
PacketQueue audioq; // 音频包队列
int audio_hw_buf_size; // 硬件缓冲大小
uint8_t *audio_buf; // 音频缓冲区
uint8_t *audio_buf1; // 音频缓冲区1
unsigned int audio_buf_size; // 音频缓冲大小 /* in bytes */
unsigned int audio_buf1_size; // 音频缓冲大小1
int audio_buf_index; // 音频缓冲索引 /* in bytes */
int audio_write_buf_size; // 音频写入缓冲大小
int audio_volume; // 音量
int muted; // 是否静音
struct AudioParams audio_src; // 音频参数
#if CONFIG_AVFILTER
struct AudioParams audio_filter_src; // 音频过滤器
#endif
struct AudioParams audio_tgt; // 音频参数
struct SwrContext *swr_ctx; // 音频转码上下文
int frame_drops_early; //
int frame_drops_late; //
enum ShowMode { // 显示类型
SHOW_MODE_NONE = -1, // 无显示
SHOW_MODE_VIDEO = 0, // 显示视频
SHOW_MODE_WAVES, // 显示波浪,音频
SHOW_MODE_RDFT, // 自适应滤波器
SHOW_MODE_NB //
} show_mode;
int16_t sample_array[SAMPLE_ARRAY_SIZE]; // 采样数组
int sample_array_index; // 采样索引
int last_i_start; // 上一开始
RDFTContext *rdft; // 自适应滤波器上下文
int rdft_bits; // 自使用比特率
FFTSample *rdft_data; // 快速傅里叶采样
int xpos; //
double last_vis_time; //
SDL_Texture *vis_texture; // 音频Texture
SDL_Texture *sub_texture; // 字幕Texture
SDL_Texture *vid_texture; // 视频Texture
int subtitle_stream; // 字幕码流Id
AVStream *subtitle_st; // 字幕码流
PacketQueue subtitleq; // 字幕包队列
double frame_timer; // 帧计时器
double frame_last_returned_time; // 上一次返回时间
double frame_last_filter_delay; // 上一个过滤器延时
int video_stream; // 视频码流Id
AVStream *video_st; // 视频码流
PacketQueue videoq; // 视频包队列
double max_frame_duration; // 最大帧显示时间 // maximum duration of a frame - above this, we consider the jump a timestamp discontinuity
struct SwsContext *img_convert_ctx; // 视频转码上下文
struct SwsContext *sub_convert_ctx; // 字幕转码上下文
int eof; // 结束标志
char *filename; // 文件名
int width, height, xleft, ytop; // 宽高,其实坐标
int step; // 步进
#if CONFIG_AVFILTER
int vfilter_idx; // 过滤器索引
AVFilterContext *in_video_filter; // 第一个视频滤镜 // the first filter in the video chain
AVFilterContext *out_video_filter; // 最后一个视频滤镜 // the last filter in the video chain
AVFilterContext *in_audio_filter; // 第一个音频过滤器 // the first filter in the audio chain
AVFilterContext *out_audio_filter; // 最后一个音频过滤器 // the last filter in the audio chain
AVFilterGraph *agraph; // 音频过滤器 // audio filter graph
#endif
// 上一个视频码流Id、上一个音频码流Id、上一个字幕码流Id
int last_video_stream, last_audio_stream, last_subtitle_stream;
SDL_cond *continue_read_thread; // 连续读线程
} VideoState;
EventLoop
refresh_loop_wait_event(cur_stream, &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;
}
//使用av_usleep来控制视频刷新的帧率
if (remaining_time > 0.0)
av_usleep((int64_t)(remaining_time * 1000000.0));
remaining_time = REFRESH_RATE;
//通知刷新的话。is->force_refresh==1.
if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
//进入绘制
video_refresh(is, &remaining_time);
//继续下一个事件循环
SDL_PumpEvents();
}
}
通过这个方法,不断的来取出SDL的事件。同时通过这样的事件循环,不断video_refresh
送显绘制。
read_thread
如上部分所示,通过stream_open
方法,开启read_thread
read_thread作为读取线程。
AVformatContext
和AVStream
。AVStream
之后,它还负责初始化好对应的AVCodec
和AVCodecContext
,然后开启对应的解码线程。av_read_frame
,将数据读取出,送入队列。等待解码。AVformatContext
和AVStream
。1. 显示先创建 AVformatContext
ic = avformat_alloc_context();
** 2. 设置解码中断回调方法** 这个方法,会在网络中断的时候,发生调用
ic->interrupt_callback.callback = decode_interrupt_cb;
// 设置中断回调参数
ic->interrupt_callback.opaque = is;
decode_interrupt_cb 方法
static int decode_interrupt_cb(void *ctx)
{
VideoState *is = ctx;
return is->abort_request;
}
3. 打开avformat_open_input
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
//同时,将其注入成全局的一个变量。但是具体如何使用呢??
av_format_inject_global_side_data(ic);
4. 查找AVStream
找到不同的AVStream
,并将其放入对应的数组中。等待后续使用。
找到视频流之后,还会再次同步显示的比例和视频的长宽
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;
}
}
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;
//找到视频流之后,还会再次同步显示的比例和视频的长宽
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);
}
打开stream_component_open
对应的AVStream
。打开解码线程。
ffplay
中对应三种码流。(视频、音频和字幕,对应打开自己的解码线程)
/* open the streams */
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]);
}
开启对应的线程之前,会初始化好每个码流的AVCodec
和AVCodecContext
AVCodec
和AVCodecContext
//创建AVCodecContext
avctx = avcodec_alloc_context3(NULL);
if (!avctx)
return AVERROR(ENOMEM);
//从找到对应的流中的codecpar,codecpar其实是avcodec_parameters,
// 然后将它完全复制到创建的AVCodecContext
ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
if (ret < 0)
goto fail;
avctx->pkt_timebase = ic->streams[stream_index]->time_base;
//根据codec_id找出最合适的codec
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;
}
avctx->codec_id = codec->id;
// 下面是设置属性,也不大懂。。这些属性的意思
if (stream_lowres > codec->max_lowres) {
av_log(avctx, AV_LOG_WARNING, "The maximum value for lowres supported by the decoder is %d\n",
codec->max_lowres);
stream_lowres = codec->max_lowres;
}
avctx->lowres = stream_lowres;
if (fast)
avctx->flags2 |= AV_CODEC_FLAG2_FAST;
opts = filter_codec_opts(codec_opts, avctx->codec_id, ic, ic->streams[stream_index], codec);
//av_dict_set方法,传入参数的效果,等同于我们用命令行时 - 的传递方式
if (!av_dict_get(opts, "threads", NULL, 0))
av_dict_set(&opts, "threads", "auto", 0);
if (stream_lowres)
av_dict_set_int(&opts, "lowres", stream_lowres, 0);
if (avctx->codec_type == AVMEDIA_TYPE_VIDEO || avctx->codec_type == AVMEDIA_TYPE_AUDIO)
av_dict_set(&opts, "refcounted_frames", "1", 0);
if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) {
goto fail;
}
if ((t = av_dict_get(opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
ret = AVERROR_OPTION_NOT_FOUND;
goto fail;
}
decoder_init
方法初始化Decoder
结构体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;
d->pkt_serial = -1;
}
和decoder_start
正式开启线程。
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;
}
packet_queue_start 开启线程之前,将flush_pkt
送入PacketQueue
队列当中。
static void packet_queue_start(PacketQueue *q)
{
SDL_LockMutex(q->mutex);
q->abort_request = 0;
packet_queue_put_private(q, &flush_pkt);
SDL_UnlockMutex(q->mutex);
}
av_read_frame
,将数据读取出,送入队列ps:快进的逻辑后面分析
读取av_read_frame
ret = av_read_frame(ic, pkt);
读取失败,各个流中送入空包,同时锁住continue_read_thread
,等待10ms
if (ret < 0) {
// 读取结束或失败。会想各个流,送入一个空的packet.为什么要送入空的packet??
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;
//读取失败的话,读取失败的原因有很多,其他地方可能会重新Signal这个锁condition。如果没有singal这个condition的话,就会等待10ms之后,再释放,重新循环读取. 那这个continue_read_thread 到底是锁了哪呢?
SDL_LockMutex(wait_mutex);
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
} else {
is->eof = 0;
}
todo:continue_read_thread 等同于队列中的empty_queue_cond
???
读取成功,送入队列
变量pkt
中就是我们读取到的数据。
/* check if packet is in play range specified by user, then queue, otherwise discard */
//记录stream_start_time
stream_start_time = ic->streams[pkt->stream_index]->start_time;
//如果没有pts, 就用dts
pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
//判断是否在范围内。如果duration还没被定义的话,通过
//或者在定义的duration内才可以,用当前的pts-start_time .
//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);
// 将解复用得到的数据包添加到对应的待解码队列中
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);
}
最后退出时,需要关闭对应资源 这个线程关闭的时候。会清空AVFormatContext 和锁资源
if (ic && !is->ic)
avformat_close_input(&ic);
if (ret != 0) {
SDL_Event event;
event.type = FF_QUIT_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
}
SDL_DestroyMutex(wait_mutex);
return 0;
整体的流程如上,让我们在特别关注一下,具体的入列的操作
MyAVPacketList结构体
PacketList中存储的单元是自己定义的MyAVPacketList
结构体
typedef struct MyAVPacketList {
AVPacket pkt;
struct MyAVPacketList *next;
int serial;
} MyAVPacketList;
结构体中主要是保存了AVPacket
数据和队列的下一个的指针。
同时还保留了一个serial
变量。它可以理解成操作数。在初始化和快进的时候,会增加操作数。小于的操作数,在下一次显示的时候,会直接被抛弃。
队列操作packet_queue_put
static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
int ret;
//锁住队列中的互斥锁。这个锁是每个队列自己的
SDL_LockMutex(q->mutex);
ret = packet_queue_put_private(q, pkt);
SDL_UnlockMutex(q->mutex);
// 如果不是flush类型的包,并且没有成功入队,则销毁当前的包
if (pkt != &flush_pkt && ret < 0)
av_packet_unref(pkt);
return ret;
}
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
MyAVPacketList *pkt1;
// 如果队列本身处于舍弃状态,则直接返回-1
if (q->abort_request)
return -1;
// 创建一个包
pkt1 = av_malloc(sizeof(MyAVPacketList));
if (!pkt1)
return -1;
pkt1->pkt = *pkt;
pkt1->next = NULL;
// 判断包是否数据flush类型,调整包序列
// 在创建decoder thread时,会刷入一个flush_pkt,这个时候会提高serial
if (pkt == &flush_pkt)
q->serial++;
pkt1->serial = q->serial;
// 调整指针。存入队尾,若没有队尾,就放在开头。
if (!q->last_pkt)
q->first_pkt = pkt1;
else
q->last_pkt->next = pkt1;
q->last_pkt = pkt1;
q->nb_packets++;
q->size += pkt1->pkt.size + sizeof(*pkt1);
q->duration += pkt1->pkt.duration;
/* XXX: should duplicate packet data in DV case */
// 条件信号
//通知读的部分,有数据了,可以继续读取了
//q->cond 在读取的时候,如果没有数据就会锁住,这样,生产了数据,就会通知读取。相等于一个读锁
SDL_CondSignal(q->cond);
return 0;
}
队列操作packet_queue_put_nullpacket
在读取错误时,也会丢入一个空白。
static int packet_queue_put_nullpacket(PacketQueue *q, int stream_index)
{
// 创建一个空数据的包
AVPacket pkt1, *pkt = &pkt1;
av_init_packet(pkt);
pkt->data = NULL;
pkt->size = 0;
pkt->stream_index = stream_index;
return packet_queue_put(q, pkt);
}
弄清楚q->mutex 是什么锁 主线程初始化中,进行初始化的,是每个PacketQueue 都有一个自己的互斥锁和条件锁的。
static int packet_queue_init(PacketQueue *q)
{
// 为一个包队列分配内存
memset(q, 0, sizeof(PacketQueue));
// 创建互斥锁
q->mutex = SDL_CreateMutex();
if (!q->mutex) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
return AVERROR(ENOMEM);
}
// 创建条件锁
q->cond = SDL_CreateCond();
if (!q->cond) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
return AVERROR(ENOMEM);
}
// 默认情况舍弃入队的数据
q->abort_request = 1;
return 0;
}
条件锁q->cond
在读取的时候,如果没有数据就会锁住,这样,生产了数据,就会通知读取。相等于一个读锁
video_thread
在read_thread
的中对应视频流时,初始化好了AVCodec
和AVCodecContext
。通过decoder_start
方法,开启了video_thread
。
在video_thread
中需要创建AVFrame来接受解码后的数据,确定视频的帧率。
然后开启解码循环。
不断的从队列中获取解码前的数据,然后送入解码器解码。
再得到解码后的数据,在送入对应的队列当中。
创建AVFrame和得到大致的视频帧率
//创建AVFrame
AVFrame *frame = av_frame_alloc();
//设置好time_base和frame_rate
AVRational tb = is->video_st->time_base;
// 猜测视频帧率
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
从队列中取得解码器的数据packet_queue_get
取到要解码的数据,放到 &d->pkt_temp上
do {
int ret = -1;
// 如果处于舍弃状态,直接返回
if (d->queue->abort_request)
return -1;
// 如果当前没有包在等待,或者队列的序列不相同时,取出下一帧
if (!d->packet_pending || d->queue->serial != d->pkt_serial) {
AVPacket pkt;
do {
// 队列为空
if (d->queue->nb_packets == 0)
//当队列为空的时候,就会通知释放这个条件锁。之前在读取线程,读取失败的时候,也有锁住这个锁进行等待。
SDL_CondSignal(d->empty_queue_cond);
// 从队列中取数据
if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
return -1;
// 如果是第一个数据,则会把数据清空一遍。然后开始获取
if (pkt.data == flush_pkt.data) {
//重置解码器的状态,因为第一次开始解码或者快进的时候,会先存入一个flush_data,当取到这个的时候,就需要去/重置解码器的状态
//Reset the internal decoder state / flush internal buffers. Should be called
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}
} while (pkt.data == flush_pkt.data || d->queue->serial != d->pkt_serial);
// 释放包
av_packet_unref(&d->pkt);
// 更新包
d->pkt_temp = d->pkt = pkt;
// 包等待
d->packet_pending = 1;
}
得到pkt_temp之后,送入解码器进行解码 解码成功后,还需要得去对应的pts。
// 根据解码器类型判断是音频还是视频还是字幕
switch (d->avctx->codec_type) {
// 视频解码
case AVMEDIA_TYPE_VIDEO:
// 解码视频
ret = avcodec_decode_video2(d->avctx, frame, &got_frame, &d->pkt_temp);
// 解码成功,更新时间戳
if (got_frame) {
if (decoder_reorder_pts == -1) {
frame->pts = av_frame_get_best_effort_timestamp(frame);
} else if (!decoder_reorder_pts) { // 如果不重新排列时间戳,则需要更新帧的pts
frame->pts = frame->pkt_dts;
}
}
break;
// 音频解码
case AVMEDIA_TYPE_AUDIO:
// 音频解码
ret = avcodec_decode_audio4(d->avctx, frame, &got_frame, &d->pkt_temp);
// 音频解码完成,更新时间戳
if (got_frame) {
AVRational tb = (AVRational){1, frame->sample_rate};
// 更新帧的时间戳
if (frame->pts != AV_NOPTS_VALUE)
frame->pts = av_rescale_q(frame->pts, av_codec_get_pkt_timebase(d->avctx), 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;
// 字幕解码
case AVMEDIA_TYPE_SUBTITLE:
ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &d->pkt_temp);
break;
}
// 判断是否解码成功
// 如果解码失败的话,包继续等待
if (ret < 0) {
d->packet_pending = 0;
} else {
d->pkt_temp.dts =
d->pkt_temp.pts = AV_NOPTS_VALUE;
//下面这个操作不明白???
if (d->pkt_temp.data) {
if (d->avctx->codec_type != AVMEDIA_TYPE_AUDIO)
ret = d->pkt_temp.size;
d->pkt_temp.data += ret;
d->pkt_temp.size -= ret;
if (d->pkt_temp.size <= 0)
d->packet_pending = 0;
} else {
if (!got_frame) {
d->packet_pending = 0;
d->finished = d->pkt_serial;
}
}
}
得到pkt_temp之后,还需要判断,是否需要抛弃
static int get_video_frame(VideoState *is, AVFrame *frame)
{
int got_picture;
// 解码视频帧
if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
return -1;
// 判断是否解码成功
if (got_picture) {
double dpts = NAN;
if (frame->pts != AV_NOPTS_VALUE)
//通过 pts*av_q2d(timebase)可以得到准确的时间
dpts = av_q2d(is->video_st->time_base) * frame->pts;
//重新得到视频的比例
frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);
// 判断是否需要舍弃该帧。
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;
}
}
}
}
return got_picture;
}
计算时间和入列
// 计算帧的pts、duration等
//每一帧的时长,应该就是等于帧率的倒数
duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
// 放入到已解码队列
ret = queue_picture(is, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
av_frame_unref(frame);
将已经解码的数据放到队列当中queue_picture
static int queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
Frame *vp;
#if defined(DEBUG_SYNC)
printf("frame_type=%c pts=%0.3f\n",
av_get_picture_type_char(src_frame->pict_type), pts);
#endif
//先从队列中取。因为要queue的大小有限,所以先判断是否可以继续写入
if (!(vp = frame_queue_peek_writable(&is->pictq)))
return -1;
vp->sar = src_frame->sample_aspect_ratio;
vp->uploaded = 0;
vp->width = src_frame->width;
vp->height = src_frame->height;
vp->format = src_frame->format;
vp->pts = pts;
vp->duration = duration;
vp->pos = pos;
vp->serial = serial;
set_default_window_size(vp->width, vp->height, vp->sar);
//将src中的数据送入vp当中,并且重置src
av_frame_move_ref(vp->frame, src_frame);
//重新推入
frame_queue_push(&is->pictq);
return 0;
}
/**
* 查找可写帧
* @param f [description]
* @return [description]
*/
static Frame *frame_queue_peek_writable(FrameQueue *f)
{
/* wait until we have space to put a new frame */
SDL_LockMutex(f->mutex);
// 如果帧队列大于最大
while (f->size >= f->max_size &&
!f->pktq->abort_request) {
SDL_CondWait(f->cond, f->mutex);
}
SDL_UnlockMutex(f->mutex);
if (f->pktq->abort_request)
return NULL;
//writable index 是 windex
return &f->queue[f->windex];
}
static void frame_queue_push(FrameQueue *f)
{
//会将可写的index偏移
if (++f->windex == f->max_size)
f->windex = 0;
SDL_LockMutex(f->mutex);
f->size++;
SDL_CondSignal(f->cond);
SDL_UnlockMutex(f->mutex);
}
已放入队列,最后
av_frame_unref(frame)
原来的创建的frame就可以了释放了。
attached_pic入列 attached_pic的意思是有附带的图片。比如说一些MP3,AAC音频文件附带的专辑封面。所以,就是如果有,就去显示吧。
is->queue_attachments_req = 1;
在主线程的EventLoop开启之后,会进行绘制。先会根据主的时间钟进行同步,然后进行显示。这里我们因为只分析视频部分。所以就不关注时间钟的同步了。
同步后时间,取到具体的frame时,就送入显示。
/* display the current picture, if any */
static void video_display(VideoState *is)
{
//当window还没创建的时候,就是第一次,会去初始化
if (!window)
video_open(is);
//SDL常规操作两则
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO)
video_audio_display(is);
else if (is->video_st)
//video走这里
video_image_display(is);
//送显
SDL_RenderPresent(renderer);
}
video_open
。
先去创建SDL_Window
和SDL_Renderer
。
举Android的例子来说,在Android中SDL使用的是OpenGL。
SDL_CreateWindow
就是通过ANativeWindow 来创建一个GL Surface。同时创建GLContext。
SDL_CreateRenderer
就是glmakeCurrent
, 同时对Renderer的各个方法进行初始化,以供后面调用。SDL_Window
和SDL_Renderer
之后,就会调用
video_image_display
进行显示。static void video_image_display(VideoState *is)
{
Frame *vp;
Frame *sp = NULL;
SDL_Rect rect;
vp = frame_queue_peek_last(&is->pictq);
//省略了关于字幕的部分...
//计算SDL_Rect 就是当前显示的范围
calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height, vp->width, vp->height, vp->sar);
//如果这一帧还没显示过
if (!vp->uploaded) {
int sdl_pix_fmt = vp->frame->format == AV_PIX_FMT_YUV420P ? SDL_PIXELFORMAT_YV12 : SDL_PIXELFORMAT_ARGB8888;
//如果需要重新创建纹理的话
if (realloc_texture(&is->vid_texture, sdl_pix_fmt, vp->frame->width, vp->frame->height, SDL_BLENDMODE_NONE, 0) < 0)
return;
//刷新纹理
if (upload_texture(is->vid_texture, vp->frame, &is->img_convert_ctx) < 0)
return;
vp->uploaded = 1;
vp->flip_v = vp->frame->linesize[0] < 0;
}
SDL_RenderCopyEx(renderer, is->vid_texture, NULL, &rect, 0, NULL, vp->flip_v ? SDL_FLIP_VERTICAL : 0);
if (sp) {
#if USE_ONEPASS_SUBTITLE_RENDER
SDL_RenderCopy(renderer, is->sub_texture, NULL, &rect);
#else
int i;
double xratio = (double)rect.w / (double)sp->width;
double yratio = (double)rect.h / (double)sp->height;
for (i = 0; i < sp->sub.num_rects; i++) {
SDL_Rect *sub_rect = (SDL_Rect*)sp->sub.rects[i];
SDL_Rect target = {.x = rect.x + sub_rect->x * xratio,
.y = rect.y + sub_rect->y * yratio,
.w = sub_rect->w * xratio,
.h = sub_rect->h * yratio};
SDL_RenderCopy(renderer, is->sub_texture, sub_rect, &target);
}
#endif
}
}
通过calculate_display_rectton
这里的就是计算需要显示的区域。
然后如果还没创建纹理的话,在realloc_texture
内调用SDL_CreateTexture
来创建纹理。
在upload_texture
内调用SDL_UpdateTexture
来更新纹理。
最后是进行SDL_RendererCopy
.或者SDL_RendererCopyEx
(SDL_RendererCopy
的加强版,可以通知显示区域和翻转)。
然后送显SDL_CreateTexture
。
这里可以看到依旧是SDL视频送显的经典套路。
SDL视频送显的经典套路.png
这部分暂时掠过...
整体的流程.jpg
image.png