H264码流RTP封装方式详解:rfc3984

2023-10-27

264码流RTP封装方式详解
文章目录
H264码流RTP封装方式详解
1 H264基本概念
2 NALU Header介绍
3 RTP封装H264码流
3.1 单一NALU模式
3.2 组合帧封装模式
3.3 分片封装模式
4 代码解析
在流媒体传输领域,经常使用的传输协议是RTP/RTCP,本文将对RTP对H264进行封装的过程进行详解
有关H264视频的RTP封装格式的规范参照:RFC-3984
有关RTP协议基础知识介绍参照:RTP协议与实战

1 H264基本概念
H264是一种视频压缩的标准,与H265、SVAC等压缩方式一样,主要目的是对视频流进行压缩,以便减少网络传输对网络带宽的占用,H264压缩后的帧类型分为I帧/P帧/B帧。
这里解释下帧类型:

I帧,为关键帧,采用帧内压缩技术,通过H264压缩算法解对完整帧数据进行压缩,因此I帧不依赖其前后帧数据,直接可以解压成raw数据
P帧,为向前参考帧,对raw帧压缩时,需要参考前面的I帧或P帧,采用帧间压缩技术,因此P帧解码不能单独解析,需要依赖前面的帧数据,如果其前面帧有丢失,会导致花屏。
B帧,为双向参考帧,对raw进行压缩时既参考前面的帧,又参考他后面的帧,采用帧间压缩技术,因此其解码是需要前面帧及后面帧同时参与。
在baseline编码规格下,无B帧,在main profile规格下一般有B帧,通常来说B帧参与编码会提高压缩率,降低帧大小,但是会增加编解码复杂性。

H264编码后的码流又NAL(网络抽象层)和VCL(视频编码层)构成,VCL数据传输或者存储之前,会被映射到一个NALU中,H264数据包含一个个的NALU,H.264的编码帧序列包括一系列的NAL单元,每个NAL单元包含一个RBSP,单元的信息头定义了RBSP单元的类型,NAL单元其余部分为RBSP数据,这里对NALU头字节进行i二少,因为RTP封装方式,主要针对NALU头进行处理。

2 NALU Header介绍
NALU头前通常包含一个 StartCode,StartCode 必须是 0x00000001 或者 0x000001,紧接着就是一个字节的nalu header,nalu header格式如下:

这里注意上面的0~7b不是字节内位的高低,只是表示占了多少位,从左到右顺序才表示从高位到低位,其详解如下:

F:forbidden_zero_bit,H264 规范要求该位为 0,占1位,其所在字节的最高位(第7位)
NRI:nal_ref_idc,取值 03,占2位,其在字节的56位,指示该 NALU 重要性,对于 NRI=0的NALU 解码器可以丢弃它而不影响解码,该值越大说明NALU越重要。如果当前NALU属于参考帧的数据,或者是序列参数集,图像参数集等重要信息,则该值必须大于0
Type:NALU 数据类型,占5 位,其在字节的0~4位,取值 0 ~ 31
有关Type在H264码流中的常用取值如下:

type    描述    备注
1    非IDR的图像片    一般指P帧
5    IDR 图像的片    一般指I帧
6    SEI辅助增强信息帧    解码可不用
7    SPS序列参数集    一般SPS/PPS/I帧组合出现
8    PPS图像参数集    一般SPS/PPS/I帧组合出现
9    分节符    解码可不需要
10    序列结束    很少用到
11    码流结束    很少用到
12    填充    很少用到
以下是截取的H264视频流的二进制数据,结合此二进制数据,我们来分析下其帧类型


0000000109,其中00000001是帧起始位标识,09是nalu header,二进制为00001001,F(7位)为0标识非禁止,NRI(56位)为0标识可丢弃帧,type(04位)为9,表示分隔符,此帧可丢弃,表示分隔符

0000000167,同上面的分析,NALU头为67,二进制为01100111,F=0,NRI=3,type=7,帧类型为SPS帧,非常重要不可丢弃

0000000168,NALU头为68,二进制为01101000,F=0,NRI=3,type=8,帧类型为PPS帧,非常重要不可丢弃

0000000106,NALU头为06,二进制为00000110,F=0,NRI=0,type=6,帧类型为SEI,可丢弃

