热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

vlc源码分析播放速度控制原理,pts,dts

一:解码的速率控制视频都有帧率,即每一秒应该显示的帧数,怎么根据这个帧率,来匀速地播放视频?播放的时候怎么实现

一:解码的速率控制
视频都有帧率,即每一秒应该显示的帧数,怎么根据这个帧率,来匀速地播放视频?播放的时候怎么实现快速播放,而不是跳帧式地快进?探究一下vlc源码里面对帧率和播放速度的控制。(选用只包含一个视频流且帧率的1的视频文件做的分析
https://blog.csdn.net/u012459903/article/details/89416892)

pts和dts:

pts,播放时间戳,严格地说,它只是一个序号,表示播放的顺序。
dts,解码时间戳,也可以说是表示解码的顺序。所以这两个并不是表示 应该播放的时间点和应该解码的时间点,和实际的系统时间没有关系。一般程序会有一套转换到系统实际时间的转换函数,需要的时候将其计算对应到系统时间

在vlc播放 rtsp流数据的时候,两者是相等的,也即 视频帧的播放顺序和解码顺序是一致的。 那么为什么还要放两个值?因为ffmpeg的需要,在往解码器ffmpeg相关解码库输入数据的时候,是需要给上这两个值,这么做的原因:

对于一个电影,帧是这样来显示的:I B B P。现在我们需要在显示B帧之前知道P帧中的信息。因此,帧可能会按照这样的方式来存储:IPBB。这就是为什么我们会有一个解码时间戳和一个显示时间戳的原因。解码时间戳告诉我们什么时候需要解码,显示时间戳告诉我们什么时候需要显示。所以,在这种情况下,我们的流可以是这样的:
PTS: 1 4 2 3
DTS: 1 2 3 4
Stream: I P B B
通常PTS和DTS只有在流中有B帧的时候会不同。这也就解释了为什么我们可能ffmpeg在调用avcodec_decode_video以后会得不到一帧图像
所以目前来看,rtsp流中的数据只有一个pts, 收到的流就按照 一个顺序进行解码。

先上结论:
vlc 版本 3.0.5
vlc 由多个 module组成,多线程机制,核心的一个线程,在src/input.c 该线程调用demux的接口 解复用数据,demux模块的这个demux接口解复用完数据,输出 element stream 到 es_out模块。帧速度的控制,在input.c 线程中,根据视频帧率和设置的播放速度,进行延时,然后按指定的时间间隔输出到 es_out,即数据从demux出来的时候,已经是按时间间隔均匀输出了。
要达到指定速度播放,要么输入间接性输入一大块数据,输出取数据的时候匀速取,取完再输入一大块数据。要么输入的时候,就按照指定速度输入数据,输出取数据有数据就取走,显然vlc这里是第二种方式,读文件的时候根据需要匀速读数据,如果用户有需要可以倍速。

对应到代码,即es_out_timeshfit.c中的 Send函数,被demux模块调用的时候,已经是按照 播放显示的时间间隔来调用了,可以添加如下代码到 Send函数的开头来实际查看:

struct timeval test_time;struct tm *st_tm = NULL;gettimeofday(&test_time,NULL);st_tm = gmtime(&test_time.tv_sec);printf("wang timeshiftsend [%d %s]%d:%d:%d; tv_usec %d \n",__LINE__,__FUNCTION__,st_tm->tm_hour,st_tm->tm_min,st_tm->tm_sec,test_time.tv_usec);

在es_out_timeshift.c文件加上上诉两个函数的头文件
#include
#include
如果是fps = 1的存视频文件,按照默认速度播放,上面的输出应该是每秒一次。

实际代码的延时位置,在input.c文件中的输入线程 循环体 MainLoop()
{…
  while()
  {…
    for( ;; )
    {…
      ControlPop( p_input, &i_type, &val, i_deadline, b_postpone )
      {
        vlc_cond_timedwait( &p_sys->wait_control, &p_sys->lock_control,
i_deadline )
      }
    }
  }
}

上面的vlc_cond_timedwait函数作用即休眠指定的 i_deadline时间间隔。

Mainloop循环调用MainLoopDemux()函数,MainLoopDemux函数的作用是解复用获得一帧数据,这个函数被调用的频率是实际 播放 帧率的4倍(目前的精度值)。其中的一次调用会得到数据,这应该是一个精度的问题,所以上面的 i_deadline时间间隔,并不是简单的播放速度 两帧的时间间隔,也应该是 1/4的时间间隔.
拿一播放一个 MP4文件来看,demux/mp4.c中的demux函数,会使用到一个 p_sys->i_nztime += DEMUX_INCREMENT;
这个i_nztime 时间戳(vlc中的时间片都已 microsecond 微秒 为单位,即百万分之一秒,单位表示us,非 ms(毫秒)。 按照每一次调用 demux函数就自增 DEMUX_INCREMENT 的值来变化.

上面的 休眠时间间隔的获取,Mainloop 通过调用es_out_GetWakeup,从es_out中获取
/* Get date to wait before demuxing more data */
ES_OUT_GET_WAKE_UP,
即 input线程通过 cotrol从es_out模块获取到相应的休眠时间,进行休眠然后得以按指定频率调用demux模块来解复用数据,在给到es_out.

es_out中关于休眠时间的计算,得益于其中的 input_clock_t 结构,对应的代码在clock.c,外界播放视频使用 libvlc_media_player_set_rate设置播放速度的时候,会修改"rate" 变量值,从而触发改变变量修改时候的回调,进入input.c 的control 的INPUT_CONTROL_SET_RATE分支,转而进入es_out_SetRate, -->ES_OUT_SET_RATE. 休眠的时间间隔就会改变。

二:显示图片时的控制
上面按匀速将数据输入到解码器,解码,然后放到显示队列等待显示,正常情况下当然期望给一张图片他就解码一张,并且及时显示到屏幕,这样看到的才是按照我们设置的 播放速度在播放,不过由于某些原因,比如解码器解码太慢,会导致数据在给到显示端的时候滞后并且堆积,这个时候就要一个机制,要把这些滞后的数据帧给 drop掉,不至于堆积成山,这就需要用到 dts,pts时间戳了。

数据给到解码线程后:
decoder.c文件中,DecoderThread 线程函数
中,调用到DecoderProcess之后,分两种情况,
1: DecoderProcessSout();//数据给到sout
2: DecoderDecode();//Drain
the decoder module 数据下方到解码模块

涉及到四个函数:
DecoderPlaySout();
DecoderPlayVideo();
DecoderPlayAudio();
DecoderQueueSpu();
这四个函数,都会执行两个操作,
DecoderWaitUnblock( p_dec );//条件等待,阻塞
DecoderFixTs(); //修正pts时间戳,里面会用到延时配置数据
上面DecoderProcessSout()里面,给到sout之前会调用DecoderPlaySout()进行处理,
在DecoderDecode函数中,会调用已经注册的解码模块中的p_dec->pf_decode,解码
所有的解码模块的pf_decode接口,比如 codec/avcodec/video.c ffmpeng软件模块,执行完解码输出一帧picture,然后回调 到上层设置的 DecoderPlayVideo();
也就是说解码出数据后,再回调DecoderPlayVideo();或者DecoderPlayAudio(); 进行延时和修正时间戳,再给到输出。

pts, dts . pts即 play time stampe, dts 即 decode time stampe
以播放一个 MP4文件为例,vlc在播放MP4文件时,按照指定的播放速率从文件中读取解析出一帧图像数据,然后对这一帧数据打上时间pts , dts 时间戳,这个时候这两个时间戳起始值相等都是从一个固定初值开始,后面按照播放速度在匀速增长,比如播放的 为 fps = 1的视频,解复用的时候的输出是:

wang es_out_outdata p_block :ipts 00000000 dts 00000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 01000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 02000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 03000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 04000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 05000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 06000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 07000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 08000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 09000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 10000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 11000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 12000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 13000001 length 00000000[2137EsOutSend]
wang es_out_outdata p_block :ipts 00000000 dts 14000001 length 00000000[2137EsOutSend]

以上是源码的 es_out.c EsOutSend()函数里面添加的调试信息,表示数据解复用出来到放进p_dec->p_owner队列的时候时间戳
然后数据在decoder线程取出,(注: 个人现在的播放情况是:
libvlc_media_add_option(media, “:sout=#display{delay=1505}}”);
即数据从sout 输出,然后给到display 模块显示,给到display模块的时候添加上 1505ms的延时
那么这样一来,数据 在decoder 线程中,从 p_dec->p_owner队列取出来后并不会立即给到解码器,而是走了DecoderPlaySout(); 在 其中进行 DecoderFixTs 修改,这个时候的数据就已经和系统时间相关联了。 后再给到display模块,display模块send数据到自己的decoder线程之前又加上 刚刚设置的 1505ms延时
decoderfixTs 前后的timestampe:

fix_out befor ->i_pts 1 p_block->i_dts 1 i_length 1000000 fix_out after ->i_pts 38934920084 p_block->i_dts 38934920084 i_length 1000000 fix_out befor ->i_pts 1000001 p_block->i_dts 1000001 i_length 1000000 fix_out after ->i_pts 38935920084 p_block->i_dts 38935920084 i_length 1000000 fix_out befor ->i_pts 2000001 p_block->i_dts 2000001 i_length 1000000 fix_out after ->i_pts 38936920084 p_block->i_dts 38936920084 i_length 1000000 fix_out befor ->i_pts 3000001 p_block->i_dts 3000001 i_length 1000000 fix_out after ->i_pts 38937920084 p_block->i_dts 38937920084 i_length 1000000 fix_out befor ->i_pts 4000001 p_block->i_dts 4000001 i_length 1000000 fix_out after ->i_pts 38938920084 p_block->i_dts 38938920084 i_length 1000000 fix_out befor ->i_pts 5000001 p_block->i_dts 5000001 i_length 1000000 fix_out after ->i_pts 38939920084 p_block->i_dts 38939920084 i_length 1000000 fix_out befor ->i_pts 6000001 p_block->i_dts 6000001 i_length 1000000 fix_out after ->i_pts 38940920084 p_block->i_dts 38940920084 i_length 1000000

DecoderPlaySout内调用 DecoderFixTs 前后进行的输出。printf用%ld ,64位数
这个时间戳已经成为系统 运行起来的时间相关了,fix会使用系统已经运行的累计时间值来和pts,dts进行修改,fix之后的时间就已经是以 微秒, 百万分之一秒为单位的 系统运行时间相关的值了
个人系统的运行时间:
在这里插入图片描述
38956 秒,系统已经运行 38956s即 38956 x 1000000 (当前的这个值是调试程序之后cat的,并不能完全对应,不过基本上是一个数量级的)
看fix之后的第一帧 38934920084 ; 给到display的decoder之前在加上 1505ms。即 1505 000
则=38936425084
看给到解码器之前的输出:

[/decoder.c] p_block->i_pts 38936425084 p_block->i_dts 38936425084 i_length 1000000
[/decoder.c] p_block->i_pts 38937425084 p_block->i_dts 38937425084 i_length 1000000
[/decoder.c] p_block->i_pts 38938425084 p_block->i_dts 38938425084 i_length 1000000
[/decoder.c] p_block->i_pts 38939425084 p_block->i_dts 38939425084 i_length 1000000
[/decoder.c] p_block->i_pts 38940425084 p_block->i_dts 38940425084 i_length 1000000
[/decoder.c] p_block->i_pts 38941425084 p_block->i_dts 38941425084 i_length 1000000
[/decoder.c] p_block->i_pts 38942425084 p_block->i_dts 38942425084 i_length 1000000
[/decoder.c] p_block->i_pts 38943425084 p_block->i_dts 38943425084 i_length 1000000
[/decoder.c] p_block->i_pts 38944425084 p_block->i_dts 38944425084 i_length 1000000

和添加上delay计算的一致。

vlc 播放主动丢弃数据的部分。
目前看到两块地方
1:解码前丢弃。在avcodec/video (h264软解)的解码函数DecodeBlock函数中 (从解码器队列取数据开始解码前丢弃,这个可能会造成花屏现象的出现,因为可能丢弃了关键帧,导致后续的参考帧解码没有参考数据)
1.1

current_time = mdate();
if( p_dec->b_frame_drop_allowed && check_block_being_late( p_sys, p_block, current_time) )
{
msg_Err( p_dec, "more than 5 seconds of late video -> "
"dropping frame (computer too slow ?)" );
return NULL;
}

这里每次进入,顺利解码完成之后都会马上记录 mdate()时间(这个就是用的系统启动运行的微秒数),然后开始解码前拿当前的mdate()和上一次解码的系统时间点进行比较,如果说超过了5s,就丢了,不解码。

1.2

if( p_sys->b_hurry_up ){/* Check also if we should/can drop the block and move to next blockas trying to catchup the speed*/if( p_dec->b_frame_drop_allowed &&check_frame_should_be_dropped( p_sys, p_context, &b_need_output_picture ) ){ msg_Warn( p_dec, "More than 11 late frames, dropping frame" );return NULL;}}

数据太慢了

2:解码后丢弃。在video_ouput.c文件中进行显示解码完之后的图片时(如果单纯是这里丢弃数据。不会是花屏,因为丢弃的是解码完之后一帧的图像,解码不受影响)

decoded = picture_fifo_Pop(vout->p->decoder_fifo); if (decoded) { if (is_late_dropped && !decoded->b_force) { mtime_t late_threshold; if (decoded->format.i_frame_rate && decoded->format.i_frame_rate_base) late_threshold = ((CLOCK_FREQ/2) * decoded->format.i_frame_rate_base) / decoded->format.i_frame_rate;
else late_threshold = VOUT_DISPLAY_LATE_THRESHOLD; const mtime_t predicted = mdate() + 0; /* TODO improve */ const mtime_t late = predicted - decoded->date; if (late > late_threshold) {msg_Warn(vout, "picture is too late to be displayed (missing %"PRId64" ms)", late/1000);picture_Release(decoded);vout_statistic_AddLost(&vout->p->statistic, 1);continue;} else if (late > 0) {msg_Dbg(vout, "picture might be displayed late (missing %"PRId64" ms)", late/1000);}}

解码完后,获取当前的时间,和时间戳上已经同系统时间对应关联之后的时间进行比较,如果超过正常帧平均占空比,就认为解码超时,丢弃已经解完的图像。
上面的((CLOCK_FREQ/2) * decoded->format.i_frame_rate_base) / decoded->format.i_frame_rate; 一秒的总数 CLOCK_FREQ x 帧率的倒数 结果再取一半。就是每一帧的占空时间


推荐阅读
author-avatar
香音的茶
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有