最近在看RTP发送H264数据的文章,感觉很乱,没有比较清晰易懂的教程,自己整理了一下各种资料,备忘!

--------Part A  ----

先说说H264数据,H264在网络传输的是NALU(NAL单元),NALU的结构是:NAL头+RBSP,实际传输中的数据流如图所示:

NALU头用来标识后面的RBSP是什么类型的数据,他是否会被其他帧参考以及网络传输是否有错误。

NALU头结构为1个字节,既 forbidden_bit(1bit) + nal_reference_bit(2bit) + nal_unit_type(5bit),如下

1.forbidden_bit: 禁止位,初始为0,当网络发现NAL单元有比特错误时可设置为1,以便接收方纠错或丢掉该单元。

2.nal_reference_bit:nal重要性指示,标志该NAL单元的重要性,值越大,越重要,解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU

3.nal_unit_type:NALU类型取值如下表所示:

H264的NALU头里面.nal_unit_type的有效值只会是1~23,在打RTP包时,在某些情况下我们会填24-31,后面我们会解释,如下:

图3(打RTP包时会用到的扩展类型)

对H264数据打RTP包而言,我们了解这些基础知识就ok了,深入了解H264,请查阅其它资料.

--------Part B  ----

发送RTP数据报时,需要设置头部(Header)和负载(Payload)两部分,也就是“数据头+数据”这样的形式。先来看下Header。

V版版号(2bit),

P填充位(1bit),

X扩展位(1bit),

CC是CSRC的计数位(4bits);

M标记位(1bit);

PT有效载荷的类型(7bits),比如h264视频对应的值就是PT= 96;

sequence number(2Bytes),RTP包的发送序号;

timestamp时间戳位(4Bytes);

SSRC同步标识位(4Bytes);

CSRC不是RTP必须的(4Bytes)。

这样的话,用一个结构体来存储RTP的Header数据,如下,

typedef struct
{
/**//* 1byte (0) */
unsigned char csrc_len:4; /* expect 0, csrc计数器,没啥用*/
unsigned char extension:1; /* expect 1, see RTP_OP below ,扩展位,不用关心*/
unsigned char padding:1; /*expect 0 , 填充位,不用关心*/
unsigned char version:2; /* expect 2,版本号,固定为2 */
/**//* 1byte (1) */
unsigned char payload:7; /* RTP_PAYLOAD_RTSP , 负载类型*/
unsigned char marker:1; /* expect 1,是否是尾包,后面解释 */
/**//* 2bytes (2, 3) */
unsigned short seq_no; /* RTP包序号,比如100,101,102,类推 */
/**//* 4bytes (4-7) */
unsigned long timestamp; /* 时间戳位 */
/**//* 4bytes (8-11) */
unsigned long ssrc; /* stream number is used here. 在本RTP会话中全局唯一就行*/
// CSRC(4Bytes)不是RTP必须的,因此不定义它
} RTP_FIXED_HEADER;

在讲负载(Payload)前,我们先看看RTP以UDP发送h264数据时的3种打包情况。

由于UDP数据报长度超过1500字节时(俗称MTU),会自动拆分发送,增大了丢包概率,那么去除UDP数据报头以及RTP的Header部分,一般设置Payload部分最大长度为1400字节即可,那么对H264的NALU单元打RTP就意味着3种情况.

第一:RTP包里只包含一个NALU,(它的数据小于1400字节)

第二:RTP包里只包含N个NALU,(N个NALU的数据累加小于1400字节)

第三:NALU数据大于1400字节, (比如5400字节,5400/1400>3.8,要拆分分4个RTP包)

但是我们处理H264数据时,一般是对NALU逐一进行处理的,因此我们只考虑第一和第三种情况。

我们来看第一种情况,RTP包里只包含一个NALU的情况,这时的RTP负载(Payload)部分如下图

从内存分布上可以理解为 RTP PlayLoad = [PlayLoad_head] + [RBSP]

我们知道一个 NALU = [NAL_head] + [RBSP]

我们还发现,PlayLoad_head 和 NAL_head 都是一个字节,结构相同,如下

在这种情况下,PlayLoad_head 和 NAL_head的结构和值都是一样的,因此

RTP PlayLoad = NALU= [NAL_head] + [RBSP]

假如一个H264的NALU是这样的:[00 00 00 01 67 42 A0 1E 23 56 0E 2F ... ]

这是一个序列参数集NAL单元。[00 00 00 01]是四个字节的开始码,67是NALU头,42开始的数据是NALU内容(RBSP),封装成RTP包将如下:  [RTP HEADER] + [67 42 A0 1E 23 56 0E 2F ...]

发送单一NALU单元的伪代码如下:

