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

【音视频】H265解码Nalu后封装rtp包

概述

基于ZLM流媒体框架以及简单RTSP服务器开源项目分析总结,相关源码参考以下链接

H265-rtp提取Nalu逻辑

通过rtsp流地址我们可以获取视频流中的多个rtp包,其中每个RTP包中又会包含一个或者多个Nalu,将其提取处理

总体逻辑分析

核心逻辑在于对H265 RTP解复用器的使用,从RTP包中提取出来完整Nalu或者分片的Nalu

  • 接收RTP数据包
  • 识别负载数据,然后处理RTP拓展头部信息
  • 识别NALU的类型,主要用于区分其是单一的Nalu还是分片的Nalu(FU类型为49)
  • 单一Nalu处理逻辑
    • 添加起始码,通过回调函数传递Nalu
  • 分片Nalu的处理
    • 重组Nalu:首先会在缓冲区中存储来自多个rtp包的分片数据,然后逐步重组完整的Nalu
    • 通过FU头部标志(S,E)识别起始、中间和结束分片
      • 在起始分片中,从FU头部恢复原始的Nalu类型
      • 结束分片的时候,添加起始码和重组的Nalu头部,并通过回调函数传递完整的Nalu数据
  • 最后通过回调函数进一步对H265的Nalu进行处理

参考(RTP)

 *    0                   1                   2                   3*    0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*   |V=2|P|X|  CC   |M|     PT      |       sequence number         |*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*   |                           timestamp                           |*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*   |           synchronization source (SSRC) identifier            |*   +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+*   |            contributing source (CSRC) identifiers             |*   :                             ....                              :*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+**/

代码实现分析

  • 开始处理H265的RTP包,然后计算RTP包的负载数据的起始地址以及负载数据的长度
  • 计算的主要方法就是跳过RTP头部大小
struct RtpHeader *header = (struct RtpHeader *)data;
int payload_type = header->payloadType;
if(payload_type != payload_){return;
}
const uint8_t* payload = data + sizeof(struct RtpHeader);
size_t payload_len = size - sizeof(struct RtpHeader);
  • 处理RTP拓展头部,首先需要判断其拓展头部是否存在
  • 存在拓展头部的处理
    • 计算拓展头部的长度
    • 然后通过偏移量跳过RTP的头部和拓展头部,从而使得负载数据指向争取的位置
    • 目的是保证其提取正确的Nalu数据
if (header->extension){const uint8_t *extension_data = payload;size_t extension_length = 4 * (extension_data[2] << 8 | extension_data[3]);size_t payload_offset = 4 + extension_length;payload = payload + payload_offset;payload_len = payload_len - payload_offset;
}
  • 判断Nalu的头部和分片类型
    • 如果是49分片类型,那么需要对分片数据进行特别处理
struct H265NaluHeader *h265_header = (struct H265NaluHeader *)payload;
if(h265_header->type == 49){ // 分片 (Fragmentation Unit - FU)struct H265FUHeader *fu_header = (struct H265FUHeader *)&payload[2];// ... 分片 NALU 的处理逻辑 ...
}
else{ // 单一封包 (Single NAL Unit)// ... 单一 NALU 的处理逻辑 ...
}
  • 首先访问负载数据的头部
  • 处理起始分片
  • 然后将起始分片后的分片放入缓冲区
  • 最后遇到结束分片的时候,将其统一封装成一个Nalu即可
struct H265FUHeader *fu_header = (struct H265FUHeader *)&payload[2];
if(fu_header->s == 1){ // 起始分片 (Start fragment)find_start_ = true;if(pos_buffer_ == 0){ // 首次接收到起始分片struct H265NaluHeader header = *h265_header;header.type =  fu_header->type; // 从 FU 头部恢复原始 NALU 类型buffer_[0] = 0;buffer_[1] = 0;buffer_[2] = 0;buffer_[3] = 1; // NALU 起始码前缀memcpy(buffer_ + 4, &header, sizeof(struct H265NaluHeader)); // 复制 NALU 头部pos_buffer_ += 4 + sizeof(struct H265NaluHeader);}memcpy(buffer_ + pos_buffer_, payload + 3, payload_len - 3); // 复制分片数据pos_buffer_ += payload_len - 3;
}
else if(fu_header->e == 1){ // 结束分片 (End fragment)if(find_start_ == false){ // 尚未接收到起始分片return;}memcpy(buffer_ + pos_buffer_, payload + 3, payload_len - 3); // 复制分片数据pos_buffer_ += payload_len - 3;if(call_back_){ // 调用回调函数,传递完整的 NALUcall_back_->OnVideoData(ntohl(header->timestamp),  buffer_, pos_buffer_);}find_start_ = false; // 重置状态,准备接收下一个 NALUpos_buffer_ = 0;
}
else { // 中间分片 (Middle fragment)if (!find_start_) { // 尚未接收到起始分片return;}memcpy(buffer_ + pos_buffer_, payload + 3, payload_len - 3); // 复制分片数据pos_buffer_ += payload_len - 3;
}
  • 处理单一封包的Nalu
    • 直接跳过起始码即可提取出RTP包中的Nalu
else{ // 单一封包 (Single NAL Unit)buffer_[0] = 0;buffer_[1] = 0;buffer_[2] = 0;buffer_[3] = 1;memcpy(buffer_ + 4, payload, payload_len);if(call_back_){call_back_->OnVideoData(ntohl(header->timestamp),  buffer_, payload_len + 4);}
}

H265-Nalu组成RTP包

逻辑分析

核心流程

  • 确定 RTP 负载类型 (Payload Type, PT)
    • 参考SDP协商出来的信息
  • 构建 RTP 头部 (RTP Header)
    • 版本 (Version, V): RTP 版本号,通常为 2
    • 填充 (Padding, P): 指示 RTP 包末尾是否有填充字节。通常为 0
    • 扩展 (Extension, X): 指示 RTP 头部后面是否有扩展头部。通常为 0,除非你需要添加扩展信息
    • CSRC 计数器 (CSRC Count, CC): CSRC 标识符的数目。通常为 0,除非有贡献源
    • 标记位 (Marker, M): 标记 RTP 包的事件,例如,可以用来标记帧的结束。对于视频,可以用来标记每个帧的最后一个 RTP 包
    • 负载类型 (Payload Type, PT): 你选择的负载类型,例如 96
    • 序列号 (Sequence Number): 每个 RTP 包的序列号,从一个随机值开始,然后每个包递增 1。用于检测包丢失和重排序
    • 时间戳 (Timestamp): 指示 RTP 包中第一个字节的采样时间。对于视频,时间戳应该反映视频帧的显示时间。时间戳时钟频率需要根据视频编码的帧率来确定
    • 同步源标识符 (Synchronization Source Identifier, SSRC): 标识 RTP 流的源,为一个 32 位的随机数,在 RTP 会话中应保持唯一
  • Nalu分片与封装
    • 总结:如果一个Nalu的大小超过MTU(最大传输单元),那么就需要对其分片处理,打成多个RTP包
    • 首先判断是否需要分片
    • FU-A分片
      • 起始分片
        • RTP头部,同上
        • FU Indicator (1 字节): NALU 头部的前两个字节,但 NALU type 字段设置为 49 (FU 类型)
        • FU Header (1 字节)
          • S (Start bit): 设置为 1,表示是分片的开始
          • E (End bit): 设置为 0
          • R (Reserved bit): 必须为 0
          • FU type: 原始 NALU 类型的后 6 位 (从原始 NALU 头部中提取)
        • 分片数据:NALU的一部分数据
      • 中间分片
        • RTP头部信息
        • FU Indicator (1 字节):同起始分片
        • FU Header (1 字节)
          • S (Start bit): 设置为0
          • E (End bit): 设置为 0
          • R (Reserved bit): 必须为 0
          • FU type: 这里需要与起始分片相同
        • 分片数据:Nalu的一部分数据
      • 结束分片
        • RTP头部:序列号和时间戳递增,标记位 M 可以设置为 1,如果这是当前帧的最后一个 RTP 包
        • FU Indicator (1 字节):同起始分片
        • FU Header (1 字节)
          • S (Start bit): 设置为0
          • E (End bit): 设置为 1,表示分片的结束
          • R (Reserved bit): 必须为 0
          • FU type: 这里需要与起始分片相同
        • 分片数据:Nalu最后一部分的数据
  • 单一分片封装
    • RTP头部,标记位M可以设置为1吗,当前帧是最后一个Nalu的时候
    • Nalu数据(这里是去除Nalu的起始码,直接放入Nalu头部+负载数据)
  • 时间戳和序列号管理
    • 时间戳: 对于每个视频帧的第一个 RTP 包,设置时间戳为当前帧的显示时间。对于同一帧的后续分片包,时间戳保持不变。
      • 下一个视频帧的第一个 RTP 包使用新的时间戳,时间戳的增量应该与视频帧率和时钟频率一致
    • 序列号: 为每个 RTP 包分配递增的序列号,起始序列号随机选择

