ffplay的解码线程独立于读线程,并且每种类型的流(AVStream)都有其各自的解码线程,如video_thread用于解码video stream,audio_thread用于解码audio stream,subtitle_thread用于解码subtitle stream。
为方便阅读,先列一张表格,梳理各个变量、函数名称。
其中PacketQueue用于存放从read_thread取到的各自播放时间内的AVPacket。FrameQueue用于存放各自解码后的AVFrame。Clock用于同步音视频。解码线程负责将PacketQueue数据解码为AVFrame,并存入FrameQueue。
对于不同流,其解码过程大同小异,我们先看下video_thead。
对于滤镜部分,先不做分析 ,简化后的代码如下(为方便阅读,以下代码还省略了部分局部变量的声明):
static int video_thread(void *arg)
{
AVRational tb = is->video_st->time_base;
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
for (;;) {
ret = get_video_frame(is, frame); //解码获取一帧视频画面
if (ret < 0)//解码结束
goto the_end;
if (!ret)//没有解码得到画面
continue;
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);//将pts转化为秒为单位
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);//将解码后的帧存入FrameQueue
av_frame_unref(frame);
if (ret < 0)
goto the_end;
}
the_end:
av_frame_free(&frame);
return 0;
}
线程的总体流程很清晰:
- 调用
get_video_frame
解码一帧图像 - “计算”时长和pts
- 调用
queue_picture
放入FrameQueue
get_video_frame
简化如下:
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) {
frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);
//……
}
return got_picture;
}
主要是调用decoder_decode_frame
解码。
被简化的部分主要是针对丢帧的一个处理:
//if (got_picture) {
double dpts = NAN;
if (frame->pts != AV_NOPTS_VALUE)
dpts = av_q2d(is->video_st->time_base) * frame->pts;
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) {
av_log(NULL, AV_LOG_ERROR, "drop early: %d\n", is->videoq.nb_packets);
is->frame_drops_early++;
av_frame_unref(frame);
got_picture = 0;
}
}
}
//}
丢帧的主要条件是diff - is->frame_last_filter_delay < 0
,frame_last_filter_delay
与滤镜有关,可以先忽略,也就是diff < 0
的时候丢帧——pts < get_master_clock(is)
的时候丢帧。
控制是否丢帧的开关变量是framedrop
,为1,则始终判断是否丢帧;为0,则始终不丢帧;为-1(默认值),则在主时钟不是video的时候,判断是否丢帧。
接下来看下真正解码的过程——decoder_decode_frame
,这个函数也包含了对audio和subtitle的解码,同样,先看简化后的主干代码:
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
for (;;) {
//1. 流连续的情况下,不断调用avcodec_receive_frame获取解码后的frame
if (d->queue->serial == d->pkt_serial) {
do {
ret = avcodec_receive_frame(d->avctx, frame);
if (ret == AVERROR_EOF) {
return 0;
}
if (ret >= 0)
return 1;
} while (ret != AVERROR(EAGAIN));
}
//2. 取一个packet,顺带过滤“过时”的packet
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;
} while (d->queue->serial != d->pkt_serial);
//3. 将packet送入解码器
avcodec_send_packet(d->avctx, &pkt);
}
}
decoder_decode_frame
的主干代码是一个循环,要拿到一帧解码数据,或解码出错、文件结束,才会返回。
循环内可以分解为3个步骤:
- 流连续的情况下,不断调用avcodec_receive_frame获取解码后的frame。
d->queue
就是video PacketQueue(videoq),d->pkt_serial
是最近一次取的Packet的序列号。在判断完d->queue->serial == d->pkt_serial
确保流连续后,循环调用avcodec_receive_frame
,有取到帧,就返回。(即使还没送入新的Packet,这是为了兼容一个Packet可以解出多个Frame的情况) - 取一个packet,顺带过滤“过时”的packet。主要阻塞调用
packet_queue_get
(参考PacketQueue的分析:https://zhuanlan.zhihu.com/p/43295650)。另外,会在PacketQueue为空时,发送empty_queue_cond
条件信号,通知读线程继续读数据。(empty_queue_cond
就是continue_read_thread
,可以参考read线程的分析,查看读线程何时会等待该条件量:https://zhuanlan.zhihu.com/p/43672062) - 将packet送入解码器。
avcodec_decode_video2已经被标记为废弃。
@deprecated Use avcodec_send_packet() and avcodec_receive_frame().
建议使用这两个函数进行解码。
在省略代码中有一个packet_pending
的概念,用于在send失败时重新发送:
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);
}
如果avcodec_send_packet
返回EAGAIN
,则把当前pkt
存入d->pkt
,然后置标志位packet_pending
为1。
packet_pending
的读取在取Packet时:
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);
//如果有待重发的pkt,则先取待重发的pkt,否则从队列中取一个pkt
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);
省略代码中还有针对flush_pkt
的处理:
if (pkt.data == flush_pkt.data) {
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}
了解过PacketQueue的代码,我们知道在往PacketQueue送入一个flush_pkt后,PacketQueue的serial值会加1,而送入的flush_pkt和PacketQueue的serial值保持一致。所以如果有“过时”Packet,过滤后,取到的第一个pkt将是flush_pkt。
根据api要求,此时需要调用avcodec_flush_buffers
。
上面,我们就分析完了video_thread中关键的get_video_frame
函数,根据所分析的代码,已经可以取到正确解码后的一帧数据了。接下来就要把这一帧放入FrameQueue:
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, frame->pkt_pos, 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 (!(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);
av_frame_move_ref(vp->frame, src_frame);
frame_queue_push(&is->pictq);
return 0;
}
queue_picture
的代码很直白,先frame_queue_peek_writable
取FrameQueue的当前写节点,然后把该拷贝的拷贝给节点(struct Frame)保存,然后frame_queue_push
,“push”节点到队列中。唯一需要关注的是,AVFrame的拷贝是通过av_frame_move_ref
实现的,所以拷贝后src_frame
就是无效的了。
关于FrameQueue的操作,和分析,见: https://zhuanlan.zhihu.com/p/43564980
至此,video的解码过程,除fitler部分,基本都分析完了。
audio的解码过程,不考虑filter部分,与video的解码几乎一样。就不重复分析了。
subtitle的解码过程略有不同,关注下不同的地方。
static int subtitle_thread(void *arg)
{
VideoState *is = arg;
Frame *sp;
int got_subtitle;
double pts;
for (;;) {
//注意这里是先frame_queue_peek_writable再decoder_decode_frame
if (!(sp = frame_queue_peek_writable(&is->subpq)))
return 0;
if ((got_subtitle = decoder_decode_frame(&is->subdec, NULL, &sp->sub)) < 0)
break;
pts = 0;
if (got_subtitle && sp->sub.format == 0) {
if (sp->sub.pts != AV_NOPTS_VALUE)
pts = sp->sub.pts / (double)AV_TIME_BASE;
sp->pts = pts;
sp->serial = is->subdec.pkt_serial;
sp->width = is->subdec.avctx->width;
sp->height = is->subdec.avctx->height;
sp->uploaded = 0;
/* now we can update the picture count */
frame_queue_push(&is->subpq);
} else if (got_subtitle) {
avsubtitle_free(&sp->sub);
}
}
return 0;
}
主要流程是:
-
frame_queue_peek_writable
先取一个可写的帧节点 -
decoder_decode_frame
解码一帧字幕
我们会发现subtitle_thread中的主流程顺序和video_thread是相反的,video_thread是先解码后取可写的帧节点,然后写入。
这里,我的猜测是:由于字幕的FrameQueue消耗比较慢(比如常见的一句话对应一帧),这就导致FrameQueue经常是处于堆满的状态。如果先解码,再调用frame_queue_peek_writable
,大概率会被阻塞,期间如果发生seek,则解码的这一帧就浪费了。与其先解码再等锁,不如先等锁再解码。当然,这只是一个猜测,具体原因,彻底搞明白了再更新。
解码字幕的时候还走avcodec_decode_subtitle2
,对应的decoder_decode_frame
简化为:
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
//1. 判断返回值
if (d->queue->serial == d->pkt_serial) {
do {
if (ret == AVERROR_EOF) {
return 0;
}
if (ret >= 0)
return 1;
} while (ret != AVERROR(EAGAIN));
}
//2. 取pkt
packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial);
//3.解码
ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &pkt);
if (ret < 0) {
ret = AVERROR(EAGAIN);
} else {
if (got_frame && !pkt.data) {//如果是null_packet,且置为pending,下次继续
d->packet_pending = 1;
av_packet_move_ref(&d->pkt, &pkt);
}
ret = got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVERROR_EOF);
}
}
}
因为和audio/video共用一个函数,而又用的avcodec_decode_subtitle2
(相比send/receive只一个函数完成解码)解字幕,所以代码顺序看起来会有些奇怪:
- 判断返回值
- 取pkt
- 解码
步骤1,在步骤3后更合适,但因为是循环体内,所以,也是可以正确运行的。
对于avcodec_decode_subtitle2
这里有一个null_packet的处理,null_packet一般在文件读取结束后发出。
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);
}
在解码字幕时,null_packet用于最后将解码器内剩余的解码数据取出。如果还能取到数据(got_frame == 1),则将null_packet暂存d->pkt,置packet_pending,下次继续取。直到avcodec_decode_subtitle2
返回got_frame == 0.
关于这部分的逻辑,在avcodec_decode_subtitle2
的注释中有说明:
/* Some decoders (those marked with AV_CODEC_CAP_DELAY) have a delay between input
* and output. This means that for some packets they will not immediately
* produce decoded output and need to be flushed at the end of decoding to get
* all the decoded data. Flushing is done by calling this function with packets
* with avpkt->data set to NULL and avpkt->size set to 0 until it stops
* returning subtitles. It is safe to flush even those decoders that are not
* marked with AV_CODEC_CAP_DELAY, then no subtitles will be returned.
*/
至此,关于ffplay的解码部分就分析完了。