{
RTP_FIXED_HEADER *rtp_hdr;
rtp_hdr =(RTP_FIXED_HEADER*)&sendBuf[0];
rtp_hdr->payload = H264; //也就是96
rtp_hdr->version = 2;
rtp_hdr->marker = 0;
rtp_hdr->ssrc = htonl(10); //全局唯一就行

if(nalu_t->len<=MAX_RTP_PKT_LENGTH)
{
rtp_hdr->marker = 1; //一个Nalu单元只用了一个RTP包,当然也算尾包
rtp_hdr->seq_no = htons(seq_num ++); //序号+1

nalu_payload = &sendBuf[12]; //RTP头就12字节
// nalu_t->buf第一个字节就是NALU_head
memcpy(nalu_payload, nalu_t->buf, nalu_t->len);

ts_current=ts_current+timestamp_increse; //时间戳
rtp_hdr->timestamp = htonl(ts_current);
bytes = nalu_t->len + 12 ;

::send(socketFd, sendBuf, bytes, 0);
}

我们来看第三种情况,一个NALU拆分为N个RTP包的情况,这时的RTP负载(Payload)部分如下图

这种模式叫分片封包模式,其中 FU Indicator和FU header都是1个字节,结构如下:

我们发现FU Indicator的结构和NALU头结构是一样的,不过值不完全一样

U Indicator->F    = NALU头结构-> F
FU Indicator->NRI  = NALU头结构->NRI
FU Indicator->Type  = 28 (也就是扩展类型,FU-A)
另外

FU header->Type = NALU头结构->Tpye
FU header->S = 开始位,设置成1,指示分片NAL单元的开始,也就是开始包
FU header->D = 结束位,设置成1,指示分片NAL单元的结束,也就是尾包
FU header->R = 保留位必须设置为0

分包模式下的RTP包如下:

[RTP HEADER] + [FU Indicator] + [FU header] + [RBSP]

分包模式的伪代码如下:

RTP_FIXED_HEADER *rtp_hdr;
rtp_hdr =(RTP_FIXED_HEADER*)&sendBuf[0];
rtp_hdr->payload = H264; //
rtp_hdr->version = 2;
rtp_hdr->marker = 0;
rtp_hdr->ssrc = htonl(10);

int k=0,l=0;
k = nalu_t->len/MAX_RTP_PKT_LENGTH; // 比如k为7,表示有7个完整RTP包+1个尾包
l = nalu_t->len%MAX_RTP_PKT_LENGTH; // 比如l为126,表示尾包的RBSP为126个字节
int t=0;
ts_current = ts_current+timestamp_increse; //时间戳
rtp_hdr->timestamp = htonl(ts_current);

while(t<=k)
{
rtp_hdr->seq_no = htons(seq_num ++); //序列号累加
if(!t) //第一包
{
rtp_hdr->marker=0; // 不是尾包
fu_ind =(FU_INDICATOR*)&sendBuf[12];
fu_ind->F=nalu_t->forbidden_bit;
fu_ind->NRI=nalu_t->nal_reference_idc>>5;
fu_ind->TYPE=28;

fu_hdr =(FU_HEADER*)&sendBuf[13];
fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->S=1;
fu_hdr->TYPE=nalu_t->nal_unit_type;

nalu_payload=&sendBuf[14];
memcpy(nalu_payload,nalu_t->buf+1,MAX_RTP_PKT_LENGTH);

bytes=MAX_RTP_PKT_LENGTH+14;
::send( socketFd, sendBuf, bytes, 0 );
t++;
}
else if(k==t) // 尾包的情况
{
rtp_hdr->marker=1; // 尾包
fu_ind =(FU_INDICATOR*)&sendBuf[12];
fu_ind->F=nalu_t->forbidden_bit;
fu_ind->NRI=nalu_t->nal_reference_idc>>5;
fu_ind->TYPE=28;

fu_hdr =(FU_HEADER*)&sendBuf[13];
fu_hdr->R=0;
fu_hdr->S=0;
fu_hdr->TYPE=nalu_t->nal_unit_type;
fu_hdr->E=1;

nalu_payload=&sendBuf[14];
memcpy(nalu_payload,nalu_t->buf+t*MAX_RTP_PKT_LENGTH+1,l-1);
bytes=l-1+14;
::send( socketFd, sendBuf, bytes, 0 );
t++;
}
else if(t<k&&0!=t) // 注意,头包,中间包,尾包,fu_hdr->R,fu_hdr->S,fu_hdr->E 的值不一样
{
rtp_hdr->marker=0;
fu_ind =(FU_INDICATOR*)&sendBuf[12];
fu_ind->F=nalu_t->forbidden_bit;
fu_ind->NRI=nalu_t->nal_reference_idc>>5;
fu_ind->TYPE=28;

fu_hdr =(FU_HEADER*)&sendBuf[13];
fu_hdr->R=0;
fu_hdr->S=0;
fu_hdr->E=0;
fu_hdr->TYPE=nalu_t->nal_unit_type;

nalu_payload=&sendBuf[14];
memcpy(nalu_payload,nalu_t->buf+t*MAX_RTP_PKT_LENGTH+1,MAX_RTP_PKT_LENGTH);
bytes=MAX_RTP_PKT_LENGTH+14;
::send( socketFd, sendBuf, bytes, 0 );
t++;
}
}

这里只列出了FU-A(Type = 28)的分包模式,FU-B的分包模式没测试过,应该差不多

NALU数据打RTP包流程详解的更多相关文章

  1. linux驱动由浅入深系列:高通sensor架构实例分析之三(adsp上报数据详解、校准流程详解)【转】

    本文转载自:https://blog.csdn.net/radianceblau/article/details/76180915 本系列导航: linux驱动由浅入深系列:高通sensor架构实例分 ...

  2. Lucene系列六:Lucene搜索详解(Lucene搜索流程详解、搜索核心API详解、基本查询详解、QueryParser详解)

    一.搜索流程详解 1. 先看一下Lucene的架构图 由图可知搜索的过程如下: 用户输入搜索的关键字.对关键字进行分词.根据分词结果去索引库里面找到对应的文章id.根据文章id找到对应的文章 2. L ...

  3. 大数据入门第八天——MapReduce详解(三)MR的shuffer、combiner与Yarn集群分析

    /mr的combiner /mr的排序 /mr的shuffle /mr与yarn /mr运行模式 /mr实现join /mr全局图 /mr的压缩 今日提纲 一.流量汇总排序的实现 1.需求 对日志数据 ...

  4. 抓包工具:tcpdump抓包命令详解

    抓包工具:tcpdump抓包命令详解 简介: tcpdump全称:dump the traffic on a network,根据使用者的定义对网络上的数据包进行截获的包分析工具. tcpdump可以 ...

  5. FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  6. Java构造和解析Json数据的两种方法详解二

    在www.json.org上公布了很多JAVA下的json构造和解析工具,其中org.json和json-lib比较简单,两者使用上差不多但还是有些区别.下面接着介绍用org.json构造和解析Jso ...

  7. golang格式化输出-fmt包用法详解

    golang格式化输出-fmt包用法详解 注意:我在这里给出golang查询关于包的使用的地址:https://godoc.org    声明: 此片文章并非原创,大多数内容都是来自:https:// ...

  8. git概念及工作流程详解

    git概念及工作流程详解 既然我们已经把gitlab安装完毕[当然这是非必要条件],我们就可以使用git来管理自己的项目了,前文也多多少少提及到git的基本命令,本文就先简单对比下SVN与git的区别 ...

  9. JPEG图像压缩算法流程详解

    JPEG图像压缩算法流程详解 JPEG代表Joint Photographic Experts Group(联合图像专家小组).此团队创立于1986年,1992年发布了JPEG的标准而在1994年获得 ...

随机推荐

  1. FPGA中IBERT核的应用(转)

    https://wenku.baidu.com/view/50a12d8b9ec3d5bbfd0a74f7.html (必看)    摘要 IBERT即集成式比特误码率测试仪,是Xilinx专门用于具 ...

  2. 1.Python爬虫入门一之综述

    要学习Python爬虫,我们要学习的共有以下几点: Python基础知识 Python中urllib和urllib2库的用法 Python正则表达式 Python爬虫框架Scrapy Python爬虫 ...

  3. <Using parquet with impala>

    Operations upon Impala Create table stored as parquet like parquet '/user/etl/datafile1' stored as p ...

  4. 百杂讲堂之为什么32位系统只能操作4g内存

    百杂讲堂之为什么32位系统只能操作4g内存 计算机内存中很多的单元,每一个单元就是一个字节,一个字节有8位.每一个单元有两种状态:0和1. 所以 两个单元就有4个组合: 3个单元就有8个组合: 依次类 ...

  5. kbmMW User authentication

    任何信息系统的一个非常重要的部分是能够对用户进行身份验证. kbmMW在这里提供了非常强大的机制. TkbmMWSimpleClient提供简单的用户身份验证机制,您可以在连接到应用程序服务器时传递U ...

  6. orm的理解

    orm:是对象->关系->映射,的简称. mvc或者mvc框架中包括一个重要的部分,就是orm,它实现了数据模型于数据库的解耦,即数据模型的设计不需要依赖于特定的数据库,通过简单的配置就可 ...

  7. java学习笔记12(final ,static修饰符)

    final: 意思是最终的,是一个修饰符,有时候一个功能类被开发好了,不想被子类重写就用final定义, 用final修饰的最终数据成员:如果一个类的数据成员用final修饰符修饰,则这个数据成员就被 ...

  8. 基于链路的OSPFMD5口令认证

    实验要求:掌握基于链路的OSPFMD5口令认证 拓扑如下: 配置如下: R1enable configure terminal interface s0/0/0ip address 192.168.1 ...

  9. Spring Boot 揭秘与实战(八) 发布与部署 - 远程调试

    文章目录 1. 依赖 2. 部署 3. 调试 4. 源代码 设置远程调试,可以在正式环境上随时跟踪与调试生产故障. 依赖 在 pom.xml 中增加远程调试依赖. <plugins> &l ...

  10. tomcat自动缓存的几种解决方式

    第一种方法:打开一个项目,这里我打开的Mail项目,然后点击Myeclipse菜单栏中的project-选择clean: 选择要clean的项目,确定即可不用进入tomcat服务器直接清理缓存. 上面 ...