0000000165,NALU头为65,二进制为01100101,F=0,NRI=3,type=5,帧类型为I帧,非常重要不可丢弃

从上上面的分析可以看出,IDR(SPS/PPS/I)帧通常一起出现,极少数编码单独出现I帧,但是IDR与I帧单独出现也符合规范。

如下为P帧的帧头,可按照上述方式进行解析。


3 RTP封装H264码流
由于H264帧大小差别较大,较小的帧小于MTU,则可单包直接发送,或者多帧组合发送,较大的帧大于MTU需要分片发送,RTP发送H264模式主要有三种:单一NALU模式、组合帧封装模式、分片封装模式,有关组合帧封包和分片封包类型包含好几种,这里介绍常用的两种:STAP-A和FU-A。

这里把RTP头格式先贴出来,具体参数详解看下RFC-3984,后面说明用到


3.1 单一NALU模式
此模式一个RTP包包含一个完整的视频帧,RTP头部之后的一个字节为 NALU Header,之后是NALU数据部分,此视频帧大小需要小于MTU,可以单帧通过网络发送,其RTP封装格式如下:

以SPS为例,SPS一般小于MTU,可采用单NALU封装模式,其封包后的二进制如下:

80 e0 be 8e 8c e8 56 d5 4a 9b 57 b3 67 64 00 29 ad 84 05 45 62 b8 ac 54 74 20 2a 2b 15 c5 62 a3 a1 01 51 58 ae 2b 15 1d 08 0a 8a c5 71 58 a8 e8 40 54 56 2b 8a c5 47 42 02 a2 b1 5c 56 2a 3a 10 24 85 21 39 3c 9f 27 e4 fe 4f c9 f2 79 b9 b3 4d 08 12 42 90 9c 9e 4f 93 f2 7f 27 e4 f9 3c dc d9 a6 b4 03 c0 11 3f 2c a9 00 00 03 00 78 00 00 15 90 60 40 00 3e 80 00 00 11 94 06 f7 be 17 84 42 35
1
其中80 e0 be 8e 8c e8 56 d5 4a 9b 57 b3为RTP头,按照RTP协议格式解析如下:V=10B=2,Padding=0,Extension=0,CC=0,Mark=1,PT=96,SN=48782,TS=2364036821,SSRC=0x4a9b57b3

RTP头之后一个字节为NALU头,就是SPS帧去掉00000001起始位标识,为67,之后为NALU单元RBSP数据,在编码是简单的做法就是RTP头后直接附加去除起始标识的NALU数据即可。

3.2 组合帧封装模式
此模式是针对多个较小的帧,采用组合成一个RTP包发送的方式,此种方式在H264视频传输中较少使用,一般较小的帧都是通过单一NALU模式发送,此处以STAP-A组合类型为例,组合发送SPS和PPS
组合封包模式格式如下:


假设SPS和PPS的裸流如下:

sps
00 00 00 01 67 64 00 29 ad 84 05 45 62 b8 ac 54 74 20 2a 2b 15 c5 62 a3 a1 01 51 58 ae 2b 15 1d 08 0a 8a c5 71 58 a8 e8 40 54 56 2b 8a c5 47 42 02 a2 b1 5c 56 2a 3a 10 24 85 21 39 3c 9f 27 e4 fe 4f c9 f2 79 b9 b3 4d 08 12 42 90 9c 9e 4f 93 f2 7f 27 e4 f9 3c dc d9 a6 b4 03 c0 11 3f 2c a9 00 00 03 00 78 00 00 15 90 60 40 00 3e 80 00 00 11 94 06 f7 be 17 84 42 35
pps
00 00 00 01 68 33 3c b0 00 00
1
2
3
4
SPS和PPS组合帧封包后如下:
SPS去掉起始标志,size为117,十六进制为0x75;PPS去掉起始标志,size为0x06

[RTP header 12字节][78 00 75 67 64 00 29 ad 84 05 45 62 b8 ac 54 74 20 2a 2b 15 c5 62 a3 a1 01 51 58 ae 2b 15 1d 08 0a 8a c5 71 58 a8 e8 40 54 56 2b 8a c5 47 42 02 a2 b1 5c 56 2a 3a 10 24 85 21 39 3c 9f 27 e4 fe 4f c9 f2 79 b9 b3 4d 08 12 42 90 9c 9e 4f 93 f2 7f 27 e4 f9 3c dc d9 a6 b4 03 c0 11 3f 2c a9 00 00 03 00 78 00 00 15 90 60 40 00 3e 80 00 00 11 94 06 f7 be 17 84 42 35 00 06  68 33 3c b0 00 00]
1
其中:

