[ffmpeg] h264并行解码

ffmpeg中的并行解码分为两种:html

 

Frame-level Parallelism

帧间依赖

咱们以前讨论过Frame-level Parallelism。在以前的文章中,咱们说过在进行帧级的并行处理时,因为I、P帧是做为参考帧(B帧也能做为参考帧),所以不能对其进行并行处理,只有非参考B帧才是最适宜进行并行处理的帧。不过其实若是咱们能正确地处理好各个帧之间的依赖关系,不管是I、P仍是B帧都能进行并行处理。FFmpeg为了达到这一目的,对咱们以前所讨论的Frame-level Parallelism进行了改进:添加帧间依赖。多线程

h264的帧间依赖主要有两个:函数

  • 进行运动补偿时须要参考帧提供参考区域的像素数据。在解码一个宏块的过程当中,获得宏块中每一个分块的运动向量与参考帧后,就能肯定宏块的各个分块的参考区域,而后就能够从这些区域提取像素数据进行运动补偿。
  • 编码模式为B Direct的宏块在计算运动向量时,须要位于第一个后向参考帧的co-located块的运动向量,用来计算出当前帧的前向以及后向运动向量。

image

FFmpeg对于上述依赖的解决方案是:编码

  • 对于每一个分块,在进行运动补偿以前,等待该分块所依赖的参考区域的最后一行(以像素为单位,表示这一行的值row是相对于整幅图像来讲的,下同)的就绪消息[1]
  • 对于编码模式为B Direct的宏块,在计算运动向量以前,等待co-located块的第一行的就绪消息(因为咱们只须要该co-located块的运动向量,既然第一行像素已就绪,那么运动向量确定是已经就绪了)[2]
  • 解码器每解码完成一行宏块,就通知发送该宏块的最后一行的就绪消息(若是当前视频指定了deblocking,因为当前行宏块的deblocking须要用到下一行宏块的上方4行像素,所以就绪的row值须要进行相应的调整)[3]

 

Packet与Frame

在讨论FFmpeg的实现以前,咱们须要先了解packet(AVPacket)与frame(AVFrame)之间的关系。不一样的编码格式也许会有所不一样,不过h264在FFmpeg中的一个packet中所包含的数据就是一个frame(一帧)。通常状况下一帧就是一个slice,这样的话一个packet中只有一个slice;固然,一帧也有可能会分为多个slice,若是是这种状况的话,一个packet中会包含这一帧全部的slice。线程

咱们之因此在这里讨论这二者之间的关系,是由于FFmpeg每次都是以一个packet为单位向解码器传入须要解码的数据,也就是说每次会向h264解码器传入一帧的数据。3d

 

实现方案[4]

FFmpeg实现方案以下:code

image

Thread List,线程列表,线程列表中的每一项都映射一个解码线程。主线程会从线程列表中按照序号由小到大(循环)提取解码线程,并把解码任务提交到该解码线程。同时主线程在提交完解码任务后也会从线程列表中按照序号由小到大(循环)提取解码线程,并尝试从该解码线程获取解码完成的帧。orm

M,主线程,主要目的有两个:视频

  • 向解码线程提交解码任务。FFmpeg中是以packet为单位进行解码任务的提交的,按照前一小节的描述,FFmpeg就是以frame为单位进行解码任务的提交的。
  • 从解码线程获取解码所得的帧并进行返回。不过在第一轮进行任务提交的时候是不会去获取帧,在第一轮任务提交完成后,此时全部解码线程都已经开始进行了解码做业,那么主线程就能够开始等待第一个线程解码完成,而后尝试去得到解码完成的帧(这里的“尝试”,是由于就像单线程解码时那样,并不必定是每次调用解码API都会返回一帧的。因为h264编码的视频中经常包含B帧,这会使得码流的解码顺序并不是帧的播放顺序,可是解码API必须按照帧的播放顺序进行返回,所以在进行帧的返回时会进行相应的调整)。接下来每次向一个线程提交一个解码任务后,都须要等待下一个线程空闲并尝试返回帧。

