GB/T 28181
协议中规定了传输流的要求:媒体流在联网系统IP网络上传输时应支持RTP传输,媒体流发送源端应支持控制媒体流发送峰值功能。RTP的负载应采用如下两种格式之一:基于PS封装的视音频数据或视音频基本流数据。媒体流的传输应采用RFC3550规定的RTP协议,提供实时数据传输中的时间戳信息及各数据流的同步;应采用RFC3550规定的 RTCP协议,为按序传输数据包提供可靠保证,提供流量控制和拥塞控制。
在上一期内容中,我们深入探讨了GB/T 28181标准下点播服务的信令流程及其详尽解析。本篇我们将更进一步,细致解构“国标流”的概念,特别是当它以RTP(Real-time Transport Protocol)协议承载视频数据时的具体表现形式与内部结构。
1. RTP封装
首先我们了解下RTP协议,RTP协议以其低延迟、多播能力及与RTCP配合的高效传输管理,成为实时音视频通信中灵活且广泛采纳的标准。
RTP
(Real-time Transport Protocol,实时传输协议)是一种网络传输协议,专门设计用于在互联网上提供端到端的实时传输服务,主要应用于音频、视频和其它多媒体数据的传输。它是IETF(Internet Engineering Task Force)制定的标准之一,最初在RFC 1889中被定义,后来的主要规范更新至RFC3550。
我们通过一张直观图表来全局展示RTP协议在网络架构中的位置,以及其协议头结构,以便快速把握要点。
上述图表展示了基于
RTP OVER UDP
的配置,该方式在局域网环境中能极好地确保实时传输性能。然而,在GB/T 28181协议广泛应用的场景中,互联网传输的稳定性成为关键,此时UDP的无连接特性可能不足以维持视频流的连续性。因此,采用RTP OVER TCP
方案,利用TCP的可靠传输机制来传送RTP数据包,成为保障跨网络视频流稳定性的另一优选策略。GB/T 28181 2016和GB/T 28181 2022都是按RFC4571来的。使用TCP发送RTP包,前面加个16位(2个字节)无符号长度字段就好(网络字节序),即tcp_len[0] = (len >> 8) & 0xff; tcp_len[1] = len & 0xff;
,如下图所示:
1.1 RTP头
1.1.1 协议解析
接下来,我们将详细解读RTP协议中数据包头部的具体构成:
-
V:RTP协议的版本号,占2位,当前协议版本号为2
-
P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分
-
X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头
-
CC:CSRC计数器,占4位,指示CSRC 标识符的个数
-
M: 标记,占1位,不同的有效载荷有不同的含义,
对于视频,标记一帧的结束;对于音频,标记会话的开始
-
PT: 有效荷载类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等,在流媒体中大部分是用来区分音频流和视频流的,这样便于客户端进行解析。具体标识号参考RFC3551表中动态范围(96~127)
-
序列号(sequence number):占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。这个字段当下层的承载协议用UDP的时候,网络状况不好的时候可以用来检查丢包。同时出现网络抖动的情况可以用来对数据进行重新排序,序列号的初始值是随机的,同时音频包和视频包的sequence是分别记数的。
-
时间戳(timestamp):占32位,必须使用90 kHz 时钟频率。时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。
暂定视频帧率为25帧每秒(fps)和RTP时间戳的时钟频率为90kHz(即每秒90,000个时间单位),我们可以计算出每帧时间戳的增量:
每帧时间间隔:已知每秒25帧,故每帧时间间隔为:
时间戳单位转换:将每帧的时间间隔转换为90kHz时钟频率下的时间戳增量:
因此,当视频帧率为25fps且采用90kHz时钟频率时,每发送一帧视频,RTP时间戳应增加3,600个时间戳单位(ticks)。这样可以确保时间戳准确反映视频帧之间的相对时间,并保持媒体流的同步播放。
- 同步信源(SSRC)标识符:占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。
- 特约信源(CSRC)标识符:每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源。
1.1.2 抓包解析
1.2 RTP荷载H.264裸流
H.264包的 RTP封装格式参照RFC3984,RTP的主要参数设置如下:
- 负载类型(payload type): 98;
- SDP描述中“m”字段的“media”项: video。
1.3 RTP荷载H.265裸流
H.265包的 RTP封装格式参照RFC7798,RTP的主要参数设置如下:
- 负载类型(payload type): 100;
- SDP描述中“m”字段的“media”项: video。
1.4 RTP荷载音频流
语音比特流宜采用标准的RTP协议进行打包。 在一个RTP包中,音频载荷数据应为整数个音频编码帧,且时间长度在20ms~180ms之间。音频载荷数据的RTP封装参数如下:
-
G.711的主要参数 G.711A律语音编码 RTP包的负载类型(PayloadType)的参数规定如下(见IETFRFC3551— 2003中的表4):
- 负载类型(PT):8;
- 编码名称(encodingname):PCMA;
- 时钟频率(clockrate):8kHz;
- 通道数:1;
- SDP描述中“m”字段的“media”项:audio。 b) SVAC音频的主要参数
-
SVAC语音编码 RTP包的负载类型(PayloadType)的参数规定如下:
- 负载类型(PT):20;
- 编码名称(encodingname):SVACA;
- 时钟频率(clockrate):8kHz;
- 通道数:1;
- SDP描述中“m”字段的“media”项:audio。 c) G.723.1的主要参数
-
G.723.RTP包的负载类型(PayloadType)的参数规定参照IETFRFC3551—2003 4中的 G.723,具体如下:
- 负载类型(PT):4;
- 编码名称(encodingname):G723;
- 时钟频率(clockrate):8kHz;
- 通道数:1;
- SDP描述中“m”字段的“media”项:audio。
-
G.729语音编码 RTP 包的负载类型(PayloadType)的参数规定如下(见IETFRFC3551— 2003中的表4):
- 负载类型(PT):18;
- 编码名称(encodingname):G729;
- 时钟频率(clockrate):8kHz;
- 通道数:1;
- SDP描述中“m”字段的“media”项:audio。
-
G.722.表1语音编码 RTP包的负载类型(PayloadType)的参数规定参照IETFRFC3551—2003 4中的 G.722,具体如下:
- 负载类型(PT):9;
- 编码名称(encodingname):G722;
- 时钟频率(clockrate):8kHz;
- 通道数:1;
- SDP描述中“m”字段的“media”项:audio。
1.5 RTP荷载PS复合流
1.5.1 简述流程
RTP
荷载PS
(Program Stream)复合流是指在实时传输协议(RTP)框架下,将视频(如H.264编码)和可能伴随的音频等多媒体内容封装成Program Stream格式进行传输的过程。PS流是一种适用于存储和播放的多媒体容器格式,它包含了节目流的同步信息以及多个 Elementary Streams(ES,基本流)如视频流和音频流,这些流被进一步封装进Packetized Elementary Stream(PES)包中,最后组成PS。
在RTP荷载PS流的具体实现中,针对H.264视频编码,通常的做法是:
- NAL Unit封装:H.264视频数据由一系列的NAL(Network Abstraction Layer)单元组成,包括SPS(Sequence Parameter Set,序列参数集)、PPS(Picture Parameter Set,图像参数集)和IDR(Instantaneous Decoding Refresh,即时解码刷新)等。为了能够在PS流中正确解析,每个IDR NALU之前通常会包含SPS和PPS NALU,这些一起被封装为一个逻辑上的访问单元(Access Unit)。
- PES封装:将上述访问单元进一步封装到PES包中,PES包包含了流的时序信息和流的类型标识,允许解码器正确解析和同步。
- PS封装:PES包随后被嵌入到PS包中,PS包包含PS头、PS系统头、PS系统地图等信息,这些信息帮助解码器识别和解析流中的不同节目元素。
- RTP封装:最终,PS包会被封装进RTP包中进行网络传输。RTP包头包含了时间戳、序列号等重要信息,用于确保数据包在网络中的正确排序和同步播放。
1.5.2 相关参数
详细的PS封装格式的介绍在下文中,这里我们现列举中在RTP包中的参数。参数定义如下(2022版本丰富了多种格式,在这里我们直接列举最全):
PS包的RTP封装格式参照RFC2250,RTP的主要参数设置如下:
- 负载类型(payload type):
96
;- 编码名称(encoding name):
PS
;- 时钟频率(clock rate):
90kHz
;- SDP描述中“m”字段的“media”项:
video
。
系统头应包含对 PS包中码流种类的描述,其中视频和音频的流ID(stream_id)取值如下:
码流种类 | 流ID(stream_id) |
---|---|
视频流ID | 0xE0 |
音频流ID | 0xC0 |
海康私有流 | 0xBD |
针对本文档规定的几种视音频格式,PSM 中流类型(stream_type)的取值如下:
视音频格式 | 流类型(stream_type) |
---|---|
MPEG-4视频流 | 0x10 |
H.264视频流 | 0x1B |
SVAC视频流 | 0x80 |
H.265视频流 | 0x24 |
G.711 A律音频流 | 0x90 |
G.711 U律音频流 | 0x91 |
G.722.1音频流 | 0x92 |
G.723.1音频流 | 0x93 |
G.729音频流 | 0x99 |
SVAC音频流 | 0x9B |
AAC音频流 | 0x0F |
1.5.3 其他
一般情况下IDR帧很大,超过了RTP的负载长度限制(由MTU限制,一般情况下为1400字节),这一I帧要拆分成若干包RTP分多次发送。 第一包的结构如下图所示。
RTP header | PS packet |
---|
第二包以后RTP的结构就简单多了,没有PS头
RTP header | ES remaining packet(例如H.264剩余帧) |
---|
2. PS封装
ISOIEC 13818-1标准,
MPEG-2
中定义了两种复合信息流:传送流(TS
:TransportStream)和节目流(PS
:ProgramStream)。在
GB/T 28181
体系内相关的有
- PS:主要应用在安防领域,GB/T 28181规定了码流推荐打包方式是RTP荷载PS。
- TS:主要应用在广电领域,因为TS的易于恢复的特性,苹果HLS协议的码流封装格式也是TS,可用于存储分片。
这里我们主要了解下28181协议中明确介绍的PS封装。
一般来说一个完整的PS包封装格式如下所示:
PS层主要由PS header,PS system header,PS system map加上PES packets组成。
对于包含IDR帧的PS包的内容为:
PS pack header | PS system header | PSM | PES header | PES payload | PES header | PES payload | .. |
---|
对于包含非IDR帧的PS包的内容为:
PS pack header | PES header | PES payload | PES header | PES payload | .. |
---|
PS流总是以0x000001BA开始,以0x000001B9结束,对于一个PS文件,有且只有一个结束码0x000001B9;不过对于直播的PS流,基本上看不到结束码的。
2.1 Program stream pack header -> PS header
2.1.1 字段说明
- pack_start_code:起始码,占位32bit,标识PS包的开始,固定为
0x000001BA
; 01 字段:占位2bit; - SCR:占位46bit,其中包含42bit的SCR值和4个bit的marker_bit值,其中SCR值由 system_clock_reference_base 和 system_clock_reference_extension 两部分组成,字节顺序依次是:
- system_clock_reference_base [32..30]:占位3bit;
- marker_bit:占位1bit;
- system_clock_reference_base [29..15]:占位15bit;
- marker_bit:占位1bit;
- system_clock_reference_base [14..0]:占位15bit;
- marker_bit:占位1bit;
- system_clock_reference_extension:占位9bit;
- marker_bit:占位1bit;
- program_mux_rate:速率值字段,占位22bit,正整数,表示P-STD接收此字段所在包的PS流的速率;这个值以每秒50字节作为单位;禁止0值;
- marker_bit:标记字段,占位1bit,固定为 1;
- marker_bit:标记字段,占位1bit,固定为 1;
- reserved:保留字段,占位5bit;
- pack_stuffing_length:长度字段,占位3bit;规定了此字段之后填充字段的长度;
- LOOP
- stuffing_byte:填充字段,固定为0xFF;长度由前一字段确定;
- LOOP END
2.1.2 Rust实现
// 定义PS头长度的常量
pub(crate) const PS_HDR_LEN: usize = 14;
// 定义bits_buffer结构体,用于存储和操作位数据
struct BitsBuffer {
data: [u8; PS_HDR_LEN],
size: usize,
mask: u8,
}
impl BitsBuffer {
// 创建一个新的BitsBuffer实例
fn new() -> Self {
Self {
data: [0; PS_HDR_LEN],
size: PS_HDR_LEN,
mask: 0x80,
}
}
// 写入位数据
fn bits_write(&mut self, num_bits: usize, value: u32) {
let mut value = value;
let mut current_byte = 0;
let mut current_bit = 7;
for _ in 0..num_bits {
if self.mask == 0 {
current_byte += 1;
current_bit = 7;
self.mask = 0x80;
}
self.data[current_byte] |= ((value & 1) as u8) << current_bit;
value >>= 1;
self.mask >>= 1;
current_bit -= 1;
}
}
}
pub(crate) fn gb28181_make_ps_header(p_data: &mut [u8], s64_scr: u64) -> i32 {
let s64_scr = s64_scr / 100;
let mut bits_buffer = BitsBuffer::new();
bits_buffer.bits_write(32, 0x000001BA); // 起始码 start codes
bits_buffer.bits_write(2, 1); // 标记位 marker bits '01b'
bits_buffer.bits_write(3, (s64_scr >> 30) as u32 & 0x07); // System clock [32..30]
bits_buffer.bits_write(1, 1); // 标记位 marker bit
bits_buffer.bits_write(15, (s64_scr >> 15) as u32 & 0x7FFF); // System clock [29..15]
bits_buffer.bits_write(1, 1); // 标记位 marker bit
bits_buffer.bits_write(15, s64_scr as u32 & 0x7fff); // System clock [14..0]
bits_buffer.bits_write(1, 1); // 标记位 marker bit
bits_buffer.bits_write(9, 0); // SCR extension
bits_buffer.bits_write(1, 1); // 标记位 marker bit
bits_buffer.bits_write(22, 255); // 比特率 (n单位每秒50字节)
bits_buffer.bits_write(2, 3); // 标记位 marker bits '11'
bits_buffer.bits_write(5, 0x1f); // 预留字段 (保留供将来使用)
bits_buffer.bits_write(3, 0); // 填充长度
// 将bits_buffer中的数据复制到p_data
p_data.copy_from_slice(&bits_buffer.data);
0
}
2.2 Program stream system header -> PS system header (关键帧才有)
节目流系统头为解码器提供了关于节目流整体特性的全面视图,包括如何处理视频和音频数据、版权信息以及时间同步机制。这些信息是解码器正确初始化、解码流程配置和内容呈现所必需的,确保了跨平台和设备间的一致性和兼容性。
2.2.1 字段说明
- system_header_start_code:系统头部起始码,占位32bit,值固定为
0x000001BB
,标志系统首部的开始 - header_length:头部长度字段,占位16bit,表示此字段之后的系统首部字节长度
- marker_bit:占位1bit,固定值为1
- rate_bound:整数值,占位22bit,为一个大于或等于PS流所有PS包中的最大program_mux_rate值的整数;可以被解码器用来判断是否可以对整个流进行解码
- marker_bit:占位1bit,固定值为1
- audio_bound:占位6bit;取值范围0到32间整数;大于或等于同时进行解码处理的PS流中的音频流的最大数目
- fixed_flag:标志位,占位1bit;置位1表示固定比特率操作,置位0则为可变比特率操作
- CSPS_flag:CSPS标志位,占位1bit;置位1表示此PS流满足标准的限制
- system_audio_lock_flag:标志位,占位1bit,表示音频采样率和 STD 的 system_clock_frequency 之间有一特定常数比例关系
- system_video_lock_flag:标志位,占位1bit,表示在系统目标解码器system_clock_frequency和视频帧速率之间存在一特定常数比例关系
- marker_bit:占位1bit,固定值为1
- video_bound:整数,占位5bit,取值范围0到16;大于或等于同时进行解码处理的PS流中的视频流的最大数目
- packet_rate_restriction_flag:分组速率限制标志字段,占位1bit,若CSPS_flag == 1,则此字段表示哪种限制适用于分组速率;若CSPS_flag == 0,则此字段无意义
- reserved_bits:保留字段,占位7bit,固定为’1111111’
- LOOP
- stream_id:占位8bit,表示其后的 P-STD_buffer_bound_scale 和 P-STD_buffer_size_bound 字段所涉及的流的编码和基本流的号码,若 stream_id == 1011 1000,则其后的 P-STD_buffer_bound_scale 和 - P-STD_buffer_size_bound 字段对应PS流中的所有音频流;若stream_id == 1011 1001,则其后的P-- STD_buffer_bound_scale和P-STD_buffer_size_bound字段对应PS流中的所有视频流;若取其他值,则应大于 1011 1100 ,且按照标准对应Stream id(详见附录1);PS流中的每个原始流都应在每个系统首部中通过这种机制精确地规定一次它的 P-STD_buffer_bound_scale 和 P-STD_buffer_size_bound;
- 11:占位2bit;
- P-STD_buffer_bound_scale:占位1bit,表示用来解释后面P-STD_buffer_size_bound字段的比例因子;如果之前的stream_id表示音频流,则此值应为0,若之前的stream_id表示视频流,则此值应为1,对于其他stream类型,此值可以0或1;
- P-STD_buffer_size_bound:占位13bit,无符号整数;大于或等于所有PS流分组的P-STD输入缓冲区大小BSn的最大值;若P-STD_buffer_bound_scale == 0,则P-STD_buffer_size_bound以128字节为单位;若P-STD_buffer_bound_scale == 1,则P-STD_buffer_size_bound以1024字节为单位;
- LOOP END
2.2.2 Rust实现
// 定义PS头长度的常量
pub(crate) const PS_HDR_LEN: usize = 14;
// 定义bits_buffer结构体,用于存储和操作位数据
struct BitsBuffer {
data: [u8; PS_HDR_LEN],
size: usize,
mask: u8,
}
impl BitsBuffer {
// 创建一个新的BitsBuffer实例
fn new() -> Self {
Self {
data: [0; PS_HDR_LEN],
size: PS_HDR_LEN,
mask: 0x80,
}
}
// 写入位数据
fn bits_write(&mut self, num_bits: usize, value: u32) {
let mut value = value;
let mut current_byte = 0;
let mut current_bit = 7;
for _ in 0..num_bits {
if self.mask == 0 {
current_byte += 1;
current_bit = 7;
self.mask = 0x80;
}
self.data[current_byte] |= ((value & 1) as u8) << current_bit;
value >>= 1;
self.mask >>= 1;
current_bit -= 1;
}
}
}
pub(crate) fn gb28181_make_ps_header(p_data: &mut [u8], s64_scr: u64) -> i32 {
let s64_scr = s64_scr / 100;
let mut bits_buffer = BitsBuffer::new();
bits_buffer.bits_write(32, 0x000001BA); // 起始码 start codes
bits_buffer.bits_write(2, 1); // 标记位 marker bits '01b'
bits_buffer.bits_write(3, (s64_scr >> 30) as u32 & 0x07); // System clock [32..30]
bits_buffer.bits_write(1, 1); // 标记位 marker bit
bits_buffer.bits_write(15, (s64_scr >> 15) as u32 & 0x7FFF); // System clock [29..15]
bits_buffer.bits_write(1, 1); // 标记位 marker bit
bits_buffer.bits_write(15, s64_scr as u32 & 0x7fff); // System clock [14..0]
bits_buffer.bits_write(1, 1); // 标记位 marker bit
bits_buffer.bits_write(9, 0); // SCR extension
bits_buffer.bits_write(1, 1); // 标记位 marker bit
bits_buffer.bits_write(22, 255); // 比特率 (n单位每秒50字节)
bits_buffer.bits_write(2, 3); // 标记位 marker bits '11'
bits_buffer.bits_write(5, 0x1f); // 预留字段 (保留供将来使用)
bits_buffer.bits_write(3, 0); // 填充长度
// 将bits_buffer中的数据复制到p_data
p_data.copy_from_slice(&bits_buffer.data);
0
}
2.3 Program stream map -> PSM(关键帧才有)
2.3.1 字段说明
- packet start code prefix:包头起始码,固定为0x000001,占位24bit;与后面的字段map_stream_id一起组成分组开始码,标志着分组的开始;
- map_stream_id:类型字段,标志此分组是什么类型,占位8bit;如果此值为0xBC,则说明此PES包为PSM;
- program_stream_map_length:长度字段,占位16bit;表示此字段之后PSM的总长度,最大值为1018(0x3FA);
- current_next_indicator:标识符,占位1bit;置位1表示当前PSM是可用的,置位0则表示当前PSM不可以,下一个可用;
- reserved:保留字段,占位2bit;
- program_stream_map_version:版本字段,占位5bit;表示PSM的版本号,取值范围1到32,随着PSM定义的改变循环累加;若current_next_indicator == 1,表示当前PSM的版本号,若current_next_indicator == 0,表示下一个PSM的版本号;
- reserved:保留字段,占位7bit;
- marker_bit:标记字段,占位1bit,固定为1;
- program_stream_info_length:长度字段,占位16bit;表示此字段后面的descriptor字段的长度;
- LOOP
- descriptor:program Stream信息描述字段,长度由前个字段确定;
- LOOP END
- elementary_stream_map_length:长度字段,占位16bit;表示在这个PSM中所有ES流信息的总长度;包括stream_type, elementary_stream_id, elementary_stream_info_length的长度,即N*32bit;是不包括具体ES流描述信息descriptor的长度的;
- LOOP
- stream_type:类型字段,占位8bit;表示原始流ES的类型;这个类型只能标志包含在PES包中的ES流类型;值0x05是被禁止的;常见取值类型有MPEG-4 视频流:0x10;H.264 视频流:0x1B;G.711 音频流:0x90;因为PSM只有在关键帧打包的时候,才会存在,所以如果要判断PS打包的流编码类型,就根据这个字段来判断;
- elementary_stream_id:流ID字段,占位8bit;表示此ES流所在PES分组包头中的stream_id字段的值;其中0x(C0-DF)指音频,0x(E0-EF)为视频;
- elementary_stream_info_length:长度字段,占位16bit;表示此字段之后的,ES流描述信息的长度;
- descriptor:描述信息,长度由前个字段确定;表示此类型的ES流的描述信息,这个描述信息的长度是不包含在elementary_stream_map_length字段里面的;
- LOOP END
- CRC_32:CRC字段,占位32bit,CRC校验值;
2.3.2 Rust实现
// 定义PSM头长度的常量
const PSM_HDR_LEN: usize = 24;
// 定义BitsBuffer结构体,用于存储和操作位数据
struct BitsBuffer {
data: [u8; PSM_HDR_LEN],
mask: u8,
current_byte: usize,
current_bit: usize,
}
impl BitsBuffer {
// 创建一个新的BitsBuffer实例
fn new() -> Self {
Self {
data: [0; PSM_HDR_LEN],
mask: 0x80,
current_byte: 0,
current_bit: 7,
}
}
// 写入位数据
fn bits_write(&mut self, num_bits: usize, value: u32) {
let mut value = value;
for _ in 0..num_bits {
if self.mask == 0 {
self.current_byte += 1;
self.current_bit = 7;
self.mask = 0x80;
}
self.data[self.current_byte] |= ((value & 1) as u8) << self.current_bit;
value >>= 1;
self.mask >>= 1;
self.current_bit -= 1;
}
}
}
pub fn gb28181_make_psm_header(p_data: &mut [u8]) -> i32 {
let mut bits_buffer = BitsBuffer::new();
// system header
bits_buffer.bits_write(24, 0x000001); // 起始码 start code
bits_buffer.bits_write(8, 0xBC); // map stream id
bits_buffer.bits_write(16, 18); // program stream map length
bits_buffer.bits_write(1, 1); // current next indicator
bits_buffer.bits_write(2, 3); // reserved
bits_buffer.bits_write(5, 0); // program stream map version
bits_buffer.bits_write(7, 0x7F); // reserved
bits_buffer.bits_write(1, 1); // marker bit
bits_buffer.bits_write(16, 0); // programe stream info length
bits_buffer.bits_write(16, 8); // elementary stream map length
// video
bits_buffer.bits_write(8, 0x1B); // stream_type 视频编码格式H.264
bits_buffer.bits_write(8, 0xE0); // elementary_stream_id
bits_buffer.bits_write(16, 0); // elementary_stream_info_length
// audio
bits_buffer.bits_write(8, 0x90); // stream_type 音频编码格式G711
bits_buffer.bits_write(8, 0xC0); // elementary_stream_id
bits_buffer.bits_write(16, 0); // elementary_stream_info_length
// crc (2e b9 0f 3d)
bits_buffer.bits_write(8, 0x45); // crc (24~31) bits
bits_buffer.bits_write(8, 0xBD); // crc (16~23) bits
bits_buffer.bits_write(8, 0xDC); // crc (8~15) bits
bits_buffer.bits_write(8, 0xF4); // crc (0~7) bits
// 将bits_buffer中的数据复制到p_data
p_data.copy_from_slice(&bits_buffer.data);
0
}
2.4 PES packet
PES包由包
header
和payload
组成,一帧PS包中PES 可能有多个。
2.4.1 字段说明
2.4.2 Rust实现
// 定义PES头长度的常量
const PES_HDR_LEN: usize = 19;
// 定义BitsBuffer结构体,用于存储和操作位数据
struct BitsBuffer {
data: [u8; PES_HDR_LEN],
mask: u8,
current_byte: usize,
current_bit: usize,
}
impl BitsBuffer {
// 创建一个新的BitsBuffer实例
fn new() -> Self {
Self {
data: [0; PES_HDR_LEN],
mask: 0x80,
current_byte: 0,
current_bit: 7,
}
}
// 写入位数据
fn bits_write(&mut self, num_bits: usize, value: u32) {
let mut value = value;
for _ in 0..num_bits {
if self.mask == 0 {
self.current_byte += 1;
self.current_bit = 7;
self.mask = 0x80;
}
self.data[self.current_byte] |= ((value & 1) as u8) << self.current_bit;
value >>= 1;
self.mask >>= 1;
self.current_bit -= 1;
}
}
}
pub fn gb28181_make_pes_header(p_data: &mut [u8], stream_id: u8, payload_len: usize, pts: u64, dts: u64) -> i32 {
let mut bits_buffer = BitsBuffer::new();
// PES头部
bits_buffer.bits_write(24, 0x000001); // 起始码 start code
bits_buffer.bits_write(8, stream_id as u32); // streamID
bits_buffer.bits_write(16, (payload_len + 13) as u32); // packet_len pes剩余头部以及后面的es长度之和,比如SPS长度+13
bits_buffer.bits_write(2, 2); // '10'
bits_buffer.bits_write(2, 0); // scrambling_control
bits_buffer.bits_write(1, 1); // priority
bits_buffer.bits_write(1, 1); // data_alignment_indicator
bits_buffer.bits_write(1, 0); // copyright
bits_buffer.bits_write(1, 0); // original_or_copy
bits_buffer.bits_write(1, 1); // PTS_flag 是否有PTS
bits_buffer.bits_write(1, 1); // DTS_flag 是否有DTS信息
bits_buffer.bits_write(1, 0); // ESCR_flag
bits_buffer.bits_write(1, 0); // ES_rate_flag
bits_buffer.bits_write(1, 0); // DSM_trick_mode_flag
bits_buffer.bits_write(1, 0); // additional_copy_info_flag
bits_buffer.bits_write(1, 0); // PES_CRC_flag
bits_buffer.bits_write(1, 0); // PES_extension_flag
bits_buffer.bits_write(8, 10); // header_data_length
// PTS,DTS
bits_buffer.bits_write(4, 3); // '0011'
bits_buffer.bits_write(3, (pts >> 30) as u32 & 0x07); // PTS[32..30]
bits_buffer.bits_write(1, 1);
bits_buffer.bits_write(15, (pts >> 15) as u32 & 0x7FFF); // PTS[29..15]
bits_buffer.bits_write(1, 1);
bits_buffer.bits_write(15, pts as u32 & 0x7FFF); // PTS[14..0]
bits_buffer.bits_write(1, 1);
bits_buffer.bits_write(4, 1); // '0001'
bits_buffer.bits_write(3, (dts >> 30) as u32 & 0x07); // DTS[32..30]
bits_buffer.bits_write(1, 1);
bits_buffer.bits_write(15, (dts >> 15) as u32 & 0x7FFF); // DTS[29..15]
bits_buffer.bits_write(1, 1);
bits_buffer.bits_write(15, dts as u32 & 0x7FFF); // DTS[14..0]
bits_buffer.bits_write(1, 1);
// 将bits_buffer中的数据复制到p_data
p_data.copy_from_slice(&bits_buffer.data);
0
}
2.5 抓包实例
现在我们就抓取RTP荷载ps的关键帧进行分析,首先关键帧包都是比较大的,可以看RTP包头中时间相同的且mark标记在最后的包即为关键帧包。
我们取第一个包拆解分析:
00 00 01 ba
44 17 2d de 84 01 00 5f 6b f8 #ps header
00 00 01
e0 61 bc 80 80 05 21 05 cb 93 c1 #ps system header
,e0: 视频流
00 00 00 01
46 01 50 #psm
00 00 00 01
02 01 d0 01 4d c4 08 60 fe c3 64 88 f5 d0 ad 67 31 8b 3d f9 55 ed 6f b8 ef ea 1f f7 af a7 a3 6f 05 5c 65 31 cd 99 44 55 1d 66 06 1c 94 9e d3 3e 5f... #pes