78 STAP-A类型头,其中F为0 NRI为3,type为24,24标识STAP-A类型,此类型标识后续负载为组合帧

00 75 表示SPS的size,后面跟的0x75个自己为去掉起始标志的SPS数据

00 06 表示PPS的size,后面跟的6个字节为去掉起始位的PPS数据

3.3 分片封装模式
如果视频帧大小超过MTU,则RTP封装需要分片封装,H264较常用的分片模式位FU-A,这里详细说明的是FU-A分片方式,其格式如下:

RTP头部之后的第一个字节为 FU indicator,第二个字节为 FU header

FU indicator结构如下所示:


其与nalu header类似,F和NRI取分片的nalu header中对应的F和NRI,Type为分片类型,这里是28,不做详细说明

FU header结构如下:


其中

S:start标记位,当为1时表示NALU分片的起始分片。
E:end标记位,当为1时表示NALU分片的最后一个分片。
R:保留位,可忽略。
Type:NALU头里的Type类型,等于帧类型
这两个字节之后跟的是NALU的数据,去掉起始位及NALU header之后的数据,按照分片大小进行分包,同一个帧的分片的头两个字节除了起始和结束FU header中的S和E位不同,其他分片这两个自己都一样,这里起始分片要注意去掉H264起始字符和nalu header,通过FU indicator的F/NRI以及FU header即可组合成NALU header,RTP解封装的时候注意生成NALU头及起始标识。
实例如下:


0x7c:其二进制为:01111100,F=0,NRI=3,type=28表示FU-A分片,FU indicator
0x85:其二进制为:10000101,S=1,E=0,type=5,表示I帧的起始FU-A分片,fu header
其数据通过wireshark解析后如下图:

有关rtp相关知识及抓包示例,可关注公众号:壹零仓,发送消息rtp获取。

4 代码解析
一般H264进行RTP封装,SPS/PPS采用单一NALU封装方式,I帧/P帧采用FU-A分片模式,如果带有SEI及AUD可过滤掉,也可以采用单一NALU封装方式
有关H264采用单一NALU及FU-A分片进行RTP封装发送的相关代码详解,这里引用FFMPEG源码进行解析,这里引用部分打包的代码,解码和这个过程相反
ffmpeg源码
在libavformat/rtpenc_h264_hevc.c中,如下函数对H264及H265(HEVC)打包并发送

static void nal_send(AVFormatContext *s1, const uint8_t *buf, int size, int last)
{
    RTPMuxContext *s = s1->priv_data;
    enum AVCodecID codec = s1->streams[0]->codecpar->codec_id;

    av_log(s1, AV_LOG_DEBUG, "Sending NAL %x of len %d M=%d\n", buf[0] & 0x1F, size, last);
    if (size <= s->max_payload_size) {//判断包大小是否小于等于RTP最大负载长度,一般RTP最大负载长度+RTP头小于MTU
        ...
            flush_buffered(s1, 0);
            ff_rtp_send_data(s1, buf, size, last);//这里调用此函数直接发送,ff_rtp_send_data中会对数据直接打RTP头后直接发送,
                                                  //这里由于小于MTU,所以采用单一帧发送模式
        ...
    } else {//视频帧长度大于MTU时,采用FU分片
        ...
        if (codec == AV_CODEC_ID_H264) {//只对H264进行注释,H265后续在说
            uint8_t type = buf[0] & 0x1F;//这里buf已经去掉起始标识00000001,buf[0]标识nalu header,这里取0~4位,即帧类型
            uint8_t nri = buf[0] & 0x60;//这里取5-6位,即:NRI,这里只是通过按位与的方式,保留了5-6位,并未真正转换为真实值,方面后买你组合
            //FU indicator字节
            s->buf[0] = 28;        /* FU Indicator; Type = 28 ---> FU-A */
            s->buf[0] |= nri; //因为nri只是保留了5-6位,这里直接按位或,即可组成fu-indicator
            // fu header
            s->buf[1] = type; //0~4帧类型
            s->buf[1] |= 1 << 7; //最高位起始位为1标识开始
            buf  += 1; //原始H264起始分片需要去掉nalu heder字节,这里直接跳过帧头
            size -= 1; //去掉头字节后,size要减去1

            flag_byte   = 1;
            header_size = 2;//fu-a 头长度
        } else {
            ...
        }

        while (size + header_size > s->max_payload_size) {
            memcpy(&s->buf[header_size], buf, s->max_payload_size - header_size);//发送缓冲buf中已经有了FU-A的2个头字节
            ff_rtp_send_data(s1, s->buf, s->max_payload_size, 0);//发送分片
            buf  += s->max_payload_size - header_size;//h264码流指针移动到未发送的起始位置
            size -= s->max_payload_size - header_size;//未发送的码流数据
            s->buf[flag_byte] &= ~(1 << 7);//更改fu-header,中间分片为00
        }
        s->buf[flag_byte] |= 1 << 6;//更改fu-header,最后一个分片,结束标志值为1
        memcpy(&s->buf[header_size], buf, size);
        ff_rtp_send_data(s1, s->buf, size + header_size, last);//打RTP头并发送
    }
}
————————————————
版权声明:本文为CSDN博主「壹零仓」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/water1209/article/details/126019272

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