image

T,解码线程,接收解码任务并进行解码。解码线程是以frame为单位进行处理的。解码线程解码主线程所提交的packet,执行与单线程时同样的解码做业,固然在解码做业期间会碰到咱们上面所述的帧间依赖并进行处理。htm

 

隐式的帧间依赖

帧间依赖除了上面所述的明显存在的帧间依赖以外,还有一处较为隐蔽的帧间依赖。

解码所需的参考图像列表依赖于POC,而在计算图像POC时,须要对相邻两个frame(或者说slice)头部的pic_order_cnt_lsb或者frame_num进行比较。这就代表在开始一个frame的解码以前,须要把上一个frame的这些参数传入当前frame。有了上一个frame头部的这些参数,当前的frame就能按照单线程解码那样准确地计算出当前frame的POC。

FFmpeg把这参数传入操做实如今了ff_h264_update_thread_context当中,该函数会在提交解码任务前被调用[5]

 

 

Slice-level Parallelism

如咱们以前讨论过的Slice-level Parallelism,ffmpeg的slice级并行只能在帧内并行。所以,若是在某个视频在编码时,一帧图像分为多个slice进行编码的话,那么在使用ffmpeg解码时调用slice级并行解码就会获得不错的效果。而在实际应用中,大多数h264编码的视频都是一帧只有一个slice,对于这种视频,就算采用了slice级并行,也只有一个线程在进行解码做业。

 

实现方案

若是一帧,即一个packet分为几个slice时,会先把这一帧前面的slice加入队列,到最后一个slice时统一对这一帧的全部slice进行并行解码[6]。其中涉及到的关键要素以下:

image

Slice Context List,slice的上下文是slice context(FFmpeg中的变量为slice_ctx),若是一帧中有多个slice,那么会把slice上下文组成一个列表。前面所说的入队列操做会对该列表进行填充以供后续解码使用。

M,主线程,如单线程同样的流程,从用户调用解码API一直执行到咱们前面所说的入队列,到最后一个slice时会调用一个入口函数启动多线程解码操做。在调用入口函数后,主线程参与的多线程解码过程一共包含三个步骤[7]

  1. 经过发送启动消息激活其它正在等待的解码线程。
  2. 在启动多线程解码后,主线程也会一同做为其中一个线程进行slice的解码。
  3. 最后等待全部线程完成任务后返回。

T,解码线程,接收到主线程所发起的启动消息后,解码线程会到Slice Context List去提取其中一个slice context(原子操做),而后进行slice解码[8]

 

※在进行slice并行解码时deblocking是没法超越slice边界的,若是视频指定了超越边界的deblocking,那么deblocking须要要留到全部slice解码完成后再作。与此同时,若是指定ffmpeg进行快速解码,也会在解码线程内进行deblocking,不过此时的deblocking就是对本来进行超越边界的deblocking进行了非超越边界的deblocking,会影响视频图像质量[9]

 

Example

ffmpeg只要在打开解码器以前进行以下设置就能够执行并行解码。

    avcodec_alloc_context3(NULL);
    avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[Stream]->codecpar);
    Codec = avcodec_find_decoder(pCodecCtx->codec_id);

    pCodecCtx->thread_count = 4;
    pCodecCtx->thread_type = FF_THREAD_FRAME;
    //pCodecCtx->thread_type = FF_THREAD_SLICE;

    avcodec_open2(pCodecCtx, pCodec, NULL);

两行分别为:

设置并行解码数目,即解码线程数。

设置并行解码类型为FF_THREAD_FRAME或者FF_THREAD_SLICE,分别对应Frame-level Parallelism以及Slice-level Parallelism。

 

Reference:

1. await_references

2. await_reference_mb_row

3. decode_finish_row

4. ff_thread_decode_frame

5. submit_packet

6. decode_nal_units

7. avpriv_slicethread_execute

8. thread_worker, run_jobs, worker

9. h264_slice_init

相关文章
相关标签/搜索