前言
video_thread线程主要负责解码视频数据。本文是把CONFIG_AVFILTER设置为0去掉滤镜功能后分析的。
一、video_thread
video_thread线程简化后代码如下:
static int video_thread(void *arg)
{
VideoState *is = arg;
AVFrame *frame = av_frame_alloc();
double pts;
double duration;
int ret;
AVRational tb = is->video_st->time_base;
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
if (!frame)
return AVERROR(ENOMEM);
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);
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
av_frame_unref(frame);
if (ret < 0)
goto the_end;
}
the_end:
av_frame_free(&frame);
return 0;
}
video_thread线程的主要代码是for循环,在for循环中调用get_video_frame()获取解码数据,再调用queue_picture()把解码数据更新到FrameQueue的过程。
1、首先通过av_frame_alloc()函数创建AVFrame
AVFrame在调用解码函数avcodec_receive_frame()时用到,用于保存解码后的数据
AVFrame *frame = av_frame_alloc();
2、获取视频流的时间基
AVRational tb = is->video_st->time_base;
从视频流AVStream中获取时间基,ffmpeg的时间基是{1,90000},即把1秒分成了90000份,每一份代表1/90000秒。AVRational{num,den}是个结构体,用于表示分数,前面的数字num是分子,后面den是分母。ffmpeg为了保持数据的精度,所以使用AVRational结构体而不是浮点型,因为1/3不能用0.3或0.33表示,这样会损失精度。AVRational结构体如下
/**
* Rational number (pair of numerator and denominator).
*/
typedef struct AVRational{
int num; ///< Numerator
int den; ///< Denominator
} AVRational;
3、获取帧率
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
用av_guess_frame_rate()函数,根据媒体格式上下文和媒体流的信息,来计算帧率。获取的帧率也是AVRational结构体。例如帧率是24,返回的AVRational结构是{24,1},分子分母是反的。
二、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) {
double dpts = NAN;
if (frame->pts != AV_NOPTS_VALUE)
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);
//framedrop初始值-1
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;
}
get_video_frame()有两部分功能,第一部分调用decoder_decode_frame()解码,第二部分判断解码后的AVFrame是否有效。
1、decoder_decode_frame()解码
详见链接:
ffplay源码分析__解码函数decoder_decode_frame-优快云博客
decoder_decode_frame()解码成功后返回1,解码到文件末尾返回0,解码错误返回负数。
2、判断解码后的AVFrame是否有效
if (frame->pts != AV_NOPTS_VALUE)
dpts = av_q2d(is->video_st->time_base) * frame->pts;
frame->pts是该帧的显示时间戳,单位是AVStream的时间基,需要用av_q2d函数转换成秒,代表该帧的显示时刻。例如frame->pts为9000,那么转成秒后为9000*(1/90000)=0.1秒,即该帧在0.1秒的时刻显示。
接下来判断该帧是否有效。
//framedrop初始值-1
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;
}
}
}
这串代码是判断解码出的AVFrame,是否已经过了应该显示的时间。
diff,代表该frame的显示时钟和主时钟相差多少秒,在去掉滤镜的情况下,is->frame_last_filter_delay始终为0。所以判断语句简化为:
if (!isnan(diff) // diff有值
&& fabs(diff) < AV_NOSYNC_THRESHOLD && // diff的绝对值小于10
diff < 0 && // diff<0,代表该帧过时了
is->viddec.pkt_serial == is->vidclk.serial && //序列号一致
is->videoq.nb_packets) // 视频PacketQueue还有数据包
最重要的判断语句是diff<0,代表该帧的播放时刻已经晚于当前音频帧的播放时刻,那么就丢弃。
其余的条件还有时钟差绝对值小于10,序列号一致以及视频PacketQueue中还有数据。
如果视频帧过时,那么got_picture返回0。如果视频帧有效,got_picture则返回1。接下来又返回到video_thread线程中。
三、queue_picture函数
1、计算视频帧的持续时长
duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
av_q2d()的计算方式:num / (double) den,注意此时frame_rate.den对应AVRational的num(1),frame_rate.num对应AVRational的den(24),所以duration=1/24=0.041,即1帧持续0.041秒。
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
同样计算pts,tb是AVStream的时间基
2、解码后的数据更新到FrameQueue
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
调用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()和frame_queue_push()。首先判断队列是否可写,不可写返回-1,可写的情况下对Frame *vp做相应的赋值工作。
3、frame_queue_peek_writable()
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;
return &f->queue[f->windex];
}
在frame_queue_peek_writable()中,如果 f->size >= f->max_size && !f->pktq->abort_request为true,说明FrameQueue队列满了,程序会一直阻塞在SDL_CondWait(f->cond, f->mutex)处等待,直到消费线程(视频的消费线程是主线程)调用SDL_CondSignal()发送信号为止。调用SDL_CondSignal()的代码在video_refresh()-->frame_queue_next()中,当frame_queue_next()消费掉视频数据时调用SDL_CondSignal,说明队列有可写空间了,通知生产线程(解码线程video_thread)可以继续解码了,下面是SDL_CondSignal()发送信号的代码。

如果可写的情况下,frame_queue_peek_writable()返回&f->queue[f->windex],即FrameQueue中Frame数组的指针。接下来通过对Frame *vp变量赋值
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数组在初始化时已经分配好了内存,所以对Frame *vp赋值相当于把数据更新到队列中。
其中vp的序列号被赋值为is->viddec.pkt_serial,即视频Decoder的pkt_serial,Decoder的pkt_serial等于PacketQueue的序列号,也等于MyAVPacketList的序列号,所以Frame的序列号 ,Decoder的序列号,PacketQueue的序列号,以及MyAVPacketList的序列号此时都是相等的。
通过av_frame_move_ref(),把AVFrame的解码数据拷贝到vp->frame,这样解码数据也插入到队列中了。
4、frame_queue_push()分析
static void frame_queue_push(FrameQueue *f)
{
if (++f->windex == f->max_size)
f->windex = 0;
SDL_LockMutex(f->mutex);
f->size++;
SDL_CondSignal(f->cond);
SDL_UnlockMutex(f->mutex);
}
在frame_queue_push()中了环形缓冲区的处理方式,对写指针windex累加。如果到了最大值写指针windex重新从0开始,并同时更新队列的大小。
从以上代码看,frame_queue_peek_writable()返回数组元素的指针
queue_picture()负责更新数据
frame_queue_push()负责更新队列的写指针和队列大小
四、总结
video_thread线程是在read_thread线程中创建的,主要作用是从视频PacketQueue队列中获取AVPacket数据用于解码,并把解码后的数据封装成Frame,保存到视频的FrameQueue中。真正的解码是在decoder_decode_frame()中完成。
把解码数据插入到队列是用三个函数完成,分别是frame_queue_peek_writable(),queue_picture(),frame_queue_push()。frame_queue_peek_writable()返回可写数组,queue_picture()负责插入数据,frame_queue_push()负责更新队列的写索引和大小。
1277

被折叠的 条评论
为什么被折叠?