H264码流RTP封装方式详解:rfc3984 的相关文章

  • 配置:错误:无法运行C编译的程序

    我正在尝试使用 Debian Wheezy 操作系统在我的 Raspberry Pi 上安装不同的软件 当我运行尝试配置软件时 我尝试安装我得到此输出 checking for C compiler default output file
  • dlopen 或 dlclose 未调用信号处理程序

    我在随机时间内收到分段错误 我注册了信号 但发生分段错误时未调用信号处理程序 include
  • 如何在gnuplot中将字符串转换为数字

    有没有办法将表示数字 以科学格式 的字符串转换为 gnuplot 中的数字 IE stringnumber 1 0e0 number myconvert stringnumber plot 1 1 number 我可能使用 shell 命令
  • 使用 gcc 理解共享库

    我试图理解 C 中共享库的以下行为 机器一 cat one c include
  • 在 Docker 容器中以主机用户身份运行

    在我的团队中 我们在进行开发时使用 Docker 容器在本地运行我们的网站应用程序 假设我正在开发 Flask 应用程序app py具有依赖关系requirements txt 工作流程大致如下 I am robin and I am in
  • C++:Linux平台上的线程同步场景

    我正在为 Linux 平台实现多线程 C 程序 其中我需要类似于 WaitForMultipleObjects 的功能 在搜索解决方案时 我发现有一些文章描述了如何在 Linux 中实现 WaitForMultipleObjects 功能
  • 如何从远程 ssh 连接上运行的 tmux(复制模式)复制到本地剪贴板

    我通过 OS X 上的 VirtualBox 运行 Linux 我通过在无头状态下运行虚拟机 然后使用端口转发 sshing 到 Linux 机器来实现这一点 现在 无论复制到我的虚拟机上的剪贴板 我都可以粘贴到我的远程 ssh 会话上 但
  • XAMPP Windows 上的 Php Cron 作业

    嗯 我是这个词的新手CRON 据我所知 这是一个Unix安排特定操作在定义的时间间隔后执行的概念 我需要运行一个php文件 每小时更新一次数据库 但我的困惑在于安排执行 我在用XAMPP用于 Windows 7 上的本地开发测试 我发现了什
  • 计算 TCP 重传次数

    我想知道在LINUX中是否有一种方法可以计算一个流中发生的TCP重传的次数 无论是在客户端还是服务器端 好像netstat s解决了我的目的
  • 裸机交叉编译器输入

    裸机交叉编译器的输入限制是什么 比如它不编译带有指针或 malloc 的程序 或者任何需要比底层硬件更多的东西 以及如何才能找到这些限制 我还想问 我为目标 mips 构建了一个交叉编译器 我需要使用这个交叉编译器创建一个 mips 可执行
  • 可以作为命令行参数传递多少数据?

    在 Linux 下生成进程时可以发送多少字节作为命令行参数 gahooa 推荐了一篇好文章http www in ulm de mascheck various argmax http www in ulm de mascheck vari
  • SMP 上如何处理中断?

    SMP 对称多处理器 多核 机器上如何处理中断 内存管理单元是只有一个还是多个 假设两个线程 A 和 B 运行在不同的内核上 同时 访问页表中不存在的内存页面 在这种情况下 将会出现页面错误 并从内存中引入新页面 将会发生的事件的顺序是什么
  • 在非实时操作系统/内核上执行接近实时任务的最佳方法是什么?

    在一台 GNU Linux 机器上 如果想要执行 实时 亚毫秒级时间关键 任务 您几乎总是必须经历漫长 复杂且容易出现问题的内核补丁过程 以提供足够的支持 1 http en wikipedia org wiki RTLinux Backg
  • 如何在文件夹中的 xml 文件中 grep 一个单词

    我知道我可以使用 grep 在这样的文件夹中的所有文件中查找单词 grep rn core 但我当前的目录有很多子目录 我只想搜索当前目录及其所有子目录中存在的所有 xml 文件 我怎样才能做到这一点 我试过这个 grep rn core
  • 运行 shell 命令并将输出发送到文件?

    我需要能够通过 php 脚本修改我的 openvpn 身份验证文件 我已将我的 http 用户设置为免通 sudoer 因为这台机器仅在我的家庭网络中可用 我目前有以下命令 echo shell exec sudo echo usernam
  • 如何在特定的Java版本上运行应用程序?

    如何运行具有特定 Java 版本的应用程序 我安装了三个 Java 版本 myuser mysystem sudo update alternatives config java There are 3 choices for the al
  • Grep 递归和计数

    需要在具有大量子目录的目录中搜索文件内的字符串 我在用着 grep c r string here 我怎样才能找到总数量 如何仅输出至少具有一个实例的文件 使用 Bash 的进程替换 这给出了我认为是您想要的输出 如果不是 请澄清问题 gr
  • 用于获取特定用户 ID 和进程数的 Bash 脚本

    我需要 bash 脚本来计算特定用户或所有用户的进程 我们可以输入 0 1 或更多参数 例如 myScript sh root deamon 应该像这样执行 root 92 deamon 8 2 users has total proces
  • Apache LOG:子进程 pid xxxx 退出信号分段错误 (11)

    Apache PHP Mysql Linux 注意 子进程 pid 23145 退出信号分段错误 11 tmp 中可能存在 coredump 但 tmp下没有找到任何东西 我怎样才能找到错误 PHP 代码中函数的无限循环导致了此错误
  • 如何回忆上一个 bash 命令的参数?

    Bash 有没有办法回忆上一个命令的参数 我通常这样做vi file c其次是gcc file c Bash 有没有办法回忆上一个命令的参数 您可以使用 or 调用上一个命令的最后一个参数 Also Alt can be used to r

