RTMP协议是Adobe推出的流媒体传输协议,广泛应用于直播、点播等场景,其传输的数据以帧为单位封装,解析数据帧是处理RTMP流的核心环节。C++作为高性能编程语言,适合实现低延迟的流媒体数据解析逻辑。

RTMP数据帧的基本结构
RTMP数据帧由帧头和帧负载两部分组成,帧头包含帧类型、时间戳、流ID、负载长度等关键信息,帧负载则是具体的音视频数据或者控制指令数据。常见的帧类型包括音频帧、视频帧、控制帧、命令帧等,不同类型的帧解析规则存在差异。
帧头结构说明
RTMP帧头的长度不固定,根据头部字段的编码方式分为12字节基本头、8字节缩减头、4字节最小头三种情况。核心字段如下:
- 帧类型:1字节,标识当前帧的用途,比如8代表音频帧,9代表视频帧,20代表命令帧
- 负载长度:3字节,标识后续帧负载的字节数
- 时间戳:3字节或者4字节,标识帧的生成时间,用于音视频同步
- 流ID:3字节,标识当前帧所属的流通道
C++解析数据帧的核心步骤
1. 字节流读取与缓存
RTMP数据是通过TCP传输的字节流,解析前需要先读取完整的帧数据到缓存中,避免半包导致解析错误。可以实现一个简单的字节流读取辅助类:
#include <vector>
#include <cstdint>
#include <stdexcept>
class ByteStreamReader {
private:
std::vector<uint8_t> data;
size_t pos;
public:
ByteStreamReader(const std::vector<uint8_t>& input) : data(input), pos(0) {}
// 读取1字节无符号整数
uint8_t read_uint8() {
if (pos + 1 > data.size()) {
throw std::runtime_error("读取越界");
}
return data[pos++];
}
// 读取3字节无符号整数,RTMP中部分字段为3字节大端存储
uint32_t read_uint24() {
if (pos + 3 > data.size()) {
throw std::runtime_error("读取越界");
}
uint32_t val = (data[pos] << 16) | (data[pos+1] << 8) | data[pos+2];
pos += 3;
return val;
}
// 读取4字节无符号整数,大端存储
uint32_t read_uint32() {
if (pos + 4 > data.size()) {
throw std::runtime_error("读取越界");
}
uint32_t val = (data[pos] << 24) | (data[pos+1] << 16) | (data[pos+2] << 8) | data[pos+3];
pos += 4;
return val;
}
// 获取剩余可读字节数
size_t remaining() const {
return data.size() - pos;
}
// 跳过指定字节数
void skip(size_t n) {
if (pos + n > data.size()) {
throw std::runtime_error("跳过字节数越界");
}
pos += n;
}
};
2. 帧头解析实现
根据RTMP协议规范,先解析帧类型,再根据头部类型解析对应字段:
#include <iostream>
#include <string>
// RTMP帧头结构体
struct RtmpFrameHeader {
uint8_t frame_type; // 帧类型
uint32_t payload_len; // 负载长度
uint32_t timestamp; // 时间戳
uint32_t stream_id; // 流ID
bool has_ext_timestamp; // 是否包含扩展时间戳
};
// 解析RTMP帧头
RtmpFrameHeader parse_rtmp_frame_header(const std::vector<uint8_t>& raw_data) {
ByteStreamReader reader(raw_data);
RtmpFrameHeader header;
// 读取帧类型
header.frame_type = reader.read_uint8();
// 读取负载长度(3字节大端)
header.payload_len = reader.read_uint24();
// 读取时间戳(3字节大端)
uint32_t base_timestamp = reader.read_uint24();
// 读取流ID(3字节大端)
header.stream_id = reader.read_uint24();
// 判断是否需要扩展时间戳,最高位为1时表示有扩展时间戳
if (base_timestamp >= 0xFFFFFF) {
header.has_ext_timestamp = true;
header.timestamp = reader.read_uint32();
} else {
header.has_ext_timestamp = false;
header.timestamp = base_timestamp;
}
return header;
}
3. 帧负载提取与处理
解析完帧头后,根据负载长度从后续字节流中提取负载数据,再根据帧类型做对应处理:
// 解析完整的RTMP数据帧
void parse_rtmp_frame(const std::vector<uint8_t>& frame_data) {
try {
// 先解析帧头
RtmpFrameHeader header = parse_rtmp_frame_header(frame_data);
std::cout << "帧类型: " << (int)header.frame_type << std::endl;
std::cout << "负载长度: " << header.payload_len << std::endl;
std::cout << "时间戳: " << header.timestamp << std::endl;
std::cout << "流ID: " << header.stream_id << std::endl;
// 计算帧头占用的字节数
size_t header_size = 1 + 3 + 3 + 3; // 帧类型+负载长度+时间戳+流ID
if (header.has_ext_timestamp) {
header_size += 4; // 扩展时间戳额外4字节
}
// 提取负载数据
if (frame_data.size() < header_size + header.payload_len) {
std::cout << "帧数据不完整,无法提取负载" << std::endl;
return;
}
std::vector<uint8_t> payload(frame_data.begin() + header_size,
frame_data.begin() + header_size + header.payload_len);
// 根据帧类型处理负载
if (header.frame_type == 8) {
std::cout << "当前为音频帧,负载大小: " << payload.size() << " 字节" << std::endl;
// 这里可以添加音频数据解析逻辑,比如AAC帧解析
} else if (header.frame_type == 9) {
std::cout << "当前为视频帧,负载大小: " << payload.size() << " 字节" << std::endl;
// 这里可以添加视频数据解析逻辑,比如H264帧解析
} else if (header.frame_type == 20) {
std::cout << "当前为命令帧,负载大小: " << payload.size() << " 字节" << std::endl;
// 这里可以添加命令解析逻辑,比如connect、publish等命令解析
} else {
std::cout << "其他类型帧,负载大小: " << payload.size() << " 字节" << std::endl;
}
} catch (const std::exception& e) {
std::cout << "解析帧失败: " << e.what() << std::endl;
}
}
注意事项
实际开发中需要注意几个问题:TCP传输是流式的,需要先判断当前是否收到完整的帧头再解析,避免半包解析错误;RTMP协议中部分字段采用大端存储,读取时要注意字节序转换;不同类型的音视频帧有更细分的格式规范,比如视频帧包含关键帧、非关键帧区分,需要结合具体编码格式做进一步处理。
如果是处理实时RTMP流,还需要考虑数据缓存机制,将TCP接收的零散数据拼接成完整的RTMP数据帧后再进行解析,保证解析的准确性和稳定性。