x264源码解读(二)- VCL和NAL那些事

目录

ANNEXB vs. AVCC

VCL vs. NAL

signal函数响应键盘事件

收尾清理工作

x264的编码


ANNEXB vs. AVCC

今天我们继续来说一下x264结构中非常重要的属性annexb。

小贴士:编写经典的c库的大牛们都是会按照规范来进行编码,无论是ffmpeg(后面打算再开一个专栏)还是x264,他们结构体的成员还是变量,命名方式习惯采用“数据类型_变量名”,比如b_annexb,代表boolean型,即annexb是一个开关(flag)。

annexb如果为true,代表一个NALU的开始3位或者4位是start_code,所谓start_code没有逻辑意义,只是代表一个NALU的起始,或者说作为两个NALU的分隔符。那么就是是4位还是3位是由x264_nal_t的结构体成员b_long_startcode决定的:

typedef struct x264_nal_t
{
    ... ...
    int b_long_startcode;
    ... ...
} x264_nal_t;

那么代码中优势如何来进行3位和4位的处理呢?参看x264_nal_encode函数里面的实现:

void x264_nal_encode( x264_t *h, uint8_t *dst, x264_nal_t *nal )
{
    ... ...
    if( h->param.b_annexb )
    {
        if( nal->b_long_startcode )
            *dst++ = 0x00;
        *dst++ = 0x00;
        *dst++ = 0x00;
        *dst++ = 0x01;
    }
    else /* save room for size later */
        dst += 4;
  ... ...
}

这个b_long_startcode默认就是1,即采用4位的startcode,在源码中唯一设置地方就是在nal_start:

static void nal_start( x264_t *h, int i_type, int i_ref_idc )
{
    x264_nal_t *nal = &h->out.nal[h->out.i_nal];

    nal->i_ref_idc        = i_ref_idc;
    nal->i_type           = i_type;
    nal->b_long_startcode = 1;

    nal->i_payload= 0;
    nal->p_payload= &h->out.p_bitstream[bs_pos( &h->out.bs ) / 8];
    nal->i_padding= 0;
}

讲这个地方就是告诉大家在阅读源码的时候要关注核心成员的初始值,以及如果对他们进行修改,对于你们理解他们的行为是有帮助的。

如果annexb为false,即编码规则是AVCC,那么每个NALU将会用前面的4位来表示自己长度。这个处理可以在上面的x264_nal_encode函数中的else分支看到.

这里4位是byte,每个byte8位,4byte就是32位,代表能够表达的最大长度就是2^32,这里是我经常喜欢面试的时候喜欢和大家讨论的问题,但是我觉得很多人,即使是有几年C开发经验的大叔,对于byte以及取值范围无感,这个基础知识还是要敲敲黑板的。

什么是annexb?annex是附录的意思,annex b就是附录,这种startcode的语法描述就是在ITU-U发布的H264标准文档中的附录B中,进行语法描述的故得名:

很多童鞋在看到h264的语法的时候,会有困惑,这个是什么鬼?为什么一个协议还有语法,是的,开始我也不太敢相信:h264其实已经把伪代码给你写好了,为了避免描述性语言会有歧义,ITU-T那些老家伙们(是不是也有年轻小伙)直接把类C代码已经写好了,尽量介绍描述性语言的不确定性导致的歧义;所以h264具有高昂的收费也不是没有道理的,人家的工作的确实细致的很(有时间大家可以把这个文档通读一下,了解高额收费的文档都是怎么写的)。

VCL vs. NAL

上面一大堆讲完了,作为技术敏感的童鞋应该发现还有一个非常重要的概念,就是NALU。H264将整个编码解码过程抽象为了2层,大牛喜欢做的一类事情就是分层,我印象中最开始接触的分层就是TCP/IP协议(分层)以及ISO网络参考模型,为什么要分层?就是要聚焦和定位,每一个层都只是做一类事情,这样做架构设计非常清晰;而且优化以及定位问题也十分方便。

如上图所示,在H264里面将数据抽象为两层,其中VCL主要做的事情就是关注于编码/解码,NAL层则主要负责网络传输;什么编码/解码?比如对于帧的优化处理,例如宏块分割,分片等等就是在VCL中负责的,NAL层负责的则是如何将VCL层编码而成的packet进行封装序列化,保存到到本地,或者通过网络传输走,比如码率(每秒传输多少bits的数据)的控制就是NAL层要关心的事情。

NALU(NAL Unit)就是NAL层将VCL层输出的packet(帧frame编码/封装的形式)单元,一个帧可以是被打包到一个或者多个NALU,所以NALU是网络抽象层,视频传输数据的单元。关于一个Packet和一个NALU,他们之间其实加载了很多层(对象间)关系,从图像,片组,片...一直到像素,这个关系,我们后面将会介绍,本小节大家了解H264的VCL和NAL分层,以及NALU是网络抽象层传输的最小单元即可。另外,为什么叫网络抽象层?因为网络传输其实操作系统底层做的事情,操作系统层做的socket,socket调用的是网卡驱动,网卡驱动调用了网卡,那么在网卡之上的各层都称之为抽象层,因为并没有真是的处理网卡,对于NALU也是如此,它并不是物理层传输的数据包,只是定义了数据报的结构(成员),并为这些结构成员赋值而已。