随机推荐

  • 手把手教你git入门(windows)

    git是一款版本管理工具 在日常科研过程中 我们往往会对某一个项目的文件进行多次修改 每次修改可能都会保存不同的版本 时间一长 各个版本之间的差异就会逐渐混淆 导致版本错乱 git主要的作用就是让你的本地文件保持在最新版本下 当你需要找到以
  • 《MATLAB智能算法30个案例》:第28章 支持向量机的分类——基于乳腺组织电阻抗特性的乳腺癌诊断

    MATLAB智能算法30个案例 第28章 支持向量机的分类 基于乳腺组织电阻抗特性的乳腺癌诊断 1 前言 2 MATLAB 仿真示例 3 小结 1 前言 MATLAB智能算法30个案例分析 是2011年7月1日由北京航空航天大学出版社出版的
  • C++实现:输入一行字符,分别统计出英文字母、空格、数字和其他字符的个数

    include
  • 【C3AE】《C3AE:Exploring the Limits of Compact Model for Age Estimation》

    CVPR 2019 文章目录 1 Background and Motivation 2 Related Work 3 Advantages Contributions 4 Method 5 Experiments 5 1 Datasets
  • 常用的前端大屏 适配方案

    From https blog csdn net ZXH0122 article details 128639247 方案实现方式优点缺点 vm vh1 按照设计稿的尺寸 将px按比例计算转为vw和vh1 可以动态计算图表的宽高 字体等 灵
  • STM32休眠时关闭看门狗计数的简单解决方案

    STM32休眠期关闭看门狗计数的简单解决方案 测试平台 问题的提出 问题的解决 源代码 测试平台 本文采用STM32L476进行测试 休眠模式为STOP 看门狗为独立看门狗IWDG 其余STM32芯片可参考本贴进行测试 问题的提出 在此之前
  • 关于二叉树的几种遍历方法

    转载请注明出处 http blog csdn net pony maggie article details 38390513 作者 小马 一 二叉树的一些概念 二叉树就是每个结点最多有两个子树的树形存储结构 先上图 方便后面分析 1 满二
  • SocketAsyncEventArgs Class

    Implements the connection logic for the socket server After accepting a connection all data read from the client is sent
  • 关于图片上传的问题(后端+前端)

    后端部分 首先下载multiparty 这个模块可以让你接受图片 可以接受图片 const multiparty require multiparty 配置图片上虚拟路由 router use upload express static u
  • 亲测!win11系统跳过联网,免登录账号使用

    哈喽 大家好 今天和小伙伴们来说说Windows11的那些事 相比很多小伙伴购买完新电脑后 开机预装的都是Windows11的系统吧 这也没办法 时代在发展 新机预装新系统也是必然趋势 这样问题便来了 因为很多小伙伴只想使用本地账户而不想注
  • 如何使用JavaScript将字符串转换为字符数组?

    在JavaScript中 可以使用split 和Array from 方法将字符串转换为字符串数组 下面本篇文章就来给大家介绍一下 希望对大家有所帮助 方法1 使用split 方法 split 方法用于将给定字符串拆分为字符串数组 该方法是
  • linux 设置端口密码,linux系统宝塔面板端口和密码忘记的处理方法

    登陆系统SSH 新装面板用户获取默认账号密码命令 bt default 查看端口 cat www server panel data port pl 以上只能看到最初始的登陆信息 修改密码方法 修改密码为123456 回车之后该条内容下面的
  • makefile生成动态库

    1 我要做一个动态库 动态库里面是我自己定义的函数 比如 我自定义一个dll init 函数 将其做成共享库 1 include
  • massif——程序堆内存分析工具

    1 安装教程 sudo apt get install massif visualizer sudo apt get install valgrind 2 使用方法 2 1 在roslaunch文件中增加launch prefix 例如
  • Mac苹果电脑思维导图Xmind 2022中文

    Xmind 2022中文是一款全新的思维导图软件 它具有新主题 其中一些具有更柔和的色调 以提供更现代的外观 它的独特功能是 禅宗模式 它将自动隐藏额外的面板 使您可以专注于自己的想法并添加到文档中 而不会分心 不管是UI界面设计还是性能都
  • 又一次自己编译Mono,这次是在Windows上,玩Bundle

    成功 又一次自己编译Mono 这次是在Windows上 玩Bundle 作者 V君 发布于 2017 10 30 21 07 Monday 分类 折腾手记 目标 将 net 应用程序用只用一个 exe 承载 并极大缩减体积 且能保证工作正常
  • Ansible的基础了解

    目录 第一章 Ansible概述 1 1 Ansible是什么 1 2 Ansible的特性和过程 1 3 ansible 具有如下特点 1 4 Ansible的四个组件 1 5 ansible 核心程序 1 6 ansible执行的过程
  • DevOps面试问题

    DevOps是一组过程 方法与系统的统称 用于促进开发 应用程序 软件工程 技术运营和质量保障 QA 部门之间的沟通 协作与整合 下面为大家分享DevOps系列的面试问题 持续整合问题 问题一 持续集成是什么意思 我将建议您通过给出持续集成
  • opencv分水岭算法分割硬币

    网上有Python写的 但是没有C 写的 所以自己搞了个 东拼西凑的结果 毕竟我也不是大神 凑活着看吧 倾情奉献 欢迎指教 include
  • H264码流RTP封装方式详解:rfc3984

    264码流RTP封装方式详解 文章目录 H264码流RTP封装方式详解 1 H264基本概念 2 NALU Header介绍 3 RTP封装H264码流 3 1 单一NALU模式 3 2 组合帧封装模式 3 3 分片封装模式 4 代码解析