解 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 包,组帧的时候需要去掉。

posted @ 2022-02-17 11:14  小夕nike  阅读(358)  评论(0编辑  收藏  举报