解 h264 rtp 包
参考:
- srs
1. 概述
本篇文章参考 srs,简单实现一个组帧的模块,用于记录学习。
关于 h264 码流打包 rtp,参考:https://www.cnblogs.com/moonwalk/p/15903760.html。
2. 组帧
伪代码如下,注意点已经通过注释标明:
enum class H264_PACK_TYPE {
STAPA = 0,
FUA = 1,
SINGLE = 2,
};
struct RawData {
char* data;
int size;
};
struct H264Nalu {
void append(char* data, int size);
};
struct RtpPacket {
uint32_t get_timestamp();
uint16_t get_sequence();
bool get_marker();
bool is_keyframe();
bool is_empty_payload();
H264_PACK_TYPE pack_type();
std::vector<RawData*> rtp_nalus parse_payload_stapa();
void parse_payload_fua();
void parse_payload_single();
uint8_t get_fua_header();
RawData* get_fua_payload();
RawData* get_sps();
RawData* get_pps();
};
const static uint8_t H264_ANNEX_B_START_CODE[4] = {0x00, 0x00, 0x00, 0x01};
//
// gop 缓存
//
const static int gop_cache_capacity = 3000;
std::vector<RtpPacket*> gop_cache(gop_cache_capacity);
//
// 组帧时检查的起始 rtp 序列号
//
uint16_t frame_window_start;
//
// 一个 gop cache 内最早的, 还未接收到的 rtp 序列号
//
uint16_t lost_seq;
//
// @brief 判断序列号 prev 与 now 的相对大小
// @note 原理就是如果 now 领先于 prev, 那么:
// 如果没有发生回绕, 相减大于 0
// 如果发生了回绕, 回绕数值小于 0x10000 >> 1(即 uint16_t 最高位为 1), 相减还是大于 0
//
int16_t rtp_seq_distance(uint16_t prev, uint16_t now) {
return (int16_t)(now - prev);
}
//
// @brief 清理序列号在 sequence 之前的所有 rtp 包
//
void clear_gop_cache(uint16_t sequence) {
for (int i = 0; i < gop_cache.size(); ++i) {
GoRtpPacket* rtp_pkt = gop_cache[i];
// 注意处理序列号回绕
if (rtp_pkt && (rtp_seq_distance(rtp_pkt->get_sequence(), sequence) > 0)) {
delete rtp_pkt;
gop_cache[i] = nullptr;
}
}
}
void cache_packet(RtpPacket* rtp_pkt) {
int index = rtp_pkt->get_sequence() % gop_cache_capacity;
if (gop_cache[index]) {
// warning: duplicate rtp package
delete gop_cache[index];
}
gop_cache[index] = rtp_pkt;
}
void uncahce_packet(RtpPacket* rtp_pkt) {
int index = rtp_pkt->get_sequence() % gop_cache_capacity;
delete rtp_pkt;
gop_cache[index] = nullptr;
}
//
// @berief 尝试组帧
// @return {uint16_t} start_seq 找到的帧起始序列号
// @return {uint16_t} end_seq 找到的帧结束序列号
// @return {bool} true 找到一个完整帧
// @note 此函数是一个非常关键的函数
//
bool get_complete_frame(uint16_t& start_seq, uint16_t& end_seq) {
//
// 从下一帧的起始 rtp 序列号开始找
//
RtpPacket* frame_rtp_pkt = gop_cache[frame_window_start % gop_cache_capacity];
if (frame_rtp_pkt == nullptr) {
lost_seq = frame_window_start;
return false;
}
// 一个完整帧可以通过 rtp 时间戳和 mark 标志来判断
uint32_t frame_ts = frame_rtp_pkt->get_timestamp();
//
// 尝试查找一个完整帧, 范围 [frame_window_start, frame_window_start + gop_cache_capacity]
//
for (int i = 0; i < gop_cache_capacity; ++i) {
uint16_t seq = frame_window_start + i;
int index = seq % gop_cache_capacity;
RtpPacket* rtp_pkt = gop_cache[index];
// rtp 缺失, 查找失败
if (rtp_pkt == nullptr) {
lost_seq = seq;
return false;
}
// 找到 rtp mark 标记, 可以认为找到了一个完整帧
if (rtp_pkt->get_marker()) {
start = frame_window_start;
end = seq;
frame_window_start = seq + 1;
return true;
}
// 当前 rtp packet 的 rtp timestamp 与起始的不同, 可以认为找到了一个完整帧
// 实际上这里是一个兼容处理, 发送端应该正确打上 mark 标记
if (rtp_pkt->get_timestamp() != frame_ts) {
start = frame_window_start;
end = seq - 1;
frame_window_start = seq;
return true;
}
}
return false;
}
//
// @berief 输入一个包含 sps、pps、sei nalu 的 rtp packet, 组合 nalu 流后输出
// @param {RtpPacket*} rtp_pkt rtp 包
// @return {H264Nalu*} nalu annex-b 码流格式
// @note 一般 webrtc 发送 sps、pp、sei、idr 时, sps && pps && sei 会组合到 stap-a 一个 rtp 包发出,
// idr 紧接着以 fu-a 格式多个 rtp 包发出, 他们拥有相同的 timestamp
//
H264Nalu* on_spspps(RtpPacket* rtp_pkt) {
RawData* sps = rtp_pkt->get_sps();
RawData* pps = rtp_pkt->get_pps();
if (sps == nullptr || pps == nullptr) {
// error, 错误发生
return nullptr;
}
H264Nalu* nalu = new H264Nalu;
nalu->append(sps->data, sps->size);
nalu->append(H264_ANNEX_B_START_CODE, sizeof(H264_ANNEX_B_START_CODE));
nalu->append(pps->data, pps->size);
return nalu;
}
//
// @brief 输入一个 rtp packet, 输出 nalu 数组
// @param {RtpPacket*} rtp_pkt rtp 包
// @return {std::vector<H264Nalu*>} nalu 数组
//
std::vector<H264Nalu*> on_rtp(RtpPacket* rtp_pkt) {
std::vector<H264Nalu*> nalu_queue;
//
// 每次关键帧清理 gop cache
//
if (rtp_pkt->is_keyframe()) {
clear_gop_cache(rtp_pkt->get_sequence());
frame_window_start = rtp_pkt->get_sequence();
lost_seq = frame_window_start + 1;
}
//
// 缓存 rtp packet
//
cache_packet(rtp_pkt);
//
// 如果新来的 rtp packet 填充了 gop cache 中缺失的 rtp, 才尝试组帧, 否则继续等待
//
if (rtp_pkt->get_sequence() != lost_seq) {
return nalu_queue;
}
uint16_t start_seq, end_seq;
//
// 不断尝试组帧
//
while (get_complete_frame(start_seq, end_seq)) {
//
// 每个完整帧前面可能会发一些 padding 空包, 需要先去掉
// 注意处理序列号回绕
//
for (uint16_t seq = start_seq; seq != (uint16_t)(end_seq + 1); ++seq) {
RtpPacket* frame_pkt = gop_cache[(int)seq % gop_cache_capacity];
if (frame_pkt->is_empty_payload()) {
uncache_packet(frame_pkt);
start_seq = seq + 1;
} else {
break;
}
}
H264Nalu* nalu = nullptr;
bool fua_start = true;
//
// 遍历一个完整帧的起始序列号 [start_seq, end_seq]
// 注意处理序列号回绕
//
for (uint16_t seq = start_seq; seq != (uint16_t)(end_seq + 1); ++seq) {
RtpPacket* frame_pkt = gop_cache[(int)seq % gop_cache_capacity];
//
// 当前 rtp packet 是 stap-a 类型的
//
if (frame_pkt->pack_type() == H264_PACK_TYPE::STAPA) {
if (frame_pkt->is_keyframe()) {
// 找到一个 nalu(实际上是多个 nalus), 这里可以先不用插入到结果数组,
// 因为一般 sps-pps-sei-idr 会组合在一起发送, 可以等待拼接 idr 后再插入到结果数组
nalu = on_spspps(frame_pkt);
if (nalu == nullptr) {
// error, 发生错误
}
} else {
std::vector<RawData*> rtp_nalus = frame_pkt->parse_payload_stapa();
nalu = new H264Nalu;
for (int j = 0; j < rtp_nalus.size(); ++j) {
nalu->append(rtp_nalus[j]->data, rtp_nalus[j]->size);
if (j != rtp_nalus.size() - 1) {
nalu->append(H264_ANNEX_B_START_CODE, sizeof(H264_ANNEX_B_START_CODE));
}
}
// 找到一个 nalu(实际上是多个 nalus), 插入到结果数组
nalu_queue.push(nalu);
nalu = nullptr;
}
uncache(frame_pkt);
continue;
}
//
// 当前 rtp packet 是 fu-a 类型的
//
if (frame_pkt->pack_type() == H264_PACK_TYPE::FUA) {
frame_pkt->parse_payload_fua();
if (fua_start == true) {
if (nalu == nullptr) {
nalu = new H264Nalu;
} else {
// 注意这里可能会组合上面 stap-a 的 nalu
nalu->append(H264_ANNEX_B_START_CODE, sizeof(H264_ANNEX_B_START_CODE));
}
uint8_t nalu_header = frame_pkt->get_fua_header();
nalu->append((char*)(&nalu_header), 1);
fua_start = false;
}
RawData* rtp_nalu = frame_pkt->get_fua_payload();
nalu->append(rtp_nalu->data, rtp_nalu->size);
if (seq == end_seq) {
// 找到一个 nalu(实际上可能是多个 nalus), 插入到结果数组
nalu_queue.push(nalu);
nalu = nullptr;
}
uncache(frame_pkt);
continue;
}
//
// 当前 rtp packet 是 single 类型的
//
if (frame_pkt->pack_type() == H264_PACK_TYPE::SINGLE) {
frame_pkt->parse_payload_single();
if (nalu == nullptr) {
nalu = new H264Nalu;
} else {
// 注意这里可能会组合上面 stap-a 的 nalu
nalu->append(H264_ANNEX_B_START_CODE, sizeof(H264_ANNEX_B_START_CODE));
}
RawData* rtp_nalu = frame_pkt->get_single_payload();
nalu->append(rtp_nalu->data, rtp_nalu->size);
if (seq == end_seq) {
// 找到一个 nalu, 插入到结果数组
nalu_queue.push(nalu);
nalu = nullptr;
}
uncache(frame_pkt);
continue;
}
//
// 如果不是上面 3 种类型, 可能发生了错误, 直接 uncache 掉
//
uncache(frame_pkt);
}
// 复位 nalu
if (nalu) {
delete nalu;
nalue = nullptr;
}
//
// 每个完整帧后面也可能会发一些 padding 空包, 需要先去掉
//
uint16_t padding_seq = end_seq + 1;
RtpPacket* padding_pkt = gop_cache[padding_seq % gop_cache_capacity];
while (padding_pkt) {
if (padding_pkt->is_empty_payload()) {
frame_window_start = padding_seq + 1;
uncache(padding_pkt);
padding_seq += 1;
padding_pkt = gop_cache[padding_seq % gop_cache_capacity];
} else {
break;
}
}
} // end of get_complete_frame(start_seq, end_seq)
return nalu_queue;
}
如上,整体思路就是缓存最近的一个 GOP cache,从第一个 IDR 开始不断向后扫描,不断得到一个完整帧。
如果向后扫描的时候有 rtp 空缺,需要等待收到乱序的 rtp 包后才能继续组帧。
需要注意 webrtc 客户端可能会发送一些空的 padding 包,组帧的时候需要去掉。