FFMPEG 3.4.2 - ffplay源代码分析 (三)

1. 数据结构之VideoState

FFMPEG 3.4.2 - ffplay源代码分析 (三)_第1张图片
  • VideoState是所有其他数据结构的母体。
  • main 线程启动新线程read_thread,初始化VideoState。
    • AVFormatContext保存与“读文件””和“demux”有关的上下文。
    • 在io_open_default()中,遍历protocol列表,根据url(filename)格式找出对应的处理函数集。这里假设打开的文件名是“/avm.mp4”,这是一个本地文件的地址,所以找到函数集file_xxx()。
    • AVIOContext的函数集io_xxx()是对下层的URLContext操作的一层简单包装。
    • 用AVIOContext读取文件头部的probe数据段。然后遍历input format列表,使用infput format的probe()函数检查probe数据段,分析与input format的匹配度。匹配度用一个整型变量score表示。在匹配的infput format中选择score值最高的。这里mov格式文件,所以找出demux函数集mov_xxx()。
    • 调用mov_read_header(),得到文件的流信息,保存在AVStream[]数组中。流信息包括流的数目和类型,以及对应decoder的codec_id等。这里只有一条video stream,codec_id是h264 decoder的id。
    • 查找decoder列表,根据AVStream中的codec_id找到decoder。这里对应的decode函数集是h264_xxx()。
  • read_thread线程启动新的video_thread线程。在Video_thread中做decode。然后read_thread线程继续做demux。

2. 数据结构之PacketQueue和FrameQueue

FFMPEG 3.4.2 - ffplay源代码分析 (三)_第2张图片
  • demux解析出来的AVPacket保存在PacketQueue中。
  • AVCodec读取PacketQueue中的AVPacket并decode,得到的Frame保存在FrameQueue中。
  • 在SDL设备上显示Frame。显示Frame前要进行时间同步。Clock是协助做时间同步的工具类。

3. 线程

FFMPEG 3.4.2 - ffplay源代码分析 (三)_第3张图片
  • 上图是ffplay的线程模型。
  • read_thread线程读文件并demux,将packet放入PacketQueue中;video_thread从PacketQueue取出packet并decode,将frame放入FrameQueue;main thread从FrameQueue中取出frame,显示在SDL显示设备上。
  • SDL_TimerThread可能是在早先的版本中用于frame的时间同步。现在已经不用了。

4. FrameQueue

  • FrameQueue是一个环形buffer。它的成员变量中,windex是写指针,rindex是读指针。
  • video stream与audio stream不同的一点是,video需要保留最后一个frame以便随时刷新显示。FrameQueue的keep_last和rindex_shown就是为了这个准备的。
  • 当keep_last为1时,FrameQueue总是保留最后一个frame。 这个frame还在FrameQueue中,但调用者得到的queue大小不包含它。调用者也不从FrameQueue中移除它。
FFMPEG 3.4.2 - ffplay源代码分析 (三)_第4张图片

5. 时间同步之struct Clock

  • Sample/Frame的时间戳可能有两种来源:录制时标记,或者decoder根据sample rate附加。播放frame时要参考一个本地时钟。时钟同步的实质,是在一系列的本地时间点显示与之对应的frame。
  • 如下图,Sample是frame 时间,Local Clock为本地时间。正常播放速度下,如果在本地时间2000(c1)时开始播放第一个frame(frame时间s1),那么我们应该在本地时间2040(c2)播放frame时间为140(s2)的frame,在本地时间2080(c3)播放frame时间为180(s3)的frame。
FFMPEG 3.4.2 - ffplay源代码分析 (三)_第5张图片
  • 还要考虑快进(播放速度speed > 1)或者慢进(播放速度0 < speed < 1)的情况。
  • 综上所述,时间同步的实质问题是:在s1,c1,和speed是确定常量的条件下,如何从c2得到s2?答案可以表述为以下等式:

s2 = s1 + (c2 - c1 ) * speed

如果引入新的变量重新表述:

drift = s1 - c1,last_upt = c1, cur_time = c2,

