【音视频】H265-NALU-AVpacket-PS-RTP(GB28181)
概述
国标平台的推流与直接使用RTSP或者RTMP推流的方法不同,需要先从H265格式的流地址中解析出来NALU(直接使用FFmpeg库或者自己构建一个小型的RTSP服务器实现),然后对不同类型的NALU进行处理,H265视频中的NALU类型主要有VPS SPS PPS 以及I帧等,需要将其组装成PS流然后通过RTP封包发送。
本文主要讨论这个流程实现RTP打包的可行性,具体代码实现后面总结实现
PS流
通过H265视频流解码出来NALU,然后通过NALU封装成PS流的过程,在推流过程中PS流起到很大的作用,所以首先讨论PS流
MPEG PS 标准
Program Stream (PS):MPEG PS 是一种 面向节目 (Program-oriented) 的复用格式,主要用于 存储 和 系统层处理 单个节目 (例如电影, 电视节目)。 它被设计为在 可靠的存储介质 (例如 DVD, 硬盘) 上使用,对错误鲁棒性要求不高
PES Packet (Packetized Elementary Stream Packet): PS 流的基本组成单元是 PES Packet。 每个 PES Packet 封装了 来自一个基本码流 (Elementary Stream, ES) 的数据,例如一个视频帧或一段音频数据
- PES的组成
- PES Header:也就是说每个PES包都会有一个Header,其中包含了元数据信息,例如 Stream ID (标识音视频流类型), PES Packet 长度, PTS/DTS 时间戳等
-
PES Payload: PES Header 后面紧跟着 PES Payload,包含了实际的音视频数据 (也就是类似于 H.265 NALU 数据)
-
所以在具体代码的实现中需要将NALU打包成PES,然后再进一步打包成PS流,然后通过RTP发送出去
Stream ID:PES Header 中的 Stream ID 用于 标识 PES Packet 所属的基本码流
- 0xE0 到 0xEF: 视频流 (Video Stream)
- 0xC0 到 0xDF: 音频流 (Audio Stream)
- 0xBD: Private Stream 1 (私有流 1),可以用于封装其他类型的数据
PTS (Presentation Time Stamp) 和 DTS (Decoding Time Stamp)
- 时间戳信息,主要就是用于音视频同步和解码顺序控制
- PTS表示的时显示时间戳,DTS表示的则是解码时间戳
System Header: PS 流的 系统头信息,包含了整个 PS 流的 全局参数,例如 码率限制 (Rate Bound), 缓冲区大小限制 (VBV Buffer Size Bound), 系统时钟参考 (System Clock Reference, SCR) 等。 System Header 通常在 PS 流的开头出现
- 具体代码实现中要将SYS头放到PS流的开头,也就是最后再封装
Program Stream Map (PSM): PSM 是 PS 流的 节目映射表,包含了 PS 流中 所有基本码流 (音视频流) 的 描述信息,例如 Stream Type, Elementary Stream ID 等。 解码器需要 PSM 来 识别和解析 PS 流中的音视频数据。 PSM 通常在 System Header 之后出现
下面是基于上述总结,具体结构参考下文
探究ISO/IEC 13818-1 的 Clause 2.4 Program Stream 章节内容
PS流的主要特点
- PS流设计用于复用单个节目(类似于电影或者电视节目)的数据,其与TS面向传输不同,PS更侧重于存储和系统层的处理
-
可变长度包: PS 使用可变长度的 PES Packet,这使得 PS 格式更加灵活,可以适应不同类型和大小的音视频数据
-
适用于可靠介质: PS 主要设计用于 可靠的存储介质,例如光盘 (DVD, Blu-ray) 和硬盘。 它 对错误鲁棒性要求不高,因为假定存储介质是可靠的,数据传输过程中不易出错
-
单个时间基准: PS 中的所有基本码流 (音视频流) 共享 同一个时间基准 ,简化了同步和解码器的实现
PS的复用
- PES Packet交织:PS一般通过将来自不同基本码流(类似于视频ES或者音频ES)的PES Packet交织起来,从而形成一个单一连续的码流。交织在一起的目的是为了实现音视频的同步播放
- 既然交织在一起,其通过时间戳实现同步,也就是上面提到DTS和PTS
PS的主要组成(重点)
-
Packet Start Code Prefix (分组起始码前缀): 每个 PS 语法元素的开始都以 4 字节的 0x00 0x00 0x01 起始码前缀开始,用于标识语法元素的类型
-
System Header (系统头): 全局头信息,描述了整个 PS 流的系统级参数,例如 码率限制 (Rate Bound), 缓冲区大小限制 (VBV Buffer Size Bound), 系统时钟参考 (SCR) 等。 System Header 通常在 PS 流的开头出现
-
Program Stream Map (PSM): 节目映射表,包含了 PS 流中 所有基本码流 (音视频流) 的 描述信息,例如 Stream Type (流类型), Elementary Stream ID (基本流 ID) 等。 解码器需要 PSM 来识别和解析 PS 流中的音视频数据。 PSM 通常在 System Header 之后出现
- Packetized Elementary Stream (PES) Packet (分组化基本码流包): PS 流的基本数据单元。 每个 PES Packet 封装了 来自一个基本码流 的数据 (例如一个视频帧或一段音频数据)。 PES Packet 由 PES Header 和 PES Payload 两部分组成。
PES Packet结构了解
/***
*@remark: pes头的封装,里面的具体数据的填写已经占位,可以参考标准
*@param : pData [in] 填充ps头数据的地址
* stream_id [in] 码流类型
* paylaod_len[in] 负载长度
* pts [in] 时间戳
* dts [in]
*@return: 0 success, others failed
*/
int gb28181_make_pes_header(char *pData, int stream_id, int payload_len, int64_t pts, int64_t dts)
{
bits_buffer_t bitsBuffer;
bitsBuffer.i_size = PES_HDR_LEN;
bitsBuffer.i_data = 0;
bitsBuffer.i_mask = 0x80;
bitsBuffer.p_data = (unsigned char *)(pData);
memset(bitsBuffer.p_data, 0, PES_HDR_LEN);
bits_write(&bitsBuffer, 24, 0x000001); //*start code*//*
bits_write(&bitsBuffer, 8, (stream_id)); //*streamID*//*
bits_write(&bitsBuffer, 16, (payload_len)+13); //*packet_len*//* //指出pes分组中数据长度和该字节后的长度和
bits_write(&bitsBuffer, 2, 2); //*'10'*//*
bits_write(&bitsBuffer, 2, 0); //*scrambling_control*//*
bits_write(&bitsBuffer, 1, 0); //*priority*//*
bits_write(&bitsBuffer, 1, 0); //*data_alignment_indicator*//*
bits_write(&bitsBuffer, 1, 0); //*copyright*//*
bits_write(&bitsBuffer, 1, 0); //*original_or_copy*//*
bits_write(&bitsBuffer, 1, 1); //*PTS_flag*//*
bits_write(&bitsBuffer, 1, 1); //*DTS_flag*//*
bits_write(&bitsBuffer, 1, 0); //*ESCR_flag*//*
bits_write(&bitsBuffer, 1, 0); //*ES_rate_flag*//*
bits_write(&bitsBuffer, 1, 0); //*DSM_trick_mode_flag*//*
bits_write(&bitsBuffer, 1, 0); //*additional_copy_info_flag*//*
bits_write(&bitsBuffer, 1, 0); //*PES_CRC_flag*//*
bits_write(&bitsBuffer, 1, 0); //*PES_extension_flag*//*
bits_write(&bitsBuffer, 8, 10); //*header_data_length*//*
// 指出包含在 PES 分组标题中的可选字段和任何填充字节所占用的总字节数。该字段之前
//的字节指出了有无可选字段。
//*PTS,DTS PTS DTS均为1的情况*//
bits_write(&bitsBuffer, 4, 3); //*'0011'*//*
bits_write(&bitsBuffer, 3, ((pts) >> 30) & 0x07); //*PTS[32..30]*//*
bits_write(&bitsBuffer, 1, 1);
bits_write(&bitsBuffer, 15, ((pts) >> 15) & 0x7FFF); //*PTS[29..15]*//*
bits_write(&bitsBuffer, 1, 1);
bits_write(&bitsBuffer, 15, (pts) & 0x7FFF); //*PTS[14..0]*//*
bits_write(&bitsBuffer, 1, 1);
bits_write(&bitsBuffer, 4, 1); //*'0001'*//*
bits_write(&bitsBuffer, 3, ((dts) >> 30) & 0x07); //*DTS[32..30]*//*
bits_write(&bitsBuffer, 1, 1);
bits_write(&bitsBuffer, 15, ((dts) >> 15) & 0x7FFF); //*DTS[29..15]*//*
bits_write(&bitsBuffer, 1, 1);
bits_write(&bitsBuffer, 15, (dts) & 0x7FFF); //*DTS[14..0]*//*
bits_write(&bitsBuffer, 1, 1);
return 0;
}
PES Header (PES 包头): 每个 PES Packet 的头部,包含了 PES Packet 的 元数据信息
- packet_start_code_prefix (24 bits): 固定值 0x00 0x00 0x01,标识 PES Packet 的开始。
- stream_id (8 bits): 流 ID,标识 PES Packet 所属的基本码流类型。
- 0xE0 到 0xEF: 视频流 (Video Stream)。 H.265 视频 NALU 数据通常封装在 Stream ID 0xE0 或更高值的 PES Packet 中。
- 0xC0 到 0xDF: 音频流 (Audio Stream)。
- 0xBD: Private Stream 1 (私有流 1)。 可以用于封装其他类型的数据,例如字幕、数据流等。
- PES_packet_length (16 bits): PES Packet 长度,指示 PES Packet Header 之后 Payload 数据的字节数。
- '10'b, PTS_DTS_flags (2 bits), PES_header_data_length (8 bits): PES Header 的控制信息,例如指示是否包含 PTS/DTS 时间戳,以及 PES Header 的剩余长度。
- PTS [Presentation Time Stamp] (33 bits, 如果 PTS_DTS_flags 指示存在): 显示时间戳,表示 PES Payload 中的数据应该在什么时间点 显示 或 播放。
- DTS [Decoding Time Stamp] (33 bits, 如果 PTS_DTS_flags 指示存在且需要 DTS): 解码时间戳,表示 PES Payload 中的数据应该在什么时间点 解码。 对于 B 帧,DTS 和 PTS 可能不同。
- PES_header_data_length (8 bits): PES Header 数据长度,指示 PES Header 中除固定字段外,额外扩展 Header 数据的长度 (例如 PTS/DTS 字段)。
PES Payload (PES 包负载): 紧跟在 PES Header 之后的数据部分,包含了 实际的音视频数据。 对于 H.265 视频,PES Payload 中应该包含 H.265 NALU 数据。 可以是一个或多个完整的 NALU,也可以是 NALU 的片段 (取决于封装方式和 NALU 的大小)
SYS Header
- system_header_start_code (32 bits): 固定值 0x00 0x00 0x01 B2,标识 System Header 的开始。
- system_header_length (16 bits): System Header 的长度。
- rate_bound (22 bits): 码率限制,指示 PS 流的最大码率。
- vbv_buffer_size_bound (10 bits): VBV (Video Buffering Verifier) 缓冲区大小限制,用于控制码流的复杂度,防止解码器缓冲区溢出。
- system_clock_reference_value (42 bits): 系统时钟参考 (SCR),用于同步音视频流的时间基准。
/***
*@remark: sys头的封装,里面的具体数据的填写已经占位,可以参考标准
*@param : pData [in] 填充ps头数据的地址
*@return: 0 success, others failed
*/
int gb28181_make_sys_header(char *pData, int audioCnt)
{
bits_buffer_t bitsBuffer;
bitsBuffer.i_size = SYS_HDR_LEN;
bitsBuffer.i_data = 0;
bitsBuffer.i_mask = 0x80;
bitsBuffer.p_data = (unsigned char *)(pData);
memset(bitsBuffer.p_data, 0, SYS_HDR_LEN);
/*system header*/
bits_write(&bitsBuffer, 32, 0x000001BB); /*start code*/
bits_write(&bitsBuffer, 16, SYS_HDR_LEN - 6);/*header_length 表示次字节后面的长度,后面的相关头也是次意思*/
bits_write(&bitsBuffer, 1, 1); /*marker_bit*/
bits_write(&bitsBuffer, 22, 3967); /*rate_bound*/
bits_write(&bitsBuffer, 1, 1); /*marker_bit*/
bits_write(&bitsBuffer, 6, audioCnt); /*audio_bound*/
bits_write(&bitsBuffer, 1, 0); /*fixed_flag */
bits_write(&bitsBuffer, 1, 1); /*CSPS_flag */
bits_write(&bitsBuffer, 1, 1); /*system_audio_lock_flag*/
bits_write(&bitsBuffer, 1, 1); /*system_video_lock_flag*/
bits_write(&bitsBuffer, 1, 1); /*marker_bit*/
bits_write(&bitsBuffer, 5, 1); /*video_bound*/
bits_write(&bitsBuffer, 1, 0); /*dif from mpeg1*/
bits_write(&bitsBuffer, 7, 0x7F); /*reserver*/
/*video stream bound*/
bits_write(&bitsBuffer, 8, 0xE0); /*stream_id*/
bits_write(&bitsBuffer, 2, 3); /*marker_bit */
bits_write(&bitsBuffer, 1, 1); /*PSTD_buffer_bound_scale*/
bits_write(&bitsBuffer, 13, 2048); /*PSTD_buffer_size_bound*/
/*audio stream bound*/
bits_write(&bitsBuffer, 8, 0xC0); /*stream_id*/
bits_write(&bitsBuffer, 2, 3); /*marker_bit */
bits_write(&bitsBuffer, 1, 0); /*PSTD_buffer_bound_scale*/
bits_write(&bitsBuffer, 13, 512); /*PSTD_buffer_size_bound*/
return 0;
}
PSM
- program_stream_map_start_code (32 bits): 固定值 0x00 0x00 0x01 BC,标识 PSM 的开始。
- program_stream_map_length (16 bits): PSM 的长度。
- program_stream_id_map_count (8 bits): 基本码流映射计数,指示 PSM 中描述的基本码流 (音视频流) 的数量。
- elementary_stream_map (循环结构): 基本码流映射信息,对于每个基本码流,包含:
-
- stream_type (8 bits): 流类型,例如 0x02 表示 MPEG-2 视频, 0x03 表示 MPEG-1 音频, 0x04 表示 MPEG-2 音频, 0x1B 表示 H.264 视频, 0x24 表示 H.265 视频 (HEVC Stream Type,需要查阅更新的标准文档确认,可能不是 0x24,需要查阅 HEVC over MPEG-2 Systems 的相关规范)。 注意:MPEG-2 PS 标准最初是为 MPEG-1/MPEG-2 音视频设计的,对于 H.265 视频的 Stream Type,可能需要在 HEVC over MPEG-2 Systems 的相关规范中查找。
- elementary_stream_id (8 bits): 基本码流 ID,与 PES Header 中的 Stream ID 对应,用于将 PSM 中的描述信息与实际的 PES Packet 关联起来。
/***
*@remark: psm头的封装,里面的具体数据的填写已经占位,可以参考标准
*@param : pData [in] 填充ps头数据的地址
*@return: 0 success, others failed
*/
int gb28181_make_psm_header(char *pData)
{
bits_buffer_t bitsBuffer;
bitsBuffer.i_size = PSM_HDR_LEN;
bitsBuffer.i_data = 0;
bitsBuffer.i_mask = 0x80;
bitsBuffer.p_data = (unsigned char *)(pData);
memset(bitsBuffer.p_data, 0, PSM_HDR_LEN);
bits_write(&bitsBuffer, 24, 0x000001); /*start code*/
bits_write(&bitsBuffer, 8, 0xBC); /*map stream id*/
bits_write(&bitsBuffer, 16, 18); /*program stream map length*/
bits_write(&bitsBuffer, 1, 1); /*current next indicator */
bits_write(&bitsBuffer, 2, 3); /*reserved*/
// bits_write(&bitsBuffer, 5, 0); /*program stream map version*/
bits_write(&bitsBuffer, 5, 1); /*program stream map version*/
bits_write(&bitsBuffer, 7, 0x7F); /*reserved */
bits_write(&bitsBuffer, 1, 1); /*marker bit */
bits_write(&bitsBuffer, 16, 0); /*programe stream info length*/
bits_write(&bitsBuffer, 16, 8); /*elementary stream map length is*/
/*video*/
bits_write(&bitsBuffer, 8, 0x1B); /*stream_type*/
bits_write(&bitsBuffer, 8, 0xE0); /*elementary_stream_id*/
bits_write(&bitsBuffer, 16, 0); /*elementary_stream_info_length */
/*audio*/
bits_write(&bitsBuffer, 8, 0x90); /*stream_type*/
bits_write(&bitsBuffer, 8, 0xC0); /*elementary_stream_id*/
bits_write(&bitsBuffer, 16, 0); /*elementary_stream_info_length is*/
/*crc (2e b9 0f 3d)*/
bits_write(&bitsBuffer, 8, 0x45); /*crc (24~31) bits*/
bits_write(&bitsBuffer, 8, 0xBD); /*crc (16~23) bits*/
bits_write(&bitsBuffer, 8, 0xDC); /*crc (8~15) bits*/
bits_write(&bitsBuffer, 8, 0xF4); /*crc (0~7) bits*/
return 0;
}
H265在PS流中的位置
PES Payload: 当 H.265 视频被封装到 MPEG PS 流中时,H.265 NALU 数据会被放在 PES Packet 的 Payload 部分。 一个 PES Payload 可以包含一个或多个完整的 NALU,或者 NALU 的片段
Stream ID: 封装 H.265 视频的 PES Packet 的 Stream ID 应该设置为 视频流的 Stream ID 范围 (例如 0xE0 到 0xEF),并在 PSM 中将对应的 Elementary Stream 的 Stream Type 设置为 H.265 视频的 Stream Type (需要查阅 HEVC over MPEG-2 Systems 的相关规范)
封装流程分析
正常的一个PS流应该如下图所示
GB28181平台推流下H264 PS流封装
首先针对于关键帧的处理,应该先对其SPS PPS SEI进行封装,具体参考下图
对于非关键帧的处理如下
H265封包
HEVC相比于H264仅仅多了一个VPS即可,关键帧和非关键帧的封包如下
代码层面实现(仅提取重要片段)
vector<Nalu *> nalu_vector;
char sps_data[128] = { 0 };
int sps_data_length = 0;
char pps_data[128] = { 0 };
int pps_data_length = 0;
void out_nalu(char * buffer, int size, NaluType naluType) {
//std::cout << "rtp_packet >>> " << binToHex((unsigned char *)buffer, size);
if (NALU_TYPE_SPS == naluType) {
memcpy(sps_data, buffer, size);
sps_data_length = size;
return;
}
if (NALU_TYPE_PPS == naluType) {
memcpy(pps_data, buffer, size);
pps_data_length = size;
return;
}
Nalu * nalu = new Nalu;
bool is_i_frame = (NALU_TYPE_IDR == naluType);
char * packet = (char *)malloc(is_i_frame ? (size + sps_data_length + pps_data_length) : size * sizeof(char));
if (is_i_frame) {
memcpy(packet, sps_data, sps_data_length);
memcpy(packet + sps_data_length, pps_data, pps_data_length);
memcpy(packet + sps_data_length + pps_data_length, buffer, size);
size += (sps_data_length + pps_data_length);
}
else {
memcpy(packet, buffer, size);
}
nalu->packet = packet;
nalu->length = size;
nalu->type = naluType;
nalu_vector.push_back(nalu);
}
for (auto i = 0; i < nalu_vector.size(); i++) {
auto nalu = nalu_vector.at(i);
NaluType type = nalu->type;
int length = nalu->length;
char * packet = nalu->packet;
int index = 0;
if (NALU_TYPE_IDR == type) {
gb28181_make_ps_header(ps_header, pts);
memcpy(frame,ps_header,PS_HDR_LEN);
index += PS_HDR_LEN;
gb28181_make_sys_header(ps_system_header, 0x3f);
memcpy(frame+ index, ps_system_header, SYS_HDR_LEN);
index += SYS_HDR_LEN;
gb28181_make_psm_header(ps_map_header);
memcpy(frame + index, ps_map_header, PSM_HDR_LEN);
index += PSM_HDR_LEN;
} else {
gb28181_make_ps_header(ps_header, pts);
memcpy(frame, ps_header, PS_HDR_LEN);
index += PS_HDR_LEN;
}
//封装pes
gb28181_make_pes_header(pes_header, 0xe0, length, pts, pts);
memcpy(frame+index, pes_header, PES_HDR_LEN);
index += PES_HDR_LEN;
memcpy(frame + index, packet, length);
index += length;
组包发送
//组包rtp
int rtp_packet_count = ((index - 1) / single_packet_max_length) + 1;
for (int i = 0; i < rtp_packet_count; i++) {
gb28181_make_rtp_header(rtp_header, rtp_seq, pts, atoi(ssrc.c_str()), i == (rtp_packet_count - 1));
int writed_count = single_packet_max_length;
if ((i + 1)*single_packet_max_length > index) {
writed_count = index - (i* single_packet_max_length);
}
//添加包长字节
int rtp_start_index=0;
unsigned short rtp_packet_length = RTP_HDR_LEN + writed_count;
if (rtp_protocol == "TCP/RTP/AVP") {
unsigned char packt_length_ary[2];
packt_length_ary[0] = (rtp_packet_length >> 8) & 0xff;
packt_length_ary[1] = rtp_packet_length & 0xff;
memcpy(rtp_packet, packt_length_ary, 2);
rtp_start_index = 2;
}
memcpy(rtp_packet+ rtp_start_index, rtp_header, RTP_HDR_LEN);
memcpy(rtp_packet+ +rtp_start_index + RTP_HDR_LEN, frame+ (i* single_packet_max_length), writed_count);
rtp_seq++;