前言:

大家好,我是小涂,这周继续给大家分享ffplay播放器源码解析,上次分析完了read_thread这个线程,今天我接着分析一下之前没有介绍完的视频解码线程video_thread。

好了废话就不多说,直接肝就完事!

video_thread线程源码解析:

由于这个源码有关于滤镜处理的一部分,现在暂时不看它,所以下面的代码把这部分给省略掉,专门看关于视频处理这块的代码:

视频解码线程video_thread解析!_ide

// 视频解码线程
static int video_thread(void *arg)
{
VideoState *is = arg;
AVFrame *frame = av_frame_alloc(); // 分配解码帧
double pts; // pts
double duration; // 帧持续时间
int ret;
//1 获取stream timebase
AVRational tb = is->video_st->time_base; // 获取stream timebase
//2 获取帧率,以便计算每帧picture的duration
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
if (!frame)
return AVERROR(ENOMEM);

for (;;) { // 循环取出视频解码的帧数据
// 3 获取解码后的视频帧
ret = get_video_frame(is, frame);
if (ret < 0)
goto the_end; //解码结束, 什么时候会结束
if (!ret) //没有解码得到画面, 什么情况下会得不到解后的帧
continue;
// 4 计算帧持续时间和换算pts值为秒
// 1/帧率 = duration 单位秒, 没有帧率时则设置为0, 有帧率帧计算出帧间隔
duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
// 根据AVStream timebase计算出pts值, 单位为秒
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
// 5 将解码后的视频帧插入队列
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
// 6 释放frame对应的数据
av_frame_unref(frame);
if (ret < 0) // 返回值小于0则退出线程
goto the_end;
}
av_frame_free(&frame);
return 0;
}

如果上面取视频帧里面的数据如果获取失败的话,会退出这个视频解码线程。同时这里解释一下视频帧的持续时间:

// 4 计算帧持续时间和换算pts值为秒
// 1/帧率 = duration 单位秒, 没有帧率时则设置为0, 有帧率帧计算出帧间隔
duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);

上面这句代码先是进行分子和分母进行判断是否为0,如果分子和分母都不为0的话,就用av_q2d这个内联函数来计算:

/**
* Convert an AVRational to a `double`.
* @param a AVRational to convert
* @return `a` in floating-point form
* @see av_d2q()
*/
static inline double av_q2d(AVRational a){
return a.num / (double) a.den;
}

所以通过上面的代码解读,我们可以看到这个video_thread对视频解码的一个细节处理流程分为:


  • 1、获取stream timebase,以便将frame的pts转成秒为单位
  • 2、获取帧率,以便计算每帧picture的duration
  • 3、获取解码后的视频帧,具体调⽤get_video_frame()实现
  • 4、计算帧持续时间和换算pts值为秒
  • 5、将解码后的视频帧插⼊队列,具体调⽤queue_picture()实现
  • 6、释放frame对应的数据

下面我们来解析上面的get_video_frame()接口,如果说单独看上面的代码,咋们不清楚它到底是如何获取视频帧的,所以为了清楚了解其中的面目,我们必须进去看具体的代码才行:

/**
* @brief 获取视频帧
* @param is
* @param frame 指向获取的视频帧
* @return
*/
static int get_video_frame(VideoState *is, AVFrame *frame)
{
int got_picture;
// 1. 获取解码后的视频帧
if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0) {
return -1; // 返回-1意味着要退出解码线程, 所以要分析decoder_decode_frame什么情况下返回-1
}

if (got_picture) {
// 2. 分析获取到的该帧是否要drop掉, 该机制的目的是在放入帧队列前先drop掉过时的视频帧
double dpts = NAN;

if (frame->pts != AV_NOPTS_VALUE)
dpts = av_q2d(is->video_st->time_base) * frame->pts; //计算出秒为单位的pts

frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);

if (framedrop>0 || // 允许drop帧
(framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER))//非视频同步模式
{
if (frame->pts != AV_NOPTS_VALUE) { // pts值有效
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) { // packet队列至少有1帧数据
is->frame_drops_early++;
printf("%s(%d) diff:%lfs, drop frame, drops:%d\n",
__FUNCTION__, __LINE__, diff, is->frame_drops_early);
av_frame_unref(frame);
got_picture = 0;
}
}
}
}

return got_picture;
}

上面的代码处理流程主要分为:


  • 1、调⽤ decoder_decode_frame 解码并获取解码后的视频帧
  • 2、分析如果获取到帧是否需要drop掉(逻辑就是如果刚解出来就落后主时钟,那就没有必要放⼊Frame队 列,再拿去播放,但是也是有⼀定的条件的,这个条件处理主要在if(goto_picture)条件里面处理.

这里稍微说一下这个drop帧处理流程;先确定进⼊丢帧检测流程,控制是否进⼊丢帧检测有3种情况:


  • 1、控制是否丢帧的开关变量是 framedrop ,为1,则始终判断是否丢帧
  • 2、framedrop 为0,则始终不丢帧
  • 3、framedrop 为-1(默认值),则在主时钟不是video的时候,判断是否丢帧

如果当进⼊丢帧检测流程,drop帧需要下列因素都成⽴:


  • 1、!isnan(diff):当前pts和主时钟的差值是有效值
  • 2、fabs(diff) < AV_NOSYNC_THRESHOLD:差值在可同步范围内,这⾥设置的是10秒,意思是如果差 值太⼤这⾥就不管了了,可能流本身录制的时候就有问题,这⾥不能随便把帧都drop掉
  • 3、diff - is->frame_last_filter_delay < 0:和过滤器有关系,不设置过滤器时简化为 diff < 0
  • 4、is->viddec.pkt_serial == is->vidclk.serial:解码器的serial和时钟的serial相同,即是⾄少显示了 ⼀帧图像,因为只有显示的时候才调⽤update_video_pts()设置到video clk的serial
  • 5、is->videoq.nb_packets:⾄少packetqueue有1个包

这里我说一下第4点的这个视频播放序列,通过调试当前这两个值是不同的:

视频解码线程video_thread解析!_2d_02

视频时钟的serial要在这里面update_video_pts()才会更新:

static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {
/* update current video pts */
set_clock(&is->vidclk, pts, serial);
sync_clock_to_slave(&is->extclk, &is->vidclk);
}

小结:

今天就总结这么多吧,大家先好好消化一下;下次把decoder_decode_frame()里面到底如何获取视频解码后的视频帧!