则新的等式如下:

S2 = drift + c1 + (c2 - c1) * speed
= drift + last_upt + ( cur_time - last_upt) * speed
= drift + last_upt + (cur_time - last_upt) * (1 - (1 - speed) )
= drift + cur_time - (cur_time - last_upt)*(1 - speed)

这就是ffplay中get_clock()函数使用的等式:

double get_clock(Clock *c)
{
  if (*c->queue_serial != c->serial)
      return NAN;
  if (c->paused) {
      return c->pts;
  } else {
      double time = av_gettime_relative() / 1000000.0;
      return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed); // << 这里!
  }
}
  • 注意:如果播放时用户暂停又继续播放,或者直接调整了frame的播放位置,则必须重置s1和c1。如果用户切换到快进或慢进模式,则必须重置speed。

6. 时间同步之refresh_loop_wait_event()

  • 前面说过,在ffplay早前的版本中,使用SDL_TimerThread提供的定时回调做同步,当前版本(ffmpeg 3.4)已经不再用了。现在的基本策略是:main_thread线程每隔0.01秒(定义为常量REFRESH_RATE)刷新一次显示。
  • Frame delay以0.01的间隔减少,最后一段剩余时间会比0.01短,这时下一次刷新时间会不到0.01秒。
  • 按照正常24 frame/秒的频率,每frame的delay是0.042秒,所以0.01秒够短,不至于跳过某一个frame的显示。
  • refresh_loop_wait_event() 大约每隔0.01秒调用一次video_refresh()。

7. 时间同步之video_refresh()

  • Frame有一个serial整型成员。这个成员初始值为0,每次开始新video段时递增1,所以serial可以用来判断时间同步是否需要重新开始。
    • 用户调整播放位置时会开始新段,video初始播放时也会开始新段。
    • Serial值由decoder设置。PacketQueue和FrameQueue也各有一个serial成员,标记Queue中最新packet/frame的serial。开始新段时调用decoder_start(),往PacketQueue中放入一个特殊的flush packet,并递增PacketQueue.serial。后面产生的packet会用新的serial标记。FrameQueue及其frame也同样处理。
  • 播放video有两种模式:
    • 一种是只管video stream的时钟就好了,不用与其他时钟同步,运行时带参数-sync video,如:

ffplay -sync video avm.mp4

  • 另一种是video需要参考其他时钟,可能是audio stream的时钟,也可能是其他外部时钟,运行时指定-sync audio或-sync ext。-sync ext是默认选项。

  • 第一种模式下的逻辑很简单,用Clock就可以。但是ffplay没有用Clock。

    • 如果新段开始,则设置一个变量frame_timer记录当前时间;
    • 根据前后两个frame的pts差值,计算当前显示的frame的delay。
    • 如果frame没超时(current_time < frame_timer + delay),则继续显示当前frame,否则显示下一个frame;
    • frame_timer累加delay。
  • 第二种的模式需要使用术语“master时钟”和“slave时钟”。video时钟是slave时钟,被参考的audio时钟或ext 时钟是master时钟。这种模式中,不但frame的delay要根据slave与master的差异再做修正,slave时钟本身也要修正。

    • 如果开始新段,则设置一个变量frame_timer记录当前时间;
    • 根据前后两个frame的pts差值计算当前显示的frame的delay。
    • 如果frame没超时(current_time < frame_timer + delay),则继续显示当前frame,否则显示下一个frame;
    • 比较Slave和Master,修正delay。如下图,如果本地时间为10000,并算出对应master的pts为100.0。
FFMPEG 3.4.2 - ffplay源代码分析 (三)_第6张图片

