iOS音视频开发视频开发音视频开发经验之路

Hacker Guide/Decoder:如何做一个解码器(VL

2018-01-16  本文已影响180人  团不慌

本篇笔记是在学习 Wiki - Hacker Guide /Decoder 过程中的简单翻译与记录,初学编码,敬请批评指正。

概要

1 如何书写一个解码器
  1.1 VLC中的解码器究竟是什么?
  1.2 解码器配置
  1.3 数据包的结构
  1.4 比特流(输入模块)
    1.4.1 数据包的更改与对齐问题
    1.4.2 警告
  1.5 内置解码器
    1.5.1 在当前版本的局限性
  1.6 MPEG解码器
  1.7 IDCT插件
  1.8 对称多处理


1 如何书写一个解码器

1.1 VLC中的解码器究竟是什么?

解码器是用于播放过程中将数据流按照某种规则进行解码的部件,也是流处理过程中偏数学化的部分。它与输入时的解复用过程相互分离,解复用模块主要负责管理数据包来重建输入流,在输出过程中才调用解码器对数据包(可以看成样品)进行解码,并将重构后的样本输出(播放)。所以解码器基本上无须和设备进行交互,只是一个单纯的算法。
至于解码器如何从输入中检索流以及一些API的接口将在后面讨论。

1.2 解码器的配置

输入线程从 src / input / decoder.c 中生成适当的解码器,创建解码器时依靠方法

CreateDecoder( input_thread_t *p_input,es_format_t *fmt, int i_object_type )

来创建一个 i_object_type 类型的 p_dec 变量,其中 i_object_type 在我们的 VLC_OBJECT_DECODER 中有定义。

CreateDecoder 函数调用之后我们可以使用 module_Need 函数以评分为依据去建立一个“可能模块”的能力列表。

module_Need( vlc_object_t *p_this, const char *psz_capability,const char *psz_name, bool b_strict )*
使用时可以令 *psz_capability="decoder" ,  *psz_name="$codec"

我理解的 model_Need 的列表像是对解码模块性能的评定(优先级),举个例子:

a52模快中配置:set_capability( "decoder", 100 );

即在这个模块中将我们请求解码器时 a52 模快返回的评分设置为了100,同样 VLC 中其他模块也会返回一个类似的评分,我们可以用这个评分来决定到底是否调用该模块,之后他还需要确定这个解码器是否需要将数据打包。最终通过以下的函数调用解码器:

vlc_thread_create( p_dec, "decoder", DecoderThread, i_priority, false )

1.3 数据包的结构

输入模块提供了一个很不错的API来完成传输数据流到解码器的过程,而数据包的结构的具体定义可以参看 include / input_ext-dec.h 文件。

data_packet_t 中包含了一个指向数据物理地址的指针。解码器从 p_payload_start 开始读,到 p_payload_end 结束,如果下一个包不为空就继续读取(p_next),如果发现某个包的 b_discard_payload 标志被激活则代表包内数据出现错误,并直接把这个包丢弃。

pes_packet_t 中记录了 data_packet_t 的头结点 p_first ,并把它看做一个完整的 PES 数据包(此处是以 MPEG 为例)。当然这里所谓的链表并不是平时意义上的链表,在 PS 流中一个 pes_packet_t 一般只记录一个 data_packet_t ,而在 TS 流中一个 PES 数据包会被分成十几个 TS 数据包。

一个 PES 数据包中包含的信息很多(具体可以参看 MPEG 的规范),其中主要有 PST 日期以及用来协调读取速度的内插日期(current pace of reading that should be applied for interpolating dates, 不太清具体名词,变量名为 i_rate ),以及用于指示该分组是否为随机接入点的 b_data_alignment 和记录前一个分组是否被丢弃的 b_discontinuity

1.4 比特流(输入模块)

前面的缓存区方法所代表的传统数据包读取方式太不方便(因为流可以被任意分割),所以输入模块为我们提供了更加方便的比特流读取原语*。在我们读取比特流时可以选择使用这些原语或者缓冲区,不过只能二选一(不然加锁解锁什么的会冲突的吧)。
注:既然是原语则不应也不可被打断

我们可以直接调用 GetBits 函数透明的*读取数据包到缓冲区,并在必要时对数据包进行改变。我们也无须关心边界问题和队列的维护。
注:透明的即过程对使用者不可见,过程无须干预

比特流的核心思想是引入一个32位的缓冲区 bit_fifo_t (一般是字型—— WORD_TYPE ,但64位版目前还不行),这个缓冲区包含字缓冲区以及有效位数(高位部分),输入模块提供了五个函数来管理它:

RemoveBits(bit_stream_t * p_bit_stream,unsigned int i_bits):与 GetBits() 相同,但不返回数据(可以节省几个CPU周期)。它有相同的限制,所以也必须有 RemoveBits32

1.4.1 数据包的更改与对齐问题

由于data_packet_t 应该具有偶数个字节,而且某些CPU只能读取字边界对齐的字,所以我们必须对不符合格式的数据进行截断,然后读取被截断的单词并进行对齐。

比如在 GetBits()中将从 src / input / input_ext-dec.c 中调用 UnalignedGetBits()。一般他会一个一个的读取字节,直到流重新排列。当然 UnalignedShowBits() 可能有点复杂,而且可能需要临时数据包(p_bit_stream-> showbits_data)

要使用比特流,我们必须调用 p_decoder_config-> pf_init_bit_stream(bit_stream_t * p_bit_stream,decoder_fifo_t * p_fifo)来设置所有变量。然后需要定期从数据包中获取某些信息比如PTS。如果 p_bit_stream-> pf_bit_stream_callback 不是 NULL ,那么这个函数就将在数据包更换时被调用。比如在 src / video_parser / video_parser.c 中。第二个参数可以用于区分数据包到底是一个新的 data_packet_t 还是一个新的 pes_packet_t 。我们可以将自己的结构存储在 p_bit_stream-> p_callback_arg 上。

1.4.2 警告

当我们使用 pf_init_bit_stream 时,很有可能 pf_bitstream_callback 还没有被定义,但程序仍会跳转到第一个数据包,这时候我们应该在 pf_init_bit_stream 后手动回调比特流。

1.5 内置解码器

VLC 提供了 MPEG 第一层和第二层的音频解码器、MPEG MP@ML 的视频解码器、AV3、DVD SPU、LPCM 等解码器,我们可以仿照他们写一个自己的解码器。

1.5.1 在当前版本的局限性

要添加一个新的解码器必须要添加流的类型,因为在 src / input / input_programs.c 中仍然有一段硬编码的代码。

MPEG 音频解码器是原生的但并不支持第3层解码(太麻烦), AC3 解码器来自 Aaron Holtzman 的 libac3 (原始libac3 不重复)的端口,并且 SPU 解码器是本地的。 如果想看 AC3 解码器中的 BitstreamCallback 可以看看。 不过在应该跳过 PES 数据包的前3个字节,它们不是基本流的一部分。 视频解码器有点特别,将在后面进行介绍。

1.6 MPEG解码器

VLC 播放器提供了 MPEG-1MPEG-2 Main Profile @ Main Level 解码器。这本来就是为 VLC 编写的,所以已经相当成熟了。不过它的情况有点特殊,因为它被分成了两个逻辑实体:视频分析器和视频解码器,源于最初想将位流解析函数从高度可并行化的数学算法中分离出来。它理论上应该有一个视频解析器线程(只有一个线程以防竞争),以及一个视频解码器线程池,一次对多个块进行 IDCT 和运动补偿模快。

VLC不会支持 MPEG-4DivX解码 ,因为这些不是编码器。当然它支持整个 MPEG-2 MP @ ML 规范,虽然有些功能尚未测试,比如差分运动向量。请牢记在输入基本流必须有效(比如不能直接读取 DVD 多角度 .vob 文件)。

最有趣的文件是 vpar_synchro.c ,它真的值得一看。它解释了整个丢帧算法。如果机器足够强大,我们就解码所有的 IPB ,如果我们时间足够,我们就解码所有的 IPBs。另一个有趣的文件是 vpar_blocks.c ,它描述了所有的块(包括系数和运动矢量)解析算法。这个文件的底部为每个常见的图片类型生成了一个优化的函数,和慢迭代的函数。还有几个分级的优化(编译速度较慢,但​​某些类型的文件解码更快)称作 VPAR_OPTIM_LEVEL,级别0意味着没有优化,级别1意味着优化 MPEG-1 和 MPEG-2 帧图像,级别2意味着对 MPEG -1 和 MPEG-2 字段和帧图片,等等。

运动补偿(即来自参考图片的区域的副本)非常依赖于平台(比如MMX或AltiVec),所以VLC将其移至 plugins / motion 目录下。视频解码器将更为方便,其他视频解码器(MPEG-4 之类的)也可使用插件工作。

运动插件必须定义6个函数:
  vdec_MotionFieldField420,
  vdec_MotionField16x8420,
  vdec_MotionFieldDMV420,
  vdec_MotionFrameFrame420,
  vdec_MotionFrameField420,
  vdec_MotionFrameDMV420。
等效的4:2:2和4:4:4函数因为 MP @ ML 禁止所以没被列进来。

如果你想要更多的信息可以看看C语言版本的代码。不过需要注意DMV算法还没有未经测试,可能还有点 Bug 。

1.7 IDCT插件

和运动补偿类似,IDCT 也是针对平台存在区分的,所以VLC也把他放在了插件里,具体路径是 plugins/idct 这个模块主要用来执行 IDCT 算法并将数据拷贝到最终的图像中。编写新的编解码器则需要定义一下七个函数:

目前 VLC 已经为 MMX , MMXEXT 和 AltiVec 实现了优化版本。有两个普通的 C 版本,基础的(被认为是优化的)伯克利版本(idct.c),以及来自 ISO 参考解码器( idctclassic.c )的简单的一维分离 IDCT 算法。

1.8 对称多处理

我们可以根据需要使用多处理器进行处理,主要方法就是使用处理器池一次对多个宏块进行 IDCT 或运动补偿的处理,管理多处理池的方法在 src / video_decoder / vpar_pool.c 中有调用,不过不推荐在非 SMP 机器上使用,那样可能会比单线程还慢...

上一篇 下一篇

猜你喜欢

热点阅读