本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8
FFplay 源码分析系列以一条简单的命令开始,ffplay -i a.mp4
。a.mp4下载链接:百度网盘,提取码:nl0s 。
上一篇文章已经讲解完了 stream_component_open() 的逻辑,这篇文章主要讲解 audio_thread(),音频解码线程的内部逻辑。
因为解码线程里面涉及到了 struct Decoder,PacketQueue,FrameQueue的操作,所以必须先简单介绍一下这些数据结构的关系。
struct Decoder
- AVPacket pkt; 缓存包,在 avcodec_send_packet() 返回 AVERROR(EAGAIN) 的时候用的,因为AVPacket已经从队列里面拿出来了,如果send给解码器的时候失败了,就需要把拿出来的AVPacket放到 Decoder::pkt 里面,要不AVPacket就会丢失,同时 packet_pending 置为 1,下次优先取Decoder::pkt 来 send给解码器。请看代码。
ffplay.c 668 ~ 672行
if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {
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; //注意这行代码。
av_packet_move_ref(&d->pkt, &pkt); //注意这行代码。
}
ffplay.c 637 ~ 647行
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) { //注意这行代码。
av_packet_move_ref(&pkt, &d->pkt);
d->packet_pending = 0;
} else {
if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
return -1;
}
} while (d->queue->serial != d->pkt_serial);
- PacketQueue *queue; 队列。
- AVCodecContext *avctx; 解码器上下文
- int pkt_serial; 解码器包序列,这个变量是每次从队列里面取出一个 MyAVPacketList,都会把 pkt_serial 设置为 MyAVPacketList::serial,可以理解为,这个变量是最后一个发送给解码器的 packet 的序列号。
- int finished; 解码器是否已经没有 AVFrame可以输出。在 avcodec_receive_frame() 返回 AVERROR_EOF 的时候,finished 会设置为 pkt_serial,就是设置为最后一个packet的序列号。
- int packet_pending; 在 avcodec_send_packet() 返回 AVERROR(EAGAIN) 的时候用的
- SDL_cond *empty_queue_cond; empty_queue_cond 条件变量其实等于 continue_read_thread,请看下面代码。上一篇文章分析 read_thread() 的时候,read_thread() 在某些情况下会休眠10ms,例如队列满了或超过最小缓存size,会休眠10ms,如果在5ms的时候,队列已经被消耗完了,没有frame可以播放了,那就需要尽快唤醒read_thread() 线程,这个 empty_queue_cond 条件变量就是用来唤醒 read_thread 线程的。
//read_thread 休眠 10ms
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
//注意最后一个参数,continue_read_thread 赋值给 empty_queue_cond
decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
- int64_t start_pts; 这个字段只用于音频流,在本文命令里面,这个字段的值一直是 AV_NOPTS_VALUE,不清楚在音频流什么场景下使用。
- AVRational start_pts_tb; 时间基。
- int64_t next_pts; 这个字段也是只用于音频流了。从 avcodec_receive_frame() 解码器取到的AVFrame 的pts如果有问题,就会用next_pts代替。next_pts的计算方式是上一帧的pts + 上一帧的样本数。如下代码所示:
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;
}
}
- AVRational next_pts_tb; 时间基
- SDL_Thread *decoder_tid; 解码线程 id。
struct PacketQueue
- MyAVPacketList *first_pkt, *last_pkt; 队列的头尾
- int nb_packets; 队列的包数量
- int size; 队列缓存的数据size
- int64_t duration; 队列缓存的duration,通过AVPacket->duration 累加得到。
- int abort_request; 停止解码线程,解码线程代码有好几个地方判断 abort_request 是否为1, stream_component_close() 里面会把这个字段置为1。
- int serial; 队列的序列号,队列的serial跟 MyAVPacketList 的serial 可以不一样的。跟队列serial不一样的 MyAVPacketList 就是旧的,解码的时候会丢弃旧的MyAVPacketList。
- SDL_mutex *mutex; 互斥锁,主要用于修改队列的时候加锁。
- SDL_cond *cond; 条件变量,用于解码线程跟 read_thread 线程通信。当解码线程没有packet可以读的时候,就会 wait cond 阻塞,等待,然后 read_thread 读取到packet丢进去队列之后,就会signal cond 唤醒解码线程继续解码。
预备工作,数据结构已经讲完了,下面正式开始讲解 audio_thread() 解码线程里面的逻辑。
static int audio_thread(void *arg)
{
VideoState *is = arg;
AVFrame *frame = av_frame_alloc();
...省略代码..
do {
if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0)
goto the_end;
...省略代码...
} while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
}
从上面的代码可以看到 audio_thread() 一开始就进入一个 do() while{} 的循环。在这个循环里面会不断的拿 PacketQueue 的数据,传给解码器,解出 AVFrame,然后把 AVFrame 过一遍filter,本文命令filter是空,然后再把 frame 插入 FrameQueue 队列,让播放线程取拿frame播放。
里面比较重要的一个函数是 decoder_decode_frame(),这是一个阻塞函数,它内部会不断循环等待,直到读出一个AVFrame。
重点知识:
decoder_decode_frame()返回的 got_frame 的有几个值?。
- 返回 1,获取到 AVFrame了。
- 返回 0 ,文件已经读取完毕,并且也解码完毕,没有AVFrame返回。
- 返回 -1,流关闭了,abort_request 变成了 1了,got_frame 就会是 -1。
下面分析获取到 AVFrame 之后的逻辑。
获取到 AVFrame 之后,ffplay 会对 AVFrame的采样率,格式做一次校验,这里又有一次校验,ffplay真是严谨。主要校验什么呢?校验stream容器层的采样率,格式,是不是跟 AVFrame 的数据是一致的?应该是ffpaly担心,有些音频流,虽然容器层的采样率字段是48000,但实际解码出来的AVFrame 却是 44100。校验如果不一致,就会 调 configure_audio_filters() 重新初始filter。
校验代码如下:
reconfigure =
cmp_audio_fmts(is->audio_filter_src.fmt, is->audio_filter_src.channels,frame->format, frame->channels) ||
is->audio_filter_src.channel_layout != dec_channel_layout ||
is->audio_filter_src.freq != frame->sample_rate ||
is->auddec.pkt_serial != last_serial;
is->audio_filter_src 的值是从解码器 avctx 里面来的,请看下图。
ffpaly.c 2639 行
is->audio_filter_src.freq = avctx->sample_rate;
is->audio_filter_src.channels = avctx->channels;
is->audio_filter_src.channel_layout = get_valid_channel_layout(avctx->channel_layout, avctx->channels);
is->audio_filter_src.fmt = avctx->sample_fmt;
解码器 avctx 的采样率又是从容器层来的,请看以下代码:
ffpaly.c 2581 行
ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
校验完采样率之后,就是调 av_buffersrc_add_frame() 把 AVFrame 往 filter 里面送了,然后 循环 调 av_buffersink_get_frame_flags(),不断收割经过filter的AVFrame。然后调 frame_queue_push() 插入 FrameQueue 队列,让播放线程取。
ffplay 源码分析,audio_thread() 解码线程分析完毕。
©版权所属:知识星球:弦外之音,QQ:2338195090。
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。