Slave算出对应的pts可能落在如下区间。
a. (~, 100-delay),则slave播放落后了,所以要减少delay;
b. (100-delay, 100+delay ),这是正常偏移,不做修正;
c. (100+delay, 100.1),则slave超前了,所以要小幅增加delay;
d. (100.1, ~),则slave太超前了,所以大幅增加delay。

  • frame_timer累加delay。

  • 根据当前frame的pts重置slave时钟。

  • Frame_timer是一个与frame关联的累加量,而步骤h)可能丢弃frame,所以导致frame_timer值偏小。如果偏差太大,则将frame_timer修正到本地时间。

  • 调整后的delay和framer_timer可能导致连续的frame超时。这时应该跳过前面的frame,只显示最后一个frame。

  • 从两种模式的分析看,ffplay的frame_timer没有考虑speed,所以它不支持快进和慢进!后一种模式用了Clock修正slave与master的差异,也不影响frame_timer的speed特性。

    • 从Show_help_default()的选项看,ffplay的播放控制操作也没有快进慢进控制。

+下图是video_refresh的流程图:

FFMPEG 3.4.2 - ffplay源代码分析 (三)_第7张图片

8. calculate_display_rect()

这个函数涉及到一个一般碰不到的主题: aspect ratio

DAR = SAR × PAR*

DAR = Display Aspect Ratio, SAR = Storage Aspect Ratio, PAR = Pixel Aspect Ratio

详细信息可以参考:
https://en.wikipedia.org/wiki/Aspect_ratio_(image)

Distinctions
Further information: Pixel aspect ratio

This article primarily addresses the aspect ratio of images as displayed, which is more formally referred to as the display aspect ratio (DAR). In digital images, there is a distinction with the storage aspect ratio (SAR), which is the ratio of pixel dimensions. If an image is displayed with square pixels, then these ratios agree; if not, then non-square, "rectangular" pixels are used, and these ratios disagree. The aspect ratio of the pixels themselves is known as the pixel aspect ratio (PAR) – for square pixels this is 1:1 – and these are related by the identity:

SAR × PAR = DAR.

Rearranging (solving for PAR) yields:

PAR = DAR/SAR.

For example, a 640 × 480 VGA image has a SAR of 640/480 = 4:3, and if displayed on a 4:3 display (DAR = 4:3), has square pixels, hence a PAR of 1:1. By contrast, a 720 × 576 D-1 PAL image has a SAR of 720/576 = 5:4, but is displayed on a 4:3 display (DAR = 4:3), so by this formula it would have a PAR of (4:3)/(5:4) = 16:15.

However, because standard definition digital video was originally based on digitally sampling analog television, the 720 horizontal pixels actually capture a slightly wider image to avoid loss of the original analog picture. In actual images, these extra pixels are often partly or entirely black, as only the center 704 horizontal pixels carry actual 4:3 or 16:9 image. Hence, the actual pixel aspect ratio for PAL video is a little different from that given by the formula, specifically 12:11 for PAL and 10:11 for NTSC. For consistency, the same effective pixel aspect ratios are used even for standard definition digital video originated in digital form rather than converted from analog. For more details refer to the main article.

In analog images such as film there is no notion of pixel, nor notion of SAR or PAR, and "aspect ratio" refers unambiguously to DAR. Actual displays do not generally have non-square pixels, though digital sensors might; they are rather a mathematical abstraction used in resampling images to convert between resolutions.

Non-square pixels arise often in early digital TV standards, related to digitalization of analog TV signals – whose horizontal and vertical resolutions differ and are thus best described by non-square pixels – and also in some digital videocameras and computer display modes, such as Color Graphics Adapter (CGA). Today they arise particularly in transcoding between resolutions with different SARs.

DAR is also known as image aspect ratio and picture aspect ratio, though the latter can be confused with pixel aspect ratio.

相关链接

FFMPEG 3.4.2 - ffmpeg源代码分析 (一)
FFMPEG 3.4.2 - ffmpeg源代码分析 (二)
FFMPEG 3.4.2 - ffmpeg源代码分析 (三)
FFMPEG 3.4.2 - ffmpeg源代码分析 (四)- x264
FFMPEG 3.4.2 - ffplay源代码分析 (一)
FFMPEG 3.4.2 - ffplay源代码分析 (二)
FFMPEG 3.4.2 - ffplay源代码分析 (三)

你可能感兴趣的:(FFMPEG 3.4.2 - ffplay源代码分析 (三))