到此main函数里面的x264_param_default函数就先介绍到这里,这里面重要的成员隐含的内容,我们在后面的源码解读中将会继续介绍。

signal函数响应键盘事件

main函数做的第二件事情就是signal( SIGINT, sigint_handler );signal是系统调用函数,用于响应信号,第一个参数SIGINT是一个终止信号,INT是理解为Interrupt比较直接一些,不过从Linux系统的注释来看INT是“INTeractive attention signal”,交互注意信号,简单讲就响应ctrl+c的键盘事件,第二个参数是sigint_handler,这个函数非常简单:

/* Ctrl-C handler */
static volatile int b_ctrl_c = 0;
static void sigint_handler( int a )
{
    b_ctrl_c = 1;
}

就是设置一个标志位,这个标志位将会在编码时候被处理:

static int encode( x264_param_t *param, cli_opt_t *opt )
{
    x264_t *h = NULL;
    x264_picture_t pic;
  
    ... ...
    for( ; !b_ctrl_c && (i_frame < param->i_frame_total || !param->i_frame_total); i_frame++ )
    {
        ... ...
    }
}

在循环对每一帧进行编码的时候,都会判断这个flag,所以当你编码过程中摁下了ctrl+c并不会马上出发,而是当一帧被编码完成后才会被处理。

所以不要想当然的觉得ctrl+c就是终止程序,系统大大都给我们做好的,不然,这些都是人为在程序中进行处理的,终止的时机,甚至是否被处理都是在程序中控制的。

收尾清理工作

第四件事情就是做了一些清理工作,包括释放一些文件的句柄以及x264参数的内存释放工作;

/* clean up handles */
if( filter.free )
	filter.free( opt.hin );
else if( opt.hin )
	cli_input.close_file( opt.hin );
if( opt.hout )
	cli_output.close_file( opt.hout, 0, 0 );
if( opt.tcfile_out )
	fclose( opt.tcfile_out );
if( opt.qpfile )
	fclose( opt.qpfile );
x264_param_cleanup( &param );

其中x264_param_cleanup里面展示了教科书级别的c语言缓冲区数据释放:

REALIGN_STACK void x264_param_cleanup( x264_param_t *param )
{
    strdup_buffer *buf = param->opaque;
    if( buf )
    {
        for( int i = 0; i < buf->count; i++ )
            free( buf->ptr[i] );
        free( buf );
        param->opaque = NULL;
    }
}

c语言的内存泄漏很多时候就是因为只是释放了指针(第9行),而没有释放指针的内容,因为c语言是不提供类似于Java/ Python的垃圾回收机制,不会针对变量(内存区域)的引用数量自动实现释放,所以对于通过指针来管理的内存连续区域需要手工进行逐个释放;熟记这段代码,c的内存管理的段位就上升一级咯。

x264的编码

main函数最核心的还是做得第三件事情,就是encode,上一篇文章也提到了,x264库只是实现了encode(decode部分可以使用ffmpeg自带的h264的解码器来做);那么从下节开始,我们一起解读x264的华彩乐章:编码。

 

参考:

https://www.itu.int/rec/T-REC-H.264-201704-S

https://www.jianshu.com/p/eeecb0eb2c6e

PowerPoint Presentation (hhi.de)

《新一代视频压缩编码标准(第二版)》 毕节厚 王建主编

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
VCL(Visual Component Library)是Delphi语言中的一个重要组件库,为程序员提供了丰富的可视化控件和函数库,用于开发Windows应用程序。VCL码是Delphi开发环境内置的一部分,以Delphi语言编写,用于实现VCL控件的各种功能。 VCL码主要包括一系列单元文件,每个单元文件对应一个控件或一组相关的功能。在VCL码中,我们可以看到各种控件的创建、绘制、件响应等关键代码。通过阅读VCL码,我们能够深入理解控件的工作原理,可以对控件的行为进行定制和扩展。 例如,在VCL码中,我们可以找到TButton类的实现代码。TButton是VCL中常用的按钮控件,它继承自TWinControl类。在TButton码中,我们可以看到按钮控件的绘制代码,包括按钮的外观和状态的切换等。我们也可以找到按钮控件的件回调函数,比如OnClick件。通过阅读TButton码,我们可以了解按钮控件是如何被创建、绘制和响应件的。 除了单个控件的码,VCL码还包括一些共享的实用函数和类。例如,Vcl.Graphics单元提供了一些图形处理的函数和类,如画刷、画笔、绘制文本等。Vcl.Forms单元则提供了窗体控件的基础类和一些常用方法,如创建窗体、处理消息等。 VCL码的开放性使得开发者可以深入了解和定制VCL控件的行为。通过阅读VCL码,我们可以了解到控件的底层实现细节,帮助我们更好地理解控件的使用和调试。同时,VCL码也是一个学习Delphi语言和面向对象编程的宝贵资料库,有助于提高我们的编程能力和开发效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张叫兽的技术研究院

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值