audio_thread音频解码线程分析
之前在 stream_component_open()
里面的 decode_start()
函数开启了 audio_thread
线程,如下:
if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)------------------audio_thread
goto out;
SDL_PauseAudioDevice(audio_dev, 0);
break;
audio_thread
线程主要是负责 解码 PacketQueue
队列里面的 AVPacket
的,解码出来 AVFrame
,然后丢给入口滤镜,再从出口滤镜把 AVFrame
读出来,再插入 FrameQueue
队列。
流程图如下:
如上,audio_thread 函数一开始就进入 一个 do{…}while{…} ,不断地调 decoder_decode_frame() 函数来解码出 AVFrame,然后把 AVFrame 往 入口滤镜 丢,再循环调 av_buffersink_get_frame_flags(),不断从出口滤镜收割经过 Filter 的AVFrame,最后调 frame_queue_push() 把 AVFrame 插入 FrameQueue 队列。
但是如果解码出来的 AVFrame 的音频格式与入口滤镜要求的音频格式不一样,会重建滤镜(reconfigure),如下:
static int audio_thread(void *arg)
{
。。。。。。。
if (got_frame) {
tb = (AVRational){1, frame->sample_rate};
#if CONFIG_AVFILTER
reconfigure =
cmp_audio_fmts(is->audio_filter_src.fmt, is->audio_filter_src.ch_layout.nb_channels,
frame->format, frame->ch_layout.nb_channels) ||
av_channel_layout_compare(&is->audio_filter_src.ch_layout, &frame->ch_layout) ||
is->audio_filter_src.freq != frame->sample_rate ||
is->auddec.pkt_serial != last_serial;---------------判断是否需要重置filter
if (reconfigure) {
char buf1[1024], buf2[1024];
首先,开始的入口滤镜 (audio_filter_src)的音频格式 是直接从解码器实例(avctx)里面取的,如下:
ffplay.c 2648行
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 2592 行
ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
所以以下 3 种场景会重新创建滤镜:
1,容器层记录的采样率等信息是错误的,与实际解码出来的不符。
2,解码过程中,中途解码出来的 AVFrame 的采样率,声道数或者采样格式 出现变动,与上一次解码出来的 AVFrame 不一样。
3,进行了快进快退操作,因为快进快退会导致 is->auddec.pkt_serial 递增。详情请阅读《FFplay序列号分析》。我也不知道为什么序列号变了要重建滤镜。
这3种情况,ffplay 都会处理,只要解码出来的 AVFrame 跟入口滤镜的格式不一致,都会重建滤镜,把入口滤镜的格式设置为当前的 AVFrame 的格式,这样滤镜处理才不会出错。
补充:last_serial 变量一开始是 -1,而 is->auddec.pkt_serial 一开始是 0,所以一开始是必然会执行一次 reconfigure 操作。
由于每次读取出口滤镜的数据,都会用 while 循环把缓存刷完,不会留数据在滤镜容器里面,所以重建滤镜不会导致音频数据丢失。我圈一下代码里面的重点,如下:
#if CONFIG_AVFILTER
reconfigure =
cmp_audio_fmts(is->audio_filter_src.fmt, is->audio_filter_src.ch_layout.nb_channels,
frame->format, frame->ch_layout.nb_channels) ||
av_channel_layout_compare(&is->audio_filter_src.ch_layout, &frame->ch_layout) ||
is->audio_filter_src.freq != frame->sample_rate ||
is->auddec.pkt_serial != last_serial;
if (reconfigure) {......}---------------------格式不一致,重建filter
if ((ret = av_buffersrc_add_frame(is->in_audio_filter, frame)) < 0)
goto the_end;
while ((ret = av_buffersink_get_frame_flags(is->out_audio_filter, frame, 0)) >= 0) {---------不断刷完filter里面的缓存
tb = av_buffersink_get_time_base(is->out_audio_filter);
#endif
if (!(af = frame_queue_peek_writable(&is->sampq)))
goto the_end;
audio_thread 线程的逻辑比较简单,复杂的地方都封装在它调用的子函数里面,所以本文简单讲解一下,audio_thread() 里面调用的各个函数的作用。
1,decoder_decode_frame(),从 PacketQueue 里面解码出来 AVFrame,此函数会阻塞,直到解码出来 AVFrame,或者返回错误。这个函数有 3 个返回值。
返回 1,获取到 AVFrame 。
返回 0 ,获取不到 AVFrame ,0 代表已经解码完MP4的所有AVPacket。这种情况一般是 ffplay 播放完了整个 MP4 文件,窗口画面停在最后一帧。但是由于你可以按 C 键重新循环播放,所以即便返回 0 也不能退出 audio_thread 线程。
返回 -1,代表 PacketQueue 队列关闭了(abort_request)。返回 -1 会导致 audio_thread() 函数用 goto the_end 跳出 do{}whlle{} 循环,跳出循环之后,audio_thread 线程就会自己结束了。返回 -1 通常是因为关闭了 ffplay 播放器。
2,configure_audio_filters(),创建音频滤镜函数。
3,av_buffersrc_add_frame(),往入口滤镜发送 AVFrame。
4,av_buffersink_get_frame_flags(),从出口滤镜读取 AVFrame。
5,frame_queue_peek_writable(),从 FrameQueue 里面取一个可以写的 Frame 出来。此函数也可能会阻塞。
6,frame_queue_push(),这个函数有点奇怪,他其实不是把之前的 Frame 塞进去队列,而是把队列的写索引值 +1。
audio_thread() 函数最后还有一个重点,就是当 出口滤镜 结束的时候,finished 就会设置为 非 0 。
if (ret == AVERROR_EOF)
is->auddec.finished = is->auddec.pkt_serial;
提示:只有往入口滤镜发送了 NULL 的 AVFrame ,出口滤镜才会结束。读完数据 跟 结束是两种状态,读完代表滤镜暂时没有数据可读,但是只要再往入口滤镜 发 AVFrame,出口滤镜就会又有数据可读。
而结束,代表不会再有 AVFrame 往入口滤镜发。
至此,audio_thread()
线程分析完毕。
decoder_decode_frame解码函数分析
decoder_decode_frame() 其实是一个通用的解码函数,可以解码 音频,视频,字幕的 AVPacket。不过本文主要侧重于分析音频流的解码,但其他的流也是类似的逻辑。
decoder_decode_frame() 函数的参数如下:
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub)
1,Decoder *d,这个 Decoder 结构,我把它称为解码管理实例。由于 Decoder 结构里面有解码器实例 跟 PacketQueue 队列,所以只需要传递 Decoder 给 decoder_decode_frame() 函数就能进行解码了。如下:
typedef struct Decoder {
...
PacketQueue *queue; // AVPacket 队列
AVCodecContext *avctx; //解码器实例
...
} Decoder
2, AVFrame *frame ,用来存储解码出来的音频或者视频的 AVFrame。
3, AVSubtitle *sub ,用来存储解码出来的字幕数据,字幕流的使用的数据结构不是 AVFrame,而是 AVSubtitle 。
decoder_decode_frame()
函数的流程图如下:
decoder_decode_frame()
函数的代码逻辑跟我上面画的流程图的顺序有点不一样,但是整体的逻辑就是这么一个逻辑:从 PacketQueue
队列拿数据去解码。
下面来分析一下 decoder_decode_frame()
的代码,如下:
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
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);----------------首次ret必然等于EAGAIN
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));----------------EAGAIN
}
从上代码可以看到,一开始就会用 avcodec_receive_frame()去解码器读数据,这里读者可能会有疑问,明明都还没往解码器发送 AVPacket, avcodec_receive_frame() 函数怎么可能读取到 AVFrame 呢?
答:没错,就是读取不到,因为还没发 AVPacket 给解码器解码。所以,首次 avcodec_receive_frame() 必然返回 EAGAIN,所以就会跳出这个 do{}while{} 循环。
跳出第一个 do{}while{}
循环之后,就会进入第二个 do{}while{}
循环,如下:
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);--------------唤醒read_thread线程
if (d->packet_pending) {
d->packet_pending = 0;
} else {
int old_serial = d->pkt_serial;
if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
return -1;
if (old_serial != d->pkt_serial) {--------------清空解码器缓存
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}
}
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 (avcodec_send_packet(d->avctx, d->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;-----------------发送解码器失败,把AVPacket缓存起来,下次继续发送
} else {
av_packet_unref(d->pkt);
}
}
}
}
虽然代码比较少,但是句句都是重点。
1,SDL_CondSignal(d->empty_queue_cond),首先,如果 PacketQueue 队列里面如果没有数据可读了,就需要唤醒 read_thread() 线程来读数据,之前说过, empty_queue_cond 实际上就是 continue_read_thread,这两个指针都指向同一个条件变量。
2,判断之前发送 AVPacket 给解码器是否失败了?如果失败,d->packet_peding 会是 1。如果上次失败了,d->pkt本身就是有值的,就不需要重队列里面拿数据,直接把 d->pkt 发送给解码器即可。
3,调用 packet_queue_get() 从 队列读取 AVPacket。简单讲解一下 packet_queue_get() 函数的参数,定义如下:
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
- int block 是控制 packet_queue_get() 函数阻塞读取的,如果 PacketQueue 队列里面如果没有数据可读,可以一直阻塞等到 read_thread() 线程读到数据放进去队列 为止。
- AVPacket *pkt,用来放从队列读取到的 AVPacket。
- int *serial,读取到的 AVPacket 的序列号,这是一个返回值。传的是指针。
PacketQueue 队列是一个 FIFO 的内存管理器,存的是 MyAVPacketList,如下:
typedef struct MyAVPacketList {
AVPacket *pkt;
int serial;
} MyAVPacketList;
也就是说,队列里的每一个 AVPacket 都有一个序列号的。
再回到 decoder_decode_frame() 调用 packet_queue_get() 时的传参,如下:
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) {
d->packet_pending = 0;
} else {
int old_serial = d->pkt_serial;-----------------
if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)---------------
return -1;
if (old_serial != d->pkt_serial) {--------------
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}
}
if (d->queue->serial == d->pkt_serial)------------------
break;
av_packet_unref(d->pkt);
} while (1);
可以看到从队列取出来的 AVPacket 就放在 d->pkt 里面,而序列号就放在 d->pkt_serial。
FFplay播放器其实有 3 种序列号:
1, MyAVPacketList 的 serial ,队列里面的 AVPacket 的序列号。可以看成是临时值,旧值。
2,PackeQueue 的 serial ,这是队列本身的序列号。可以看成是最新的序列号的值。
3,Frame 的 serial,本文不需要关注这个。
MyAVPacketList 的 serial 就是用PackeQueue 队列的 serial 来赋值的 。如下代码:只要不进行跳转播放,他们的序列号就是一样的。
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
MyAVPacketList pkt1;
int ret;
if (q->abort_request)
return -1;
pkt1.pkt = pkt;
pkt1.serial = q->serial;---------------取队列本身的序列号来赋值
ret = av_fifo_write(q->pkt_list, &pkt1, 1);
if (ret < 0)
return ret;
q->nb_packets++;
q->size += pkt1.pkt->size + sizeof(pkt1);
q->duration += pkt1.pkt->duration;
/* XXX: should duplicate packet data in DV case */
SDL_CondSignal(q->cond);
return 0;
}
当快进快退,或者跳转播放时间点的时候,PackeQueue 队列的序列号就会 +1,而之前已经放进去队列的 MyAVPacketList 的序列号则保持不变。
举个例子,当前MP4已经播放到了 第20秒的时刻,此时 PackeQueue 队列缓存了 5 帧第 21秒的数据,此时,我快进30秒,跳转到第 50 秒的时刻播放。
由于跳转了,所以队列的序列号会 +1,变成了 2,而之前的 5 帧 MyAVPacketList 的序列号还是 1。两者就会不一样。
因为要开始播放第 50 秒的数据,所以 PackeQueue 队列之前缓存的 5 帧数据就不可用了。丢弃这 5 帧数据就是由下面的代码实现的。
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) {
d->packet_pending = 0;
} else {
int old_serial = d->pkt_serial;
if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
return -1;
if (old_serial != d->pkt_serial) {
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}
}
if (d->queue->serial == d->pkt_serial)-------start
break;
av_packet_unref(d->pkt);---------------------end
} while (1);
注意看上面圈出来的代码,只有两者相等,才会 break 退出,如果不相等,就会直接 av_packet_unref()
释放,然后再进入 while
循环再从队列取AVPacket
。
这样,就能把无效的 5 帧数据全部丢弃,直到从队列读取到序列号一致的 AVPacket
为止。
还有一个重点,就是当从队列取出来的 AVPacket
跟上一次取的 AVPacket
序列号不一样,就会刷新解码器的缓存。
序列号不一样,肯定是因为跳转了播放时间点,而解码器要按顺序解码的,如果不清空缓存,可能会导致马赛克。
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) {
d->packet_pending = 0;
} else {
int old_serial = d->pkt_serial;
if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
return -1;
if (old_serial != d->pkt_serial) {-----------start
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}--------------------------------------------end,刷新解码器缓存
}
if (d->queue->serial == d->pkt_serial)
break;
av_packet_unref(d->pkt);
} while (1);
假设现在已经从队列读取到 序列号跟队列一样的 AVPacket
,就会把 AVPacket
发送给解码器,如下:
if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {......} else {
if (avcodec_send_packet(d->avctx, d->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;
} else {
av_packet_unref(d->pkt);
}
}
由于发送解码器可能会失败,所以 ffplay
做了一下处理,如果失败,就不 unref
,直接标记一下 d->packet_pending
,下次再继续发送。
此时,已经成功发送 AVPacket 给解码器了。这时候,decoder_decode_frame() 还没有结束,此时此刻还没跳出 最开始的 for (;😉 {} 循环。
所以又会重新进入一开始的往解码器读 AVFrame 的逻辑,decoder_decode_frame()函数的逻辑可以说是反着来的,所以看起来有点奇怪。
现在回到一开始的逻辑,如下:
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
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) {.......}
break;
case AVMEDIA_TYPE_AUDIO:
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {......}
break;
}
if (ret == AVERROR_EOF) {
d->finished = d->pkt_serial;
avcodec_flush_buffers(d->avctx);
return 0;
}
if (ret >= 0)
return 1;------------已经成功解码并读取到AVFrame,直接return返回
} while (ret != AVERROR(EAGAIN));
}
此时,avcodec_receive_frame() 仍然可能还是会返回 EAGAIN,因为不是往解码器发一个AVPacket,就一定有数据可读的。有些是 B 帧,还需要多一个P帧来解码。所以如果 avcodec_receive_frame() 返回 EAGAIN,就会从 PacketQueue 队列再拿出一个 AVPacket 往解码器丢。
现在我们假设 avcodec_receive_frame() 能读出数据了,可以看到,它赋值给了参数 frame,也就是第二个参数。
读到 AVFrame 之后,decoder_decode_frame() 就会直接 return 1 退出了。
注意,decoder_decode_frame() 只从解码器读取到一个 AVFrame 就返回了,如果解码器里面还有缓存的 AVFrame,下次就可以直接取,而不用再从队列拿 AVPacket 再发送给解码器。
这就是为什么从解码器读 AVFrame 要加上这个 if 判断:
if (d->queue->serial == d->pkt_serial) {
...
}
因为已经跳转到别的时间播放了,解码器的缓存是以前的时间点缓存的。如果还继续取,窗口画面就有短暂的不准确。
最后做下总结,decoder_decode_frame() 函数的逻辑就是从解码器读取到 一个 AVFrame,为了解码出一个AVFrame,它会从 PacketQueue 队列取 AVPacekt 发送给解码器,需要多少个就取多少个 AVPacekt,直至到能解码出一个 AVFrame。
decoder_decode_frame() 函数有 3 个返回值。
- 返回 1,获取到 AVFrame 。
- 返回 0 ,获取不到 AVFrame ,0 代表已经解码完MP4的所有AVPacket。这种情况一般是 ffplay 播放完了整个 MP4 文件,窗口画面停在最后一帧。但是由于你可以按 C 键重新循环播放,所以即便返回 0 也不能退出 audio_thread 线程。
- 返回 -1,代表 PacketQueue 队列关闭了(abort_request)。返回 -1 会导致 audio_thread() 函数用 goto the_end 跳出 do{}whlle{} 循环,跳出循环之后,audio_thread 线程就会自己结束了。返回 -1 通常是因为关闭了 ffplay 播放器。
FFplay序列号分析
在之前的几篇文章里面,都零零散散提及过序列号这个概念。但是 序列号 这个概念对于 FFplay 播放器 非常重要的,很多代码都跟序列号有关。所以单独写一篇文章介绍序列号。
序列号 主要是给 快进快退 这个功能准备的。如果不能快进快退,那其实就不需要序列号。只要解复用线程不断读取 AVPacket 放进去 PacketQueue 队列,解码线程不断从 PacketQueue 取数据来解码放进去 FrameQueue,最后有播放线程来取 FrameQueue 的数据取播放就行。
整体的流程图如下:
PacketQueue 跟 FrameQueue 都是缓存队列,都是先提取准备好要用的数据。但是就是因为加了 快进快退 这个功能,一旦跳转到别的时间点播放,之前提前准备好的数据是不是就不能用了?
所以就需要一个序列号来判断,之前缓存的数据是不是最新的,所以序列号可以看成是版本号一样的东西。
FFplay 播放器主要有 4 个序列号字段,分别如下:
1,struct PackeQueue 的 serial ,这是队列本身的序列号。可以看成是最新的序列号的值。
2, struct MyAVPacketList 的 serial ,队列里面 AVPacket 的序列号。
3,struct Decoder 的 pkt_serial,记录的是解码器上一次用来解码的 AVPacket 的序列号
4,struct Frame 的 serial,队列里面 AVFrame 的序列号。
首先,MyAVPacketList 的 serial 来源与 PackeQueue 的 serial,在 packet_queue_put_private() 函数里面可以看到:
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
MyAVPacketList pkt1;-----------------MyAVPacketList
int ret;
if (q->abort_request)
return -1;
pkt1.pkt = pkt;
pkt1.serial = q->serial;---------------serial
......
}
Frame
的 serial
是来源于 Decoder
的 pkt_serial
,如下:
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 = frame->pkt_pos;
af->serial = is->auddec.pkt_serial;------------------
af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});
而 Decoder
结构里面的 pkt_serial
又是来自哪里的呢?
**答:**是在 decoder_decode_frame()
里面进行赋值的。记录的是最近一次输入解码器的 AVPacket
的序列号,如下:
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) {
d->packet_pending = 0;
} else {
int old_serial = d->pkt_serial;
if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)---------------记录最近一次输入解码器的AVPacket的序列号
return -1;
if (old_serial != d->pkt_serial) {
avcodec_flush_buffers(d->avctx);
因此,可以说,Frame
的 serial
是间接来源于 AVPacket
的 serial
的。
现在已经弄懂了 4 个序列号赋值的地方,那这 4 个序列号字段用来哪些代码里面呢?
答:只要跟缓存有关的逻辑,都会判断序列号。
因为序列号是为了 快进快退 功能准备的,所以先简单讲解一下 快进快退的逻辑,更详细的分析请看《FFplay跳转时间点播放》。
当你按下上下左右其中一个键的时候,就会产生一个 seek 操作,这是调 stream_seek()
函数实现的,如下:
case SDLK_UP:
incr = 60.0;
goto do_seek;------------
case SDLK_DOWN:
incr = -60.0;
do_seek:
if (seek_by_bytes) {
pos = -1;
if (pos < 0 && cur_stream->video_stream >= 0)
pos = frame_queue_last_pos(&cur_stream->pictq);
if (pos < 0 && cur_stream->audio_stream >= 0)
pos = frame_queue_last_pos(&cur_stream->sampq);
if (pos < 0)
pos = avio_tell(cur_stream->ic->pb);
if (cur_stream->ic->bit_rate)
incr *= cur_stream->ic->bit_rate / 8.0;
else
incr *= 180000.0;
pos += incr;
stream_seek(cur_stream, pos, incr, 1);
} else {
pos = get_master_clock(cur_stream);
if (isnan(pos))
pos = (double)cur_stream->seek_pos / AV_TIME_BASE;
pos += incr;
if (cur_stream->ic->start_time != AV_NOPTS_VALUE && pos < cur_stream->ic->start_time / (double)AV_TIME_BASE)
pos = cur_stream->ic->start_time / (double)AV_TIME_BASE;
stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);--------------
}
break;
stream_seek() 函数的代码如下:
/* seek in the stream */
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{
if (!is->seek_req) {
is->seek_pos = pos;
is->seek_rel = rel;
is->seek_flags &= ~AVSEEK_FLAG_BYTE;
if (seek_by_bytes)
is->seek_flags |= AVSEEK_FLAG_BYTE;
is->seek_req = 1;
SDL_CondSignal(is->continue_read_thread);
}
}
上面的代码重点是设置 了 is->seek_req 标记 以及 要跳到到哪些位置播放。然后就会唤醒 read_thread() 线程。 read_thread() 线程看到 is->seek_req 这个标记,就会开始进行 seek 操作。如下:
所以,真正进行 seek 操作的地方是在 read_thread() 解复用线程里面,而不是 main 线程。
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) {
av_log(NULL, AV_LOG_ERROR,
"%s: error while seeking\n", is->ic->url);
} else {
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);---------------
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);
}
}
从上代码可以看出来,用 avformat_seek_file()
进行 seek
操作之后,就会 调 packet_queue_flush()
函数来刷新 序列号,来达到丢弃之前的无效缓存的效果。
packet_queue_flush()
是一个非常重点的函数,代码如下:
static void packet_queue_flush(PacketQueue *q)
{
MyAVPacketList pkt1;
SDL_LockMutex(q->mutex);
while (av_fifo_read(q->pkt_list, &pkt1, 1) >= 0)--------清空队列的数据
av_packet_free(&pkt1.pkt);
q->nb_packets = 0;
q->size = 0;
q->duration = 0;
q->serial++;--------------刷新序列号
SDL_UnlockMutex(q->mutex);
}
可以看到,一开始就清空了 PacketQueue 队列里面的数据,然后把序列号 +1。
我个人有个疑问,既然 +1 之前已经把 数据清空,那 解码线程 从 PakcetQueue 取数据的时候根本不需要判断序列号。因为已经清空了,解码线程不可能取到旧的 AVPacket。
先不管这个,反正 PacketQueue 的 serial 序列号刷新了。
PacketQueue 队列本身的 serial 序列号 +1 刷新,会导致一系列的连环反应。
首先是后面 解复用线程往 PacketQueue 队列 加进去的 MyAVPacketList 的 serial 也会是最新的。但是,解码管理器(struct Decoder)里面的 pkt_serial 还是旧值。
之前说过,struct Decoder 的 pkt_serial 记录的是上一次解码的 AVPacket 的序列号。
这就会导致,直接放弃解码器里面的缓存,用新的 AVPacket 进行解码,如下:
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
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) {......}
break;
case AVMEDIA_TYPE_AUDIO:
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {......}
break;
}
if (ret == AVERROR_EOF) {......}
if (ret >= 0)
return 1;
} while (ret != AVERROR(EAGAIN));
}
同样会导致 解码器的缓存被刷新,如下:
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) {
d->packet_pending = 0;
} else {
int old_serial = d->pkt_serial;--------------
if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)------------
return -1;
if (old_serial != d->pkt_serial) {------------
avcodec_flush_buffers(d->avctx);----------
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}
}
if (d->queue->serial == d->pkt_serial)
break;
av_packet_unref(d->pkt);
} while (1);
上图的逻辑是,只要本次解码的 AVPacket
的序列号跟上一次用的 AVPacket
的序列号不一样,立即刷新解码器缓存。
还有一个被连环影响的地方就是 读取 FrameQueue
的逻辑,FrameQueue
跟 PakcetQueue
不太一样,
FrameQueue
本身是没有序列号的,只是它队列里面的 struct Frame
有 序列号,所以也会受到影响,如下:
static int audio_decode_frame(VideoState *is)
{
int data_size, resampled_data_size;
av_unused double audio_clock0;
int wanted_nb_samples;
Frame *af;
if (is->paused)
return -1;
do {
#if defined(_WIN32)
......
#endif
if (!(af = frame_queue_peek_readable(&is->sampq)))----------------音频播放线程取数据
return -1;
frame_queue_next(&is->sampq);
} while (af->serial != is->audioq.serial);----------------
data_size = av_samples_get_buffer_size(NULL, af->frame->ch_layout.nb_channels,
af->frame->nb_samples,
af->frame->format, 1);
可以看到,音频播放线程取数据的时候,会判断 Frame 的序列号是不是最新的,不是就立即丢弃。
序列号应用的场景还有一个,那就是时钟(Clock),如下:
typedef struct Clock {
...
int serial; /* clock is based on a packet with this serial */
...
int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;
这个场景是音视频同步的时候用的,有点复杂。后面讲解音视频同步的时候会再分析 Clock 里面的序列号。
sdl_audio_callback音频播放线程分析
音频播放线程是之前在 audio_open() 函数里面创建的,实际上就是回调函数 ( wanted_spec.callback)。当用 SDL 打开音频硬件设备的时候,SDL 库就会创建一个线程,来及时执行回调函数 sdl_audio_callback(),至于 SDL 线程多久回调一次函数,这个我们不需要太关心,只要调 SDL_OpenAudioDevice() 函数的时候设置好相关参数即可。如下:
static int audio_open(void *opaque, AVChannelLayout *wanted_channel_layout, int wanted_sample_rate, struct AudioParams *audio_hw_params)
{
......
while (next_sample_rate_idx && next_sample_rates[next_sample_rate_idx] >= wanted_spec.freq)
next_sample_rate_idx--;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.silence = 0;---------------
wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC));-----每次回调取的样本数
wanted_spec.callback = sdl_audio_callback;------
wanted_spec.userdata = opaque;
while (!(audio_dev = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE))) {---------------SDL_OpenAudioDevice
av_log(NULL, AV_LOG_WARNING, "SDL_OpenAudio (%d channels, %d Hz): %s\n",
wanted_spec.channels, wanted_spec.freq, SDL_GetError());
wanted_spec.channels = next_nb_channels[FFMIN(7, wanted_spec.channels)];
if (!wanted_spec.channels) {
wanted_spec.freq = next_sample_rates[next_sample_rate_idx--];
wanted_spec.channels = wanted_nb_channels;
if (!wanted_spec.freq) {
av_log(NULL, AV_LOG_ERROR,
"No more combinations to try, audio open failed\n");
return -1;
}
}
av_channel_layout_default(wanted_channel_layout, wanted_spec.channels);
}
上面代码中,设置了每次回调取的样本数,设置了样本数就相当于设置了回调次数,ffplay 默认是 1秒钟最多回调 30 次 sdl_audio_callback() 函数。
sdl_audio_callback() 函数的参数如下:
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
1,void *opaque,实际上就是之前设置的 wanted_spec.userdata,传递的是 VideoState *is 全局管理器。
2,Uint8 *stream,这个指针是 SDL 内部音频数据内存的指针,只要把数据拷贝到这个指针的地址,就能播放声音了。
3,int len,此次回调需要写多少字节的数据进去 stream 指针。
虽然 len 这个参数是 SDL 传递给我们的回调函数的,但是 len 其实是可以根据 wanted_spec的样本数,声道数,格式, 算出来的。
例如,ffplay 播放 juren-5s.mp4 的场景,命令如下:
ffplay -i juren-5s.mp4
在上面的命令中,wanted_spec.samples 会被赋值为 2048,也就是说每次回调,需要往 stream 指针写 2048 个采样。那2048个样本又是多少字节?
由于 SDL_OpenAudioDevice() 打开音频设备的时候写死了 16位格式 AUDIO_S16SYS ,所以一个采样是2个字节。然后因为 juren-5s.mp4 文件的音频是双声道的,每个声道都取2048个样本,那就是 2048 2 2 = 8196 字节。有兴趣可以打印一下 sdl_audio_callback() 里面的 len 变量,在本文命令下,一直都是8196 字节。
sdl_audio_callback() 函数的流程图如下:
从上图的流程可以很容易看出, sdl_audio_callback() 函数干的事情,就是调 audio_decode_frame() 函数,把 is->audio_buf 指针指向要传输的音频数据。然后再调 SDL_MixAudioFormat() 把音频数据拷贝给 stream 指针指向的内存。这样 SDL 内部就会读 stream 指针指向的内存来播放。
虽然流程图画得比较简单,但是 sdl_audio_callback() 函数内部的代码逻辑是非常复杂的。下面我们一起来分析一下这里面的重点:
/* prepare a new audio buffer */
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
.......
while (len > 0) {
if (is->audio_buf_index >= is->audio_buf_size) {--------------audio_buf缓存已经读完
audio_size = audio_decode_frame(is);
if (audio_size < 0) {
/* if error, just output silence */
is->audio_buf = NULL;
is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
} else {
if (is->show_mode != SHOW_MODE_VIDEO)
update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
is->audio_buf_size = audio_size;------------设置buf的大小以及读取位置
}
is->audio_buf_index = 0;-----------------------
}
len1 = is->audio_buf_size - is->audio_buf_index;
if (len1 > len)
len1 = len;
if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)------以最大音量进行拷贝
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);------start
else {
memset(stream, 0, len1);
if (!is->muted && is->audio_buf)
SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume);----------以调整后的音量进行拷贝
}---------------------------------------------------end,复制音频数据至SDL的内存
len -= len1;
stream += len1;
is->audio_buf_index += len1;-----------------更新读取位置
}
......
}
}
上面代码中的 while(len>0){…} 的逻辑简单概括就是从 FrameQueue 读取数据,不断写进入 SDL 的内存,直到写入了 len 大小的字节为止。
audio_decode_frame() 函数就是负责从 FrameQueue 读取 AVFrame,然后 把 is->audio_buf 指针指向 AVFrame 的 data ,如果经过重采样, is->audio_buf 指针会指向重采样后的数据,也就是 is->audio_buf1。
audio_decode_frame() 函数的内部逻辑是比较复杂的,本文不分析它的内部逻辑,只是给你讲一下它最后做到了什么事情,这也就是函数封装的意义。
audio_decode_frame() 函数最后做到的事情就是,把is->audio_buf 指针指向可以传输的音频数据,然后返回值代表可以传输的音频数据有多少字节。
至于这个音频数据是在哪个内存里面,我们外部调用不需要关心,这是它的内部实现。
如果 audio_decode_frame() 提取数据的时候发生错误。会返回 -1,然后把 audio_buf 指向 NULL 指针,这样会导致写入静音数据。
要理解上面的 while(len>0){…} 循环,有几个变量是必须要讲解一下的:
1,is->audio_buf,可以传输的音频数据的内存指针,指向内存的第一个字节。也就是开头。
2,is->audio_buf_size,可以传输的音频数据有多大,有多少个字节。
3,is->audio_buf_index,当前已经读取到第几个字节。
理解了这 3 个变量,就容易看懂上图中的 while 逻辑,就是不断拷贝数据到 SDL 的内存,
如果拷贝够了 len 字节,is->audio_buf 里面还有数据剩下来,就下一次回调来的时候继续拷贝剩下的数据。
如果当前 is->audio_buf 的数据不足以塞满 len 长度,就循环调 audio_decode_frame() 来提取数据,然后一直到 塞满 len 长度为止。
拷贝数据到 SDL 内存的时候有一个重点,会根据音量来调不同的函数。
可以看到,如果直接 memcpy 拷贝数据,会是以最大音量进行拷贝。如果我们调整了音量,就会调 SDL_MixAudioFormat() 来拷贝数据。
因为 SDL_MixAudioFormat() 函数有调整音量的功能。
至此,sdl_audio_callback() 函数已经把 SDL 所需的音频数据全部拷贝给它了,就会跳出 while 循环,然后设置音频时钟。如下:
is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
/* Let's assume the audio driver that is used by SDL has two periods. */
if (!isnan(is->audio_clock)) {
set_clock_at(&is->audclk,is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec,is->audio_clock_serial, audio_callback_time / 1000000.0);
sync_clock_to_slave(&is->extclk, &is->audclk);
}
上面的代码主要是设置音频的时钟,记录当前这一刻,音频流播放到哪里了。下面分析一下一些变量:
is->audio_write_buf_size 代表当前缓存里还剩多少数据没有拷贝给 SDL 。
is->audio_clock 这个变量是在 audio_decode_frame() 函数里面赋值的,因为 audio_decode_frame() 会从 FrameQueue 里面提取 一个AVFrame,而 is->audio_clock 记录的就是当这个 AVFrame 播放完之后,音频流所处的位置,也就是当 这个 AVFrame 播放完了,音频流 的 pts 是多少。。
理解了 is->audio_clock 就比较容易理解如何确定当前音频流播放到哪里了。
is->audio_clock 记录的是播放完那个 AVFrame 之后的 pts,但是此时此刻 只是把 这个 AVFrame 的内存数据拷贝给了 SDL,SDL 还没开始播放呢?
同时,SDL 还没播放的数据还有它内部的 audio_hw_buf_size 长度的数据,之前说过:
SDL线程并不是没有音频数据可以播放了才调 sdl_audio_callback() 来拿数据,而是他内部还剩 audio_hw_buf_size 长度的数据就会调 sdl_audio_callback() 来拿数据,是提前拿数据的。
所以,SDL 内部还剩 audio_hw_buf_size 字节,现在又来取了 len 字节,同时我们的 audio_buf 缓存还剩 audio_write_buf_size字节。总共有 3 块内存等待播放,而这 3 块内存播放完之后的 pts 就是 is->audio_clock ,如下:
从上图可以看到,我们已经知道播放完这 3 块内存之后,音频流的位置,那就可以反向求音频流当前的播放时刻。而 len 实际上就是等于 audio_write_buf_size,只是换了下名字,所以可以直接用 audio_hw_buf_size 乘以 2,所以就产生了下面的这句代码:
音频流当前的播放时刻 = is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec
这里要注意 is->audio_clock 的单位是秒。至此,就可以正确设置音频流当前播放到哪里了。
代码里有一句注释,也讲解了为什么要乘以 2。
/* Let's assume the audio driver that is used by SDL has two periods. */
它假设了 SDL 打开的设备里面是有两段缓存的,也就是 audio_hw_buf_size + len。
最后的那句代码主要是用音频时钟来同步一下外部时钟,如下:。
sync_clock_to_slave(&is->extclk, &is->audclk);
时钟是用来记录当前的播放时刻的,方便做音视频同步。