代码实现

#include <iostream>
#include <vector>
#include <cstdint>
#include <cstring>
#include <iomanip>  // 用于十六进制输出格式化
#include <ctime>    // 用于日志中的时间戳
#include <sstream>  // 用于字符串流// --- 常量定义 ---
const uint8_t H265_PAYLOAD_TYPE = 96;
const uint16_t RTP_VERSION = 2;
const uint16_t VIDEO_STREAM_ID = 0xE0; // 视频流的 Stream ID 示例
const size_t MAX_RTP_PAYLOAD_SIZE = 1400; // RTP 负载最大尺寸示例,根据 MTU 和头部开销调整
const size_t MAX_PES_PAYLOAD_SIZE = 2048; // PES 负载最大尺寸示例,根据需要调整
const uint32_t PS_START_CODE_PREFIX = 0x000001BA;
const uint32_t PES_START_CODE_PREFIX = 0x000001;// --- 结构体定义 ---
#pragma pack(push, 1) // 确保结构体内部没有填充字节// 简化的 RTP 头部
struct RTPHeader {uint8_t version_padding_extension_csrc_count; // V, P, X, CCuint8_t marker_payload_type;                 // M, PTuint16_t sequence_number;uint32_t timestamp;uint32_t ssrc;RTPHeader() : version_padding_extension_csrc_count(0x80), marker_payload_type(H265_PAYLOAD_TYPE), sequence_number(0), timestamp(0), ssrc(0x12345678) {} // SSRC 示例
};// FU 头部 (Fragmentation Unit A, FU-A)
struct FUHeader {uint8_t fu_header; // S, E, R, FU TypeFUHeader() : fu_header(0) {}
};// FU 指示器 (Fragmentation Unit A, FU-A)
struct FUIndicator {uint8_t fu_indicator; // Type = 49 (FU), NALU type bitsFUIndicator() : fu_indicator(0) {}
};// 简化的 PES 头部 (关注 PTS)
struct PESHeader {uint32_t packet_start_code_prefix;uint8_t stream_id;uint16_t pes_packet_length; // 暂时设置为 0,之后计算uint8_t pes_scrambling_control_indicator_etc; // 标志位和指示器uint8_t pes_header_data_length;uint64_t pts_dts_flags_pts; // PTS 标志和 PTS 值 (简化示例)PESHeader() : packet_start_code_prefix(PES_START_CODE_PREFIX), stream_id(VIDEO_STREAM_ID), pes_packet_length(0),pes_scrambling_control_indicator_etc(0x80), // PTS_DTS_flags: 0b10 (仅 PTS)pes_header_data_length(5), // 仅 PTS 占用 5 字节pts_dts_flags_pts(0) {} // PTS 值稍后设置
};// 极简 PS 头部 (仅用于示例)
struct PSHeader {uint32_t packet_start_code_prefix;uint64_t system_clock_reference; // SCR (System Clock Reference) - 简化PSHeader() : packet_start_code_prefix(PS_START_CODE_PREFIX), system_clock_reference(0) {}
};#pragma pack(pop) // 恢复默认 packing// --- 全局计数器和变量 ---
static uint16_t rtp_sequence_number_counter = 0;
static uint32_t rtp_timestamp_counter = 0;
static uint64_t pes_pts_counter = 0; // PES PTS 计数器示例// --- 日志记录函数 ---
void Log(const std::string& message) {std::time_t now = std::time(nullptr);std::tm local_time;localtime_r(&now, &local_time);char timestamp_str[20];std::strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%d %H:%M:%S", &local_time);std::cout << "[" << timestamp_str << "] " << message << std::endl;
}// --- 辅助函数:将数据转换为十六进制字符串 ---
std::string ToHex(const uint8_t* data, size_t size) {std::stringstream hex_stream;hex_stream << std::hex << std::setfill('0');for (size_t i = 0; i < size; ++i) {hex_stream << std::setw(2) << static_cast<int>(data[i]) << " ";}return hex_stream.str();
}// --- 阶段 1: NALU 封装为 RTP 包 ---
std::vector<std::vector<uint8_t>> EncapsulateNALUtoRTP(const std::vector<uint8_t>& nalu_data, int nalu_type) {Log("--- 开始 NALU 到 RTP 的封装 ---");std::vector<std::vector<uint8_t>> rtp_packets;size_t nalu_size = nalu_data.size();const uint8_t* nalu_payload = nalu_data.data();size_t nalu_payload_offset = 0;if (nalu_size <= MAX_RTP_PAYLOAD_SIZE) {// 情况 1: NALU 足够小,可以放入单个 RTP 包Log("NALU 尺寸足够小,可以使用单个 RTP 包。");std::vector<uint8_t> rtp_packet_buffer(sizeof(RTPHeader) + nalu_size);RTPHeader rtp_header;rtp_header.sequence_number = htons(rtp_sequence_number_counter++);rtp_header.timestamp = htonl(rtp_timestamp_counter);rtp_header.marker_payload_type |= (1 << 7); // 设置 Marker 位 (示例,可能根据实际需求调整)memcpy(rtp_packet_buffer.data(), &rtp_header, sizeof(RTPHeader));memcpy(rtp_packet_buffer.data() + sizeof(RTPHeader), nalu_payload, nalu_size);Log("创建 RTP 头部: " + ToHex(rtp_packet_buffer.data(), sizeof(RTPHeader)));Log("RTP 负载 (NALU 数据): " + ToHex(rtp_packet_buffer.data() + sizeof(RTPHeader), nalu_size));rtp_packets.push_back(rtp_packet_buffer);} else {// 情况 2: NALU 太大,需要分片 (FU-A)Log("NALU 尺寸超过 RTP 负载限制,需要分片 (FU-A)。");int fragment_number = 0;bool first_fragment = true;bool last_fragment = false;while (nalu_payload_offset < nalu_size) {size_t fragment_size = std::min(MAX_RTP_PAYLOAD_SIZE - sizeof(FUIndicator) - sizeof(FUHeader), nalu_size - nalu_payload_offset);if (nalu_payload_offset + fragment_size == nalu_size) {last_fragment = true;}std::vector<uint8_t> rtp_packet_buffer(sizeof(RTPHeader) + sizeof(FUIndicator) + sizeof(FUHeader) + fragment_size);RTPHeader rtp_header;rtp_header.sequence_number = htons(rtp_sequence_number_counter++);rtp_header.timestamp = htonl(rtp_timestamp_counter);rtp_header.marker_payload_type &= ~(1 << 7); // 清除 Marker 位 (分片包通常不设置,除非是帧的最后一个分片)if (last_fragment) rtp_header.marker_payload_type |= (1 << 7); // 在最后一个分片包上设置 Marker 位 (示例)FUIndicator fu_indicator;fu_indicator.fu_indicator = 49 << 1; // FU 类型 = 49fu_indicator.fu_indicator |= ((nalu_type >> 5) & 0x01); // 复制原始 NALU 头部的 forbidden_zero_bitFUHeader fu_header;fu_header.fu_header = (nalu_type & 0x1F); // NAL 单元类型 (原始 NALU 类型的后 5 位)if (first_fragment) fu_header.fu_header |= (1 << 7); // 设置 S 位 (起始分片)if (last_fragment)  fu_header.fu_header |= (1 << 6); // 设置 E 位 (结束分片)memcpy(rtp_packet_buffer.data(), &rtp_header, sizeof(RTPHeader));memcpy(rtp_packet_buffer.data() + sizeof(RTPHeader), &fu_indicator, sizeof(FUIndicator));memcpy(rtp_packet_buffer.data() + sizeof(RTPHeader) + sizeof(FUIndicator), &fu_header, sizeof(FUHeader));memcpy(rtp_packet_buffer.data() + sizeof(RTPHeader) + sizeof(FUIndicator) + sizeof(FUHeader),nalu_payload + nalu_payload_offset, fragment_size);Log("创建 RTP 头部 (分片 " + std::to_string(fragment_number) + "): " + ToHex(rtp_packet_buffer.data(), sizeof(RTPHeader)));Log("FU 指示器: " + ToHex(rtp_packet_buffer.data() + sizeof(RTPHeader), sizeof(FUIndicator)));Log("FU 头部: " + ToHex(rtp_packet_buffer.data() + sizeof(RTPHeader) + sizeof(FUIndicator), sizeof(FUHeader)));Log("RTP 负载 (分片数据 " + std::to_string(fragment_number) + "): " + ToHex(rtp_packet_buffer.data() + sizeof(RTPHeader) + sizeof(FUIndicator) + sizeof(FUHeader), fragment_size));rtp_packets.push_back(rtp_packet_buffer);nalu_payload_offset += fragment_size;first_fragment = false;fragment_number++;}}rtp_timestamp_counter += 3600; // 时间戳递增示例 (90kHz 时钟, 约 40ms 帧时长)Log("--- NALU 到 RTP 封装完成,生成 " + std::to_string(rtp_packets.size()) + " 个 RTP 包。---\n");return rtp_packets;
}// --- 阶段 2: RTP 封装为 PES (更标准的做法应为 NALU 封装为 PES) ---
std::vector<uint8_t> EncapsulateRTPtoPES(const std::vector<std::vector<uint8_t>>& rtp_packets, const std::vector<uint8_t>& original_nalu_data) {Log("--- 开始 RTP (或 NALU) 到 PES 的封装 ---");std::vector<uint8_t> pes_packet_buffer;// 为了更符合 PS 流标准,理想情况下应该从 RTP 包中解封装出 NALU 数据,然后将 NALU 放入 PES。// 但为了演示 "组成 rtp 包后通过 PS 流发送出去" 的字面意思,这里我们将 *整个 RTP 包* 放入 PES 负载 (这不太标准,但在某些特定场景下可能可行)。// 在实际系统中,你可能更希望提取 RTP 中的 NALU 负载,然后放入 PES。PESHeader pes_header;pes_header.pts_dts_flags_pts = (static_cast<uint64_t>(pes_pts_counter++) << 3) | 0x02; // 仅设置 PTS 标志uint64_t pts_33_to_1 = (pes_pts_counter * 300) % 0x200000000LL; // 假设 300 ticks/ms, 90kHz clockuint32_t pts_32_to_2 = pts_33_to_1 & 0xFFFFFFFE0LL;uint8_t pts_byte0 = 0x20 | ((pts_32_to_2 >> 30) & 0x07) << 1 | 0x01;uint16_t pts_byte1_2 = (pts_32_to_2 >> 15) & 0xFFFF;uint16_t pts_byte3_4 = pts_32_to_2 & 0xFFFF;pes_header.pts_dts_flags_pts |= (static_cast<uint64_t>(pts_byte0) << 40);pes_header.pts_dts_flags_pts |= (static_cast<uint64_t>(pts_byte1_2) << 24);pes_header.pts_dts_flags_pts |= (static_cast<uint64_t>(pts_byte3_4) >> 8);size_t pes_payload_size = 0;for (const auto& rtp_packet : rtp_packets) {pes_payload_size += rtp_packet.size();}pes_header.pes_packet_length = htons(sizeof(PESHeader) + pes_payload_size - 6); // PES 包长度不包括 包起始码前缀 和 长度字段自身pes_packet_buffer.resize(sizeof(PESHeader));memcpy(pes_packet_buffer.data(), &pes_header, sizeof(PESHeader));Log("创建 PES 头部: " + ToHex(pes_packet_buffer.data(), sizeof(PESHeader)));Log("PES PTS Value: " + std::to_string(pes_pts_counter));for (const auto& rtp_packet : rtp_packets) {pes_packet_buffer.insert(pes_packet_buffer.end(), rtp_packet.begin(), rtp_packet.end());}Log("PES Payload (RTP 包数据): Total size " + std::to_string(pes_payload_size) + " bytes.");Log("--- RTP to PES Encapsulation Complete. PES Packet Size: " + std::to_string(pes_packet_buffer.size()) + " bytes. ---\n");return pes_packet_buffer;
}// --- 阶段 3: 将 PES 包放入 PS 流 (简化示例,仅包含 PS 头部和 PES 包) ---
std::vector<uint8_t> CreatePSStream(const std::vector<uint8_t>& pes_packet) {Log("--- 开始创建 PS 流 ---");std::vector<uint8_t> ps_stream_buffer;PSHeader ps_header;// SCR (System Clock Reference) 示例 - 非常简化ps_header.system_clock_reference = (static_cast<uint64_t>(pes_pts_counter * 300) << 3) | 0x01; // 90kHz clock, marker_bits = '01'ps_stream_buffer.resize(sizeof(PSHeader));memcpy(ps_stream_buffer.data(), &ps_header, sizeof(PSHeader));Log("创建 PS 头部: " + ToHex(ps_stream_buffer.data(), sizeof(PSHeader)));ps_stream_buffer.insert(ps_stream_buffer.end(), pes_packet.begin(), pes_packet.end());Log("将 PES 包添加到 PS 流. PES Packet Size: " + std::to_string(pes_packet.size()) + " bytes.");Log("--- PS 流创建完成. Total PS Stream Size: " + std::to_string(ps_stream_buffer.size()) + " bytes. ---\n");return ps_stream_buffer;
}int main() {Log("--- 示例程序开始 ---");// 示例 H.265 NALU 数据 (这里用一些虚拟数据代替实际的 H.265 NALU)std::vector<uint8_t> h265_nalu_data = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,// ... 假设这里是真实的 H.265 NALU 负载数据 ...};int nalu_type = 32; // 示例 NALU 类型 (VPS)Log("原始 H.265 NALU 数据: Size = " + std::to_string(h265_nalu_data.size()) + " bytes, Type = " + std::to_string(nalu_type));Log(ToHex(h265_nalu_data.data(), h265_nalu_data.size()));// 阶段 1: NALU 封装成 RTP 包std::vector<std::vector<uint8_t>> rtp_packets = EncapsulateNALUtoRTP(h265_nalu_data, nalu_type);// 阶段 2: RTP 包封装成 PES 包std::vector<uint8_t> pes_packet = EncapsulateRTPtoPES(rtp_packets, h265_nalu_data);// 阶段 3: 创建 PS 流std::vector<uint8_t> ps_stream = CreatePSStream(pes_packet);Log("\n--- 最终 PES 包 (放入 PS 流之前) ---");Log("PES Packet Hex Data (First 64 bytes): \n" + ToHex(pes_packet.data(), std::min((size_t)64, pes_packet.size())));Log("PES Packet Size: " + std::to_string(pes_packet.size()) + " bytes.");Log("\n--- 最终 PS 流 (First 64 bytes) ---");Log("PS Stream Hex Data (First 64 bytes): \n" + ToHex(ps_stream.data(), std::min((size_t)64, ps_stream.size())));Log("PS Stream Size: " + std::to_string(ps_stream.size()) + " bytes.");Log("--- 示例程序结束 ---");return 0;
}

Nalu封装PS流

逻辑分析

PS 流是 MPEG-2 标准中用于存储程序内容的一种格式。它主要用于存储和传输已经复用的音视频基本流。要将 RTP 包放入 PS 流中,通常的做法是先将 RTP 包转换为 PES包,然后再将 PES 包复用进 PS 流,其中GB28181平台就是需要将视频封装成PS流进行传输

注意事项

之前自己在书写项目的时候,该处的逻辑有些混淆,认为应该先将Nalu封装成RTP包然后通过PS流发送出去。这里就涉及了严重的概念混淆的错误。真正的流程应该如下,后续补充对PS流基础概念和原理的认识

将RTP作为一个传输协议,最终PS流中应该包含的是音视频的PES包,所以还需要对其进行包装一层

主要流程分析

封装成PES等包,在开源社区有已经封装好的代码,该处只简单分析其逻辑 

  • 从RTP包中解析出来Nalu
  • NALU 封装成 PES 包 (Packetized Elementary Stream)
    • PES 头部 (PES Header)
      • 包起始码前缀:0x00 0x00 0x01
      • 流ID:主要用于标识流的类
      • PES包长度:指示 PES 包的长度,不包括包起始码前缀和自身长度字段。如果长度未知可以设置为 0,但通常应计算实际长度
      • PES头部标志位:指示其是否包含有PTS/DTS信息
      • PTS/DTS
        • PTS (显示时间戳): 指示 PES 包中数据的显示时间。应该从 RTP 包的时间戳转换过来
        • DTS (解码时间戳): 指示 PES 包中数据的解码时间。对于 I 帧,DTS 通常等于 PTS。对于 B 帧,DTS 可能早于 PTS。对于 H.265,如果只包含 P 和 I 帧,DTS 通常可以等于 PTS
    • PES负载
      • PES 负载 (PES Payload): 将一个或多个完整的 NALU 放入 PES 负载中。 你可以选择每个 PES 包放一个 NALU,或者将多个小的 NALU 组合到一个 PES 包中
  • 构建PS流
    • PS 头部 (Program Stream Header): 每个 PS 包的开始
      • 包起始码前缀 (Packet Start Code Prefix):0x00 0x00 0x01
      • 系统时钟参考 (System Clock Reference, SCR): 用于同步解码器时钟
      • 复用率 (Mux Rate): 指示 PS 流的比特率
    • 系统头部 (System Header): 描述整个 PS 流的系统级别信息,通常在 PS 流的开始处出现一次
      • 系统头部起始码 (System Header Start Code): 0x00 0x00 0x01 0xBB
      • 速率边界 (Rate Bound): 流的最大比特率
      • 音频边界 (Audio Bound): 音频流的数量
      • 视频边界 (Video Bound): 视频流的数量
      • 固定标志位 (Fixed Flag): 指示是否为固定比特率
      • CSPS_flag, System_audio_lock_flag, System_video_lock_flag: 同步标志
      • 视频和音频流的 PID (Program ID): 标识视频和音频流
    • 程序流映射 (Program Stream Map, PSM): 描述程序的内容,包括音频和视频流的类型和 PID
    • PES 包: 将封装好的 PES 包按时间顺序复用进 PS 流中。可以交错放置视频和音频 PES 包
  • 时间戳处理(PTS/DTS)
    • 将 RTP 包的时间戳信息转换为 PES 包的 PTS/DTS
    • 要确保 PTS/DTS 值正确反映视频帧的显示和解码时间
    • 时间戳单位需要与 PS 流的标准时钟频率 (通常是 90kHz) 匹配

代码实现

下述代码只作为参考,详细封装参考下一个章节笔记

#include <iostream>
#include <vector>
#include <cstdint>
#include <cstring>
#include <iomanip>  // 用于十六进制输出格式化
#include <ctime>    // 用于日志中的时间戳
#include <sstream>  // 用于字符串流// --- 常量定义 ---
const uint16_t VIDEO_STREAM_ID = 0xE0; // 视频流的 Stream ID 示例
const size_t MAX_PES_PAYLOAD_SIZE = 2048; // PES 负载最大尺寸示例,根据需要调整
const uint32_t PS_START_CODE_PREFIX = 0x000001BA;
const uint32_t PES_START_CODE_PREFIX = 0x000001;
const uint8_t NAL_START_CODE_4_BYTE[] = {0x00, 0x00, 0x00, 0x01};// --- 结构体定义 ---
#pragma pack(push, 1) // 确保结构体内部没有填充字节// 简化的 PES 头部 (关注 PTS)
struct PESHeader {uint32_t packet_start_code_prefix;uint8_t stream_id;uint16_t pes_packet_length; // 暂时设置为 0,之后计算uint8_t pes_scrambling_control_indicator_etc; // 标志位和指示器uint8_t pes_header_data_length;uint64_t pts_dts_flags_pts; // PTS 标志和 PTS 值 (简化示例)PESHeader() : packet_start_code_prefix(PES_START_CODE_PREFIX), stream_id(VIDEO_STREAM_ID), pes_packet_length(0),pes_scrambling_control_indicator_etc(0x80), // PTS_DTS_flags: 0b10 (仅 PTS)pes_header_data_length(5), // 仅 PTS 占用 5 字节pts_dts_flags_pts(0) {} // PTS 值稍后设置
};// 极简 PS 头部 (仅用于示例)
struct PSHeader {uint32_t packet_start_code_prefix;uint64_t system_clock_reference; // SCR (System Clock Reference) - 简化PSHeader() : packet_start_code_prefix(PS_START_CODE_PREFIX), system_clock_reference(0) {}
};#pragma pack(pop) // 恢复默认 packing// --- 全局计数器和变量 ---
static uint64_t pes_pts_counter = 0; // PES PTS 计数器示例// --- 日志记录函数 ---
void Log(const std::string& message) {std::time_t now = std::time(nullptr);std::tm local_time;localtime_r(&now, &local_time);char timestamp_str[20];std::strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%d %H:%M:%S", &local_time);std::cout << "[" << timestamp_str << "] " << message << std::endl;
}// --- 辅助函数:将数据转换为十六进制字符串 ---
std::string ToHex(const uint8_t* data, size_t size) {std::stringstream hex_stream;hex_stream << std::hex << std::setfill('0');for (size_t i = 0; i < size; ++i) {hex_stream << std::setw(2) << static_cast<int>(data[i]) << " ";}return hex_stream.str();
}// --- 阶段 1: NALU 封装为 PES 包 ---
std::vector<uint8_t> EncapsulateNALUtoPES(const std::vector<uint8_t>& nalu_data) {Log("--- 开始 NALU 到 PES 的封装 ---");std::vector<uint8_t> pes_packet_buffer;PESHeader pes_header;pes_header.pts_dts_flags_pts = (static_cast<uint64_t>(pes_pts_counter++) << 3) | 0x02; // 仅设置 PTS 标志uint64_t pts_33_to_1 = (pes_pts_counter * 300) % 0x200000000LL; // 假设 300 ticks/ms, 90kHz clockuint32_t pts_32_to_2 = pts_33_to_1 & 0xFFFFFFFE0LL;uint8_t pts_byte0 = 0x20 | ((pts_32_to_2 >> 30) & 0x07) << 1 | 0x01;uint16_t pts_byte1_2 = (pts_32_to_2 >> 15) & 0xFFFF;uint16_t pts_byte3_4 = pts_32_to_2 & 0xFFFF;pes_header.pts_dts_flags_pts |= (static_cast<uint64_t>(pts_byte0) << 40);pes_header.pts_dts_flags_pts |= (static_cast<uint64_t>(pts_byte1_2) << 24);pes_header.pts_dts_flags_pts |= (static_cast<uint64_t>(pts_byte3_4) >> 8);pes_header.pes_packet_length = htons(sizeof(PESHeader) + nalu_data.size() - 6); // PES 包长度不包括 包起始码前缀 和 长度字段自身pes_packet_buffer.resize(sizeof(PESHeader));memcpy(pes_packet_buffer.data(), &pes_header, sizeof(PESHeader));Log("创建 PES 头部: " + ToHex(pes_packet_buffer.data(), sizeof(PESHeader)));Log("PES PTS Value: " + std::to_string(pes_pts_counter));// 将 NALU 数据直接添加到 PES 负载pes_packet_buffer.insert(pes_packet_buffer.end(), nalu_data.begin(), nalu_data.end());Log("PES Payload (NALU 数据): Size " + std::to_string(nalu_data.size()) + " bytes.");Log("--- NALU 到 PES 封装完成. PES Packet Size: " + std::to_string(pes_packet_buffer.size()) + " bytes. ---\n");return pes_packet_buffer;
}// --- 阶段 2: 将 PES 包放入 PS 流 (简化示例,仅包含 PS 头部和 PES 包) ---
std::vector<uint8_t> CreatePSStream(const std::vector<uint8_t>& pes_packet) {Log("--- 开始创建 PS 流 ---");std::vector<uint8_t> ps_stream_buffer;PSHeader ps_header;// SCR (System Clock Reference) 示例 - 非常简化ps_header.system_clock_reference = (static_cast<uint64_t>(pes_pts_counter * 300) << 3) | 0x01; // 90kHz clock, marker_bits = '01'ps_stream_buffer.resize(sizeof(PSHeader));memcpy(ps_stream_buffer.data(), &ps_header, sizeof(PSHeader));Log("创建 PS 头部: " + ToHex(ps_stream_buffer.data(), sizeof(PSHeader)));ps_stream_buffer.insert(ps_stream_buffer.end(), pes_packet.begin(), pes_packet.end());Log("将 PES 包添加到 PS 流. PES Packet Size: " + std::to_string(pes_packet.size()) + " bytes.");Log("--- PS 流创建完成. Total PS Stream Size: " + std::to_string(ps_stream_buffer.size()) + " bytes. ---\n");return ps_stream_buffer;
}int main() {Log("--- 示例程序开始 ---");// 示例 H.265 NALU 数据 (这里用一些虚拟数据代替实际的 H.265 NALU)std::vector<uint8_t> h265_nalu_data = {0x00, 0x00, 0x00, 0x01, // NALU Start Code (4-byte) - 虽然 PS 流中通常不直接包含,这里为了更贴近NALU理解先加上,实际情况可能不需要0x40, // NALU Header (example type)0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,// ... 假设这里是真实的 H.265 NALU 负载数据 ...};int nalu_type = 32; // 示例 NALU 类型 (VPS)Log("原始 H.265 NALU 数据: Size = " + std::to_string(h265_nalu_data.size()) + " bytes, Type = " + std::to_string(nalu_type));Log(ToHex(h265_nalu_data.data(), h265_nalu_data.size()));// 阶段 1: NALU 封装成 PES 包std::vector<uint8_t> pes_packet = EncapsulateNALUtoPES(h265_nalu_data);// 阶段 2: 创建 PS 流std::vector<uint8_t> ps_stream = CreatePSStream(pes_packet);Log("\n--- 最终 PES 包 (放入 PS 流之前) ---");Log("PES Packet Hex Data (First 64 bytes): \n" + ToHex(pes_packet.data(), std::min((size_t)64, pes_packet.size())));Log("PES Packet Size: " + std::to_string(pes_packet.size()) + " bytes.");Log("\n--- 最终 PS 流 (First 64 bytes) ---");Log("PS Stream Hex Data (First 64 bytes): \n" + ToHex(ps_stream.data(), std::min((size_t)64, ps_stream.size())));Log("PS Stream Size: " + std::to_string(ps_stream.size()) + " bytes.");Log("--- 示例程序结束 ---");return 0;
}

GB28181平台下H265传输逻辑总结

代码实现

root@hcss-ecs-b4a9:/home/test/rtp/nalu# ./test7
[2025-02-23 21:37:20] --- 示例程序开始 ---
[2025-02-23 21:37:20] [process_request] 开始模拟推流, NALU 数量: 5
[2025-02-23 21:37:20] 处理 NALU, Type: 32, Keyframe: Yes, Size: 50 bytes.
[2025-02-23 21:37:20] RTP 分包数: 1
[2025-02-23 21:37:20] 发送网络包, 大小: 165 bytes.
[2025-02-23 21:37:20] Packet Data (First 64 bytes): 
80 e0 00 00 00 00 00 00 12 34 56 78 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 ba 01 
[2025-02-23 21:37:20] 处理 NALU, Type: 33, Keyframe: Yes, Size: 100 bytes.
[2025-02-23 21:37:20] RTP 分包数: 1
[2025-02-23 21:37:20] 发送网络包, 大小: 265 bytes.
[2025-02-23 21:37:20] Packet Data (First 64 bytes): 
80 e0 00 01 00 00 0e a6 12 34 56 78 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f 60 61 62 63 64 65 66 
[2025-02-23 21:37:20] 处理 NALU, Type: 34, Keyframe: Yes, Size: 30 bytes.
[2025-02-23 21:37:20] RTP 分包数: 1
[2025-02-23 21:37:20] 发送网络包, 大小: 125 bytes.
[2025-02-23 21:37:20] Packet Data (First 64 bytes): 
80 e0 00 02 00 00 1d 4c 12 34 56 78 97 98 99 9a 9b 9c 9d 9e 9f a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af b0 b1 b2 b3 b4 ba 01 00 00 4c 1d 00 00 00 00 00 00 00 00 bb 01 00 00 00 02 00 00 
[2025-02-23 21:37:20] 处理 NALU, Type: 19, Keyframe: Yes, Size: 2048 bytes.
[2025-02-23 21:37:20] RTP 分包数: 2
[2025-02-23 21:37:20] 发送网络包, 大小: 1412 bytes.
[2025-02-23 21:37:20] Packet Data (First 64 bytes): 
80 60 00 03 00 00 2b f2 12 34 56 78 ba 01 00 00 f2 2b 00 00 00 00 00 00 00 00 bb 01 00 00 00 02 00 00 bc 01 00 00 00 06 00 00 00 00 00 00 01 00 00 00 e0 08 0d 80 05 1e 00 00 00 00 21 00 00 00 
[2025-02-23 21:37:20] 发送网络包, 大小: 713 bytes.
[2025-02-23 21:37:20] Packet Data (First 64 bytes): 
80 e0 00 04 00 00 2b f2 12 34 56 78 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 
[2025-02-23 21:37:21] 处理 NALU, Type: 1, Keyframe: No, Size: 1500 bytes.
[2025-02-23 21:37:21] RTP 分包数: 2
[2025-02-23 21:37:21] 发送网络包, 大小: 1412 bytes.
[2025-02-23 21:37:21] Packet Data (First 64 bytes): 
80 60 00 05 00 00 3a 98 12 34 56 78 ba 01 00 00 98 3a 00 00 00 00 00 00 00 00 01 00 00 00 e0 05 e9 80 05 27 00 00 00 00 21 00 00 00 00 98 99 9a 9b 9c 9d 9e 9f a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa 
[2025-02-23 21:37:21] 发送网络包, 大小: 145 bytes.
[2025-02-23 21:37:21] Packet Data (First 64 bytes): 
80 e0 00 06 00 00 3a 98 12 34 56 78 ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 
[2025-02-23 21:37:21] --- 示例程序结束 ---
#include <iostream>
#include <vector>
#include <cstdint>
#include <cstring>
#include <iomanip>  // 用于十六进制输出格式化
#include <ctime>    // 用于日志中的时间戳
#include <sstream>  // 用于字符串流
#include <netinet/in.h> // 包含 htons 的声明
#include <chrono>
#include <thread>
#include <numeric> // std::iota
#include <malloc.h>// --- 常量定义 ---
const uint16_t VIDEO_STREAM_ID = 0xE0; // 视频流的 Stream ID 示例
const size_t MAX_RTP_PAYLOAD_SIZE = 1400; // RTP 负载最大尺寸示例
const uint32_t PS_START_CODE_PREFIX = 0x000001BA;
const uint32_t PES_START_CODE_PREFIX = 0x000001;
const uint8_t NAL_START_CODE_4_BYTE[] = {0x00, 0x00, 0x00, 0x01};const int PS_HDR_LEN = 14; // 示例 PS Header Length
const int SYS_HDR_LEN = 8; // 示例 System Header Length
const int PSM_HDR_LEN = 12; // 示例 PSM Header Length
const int PES_HDR_LEN = 19; // 示例 PES Header Length
const int RTP_HDR_LEN = 12; // 示例 RTP Header Length// --- 结构体定义 ---
#pragma pack(push, 1) // 确保结构体内部没有填充字节// 简化的 PS 头部 (示例)
struct PSHeader {uint32_t packet_start_code_prefix;uint64_t system_clock_reference; // SCRPSHeader() : packet_start_code_prefix(PS_START_CODE_PREFIX), system_clock_reference(0) {}
};// 简化的系统头部 (示例)
struct SystemHeader {uint32_t system_header_start_code;uint16_t header_length;uint8_t rate_bound[3];uint8_t audio_bound;uint8_t fixed_flag_etc;SystemHeader() : system_header_start_code(0x000001BB), header_length(0), audio_bound(0), fixed_flag_etc(0) {rate_bound[0] = rate_bound[1] = rate_bound[2] = 0;}
};// 简化的 PSM 头部 (示例)
struct PSMHeader {uint32_t program_stream_map_start_code;uint16_t psm_length;uint8_t program_number[2];uint8_t version_current_next_indicator;uint8_t section_number;uint8_t last_section_number;uint8_t program_info_length[2];// ... (省略 Program Stream Info 和 ES Info 循环) ...PSMHeader() : program_stream_map_start_code(0x000001BC), psm_length(0), version_current_next_indicator(0), section_number(0), last_section_number(0) {program_number[0] = program_number[1] = 0;program_info_length[0] = program_info_length[1] = 0;}
};// 简化的 PES 头部 (关注 PTS)
struct PESHeader {uint32_t packet_start_code_prefix;uint8_t stream_id;uint16_t pes_packet_length;uint8_t pes_scrambling_control_indicator_etc;uint8_t pes_header_data_length;uint64_t pts_dts_flags_pts;PESHeader() : packet_start_code_prefix(PES_START_CODE_PREFIX), stream_id(VIDEO_STREAM_ID), pes_packet_length(0),pes_scrambling_control_indicator_etc(0x80), // PTS_DTS_flags: 0b10 (仅 PTS)pes_header_data_length(5),pts_dts_flags_pts(0) {}
};// 简化的 RTP 头部
struct RTPHeader {uint8_t version_padding_extension_csrc_count; // V, P, X, CCuint8_t marker_payload_type;                 // M, PTuint16_t sequence_number;uint32_t timestamp;uint32_t ssrc;RTPHeader() : version_padding_extension_csrc_count(0x80), marker_payload_type(96), sequence_number(0), timestamp(0), ssrc(0x12345678) {} // SSRC 示例
};// Nalu 结构体
struct Nalu {int type;int length;std::vector<uint8_t> packet;Nalu() : type(0), length(0) {}~Nalu() {}
};using NaluType = int;#pragma pack(pop) // 恢复默认 packing// --- 全局计数器和变量 ---
static uint64_t pes_pts_counter = 0; // PES PTS 计数器示例
static uint16_t rtp_seq_counter = 0;// --- 日志记录函数 ---
void Log(const std::string& message) {std::time_t now = std::time(nullptr);std::tm local_time;localtime_r(&now, &local_time);char timestamp_str[20];std::strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%d %H:%M:%S", &local_time);std::cout << "[" << timestamp_str << "] " << message << std::endl;
}// --- 辅助函数:将数据转换为十六进制字符串 ---
std::string ToHex(const uint8_t* data, size_t size) {std::stringstream hex_stream;hex_stream << std::hex << std::setfill('0');for (size_t i = 0; i < size; ++i) {hex_stream << std::setw(2) << static_cast<int>(data[i]) << " ";}return hex_stream.str();
}// --- 将 VPS, SPS, PPS 处理为一个流 ---
std::vector<uint8_t> vps_data;
std::vector<uint8_t> sps_data;
std::vector<uint8_t> pps_data;void out_nalu(char *buffer, int size, NaluType naluType) {if (naluType == 32) {  // VPSvps_data.resize(size);memcpy(vps_data.data(), buffer, size);} else if (naluType == 33) {  // SPSsps_data.resize(size);memcpy(sps_data.data(), buffer, size);} else if (naluType == 34) {  // PPSpps_data.resize(size);memcpy(pps_data.data(), buffer, size);} else {Nalu *nalu = new Nalu;bool is_i_frame = (naluType == 19);  // IDR framechar *packet = (char *)malloc(is_i_frame ? (size + vps_data.size() + sps_data.size() + pps_data.size()) : size);if (is_i_frame) {memcpy(packet, vps_data.data(), vps_data.size());memcpy(packet + vps_data.size(), sps_data.data(), sps_data.size());memcpy(packet + vps_data.size() + sps_data.size(), pps_data.data(), pps_data.size());memcpy(packet + vps_data.size() + sps_data.size() + pps_data.size(), buffer, size);size += (vps_data.size() + sps_data.size() + pps_data.size());} else {memcpy(packet, buffer, size);}nalu->packet = std::vector<uint8_t>(packet, packet + size);nalu->length = size;nalu->type = naluType;// 将 nalu 添加到 nalu_vector 或进行其他处理delete[] packet;}
}// --- 头部生成函数 ---
void gb28181_make_ps_header(char *header, long pts) {PSHeader ps_header_struct;ps_header_struct.system_clock_reference = pts; // 简化 SCRmemcpy(header, &ps_header_struct, sizeof(PSHeader));
}void gb28181_make_sys_header(char *header, int rate_bound) {SystemHeader sys_header_struct;sys_header_struct.header_length = htons(SYS_HDR_LEN - 6); // Length after header_length fieldsys_header_struct.rate_bound[0] = (rate_bound >> 16) & 0xFF;sys_header_struct.rate_bound[1] = (rate_bound >> 8) & 0xFF;sys_header_struct.rate_bound[2] = rate_bound & 0xFF;memcpy(header, &sys_header_struct, sizeof(SystemHeader));
}void gb28181_make_psm_header(char *header) {PSMHeader psm_header_struct;psm_header_struct.psm_length = htons(PSM_HDR_LEN - 6); // Length after psm_length fieldmemcpy(header, &psm_header_struct, sizeof(PSMHeader));
}void gb28181_make_pes_header(char *header, int stream_id, int data_len, long pts, long dts) {PESHeader pes_header_struct;pes_header_struct.stream_id = stream_id;pes_header_struct.pes_packet_length = htons(data_len + PES_HDR_LEN - 6); // Length after pes_packet_length fieldpes_header_struct.pts_dts_flags_pts = (static_cast<uint64_t>(pes_pts_counter++) << 3) | 0x02; // 仅设置 PTS 标志uint64_t pts_33_to_1 = (pes_pts_counter * 300) % 0x200000000LL; // 假设 300 ticks/ms, 90kHz clockuint32_t pts_32_to_2 = pts_33_to_1 & 0xFFFFFFFE0LL;uint8_t pts_byte0 = 0x20 | ((pts_32_to_2 >> 30) & 0x07) << 1 | 0x01;uint16_t pts_byte1_2 = (pts_32_to_2 >> 15) & 0xFFFF;uint16_t pts_byte3_4 = pts_32_to_2 & 0xFFFF;pes_header_struct.pts_dts_flags_pts |= (static_cast<uint64_t>(pts_byte0) << 40);pes_header_struct.pts_dts_flags_pts |= (static_cast<uint64_t>(pts_byte1_2) << 24);pes_header_struct.pts_dts_flags_pts |= (static_cast<uint64_t>(pts_byte3_4) >> 8);memcpy(header, &pes_header_struct, sizeof(PESHeader));
}void gb28181_make_rtp_header(char *header, int seq, long pts, int ssrc, bool marker) {RTPHeader rtp_header_struct;rtp_header_struct.sequence_number = htons(seq);rtp_header_struct.timestamp = htonl(pts);rtp_header_struct.ssrc = htonl(ssrc);if (marker) {rtp_header_struct.marker_payload_type |= (1 << 7); // Set marker bit}memcpy(header, &rtp_header_struct, sizeof(RTPHeader));
}// --- 发送网络数据包 ---
void send_network_packet(char *packet, int packet_size) {Log("发送网络包, 大小: " + std::to_string(packet_size) + " bytes.");// 模拟发送网络包,实际应用中替换为 socket send 操作Log("Packet Data (First 64 bytes): \n" + ToHex((uint8_t*)packet, std::min((size_t)64, (size_t)packet_size)));
}// --- 判断是否为关键帧 ---
bool is_keyframe(NaluType type) {// 这里可以根据 NALU 类型判断是否为关键帧// 例如,对于 H.264, IDR 帧 (type 5) 是关键帧,对于 H.265, IDR_W_RADL (type 19) 和 IDR_N_LP (type 20) 是关键帧// 在你的代码中,type 19 被认为是 IDR 帧return (type == 19 || type == 32 || type == 33 || type == 34); // 假设 VPS, SPS, PPS 也被视为关键帧,用于某些 header 的添加逻辑
}// --- 主程序 ---
int main() {Log("--- 示例程序开始 ---");// 模拟更真实的 NALU 数组,包含 VPS, SPS, PPS, IDR 关键帧, 普通帧std::vector<Nalu*> nalu_vector_sim;// 模拟 VPS, SPS, PPS (通常在 IDR 帧前)struct Nalu* vps_nalu = new Nalu();vps_nalu->type = 32; // VPS_NUTvps_nalu->length = 50;vps_nalu->packet.resize(vps_nalu->length);std::iota(vps_nalu->packet.begin(), vps_nalu->packet.end(), 1);nalu_vector_sim.push_back(vps_nalu);out_nalu((char*)vps_nalu->packet.data(), vps_nalu->length, 32);struct Nalu* sps_nalu = new Nalu();sps_nalu->type = 33; // SPS_NUTsps_nalu->length = 100;sps_nalu->packet.resize(sps_nalu->length);std::iota(sps_nalu->packet.begin(), sps_nalu->packet.end(), 51);nalu_vector_sim.push_back(sps_nalu);out_nalu((char*)sps_nalu->packet.data(), sps_nalu->length, 33);struct Nalu* pps_nalu = new Nalu();pps_nalu->type = 34; // PPS_NUTpps_nalu->length = 30;pps_nalu->packet.resize(pps_nalu->length);std::iota(pps_nalu->packet.begin(), pps_nalu->packet.end(), 151);nalu_vector_sim.push_back(pps_nalu);out_nalu((char*)pps_nalu->packet.data(), pps_nalu->length, 34);// 模拟 IDR 关键帧struct Nalu* idr_nalu = new Nalu();idr_nalu->type = 19; // IDR_W_RADL (示例 IDR 类型)idr_nalu->length = 2048;idr_nalu->packet.resize(idr_nalu->length);std::iota(idr_nalu->packet.begin(), idr_nalu->packet.end(), 201);nalu_vector_sim.push_back(idr_nalu);out_nalu((char*)idr_nalu->packet.data(), idr_nalu->length, 19);// 模拟 普通帧 (非关键帧)struct Nalu* non_idr_nalu = new Nalu();non_idr_nalu->type = 1; // TRAIL_R_NUT (示例 普通帧类型)non_idr_nalu->length = 1500;non_idr_nalu->packet.resize(non_idr_nalu->length);std::iota(non_idr_nalu->packet.begin(), non_idr_nalu->packet.end(), 2200);nalu_vector_sim.push_back(non_idr_nalu);out_nalu((char*)non_idr_nalu->packet.data(), non_idr_nalu->length, 1);// RTP 发送处理略...int time_base = 90000;int fps = 24;int send_packet_interval = 1000 / fps;int interval = time_base / fps;long pts = 0;int single_packet_max_length = 1400;int ssrc_val = 0x12345678; // 示例 SSRCstd::string rtp_protocol_type = "UDP/RTP/AVP"; // 或 "TCP/RTP/AVP"Log("[process_request] 开始模拟推流, NALU 数量: " + std::to_string(nalu_vector_sim.size()));for (auto* nalu : nalu_vector_sim) {const NaluType type = nalu->type;const int length = nalu->length;const uint8_t* packet = nalu->packet.data();const bool is_key = is_keyframe(type);Log("处理 NALU, Type: " + std::to_string(type) + ", Keyframe: " + (is_key ? "Yes" : "No") + ", Size: " + std::to_string(length) + " bytes.");// 在遇到 VPS、SPS、PPS 时,先进行封装char frame_buffer[1024 * 128]; // 帧数据缓冲区int frame_index = 0;char ps_header_buf[PS_HDR_LEN];char sys_header_buf[SYS_HDR_LEN];char psm_header_buf[PSM_HDR_LEN];char pes_header_buf[PES_HDR_LEN];char rtp_packet_buf[RTP_HDR_LEN + 1400];// 声明 rtp_header_bufchar rtp_header_buf[RTP_HDR_LEN];// 封装 VPS, SPS, PPSif (type == 32 || type == 33 || type == 34) { // VPS, SPS, PPSmemcpy(frame_buffer + frame_index, packet, length);frame_index += length;}// --- PS 封装 ---gb28181_make_ps_header(ps_header_buf, pts);memcpy(frame_buffer + frame_index, ps_header_buf, PS_HDR_LEN);frame_index += PS_HDR_LEN;if (is_key) {gb28181_make_sys_header(sys_header_buf, 0x3f); // 示例 rate_boundmemcpy(frame_buffer + frame_index, sys_header_buf, SYS_HDR_LEN);frame_index += SYS_HDR_LEN;gb28181_make_psm_header(psm_header_buf);memcpy(frame_buffer + frame_index, psm_header_buf, PSM_HDR_LEN);frame_index += PSM_HDR_LEN;}// --- PES 封装 ---gb28181_make_pes_header(pes_header_buf, 0xe0, length, pts, pts);memcpy(frame_buffer + frame_index, pes_header_buf, PES_HDR_LEN);frame_index += PES_HDR_LEN;memcpy(frame_buffer + frame_index, packet, length);frame_index += length;// --- RTP 分包发送 ---int rtp_packet_count = (frame_index + single_packet_max_length - 1) / single_packet_max_length;Log("RTP 分包数: " + std::to_string(rtp_packet_count));for (int i = 0; i < rtp_packet_count; ++i) {bool is_last_packet = (i == rtp_packet_count - 1);gb28181_make_rtp_header(rtp_header_buf, rtp_seq_counter, pts, ssrc_val, is_last_packet);int offset = i * single_packet_max_length;int data_size = std::min(single_packet_max_length, frame_index - offset);int rtp_start_index = 0;if (rtp_protocol_type == "TCP/RTP/AVP") {uint16_t packet_length = RTP_HDR_LEN + data_size;rtp_packet_buf[0] = (packet_length >> 8) & 0xFF;rtp_packet_buf[1] = packet_length & 0xFF;rtp_start_index = 2;}memcpy(rtp_packet_buf + rtp_start_index, rtp_header_buf, RTP_HDR_LEN);memcpy(rtp_packet_buf + rtp_start_index + RTP_HDR_LEN, frame_buffer + offset, data_size);send_network_packet(rtp_packet_buf, rtp_start_index + RTP_HDR_LEN + data_size);rtp_seq_counter++;}pts += interval;std::this_thread::sleep_for(std::chrono::milliseconds(send_packet_interval));delete nalu; // 模拟 Device::push_rtp_stream 中的 nalu delete}Log("--- 示例程序结束 ---");return 0;
}

基本逻辑

  •  VPS/SPS/PPS 缓冲
    • out_nalu函数会将 buffer 中的数据分别复制到全局变量中,等待遇到关键帧的时候将其加入进入
  • IDR 帧处理 (关键帧)
    • 将缓冲区中的VPS/PPS/SPS加入到Nalu之前
  • 非 IDR 帧处理 (普通帧)
    • 直接分配内存进行发送

相关文章:

【音视频】H265解码Nalu后封装rtp包

概述 基于ZLM流媒体框架以及简单RTSP服务器开源项目分析总结&#xff0c;相关源码参考以下链接 H265-rtp提取Nalu逻辑 通过rtsp流地址我们可以获取视频流中的多个rtp包&#xff0c;其中每个RTP包中又会包含一个或者多个Nalu&#xff0c;将其提取处理 总体逻辑分析 核心逻辑在…...

Linux -- I/O接口,文件标识符fd、file结构体、缓冲区、重定向、简单封装C文件接口

一、理解文件 狭隘理解&#xff08;传统视角&#xff09; 聚焦物理存储&#xff1a;文件特指存储在磁盘等外存设备上的二进制数据集合输入输出特性&#xff1a; 写入文件&#xff1a;CPU 通过总线将数据输出到磁盘读取文件&#xff1a;磁盘通过 DMA 将数据输入到内存 &#xff…...

系统讨论Qt的并发编程2——介绍一下Qt并发的一些常用的东西

目录 QThreadPool与QRunnable 互斥机制&#xff1a;QMutex, QMutexLocker, QSemaphore, QWaitCondition 跨线程的通信 入门QtConcurrent&#xff0c;Qt集成的一个并发框架 一些参考 QThreadPool与QRunnable QThreadPool自身预备了一些QThread。这样&#xff0c;我们就不需…...

【数据挖掘】Pandas之DataFrame

在 Pandas 中&#xff0c;DataFrame 提供了丰富的数据操作功能&#xff0c;包括 查询、编辑、分类和汇总。 1. 数据查询&#xff08;Filtering & Querying&#xff09; 1.1 按索引或列名查询 import pandas as pddata {"ID": [101, 102, 103, 104, 105],"…...

C++:volatile、const、mutable关键字

文章目录 volatile、const、mutable 关键字的作用、联系与区别 1️⃣ **volatile** —— 防止编译器优化&#xff0c;确保变量每次访问都从内存读取**作用****使用场景****示例** 2️⃣ **const** —— 限制变量的修改&#xff0c;保证不可变性**作用****使用场景****示例** 3️…...

linux离线安装miniconda环境

1 下载安装包 可以在官网下载最新版 https://www.anaconda.com/download/success#miniconda 或者在软件目录选择合适的版本 https://repo.anaconda.com/miniconda/ 安装包传入离线服务器 ./Miniconda3-py311_24.9.2-0-Linux-x86_64.sh2 运行安装包 ./Miniconda3-py311_24…...

考研408数据结构线性表核心知识点与易错点详解(附真题示例与避坑指南)

一、线性表基础概念 1.1 定义与分类 定义&#xff1a;线性表是由n&#xff08;n≥0&#xff09;个相同类型数据元素构成的有限序列&#xff0c;元素间呈线性关系。 分类&#xff1a; 顺序表&#xff1a;元素按逻辑顺序存储在一段连续的物理空间中&#xff08;数组实现&…...

selenium用例执行过程采集操作形成测试报告上的回复

在代码执行的过程中不断的进行截图&#xff0c;把截图拼接成gif动态图&#xff0c;放在测试报告上 1、每条用例执行启动一个线程&#xff0c;这个线程会每隔0.3秒进行截图 项目下创建一个临时目录video用来存储所有截图以及gif动态图封装不断截图的方法&#xff0c;每隔0.3秒…...

多元数据直观表示(R语言)

一、实验目的&#xff1a; 通过上机试验&#xff0c;掌握R语言实施数据预处理及简单统计分析中的一些基本运算技巧与分析方法&#xff0c;进一步加深对R语言简单统计分析与图形展示的理解。 数据&#xff1a; 链接: https://pan.baidu.com/s/1kMdUWXuGCfZC06lklO5iXA 提取码: …...

【JavaEE】线程安全

【JavaEE】线程安全 一、引出线程安全二、引发线程安全的原因三、解决线程安全问题3.1 synchronized关键字&#xff08;解决修改操作不是原子的&#xff09;3.1.1 synchronized的特性3.1.1 synchronized的使用事例 3.2 volatile 关键字&#xff08;解决内存可见性&#xff09; …...

HarmonyOS 5.0应用开发——多线程Worker和@Sendable的使用方法

【高心星出品】 文章目录 多线程Worker和Sendable的使用方法开发步骤运行结果 多线程Worker和Sendable的使用方法 Worker在HarmonyOS中提供了一种多线程的实现方式&#xff0c;它允许开发者在后台线程中执行长耗时任务&#xff0c;从而避免阻塞主线程并提高应用的响应性。 S…...

华为OD-2024年E卷-分批萨[100分]

文章目录 题目描述输入描述输出描述用例1解题思路Python3源码 题目描述 吃货"和"馋嘴"两人到披萨店点了一份铁盘&#xff08;圆形&#xff09;披萨&#xff0c;并嘱咐店员将披萨按放射状切成大小相同的偶数个小块。但是粗心的服务员将披萨切成了每块大小都完全不…...

SSH监控

创建/etc/ssh/sshrc文件 写入以命令 echo " 系统状态 " uptime free -h 每次登录会显示 如果在sshrc文件加入以下脚本每次登录就是执行这个脚本 # cat /etc/ssh/sshrc echo " 系统状态 " uptime free -h /usr/local/bin/monit.sh以…...

leetcode日记(74)扰乱字符串

很有难度的一题&#xff0c;一开始真的绕了很多思维上的弯路。 最开始的想法是递归&#xff0c;看到题目的时候想到动态规划但是完全没有思路应该怎么用&#xff0c;结果确实是递归动态规划。 最开始的想法是构建树&#xff0c;每一层包含这一步划分的方法&#xff08;实际会…...

RV1126的OSD模块和SDL_TTF结合输出H264文件

目录 一.RV1126多线程处理输出OSD字符叠加图层的流程 1.1. VI模块的初始化 1.2. 初始化VENC模块&#xff1a; 1.3. 初始化RGN模块&#xff1a; 1.4. 绑定VI模块和VENC模块&#xff0c;伪代码如下 1.5. 创建多线程进行OSD字库的叠加&#xff1a; 1.6. 获取每一帧处理过后的…...

GEE:计算长时间序列NPP与NDVI之间的相关系数

GEE中内置了计算相关系数的函数&#xff0c;可以分析两个变量之间的相关性&#xff0c;比如要分析两个波段之间的相关性&#xff0c;主要用到ee.Reducer.pearsonsCorrelation()函数。 ee.Reducer.pearsonsCorrelation() 内容&#xff1a;创建一个双输入归约器&#xff0c;用于…...

水仙花数(华为OD)

题目描述 所谓水仙花数&#xff0c;是指一个n位的正整数&#xff0c;其各位数字的n次方和等于该数本身。 例如153是水仙花数&#xff0c;153是一个3位数&#xff0c;并且153 13 53 33。 输入描述 第一行输入一个整数n&#xff0c;表示一个n位的正整数。n在3到7之间&#x…...

【对话状态跟踪】关心整个对话过程用户完整意图变化

对话状态管理器 核心逻辑是解决键冲突和验证范围有效性&#xff0c; 但需依赖外部输入的正确性。在实际应用中&#xff0c; 可能需要结合用户提示或自动修正逻辑以提高鲁棒性。 NLU 槽 值 对儿 NLU的目的是把自然语言解析成结构化语义。结构化语义有多种表示方式&#xff0c…...

【分享】网间数据摆渡系统,如何打破传输瓶颈,实现安全流转?

在数字化浪潮中&#xff0c;企业对数据安全愈发重视&#xff0c;网络隔离成为保护核心数据的重要手段。内外网隔离、办公网与研发网隔离等措施&#xff0c;虽为数据筑牢了防线&#xff0c;却也给数据传输带来了诸多难题。传统的数据传输方式在安全性、效率、管理等方面暴露出明…...

TikTok创作者市场关闭!全新平台TikTok One将带来哪些改变?

TikTok创作者市场关闭&#xff0c;全新平台TikTok One上线&#xff0c;创作者和品牌将迎来哪些新机遇&#xff1f; 近日&#xff0c;TikTok宣布关闭其原有的创作者市场&#xff08;TikTok Creator Marketplace&#xff09;&#xff0c;并推出全新平台TikTok One。这一消息在社…...

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…...

Ubuntu系统下交叉编译openssl

一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机&#xff1a;Ubuntu 20.04.6 LTSHost&#xff1a;ARM32位交叉编译器&#xff1a;arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...

ServerTrust 并非唯一

NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...

从零实现STL哈希容器:unordered_map/unordered_set封装详解

本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说&#xff0c;直接开始吧&#xff01; 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...

06 Deep learning神经网络编程基础 激活函数 --吴恩达

深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...

蓝桥杯3498 01串的熵

问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798&#xff0c; 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

优选算法第十二讲:队列 + 宽搜 优先级队列

优选算法第十二讲&#xff1a;队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

算法笔记2

1.字符串拼接最好用StringBuilder&#xff0c;不用String 2.创建List<>类型的数组并创建内存 List arr[] new ArrayList[26]; Arrays.setAll(arr, i -> new ArrayList<>()); 3.去掉首尾空格...

C#中的CLR属性、依赖属性与附加属性

CLR属性的主要特征 封装性&#xff1a; 隐藏字段的实现细节 提供对字段的受控访问 访问控制&#xff1a; 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性&#xff1a; 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑&#xff1a; 可以…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...