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

本篇笔记是在学习 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

  • 程序中的 PES 数据包

    理论上传输流中 PES 的数据包分组数量没有任何限制,在他的缓冲器中写有 PS 的首部、 PES 的首部以及有效载荷(数据)。

    输入部分和解码器都可以访问缓冲区 decoder_fifo_t ,这个缓冲区里存放着按照 FIFO 顺序排列的等待被解码的 PES 数据包们。输入部分提供了用于控制这个缓冲区的宏定义:
      DECODER_FIFO_ISEMPTY,
      DECODER_FIFO_ISFULL,
      DECODER_FIFO_START,
      DECODER_FIFO_INCSTART,
      DECODER_FIFO_END,
      DECODER_FIFO_INCEND
    当然,作为缓冲区应当属于临界资源,所以在对FIFO序列进行任何操作之前都应记得对其加锁( p_decoder_fifo-> data_lock

    获取下一个解码的数据包可以使用 DECODER_FIFO_START(* p_decoder_fifo) ,之后使用 p_decoder_fifo-> pf_delete_pes(p_decoder_fifo-> p_packets_mgt,DECODER_FIFO_START(* p_decoder_fifo))

    接着再调用 DECODER_FIFO_INCSTART(* p_decoder_fifo)PES数据包返回到缓冲区管理器。

    FIFO 队列为空时( DECODER_FIFO_ISEMPTY )进程可以阻塞,直到新的数据包带着 cond 信号出现:vlc_cond_wait(&p_fifo-> data_wait,&p_fifo-> data_lock),在此之前应当保持锁定的状态。

    文件结束或用户退出时 p_fifo->b_die 会被置1,此时应当及时释放我们建立的数据结构并调用 vlc_thread_exit()

1.4 比特流(输入模块)

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

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

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

  • u32 GetBits(bit_stream_t * p_bit_stream,unsigned int i_bits):从位缓冲区返回下一个 i_bits 位。如果没有足够的位,则从 decoder_fifo_t 中读取下一个字节。此功能只能保证最多24位。虽然它可以一直工作到31位,但这只是一个边界问题。而且由于<<运算符的原因,我们不得不为32位读取编写一个不同的函数 GetBits32

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

  • u32 ShowBits(bit_stream_t * p_bit_stream,unsigned int i_bits):与 GetBits() 相同,只是在读完之后这些位不会被刷新,需要手动调用 RemoveBits() 。这个函数不能在24位以上工作,除非能在字节边界上对齐(见下一个函数)。

  • RealignBits(bit_stream_t * p_bit_stream):丢弃 n 个高位( n   < 8),这样缓冲区的第一位就对齐了一个字节的边界。寻找对齐的起始码(如 MPEG )时需要。

  • GetChunk(bit_stream_t * p_bit_stream,byte_t * p_buffer,size_t i_buf_len):它和 memcpy() 很像(一个模拟),但是是将一个比特流作为第一个参数。必须为它分配 p_buffer ,并且至少需要 i_buf_len long 。主要是用来复制你想跟踪的数据。

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 算法并将数据拷贝到最终的图像中。编写新的编解码器则需要定义一下七个函数:

  • vdec_IDCT(decoder_config_t * p_config,dctelem_t * p_block,int):完整的 2-D IDCT.64 系数是否在 p_block 中存放。
  • vdec_SparseIDCT(vdec_thread_t * p_vdec,dctelem_t * p_block,int i_sparse_pos):在一个非 NULL (由 i_sparse_pos 指定)的块上执行 IDCT 。可以用 plugins / idct / idct_common.c 中定义的函数,函数将在初始化时对64个矩阵进行预处理。
  • vdec_InitIDCT(vdec_thread_t * p_vdec):是否需要由 vdec_SparseIDCT 初始化。
  • vdec_NormScan(u8 ppi_scan [2] [64]):通常这个函数什么都不做。对于较小的优化,一些 IDCT(MMX)需要反转 MPEG 扫描矩阵中的某些系数(参见 ISO / IEC 13818-2)时使用。
  • vdec_InitDecode(struct vdec_thread_s * p_vdec):初始化 IDCT 和可选裁剪表。
  • vdec_DecodeMacroblockC(struct vdec_thread_s * p_vdec,struct macroblock_s * p_mb):解码整个宏块并将其数据复制到最终图像,包括色彩信息。
  • vdec_DecodeMacroblockBW(struct vdec_thread_s * p_vdec,struct macroblock_s * p_mb):解码整个宏块,并将其数据复制到最终图像,不包括色彩信息(用于灰度模式)。

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

1.8 对称多处理

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271