[音视频] FFplay 解码学习笔记
这里先简单介绍一下 FFplay 的解码逻辑:
在 FFplay 中有三种 Packet Queue,分别是对应的是 Video, Audio 和 Subtitle 的 Packet 队列,在 read thread 线程中去读取 Packet 并写入到对应的队列中去,解码线程(video thread, audio thread 和 subtitle thread)去读取 Packet 并完成解码。
在 FFplay 中还有三种 Frame Queue,分别对应的是 Video, Audio 和 Subtitle 的 Frame 队列,Packet Queue 对应的是需要解码的数据,而 Frame Queue 就表示已经解码好的数据,解码好的数据就需要等待渲染线程去读取 Frame 并完成渲染,视频的渲染是在主线程中,通过 video_refresh() 方法完成渲染(前面的介绍音画同步的文章中有说过这个方法[音视频] FFmpeg 时间系统和 FFPlay 音画同步逻辑);音频的渲染在回调方法 sdl_audio_callback() 方法中。
read thread
以下是 read thread 工作的流程图:
他的工作的方法是 read_thread():
static int read_thread(void *arg)
{
// ...
// 为 format context 分配内存
ic = avformat_alloc_context();
// ...
// 打开对应的文件或者网络 IO 流
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
// ...
// 查找需要处理的 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;
}
// ...
/* 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]);
}
// ...
for (;;) {
if (is->abort_request)
// 退出读取 packet
break;
// 开始或者暂停 packet 的读取
if (is->paused != is->last_paused) {
is->last_paused = is->paused;
if (is->paused)
is->read_pause_return = av_read_pause(ic);
else
av_read_play(ic);
}
#if CONFIG_RTSP_DEMUXER || CONFIG_MMSH_PROTOCOL
if (is->paused &&
(!strcmp(ic->iformat->name, "rtsp") ||
(ic->pb && !strncmp(input_filename, "mmsh:", 5)))) {
/* wait 10 ms to avoid trying to get another packet */
/* XXX: horrible */
SDL_Delay(10);
continue;
}
#endif
// 处理 seek 请求
if (is->seek_req) {
int64_t seek_target = is->seek_pos;
int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
// FIXME the +-2 is due to rounding being not done in the correct direction in generation
// of the seek_pos/seek_rel variables
ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
if (ret < 0) {
// seek 失败
av_log(NULL, AV_LOG_ERROR,
"%s: error while seeking\n", is->ic->url);
} else {
// seek 成功,清空上次缓存的 packet,同时 packet 队列的 serial 会加 1.
if (is->audio_stream >= 0)
packet_queue_flush(&is->audioq);
if (is->subtitle_stream >= 0)
packet_queue_flush(&is->subtitleq);
if (is->video_stream >= 0)
packet_queue_flush(&is->videoq);
// 重置内部时钟,serial 修改为 0.
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);
}
if (is->queue_attachments_req) {
// 获取单帧的视频流(常见的是音乐文件)
if (is->video_st && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) {
if ((ret = av_packet_ref(pkt, &is->video_st->attached_pic)) < 0)
goto fail;
// packet 队列只塞了一个可用 packet
packet_queue_put(&is->videoq, pkt);
// 塞入一个空的 packet 表示流结束了,空的 pkt 的 stream id 对应的就是 video stream id.
packet_queue_put_nullpacket(&is->videoq, pkt, is->video_stream);
}
is->queue_attachments_req = 0;
}
/* if the queue are full, no need to read more */
// 判断 packet 队列是否已经满了,满了的话,等 10ms 重试
// 已满的逻辑判断(满足以下其一就表示满了):
// 1. 不是无限 buffer,并且三种 packet 队列占用内存和达到 15MB
// 2. 三种 packet 队列都已经满了,队列满的逻辑是单帧视频流或者 packet 的时长达到 1s
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;
}
// 这里表示已经播放完成了,如果需要循环播放的话,seek 到初始位置,如果需要退出的话就直接退出.
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;
}
}
ret = av_read_frame(ic, pkt);
if (ret < 0) {
// 读取失败.
if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
// 多媒体文件流读取完毕
// 往每个 packet 队列中塞入一个空的 packet
if (is->video_stream >= 0)
packet_queue_put_nullpacket(&is->videoq, pkt, is->video_stream);
if (is->audio_stream >= 0)
packet_queue_put_nullpacket(&is->audioq, pkt, is->audio_stream);
if (is->subtitle_stream >= 0)
packet_queue_put_nullpacket(&is->subtitleq, pkt, is->subtitle_stream);
is->eof = 1;
}
if (ic->pb && ic->pb->error) {
if (autoexit)
goto fail;
else
break;
}
// 等 10ms 再去读 packet
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;
// 当前的 pts 是否在对应的播放 range 中,如果不在 range 中会被丢掉,不进入队列
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);
// 将获取到的 packet 存入对应的 packet 队列,packet 的 serial 和队列的 serial 一致
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;
}
打开文件或者网络流
这部分代码相对比较简单,就不多说了。
初始化对应的解码器和开启解码线程
当获取到需要处理的流类型(FFplay 默认处理视频流,音频流和字幕流,我们只关注视频流和音频流)后,然后为对应的流初始化解码器和创建一个解码线程,处理这个逻辑的是 stream_component_open() 方法:
/* open a given stream. Return 0 if OK */
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;
avctx->pkt_timebase = ic->streams[stream_index]->time_base;
// 查找对应的解码器
codec = avcodec_find_decoder(avctx->codec_id);
// ...
// 设置解码最大线程数
avctx->lowres = stream_lowres;
// ...
switch (avctx->codec_type) {
case AVMEDIA_TYPE_AUDIO:
// ...
/* prepare audio output */
// 请求 audio 渲染
if ((ret = audio_open(is, &ch_layout, sample_rate, &is->audio_tgt)) < 0)
goto fail;
// ...
// 初始化 audiodec, 它的 serial 设置为 -1
if ((ret = decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread)) < 0)
goto fail;
if (is->ic->iformat->flags & AVFMT_NOTIMESTAMPS) {
is->auddec.start_pts = is->audio_st->start_time;
is->auddec.start_pts_tb = is->audio_st->time_base;
}
// 开始音频解码,线程为 audio_thread,packet 的队列的 serial 会执行加一, 现在的队列序号就是 1(默认为 0).
if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
goto out;
SDL_PauseAudioDevice(audio_dev, 0);
break;
case AVMEDIA_TYPE_VIDEO:
is->video_stream = stream_index;
is->video_st = ic->streams[stream_index];
// 初始化 videodec, 它的 serial 设置为 -1
if ((ret = decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread)) < 0)
goto fail;
// 开始视频解码,线程为 video_thread,packet 的队列的 serial 会执行加一, 现在的队列序号就是 1(默认为 0).
if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
goto out;
is->queue_attachments_req = 1;
break;
case AVMEDIA_TYPE_SUBTITLE:
// ...
default:
break;
}
goto out;
fail:
avcodec_free_context(&avctx);
out:
av_channel_layout_uninit(&ch_layout);
av_dict_free(&opts);
return ret;
}
上面的代码比较简单就不多说了,后续单独分析 video_thread 和 audio_thread。
循环读取 Packet 写入到队列中
-
检查是否请求退出
-
检查暂停或者恢复状态
-
检查
seek请求 这里需要注意,完成seek后会清空Packet Queue中的缓存,并将serial加 1,当后续解码过程发现serial改变后,也会做一些缓存清除的工作。 -
检查
attachments状态
这里介绍一下attachements:像很多的音乐文件他们都是包含专辑封面图的,这类的视频流有一个特点就是只有一帧视频。可以通过以下代码来判断这种情况:is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC可以直接通过流中的
attached_pic参数来直接完成读取,然后添加到Packet Queue中去,添加空的packet表示这个流已经读取完毕。 -
检查
Packet Queue是否已经满了
已满的逻辑判断(满足以下其一就表示满了):
- 不是无限 buffer,并且三种 packet 队列占用内存和达到 15MB
- 三种 packet 队列都已经满了,队列满的逻辑是单帧视频流或者 packet 的时长达到 1s
-
检查播放是否已经完成
-
读取
Packet将读取到的Packet存入对应的队列中,然后进入下一次循环
解码
以下是解码一帧音频帧或者视频帧的流程图:
video_thread:
static int video_thread(void *arg)
{
// ...
for (;;) {
// 解码
ret = get_video_frame(is, frame);
if (ret < 0)
goto the_end;
if (!ret)
continue;
// ...
while (ret >= 0) {
//...
// 将解码后的 frame 插入 frame 队列
ret = queue_picture(is, frame, pts, duration, fd ? fd->pkt_pos : -1, is->viddec.pkt_serial);
av_frame_unref(frame);
if (is->videoq.serial != is->viddec.pkt_serial)
break;
}
if (ret < 0)
goto the_end;
}
the_end:
avfilter_graph_free(&graph);
av_frame_free(&frame);
return 0;
}
// 返回值等于 1 成功, 0 流读取结束,-1 主动退出.
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;
// ...
return got_picture;
}
audio_thread:
static int audio_thread(void *arg)
{
// ...
do {
// 解码
if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0)
goto the_end;
if (got_frame) {
// 解码成功
// ...
while ((ret = av_buffersink_get_frame_flags(is->out_audio_filter, frame, 0)) >= 0) {
// ...
// 从 frame 队列中获取可用于写入的 frame,如果没有可用的就阻塞.
if (!(af = frame_queue_peek_writable(&is->sampq)))
goto the_end;
af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
af->pos = fd ? fd->pkt_pos : -1;
af->serial = is->auddec.pkt_serial;
af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});
// 移动 frame 数据的引用
av_frame_move_ref(af->frame, frame);
// 入 frame 队列
frame_queue_push(&is->sampq);
if (is->audioq.serial != is->auddec.pkt_serial)
break;
}
if (ret == AVERROR_EOF)
is->auddec.finished = is->auddec.pkt_serial;
}
} while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
the_end:
// ...
}
无论是视频解码还是音频解码,它们最终调用的解码方法都是 decoder_decode_frame():
// 返回值等于 1 成功, 0 流读取结束,-1 主动退出.
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
// 解码第一帧,packet 队列的 serial 为 1,decoder 的 serial 为 -1
if (d->queue->serial == d->pkt_serial) {
do {
if (d->queue->abort_request)
// 已经退出.
return -1;
switch (d->avctx->codec_type) {
case AVMEDIA_TYPE_VIDEO:
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
// 解码成功
if (decoder_reorder_pts == -1) {
frame->pts = frame->best_effort_timestamp;
} else if (!decoder_reorder_pts) {
frame->pts = frame->pkt_dts;
}
}
break;
case AVMEDIA_TYPE_AUDIO:
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
// 解码成功
AVRational tb = (AVRational){1, frame->sample_rate};
if (frame->pts != AV_NOPTS_VALUE)
frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, 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;
}
if (ret == AVERROR_EOF) {
// 当前流解码已经结束
d->finished = d->pkt_serial;
avcodec_flush_buffers(d->avctx);
return 0;
}
if (ret >= 0)
// 解码成功
return 1;
} while (ret != AVERROR(EAGAIN)); // ret == EAGAIN 需要读取更多的 packet 来完成解码
}
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) {
// 不需要读取 packet
d->packet_pending = 0;
} else {
// 需要读取 packet
// decoder 的旧的 serial
int old_serial = d->pkt_serial;
// 如果当前 packet 为空,会阻塞住,等待有可用的 packet,如果返回小于 0 表示退出
if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
return -1;
// decoder 的 serial 发生了改变
if (old_serial != d->pkt_serial) {
// 清空 decoder 缓存
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}
}
// 检查 decoder 和 packet 队列的 serial, 如果不一致表示当前的 packet 不一致,需要重新读取
if (d->queue->serial == d->pkt_serial)
break;
av_packet_unref(d->pkt);
} while (1);
if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
int got_frame = 0;
ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, d->pkt);
if (ret < 0) {
ret = AVERROR(EAGAIN);
} else {
if (got_frame && !d->pkt->data) {
d->packet_pending = 1;
}
ret = got_frame ? 0 : (d->pkt->data ? AVERROR(EAGAIN) : AVERROR_EOF);
}
av_packet_unref(d->pkt);
} else {
if (d->pkt->buf && !d->pkt->opaque_ref) {
FrameData *fd;
d->pkt->opaque_ref = av_buffer_allocz(sizeof(*fd));
if (!d->pkt->opaque_ref)
return AVERROR(ENOMEM);
fd = (FrameData*)d->pkt->opaque_ref->data;
fd->pkt_pos = d->pkt->pos;
}
if (avcodec_send_packet(d->avctx, d->pkt) == AVERROR(EAGAIN)) {
// 表示下次解码不再需要读取 packet.
av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
d->packet_pending = 1;
} else {
// 清除 packet 引用
av_packet_unref(d->pkt);
}
}
}
}
我发现很多人并不会处理 AVERROR(EAGAIN) 返回值,这里有必要解释下。
我们在读取到 packet 后,需要通过 avcodec_send_packet() 方法将需要解码的数据发送给解码器,然后通过 avcodec_receive_frame() 方法来获取已经解码成功的数据。这两个方法都可能会返回 AVERROR(EAGAIN)(取决于编码的方式),如果是 avcodec_send_packet() 返回就表示下一帧的解码不需要再去读 packet,也就是下次的解码不需要调用 avcodec_send_packet(),FFplay 中使用 packet_pending 参数来标记;avcodec_receive_frame() 方法表示需要更多的 packet 才能完成当前帧的解码,也就是需要继续调用 avcodec_send_packet() 方法来发送更多的数据来解码。
具体解码过程我就不再赘述了,通过我上面的流程图和 decoder_decode_frame() 注释已经很清楚了。
最后
希望本篇文章对你写一个高性能的播放器有所帮助,我自己也要去重写我自己的播放器了。