当前位置: 首页 > article >正文

【音视频】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-1Clause 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 HeaderPES 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 范围 (例如 0xE00xEF),并在 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++;


http://www.kler.cn/a/582336.html

相关文章:

  • 【Godot4.3】Geometry2D总结
  • Kubernetes 中的 Secrets 配置管理
  • Django部署Filemanagement
  • 共享经济时代下,鲲鹏共享科技如何逆袭改命?
  • 操作系统知识总结(三)——内存
  • 【数据结构】3顺序表
  • 从零开始学机器学习——构建一个推荐web应用
  • 华为HCIE认证用处大吗?
  • 【A2DP】深入解析A2DP协议中的音频流处理
  • Redis实现高并发排行榜的功能
  • 侯捷 C++ 课程学习笔记:C++ 新标准11/14
  • 使用AI一步一步实现若依前端(8)
  • vue3 二次封装uni-ui中的组件,并且组件中有 v-model 的解决方法
  • Vue 实现AI对话和AI绘图(AIGC)人工智能
  • Excel多级联动下拉菜单设置
  • C盘清理技巧分享:释放空间,提升电脑性能
  • Networking Based ISAC Hardware Testbed and Performance Evaluation
  • [动手学习深度学习]13.丢弃法 Dropout
  • 修改jupyter notebook的工作空间
  • 二级Python通关秘籍:字符串操作符/函数/方法全解析与实战演练