rave/src/protocol/rtsp/depacketizer.rs

1095 lines
42 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! RTP 解包器Depacketizer
//!
//! 将 RTP 包还原为完整的音视频帧:
//! - **H.264 FU-A 重组**:将多个 FU-A 分片重新拼接为原始 NALU
//! - **SPS/PPS 检测**:从 NALU 类型中识别 SPS(7)、PPS(8),合并为 sequence header 帧
//! - **帧分类**IDR(5) → 关键帧non-IDR(1) → P 帧
//! - **AAC 解包**:跳过 AU-headers-length + AU-header提取原始 AAC 数据
//!
//! 输出的 AVFrame.data 采用 FLV/AVCC 格式,与引擎其余部分兼容。
use std::sync::Arc;
use crate::codec::h264::{encode_avcc_sequence_header, nal_unit_type, nal_type};
use crate::sdk::types::{AudioCodec, AVFrame, CodecExtraInfo, FrameType, H264SeqHeader, VideoCodec};
use super::rtp::RtpPacket;
// ── NALU 类型常量 ──────────────────────────────────────────
/// FU-AFragmentation UnitNALU 类型
const NALU_TYPE_FU_A: u8 = 28;
// ── FU-A 重组器 ────────────────────────────────────────────
/// FU-A 分片重组器
///
/// 当一个 NALU 被拆分为多个 RTP 包FU-A 分片)时,
/// 逐包积累负载数据在最后一个分片end bit = 1到达时
/// 输出完整的 NALU。
struct FuAssembler {
/// 原始 NALU 头部(由 FU indicator 的高 3 位 + FU header 的低 5 位重建)
nalu_header: u8,
/// 累积的 NALU 数据nalu_header + 各分片负载)
buffer: Vec<u8>,
/// 是否正在重组中(收到 start bit 后为 true
active: bool,
}
impl FuAssembler {
/// 创建空的重组器
fn new() -> Self {
Self {
nalu_header: 0,
buffer: Vec::new(),
active: false,
}
}
/// 重置重组状态(新 FU-A 序列开始前调用)
fn reset(&mut self) {
self.nalu_header = 0;
self.buffer.clear();
self.active = false;
}
}
// ── RTP 视频解包器 ─────────────────────────────────────────
/// RTP 视频解包器
///
/// 维护 FU-A 重组状态和 SPS/PPS 缓存,
/// 将 RTP 包流转换为引擎内部使用的 AVFrame。
///
/// 每个 RTSP 推流会话Recording 模式)对应一个实例。
pub struct RtpDepacketizer {
/// FU-A 重组器
fu_assembler: FuAssembler,
/// 缓存最近收到的 SPS NALU不含起始码
pending_sps: Option<Vec<u8>>,
/// 缓存最近收到的 PPS NALU不含起始码
pending_pps: Option<Vec<u8>>,
/// 标记是否已向引擎发送过 sequence header
seq_header_emitted: bool,
/// 上一个视频帧的 RTP 时间戳(检测时间戳跳变以判断新帧)
last_video_timestamp: u32,
/// 当前访问单元Access Unit中累积的 NALU 列表
///
/// H.264 一帧画面可能包含多个 NALU如多 slice
/// 它们共享相同的 RTP 时间戳,需要合并为一个 AVFrame。
pending_nalus: Vec<Vec<u8>>,
/// 当前累积 NALU 的 RTP 时间戳
pending_timestamp: u32,
/// 时间戳归一化基准:首个视频帧的 RTP 时间戳
/// RTP 时间戳从随机值开始,转换为毫秒后可能超过 RTMP 扩展时间戳阈值 (0xFFFFFF)
/// 导致 ffmpeg 解析失败。记录首个帧的 RTP 时间戳,后续帧减去此基准值。
video_ts_base: Option<u32>,
}
impl RtpDepacketizer {
/// 创建新的视频解包器
pub fn new() -> Self {
Self {
fu_assembler: FuAssembler::new(),
pending_sps: None,
pending_pps: None,
seq_header_emitted: false,
last_video_timestamp: 0,
pending_nalus: Vec::new(),
pending_timestamp: 0,
video_ts_base: None,
}
}
/// 从 SDP sprop-parameter-sets 预设 SPS/PPS
///
/// 当推流端在 SDP 中声明了 SPS/PPS 但不在 RTP 中发送时,
/// 通过此方法预设到解包器中,避免重复发送 seq_header。
pub fn set_sps_pps(&mut self, sps: Vec<u8>, pps: Vec<u8>) {
self.pending_sps = Some(sps);
self.pending_pps = Some(pps);
self.seq_header_emitted = false;
// 预设 SPS/PPS 时重置累积状态,避免残留数据
self.pending_nalus.clear();
self.pending_timestamp = 0;
}
/// 输入一个 RTP 包,返回零个或多个完整的 AVFrame
///
/// 对于 FU-A 分片,中间包返回空;最后一个分片返回完整的帧。
/// 单个 NALU 包立即返回对应帧。
///
/// # 参数
/// - `rtp` — 已解析的 RTP 包
///
/// # 返回
/// 完整的 AVFrame 列表(可能为空)
pub fn push_rtp(&mut self, rtp: &RtpPacket) -> Vec<AVFrame> {
let payload = &rtp.payload;
if payload.is_empty() {
return Vec::new();
}
let first_byte = payload[0];
let ntype = nal_unit_type(first_byte);
if ntype == NALU_TYPE_FU_A {
// FU-A 分片模式:重组
self.handle_fu_a(rtp)
} else {
// 单 NALU 模式:直接处理
let nalu = payload.clone();
self.emit_nalu(&nalu, rtp.timestamp, rtp.marker)
}
}
/// 处理 FU-A 分片
///
/// 根据 FU header 的 start/end bit 累积或输出 NALU。
fn handle_fu_a(&mut self, rtp: &RtpPacket) -> Vec<AVFrame> {
let payload = &rtp.payload;
if payload.len() < 2 {
return Vec::new();
}
// FU indicator: forbidden(1)=0 | NRI(2) | Type(5)=28
let fu_indicator = payload[0];
// FU header: Start(1) | End(1) | Reserved(1) | Type(5)
let fu_header = payload[1];
let start = (fu_header & 0x80) != 0;
let end = (fu_header & 0x40) != 0;
if start {
// 新的 FU-A 序列开始
self.fu_assembler.reset();
// 重建原始 NALU 头部NRI 来自 FU indicatorType 来自 FU header
let nri = fu_indicator & 0x60;
let nalu_type = fu_header & 0x1F;
self.fu_assembler.nalu_header = nri | nalu_type;
self.fu_assembler.buffer = vec![self.fu_assembler.nalu_header];
// 附加第一个分片的负载(跳过 FU indicator + FU header = 2 字节)
if payload.len() > 2 {
self.fu_assembler.buffer.extend_from_slice(&payload[2..]);
}
self.fu_assembler.active = true;
} else if self.fu_assembler.active {
// 中间或最后分片:追加负载
if payload.len() > 2 {
self.fu_assembler.buffer.extend_from_slice(&payload[2..]);
}
} else {
// 未收到 start 就收到中间/末尾分片,丢弃
return Vec::new();
}
if end && self.fu_assembler.active {
// 最后一个分片:输出完整 NALU
let nalu = std::mem::take(&mut self.fu_assembler.buffer);
self.fu_assembler.reset();
self.emit_nalu(&nalu, rtp.timestamp, rtp.marker)
} else {
Vec::new()
}
}
/// 将一个完整 NALU 转换为一个或多个 AVFrame
///
/// 多 NALU 合并策略:
/// - IDR(5) 和 non-IDR(1) NALU 会被累积到 `pending_nalus` 中
/// - 当 RTP 时间戳跳变或 marker=true 时,将所有累积的 NALU 合并为一帧输出
/// - SPS(7)/PPS(8) 到达时,先刷新已累积的 NALU再缓存 SPS/PPS
///
/// 这样可以正确处理多 slice 编码的 H.264 码流,
/// 避免每个 NALU 单独输出导致拉流端解码错误(如 cabac_init_idc overflow
fn emit_nalu(&mut self, nalu: &[u8], rtp_timestamp: u32, marker: bool) -> Vec<AVFrame> {
if nalu.is_empty() {
return Vec::new();
}
let ntype = nal_unit_type(nalu[0]);
// 归一化 RTP 时间戳:减去首个帧的基准值,避免毫秒时间戳超过 RTMP 扩展阈值
let norm_ts = self.normalize_video_ts(rtp_timestamp);
let timestamp_ms = norm_ts as u64 / 90;
let mut frames = Vec::new();
match ntype {
nal_type::SPS => {
// SPS 到达意味着新的访问单元开始,先刷新已累积的 NALU
if let Some(f) = self.flush() {
frames.push(f);
}
self.pending_sps = Some(nalu.to_vec());
self.seq_header_emitted = false;
}
nal_type::PPS => {
// PPS 同理,先刷新已累积的 NALU
if let Some(f) = self.flush() {
frames.push(f);
}
self.pending_pps = Some(nalu.to_vec());
self.seq_header_emitted = false;
}
nal_type::IDR | nal_type::NON_IDR => {
// 检测时间戳跳变:如果新 NALU 的时间戳与累积的不同,
// 说明这是新的访问单元,需要先输出之前的帧
if rtp_timestamp != self.pending_timestamp && !self.pending_nalus.is_empty() {
let old_nalus = std::mem::take(&mut self.pending_nalus);
let old_timestamp_ms = self.pending_timestamp as u64 / 90;
let old_is_keyframe =
old_nalus.iter().any(|n| nal_unit_type(n[0]) == nal_type::IDR);
// 如果 seq_header 尚未发送,先发送
if !self.seq_header_emitted {
if let (Some(sps), Some(pps)) = (&self.pending_sps, &self.pending_pps) {
if let Some(sh_frame) =
self.build_seq_header_frame(sps, pps, old_timestamp_ms)
{
frames.push(sh_frame);
self.seq_header_emitted = true;
}
}
}
if let Some(f) =
self.build_video_frame_multi(&old_nalus, old_timestamp_ms, old_is_keyframe)
{
frames.push(f);
}
}
// 如果是 IDR 且是访问单元的第一个 NALU发送 seq_header
if ntype == nal_type::IDR
&& self.pending_nalus.is_empty()
&& !self.seq_header_emitted
{
if let (Some(sps), Some(pps)) = (&self.pending_sps, &self.pending_pps) {
if let Some(sh_frame) = self.build_seq_header_frame(sps, pps, timestamp_ms)
{
frames.push(sh_frame);
self.seq_header_emitted = true;
}
}
}
// 累积当前 NALU
self.pending_nalus.push(nalu.to_vec());
self.pending_timestamp = rtp_timestamp;
// marker=true 表示 RTP 包是访问单元的最后一个包,输出合并帧
if marker {
let nalus = std::mem::take(&mut self.pending_nalus);
let is_keyframe = nalus.iter().any(|n| nal_unit_type(n[0]) == nal_type::IDR);
if let Some(f) = self.build_video_frame_multi(&nalus, timestamp_ms, is_keyframe)
{
frames.push(f);
}
self.pending_timestamp = 0;
}
}
// 其他 NALU 类型SEI 等)暂时忽略
_ => {}
}
self.last_video_timestamp = rtp_timestamp;
frames
}
/// 构建 sequence header 帧FLV/AVCC 格式)
///
/// 格式:
/// ```text
/// byte[0] = 0x17 (keyframe + H264)
/// byte[1] = 0x00 (AVC sequence header)
/// byte[2..4] = 0x00 0x00 0x00 (CTTS)
/// byte[5..] = AVCDecoderConfigurationRecord
/// ```
fn build_seq_header_frame(
&self,
sps: &[u8],
pps: &[u8],
timestamp_ms: u64,
) -> Option<AVFrame> {
let avcc_record = encode_avcc_sequence_header(sps, pps);
let mut data = Vec::with_capacity(5 + avcc_record.len());
data.push(0x17); // keyframe + H.264
data.push(0x00); // AVC sequence header
data.push(0x00); // CTTS 3 bytes
data.push(0x00);
data.push(0x00);
data.extend_from_slice(&avcc_record);
let mut frame = AVFrame::new_video(
timestamp_ms,
Arc::new(data),
VideoCodec::H264,
FrameType::KeyFrame,
);
frame.codec_info = Some(CodecExtraInfo::H264SeqHeader(H264SeqHeader {
sps: Arc::new(sps.to_vec()),
pps: Arc::new(pps.to_vec()),
}));
Some(frame)
}
/// 构建多 NALU 视频帧FLV/AVCC 格式)
///
/// 当一个 H.264 访问单元包含多个 NALU如多 slice 编码)时,
/// 将所有 NALU 合并到同一个 AVCC 帧中。
///
/// 格式:
/// ```text
/// byte[0] = (frame_type << 4) | 0x07
/// byte[1] = 0x01 (AVC NALU)
/// byte[2..4] = 0x00 0x00 0x00 (CTTS)
/// byte[5..] = AVCC: [4-byte BE length + NALU1] [4-byte BE length + NALU2] ...
/// ```
fn build_video_frame_multi(
&self,
nalus: &[Vec<u8>],
timestamp_ms: u64,
is_keyframe: bool,
) -> Option<AVFrame> {
if nalus.is_empty() {
return None;
}
let frame_type_byte = if is_keyframe { 0x17 } else { 0x27 };
// 预分配空间5 字节 FLV 头 + 每个 NALU4 字节长度前缀 + 数据)
let avcc_total: usize = nalus.iter().map(|n| 4 + n.len()).sum();
let mut data = Vec::with_capacity(5 + avcc_total);
data.push(frame_type_byte);
data.push(0x01); // AVC NALU
data.push(0x00); // CTTS 3 bytes
data.push(0x00);
data.push(0x00);
// 逐个写入 AVCC 格式的 NALU4 字节大端长度 + 数据)
for nalu in nalus {
let nalu_len = nalu.len() as u32;
data.extend_from_slice(&nalu_len.to_be_bytes());
data.extend_from_slice(nalu);
}
let frame_type = if is_keyframe {
FrameType::KeyFrame
} else {
FrameType::InterFrame
};
Some(AVFrame::new_video(
timestamp_ms,
Arc::new(data),
VideoCodec::H264,
frame_type,
))
}
/// 刷新所有累积的 NALU输出为一个 AVFrame
///
/// 在时间戳跳变、SPS/PPS 到达或流结束时调用,
/// 确保之前累积的 NALU 不会丢失。
pub fn flush(&mut self) -> Option<AVFrame> {
if self.pending_nalus.is_empty() {
return None;
}
let nalus = std::mem::take(&mut self.pending_nalus);
// 只要包含 IDR NALU 即视为关键帧
let is_keyframe = nalus.iter().any(|n| nal_unit_type(n[0]) == nal_type::IDR);
let norm_ts = self.normalize_video_ts(self.pending_timestamp);
let timestamp_ms = norm_ts as u64 / 90;
self.pending_timestamp = 0;
self.build_video_frame_multi(&nalus, timestamp_ms, is_keyframe)
}
/// 归一化视频 RTP 时间戳
///
/// RTP 时间戳从随机值开始,直接转换为毫秒可能超过 RTMP 扩展时间戳阈值 (0xFFFFFF = ~4.6 小时)
/// 导致 ffmpeg 等客户端在解析 RTMP chunk 时出现 "packet size mismatch" 错误。
/// 记录首个帧的 RTP 时间戳作为基准,后续帧减去基准值,使毫秒时间戳从 0 开始。
fn normalize_video_ts(&mut self, rtp_ts: u32) -> u32 {
if self.video_ts_base.is_none() {
self.video_ts_base = Some(rtp_ts);
}
// 使用 wrapping_sub 处理 RTP 时间戳回绕32-bit 溢出)
rtp_ts.wrapping_sub(self.video_ts_base.unwrap())
}
}
// ── RTP 音频解包器 ─────────────────────────────────────────
/// RTP 音频解包器AAC-hbr 模式)
///
/// 每个 RTP 包含一个 AAC 帧,负载格式:
/// - AU-headers-length (2 字节,大端,值为 16 表示 1 个 AU header)
/// - AU-header (2 字节)[size(13 bits) | index(3 bits)]
/// - AAC 帧数据
///
/// 解包后跳过 AU 头部,只提取原始 AAC 数据。
pub fn depacketize_aac(rtp: &RtpPacket) -> Option<AVFrame> {
let payload = &rtp.payload;
if payload.len() < 4 {
return None;
}
// AU-headers-length位数为 16即 2 字节 = 1 个 AU header
let au_headers_len_bits = u16::from_be_bytes([payload[0], payload[1]]);
let au_headers_bytes = (au_headers_len_bits as usize + 7) / 8;
// AU-header 中的 size 字段(高 13 位)
let au_header = u16::from_be_bytes([payload[2], payload[3]]);
let au_size = (au_header >> 3) as usize;
// 跳过 AU-headers-length(2) + AU-header 数据
let header_total = 2 + au_headers_bytes;
if payload.len() < header_total + au_size {
// 尝试取剩余所有数据
if payload.len() > header_total {
let aac_data = payload[header_total..].to_vec();
let timestamp_ms = rtp.timestamp as u64 * 1000 / 44100;
// 封装为 FLV 音频帧格式
let mut data = Vec::with_capacity(2 + aac_data.len());
data.push(0xAF); // AAC, 44100Hz, 16bit, stereo
data.push(0x01); // AAC raw data
data.extend_from_slice(&aac_data);
return Some(AVFrame::new_audio(
timestamp_ms,
Arc::new(data),
AudioCodec::Aac,
));
}
return None;
}
let aac_data = payload[header_total..header_total + au_size].to_vec();
// AAC 采样率 44100 Hz → ms = timestamp * 1000 / 44100
let timestamp_ms = rtp.timestamp as u64 * 1000 / 44100;
// 封装为 FLV 音频帧格式:[sound_info, aac_packet_type, aac_data]
// sound_info: SoundFormat=10(AAC) | SoundRate=3(44100) | SoundSize=1(16bit) | SoundType=1(stereo) = 0xAF
// aac_packet_type: 0x01 = raw AAC data
let mut data = Vec::with_capacity(2 + aac_data.len());
data.push(0xAF); // AAC, 44100Hz, 16bit, stereo
data.push(0x01); // AAC raw data
data.extend_from_slice(&aac_data);
Some(AVFrame::new_audio(
timestamp_ms,
Arc::new(data),
AudioCodec::Aac,
))
}
// ════════════════════════════════════════════════════════════
// 测试
// ════════════════════════════════════════════════════════════
#[cfg(test)]
mod tests {
use super::*;
use crate::sdk::types::MediaType;
/// 辅助:构建 RTP 包
fn make_rtp(seq: u16, timestamp: u32, marker: bool, payload: Vec<u8>) -> RtpPacket {
RtpPacket::new(seq, timestamp, 0x12345678, 96, marker, payload)
}
/// 辅助:构建 FU-A 分片负载
///
/// - `nri`: NALU ref_idc (2 bits)
/// - `nalu_type`: 原始 NALU 类型 (5 bits)
/// - `start`: FU-A start bit
/// - `end`: FU-A end bit
/// - `fragment`: 分片负载数据
fn make_fu_a_payload(nri: u8, nalu_type: u8, start: bool, end: bool, fragment: &[u8]) -> Vec<u8> {
let fu_indicator = (nri << 5) | NALU_TYPE_FU_A;
let fu_header = (if start { 0x80 } else { 0 })
| (if end { 0x40 } else { 0 })
| (nalu_type & 0x1F);
let mut payload = vec![fu_indicator, fu_header];
payload.extend_from_slice(fragment);
payload
}
/// 辅助:构建 AAC-hbr RTP 负载
fn make_aac_payload(aac_data: &[u8]) -> Vec<u8> {
let mut payload = Vec::new();
// AU-headers-length = 16 bits
payload.extend_from_slice(&16u16.to_be_bytes());
// AU-header: size(13) | index(3)=0
let au_header = ((aac_data.len() as u16) << 3) & 0xFFF8;
payload.extend_from_slice(&au_header.to_be_bytes());
payload.extend_from_slice(aac_data);
payload
}
// ── FU-A 重组测试 ──────────────────────────────────────
/// 测试:单个 NALU非 FU-A直接输出帧
#[test]
fn test_fu_a_single_fragment_emits_nalu() {
let mut depacketizer = RtpDepacketizer::new();
// 单个 IDR NALUtype=5, ref_idc=3
let nalu = vec![0x65, 0x88, 0x84, 0x00, 0x40];
let rtp = make_rtp(1, 90000, true, nalu.clone());
let frames = depacketizer.push_rtp(&rtp);
// IDR + 未发送过的 seq_header但无 SPS/PPS所以只输出 IDR 帧)
assert!(!frames.is_empty(), "单个 NALU 应输出帧");
let frame = frames.last().unwrap();
assert_eq!(frame.media_type, MediaType::Video);
assert_eq!(frame.frame_type, FrameType::KeyFrame);
assert_eq!(frame.video_codec, VideoCodec::H264);
// FLV 格式0x17 0x01 0x00 0x00 0x00 + AVCC length + NALU
assert_eq!(frame.data[0], 0x17, "应为 keyframe+H264");
assert_eq!(frame.data[1], 0x01, "应为 AVC NALU");
}
/// 测试:三个 FU-A 分片正确重组
#[test]
fn test_fu_a_three_fragments_reassembled_correctly() {
let mut depacketizer = RtpDepacketizer::new();
// 模拟一个 IDR NALU (type=5, ref_idc=3) 被拆分为 3 个分片
let nri = 3; // ref_idc = 3
let nalu_type = 5; // IDR
// 第一个分片start=1, end=0
let frag1 = vec![0x88, 0x84];
let rtp1 = make_rtp(
1,
90000,
false,
make_fu_a_payload(nri, nalu_type, true, false, &frag1),
);
let frames1 = depacketizer.push_rtp(&rtp1);
assert!(frames1.is_empty(), "第一个分片不应输出帧");
// 第二个分片start=0, end=0
let frag2 = vec![0x00, 0x40];
let rtp2 = make_rtp(
2,
90000,
false,
make_fu_a_payload(nri, nalu_type, false, false, &frag2),
);
let frames2 = depacketizer.push_rtp(&rtp2);
assert!(frames2.is_empty(), "中间分片不应输出帧");
// 第三个分片start=0, end=1, marker=1
let frag3 = vec![0xAB, 0xCD];
let rtp3 = make_rtp(
3,
90000,
true,
make_fu_a_payload(nri, nalu_type, false, true, &frag3),
);
let frames3 = depacketizer.push_rtp(&rtp3);
assert!(!frames3.is_empty(), "最后一个分片应输出帧");
// 验证重组后的 NALU 数据
let frame = frames3.last().unwrap();
assert_eq!(frame.frame_type, FrameType::KeyFrame);
// FLV 头部 5 字节 + AVCC length 4 字节 + 重组后的 NALU
let avcc_data = &frame.data[5..];
// AVCC 长度前缀
let nalu_len = u32::from_be_bytes([
avcc_data[0],
avcc_data[1],
avcc_data[2],
avcc_data[3],
]) as usize;
let reassembled = &avcc_data[4..4 + nalu_len];
// 期望:原始 NALU header (0x65 = nri=3|type=5) + frag1 + frag2 + frag3
let expected: Vec<u8> = vec![0x65]
.into_iter()
.chain(frag1.into_iter())
.chain(frag2.into_iter())
.chain(frag3.into_iter())
.collect();
assert_eq!(reassembled, expected.as_slice(), "重组数据应匹配原始 NALU");
}
/// 测试SPS + PPS + IDR 正确输出 sequence header 帧
#[test]
fn test_sps_pps_emitted_as_seq_header() {
let mut depacketizer = RtpDepacketizer::new();
// 发送 SPStype=7, ref_idc=3 → 0x67
let sps = vec![0x67, 0x42, 0xC0, 0x1E, 0xD9, 0x00, 0xA0, 0x47, 0xFE, 0x88];
let rtp_sps = make_rtp(1, 0, false, sps.clone());
let frames_sps = depacketizer.push_rtp(&rtp_sps);
assert!(frames_sps.is_empty(), "SPS 单独不应输出帧");
// 发送 PPStype=8, ref_idc=3 → 0x68
let pps = vec![0x68, 0xCE, 0x38, 0x80];
let rtp_pps = make_rtp(2, 0, false, pps.clone());
let frames_pps = depacketizer.push_rtp(&rtp_pps);
assert!(frames_pps.is_empty(), "PPS 单独不应输出帧");
// 发送 IDRtype=5, ref_idc=3 → 0x65
let idr = vec![0x65, 0x88, 0x84, 0x00, 0x40];
let rtp_idr = make_rtp(3, 90000, true, idr);
let frames = depacketizer.push_rtp(&rtp_idr);
// 应输出 2 个帧seq_header + IDR
assert_eq!(frames.len(), 2, "应输出 seq_header 和 IDR 两个帧");
// 验证 seq_header 帧
let sh_frame = &frames[0];
assert_eq!(sh_frame.data[0], 0x17, "seq_header 应为 keyframe+H264");
assert_eq!(sh_frame.data[1], 0x00, "seq_header 应为 AVC seq header");
assert!(sh_frame.is_seq_header(), "应有 codec_info");
assert!(sh_frame.is_keyframe());
// 验证 IDR 帧
let idr_frame = &frames[1];
assert_eq!(idr_frame.data[0], 0x17, "IDR 应为 keyframe+H264");
assert_eq!(idr_frame.data[1], 0x01, "IDR 应为 AVC NALU");
assert_eq!(idr_frame.frame_type, FrameType::KeyFrame);
}
/// 测试IDR 关键帧正确分类
#[test]
fn test_idr_emitted_as_keyframe() {
let mut depacketizer = RtpDepacketizer::new();
let idr = vec![0x65, 0x88, 0x84, 0x00, 0x40];
let rtp = make_rtp(1, 90000, true, idr);
let frames = depacketizer.push_rtp(&rtp);
assert!(!frames.is_empty());
let frame = frames.last().unwrap();
assert_eq!(frame.frame_type, FrameType::KeyFrame);
assert_eq!(frame.data[0], 0x17);
}
/// 测试non-IDR P 帧正确分类
#[test]
fn test_pframe_emitted_as_interframe() {
let mut depacketizer = RtpDepacketizer::new();
// non-IDR slicetype=1, ref_idc=2 → 0x41
let nalu = vec![0x41, 0x9A, 0x24, 0x6C];
let rtp = make_rtp(1, 180000, true, nalu);
let frames = depacketizer.push_rtp(&rtp);
assert!(!frames.is_empty());
let frame = frames.last().unwrap();
assert_eq!(frame.frame_type, FrameType::InterFrame);
assert_eq!(frame.data[0], 0x27, "P 帧应为 inter+H264");
}
/// 测试AAC 解包正确提取音频数据,输出 FLV 音频帧格式
#[test]
fn test_aac_depacketized_correctly() {
let aac_data = vec![0x21, 0x10, 0x05, 0x00, 0x00];
let payload = make_aac_payload(&aac_data);
let rtp = make_rtp(1, 1024, true, payload);
let frame = depacketize_aac(&rtp).expect("应返回 AAC 帧");
assert_eq!(frame.media_type, MediaType::Audio);
assert_eq!(frame.audio_codec, AudioCodec::Aac);
// FLV 音频帧格式:[0xAF(声音信息), 0x01(AAC raw), ...AAC 数据]
assert_eq!(frame.data[0], 0xAF, "声音信息字节AAC 44100Hz 16bit stereo");
assert_eq!(frame.data[1], 0x01, "AAC packet type = raw data");
assert_eq!(&frame.data[2..], aac_data.as_slice(), "AAC 数据应保持不变");
}
/// 测试:空 RTP 负载返回空帧列表
#[test]
fn test_empty_payload_returns_no_frames() {
let mut depacketizer = RtpDepacketizer::new();
let rtp = make_rtp(1, 90000, true, vec![]);
let frames = depacketizer.push_rtp(&rtp);
assert!(frames.is_empty(), "空负载应返回空帧列表");
}
/// 测试FU-A start 分片累积但不输出
#[test]
fn test_fu_a_start_without_end_accumulates() {
let mut depacketizer = RtpDepacketizer::new();
let frag = vec![0xAA, 0xBB];
let rtp = make_rtp(
1,
90000,
false,
make_fu_a_payload(3, 5, true, false, &frag),
);
let frames = depacketizer.push_rtp(&rtp);
assert!(frames.is_empty(), "只有 start 分片时不应输出帧");
}
/// 测试:收到中间分片但没有 start 应被丢弃
#[test]
fn test_fu_a_middle_without_start_discarded() {
let mut depacketizer = RtpDepacketizer::new();
let frag = vec![0xAA];
let rtp = make_rtp(
1,
90000,
false,
make_fu_a_payload(3, 5, false, false, &frag),
);
let frames = depacketizer.push_rtp(&rtp);
assert!(frames.is_empty(), "没有 start 的中间分片应被丢弃");
}
/// 测试:连续 IDR 帧不会重复发送 seq_header
#[test]
fn test_seq_header_emitted_only_once() {
let mut depacketizer = RtpDepacketizer::new();
let sps = vec![0x67, 0x42, 0xC0];
let pps = vec![0x68, 0xCE, 0x38];
let idr = vec![0x65, 0x88, 0x84];
// 发送 SPS + PPS
depacketizer.push_rtp(&make_rtp(1, 0, false, sps));
depacketizer.push_rtp(&make_rtp(2, 0, false, pps));
// 第一个 IDR应输出 seq_header + IDR2 帧)
let frames1 = depacketizer.push_rtp(&make_rtp(3, 90000, true, idr.clone()));
assert_eq!(frames1.len(), 2, "首个 IDR 应输出 seq_header + IDR");
// 第二个 IDR只输出 IDR1 帧)
let frames2 = depacketizer.push_rtp(&make_rtp(4, 180000, true, idr));
assert_eq!(frames2.len(), 1, "后续 IDR 不应重复输出 seq_header");
}
/// 测试:新的 SPS 到达后重置 seq_header_emitted 标记
#[test]
fn test_new_sps_resets_seq_header_flag() {
let mut depacketizer = RtpDepacketizer::new();
let sps = vec![0x67, 0x42, 0xC0];
let pps = vec![0x68, 0xCE, 0x38];
let idr = vec![0x65, 0x88, 0x84];
// 发送 SPS + PPS + IDR
depacketizer.push_rtp(&make_rtp(1, 0, false, sps.clone()));
depacketizer.push_rtp(&make_rtp(2, 0, false, pps.clone()));
let f1 = depacketizer.push_rtp(&make_rtp(3, 90000, true, idr.clone()));
assert_eq!(f1.len(), 2);
// 新的 SPS → 重置标记
let new_sps = vec![0x67, 0x4D, 0x00];
depacketizer.push_rtp(&make_rtp(4, 0, false, new_sps));
// 新的 PPS
depacketizer.push_rtp(&make_rtp(5, 0, false, pps));
// 新的 IDR → 应再次输出 seq_header
let f2 = depacketizer.push_rtp(&make_rtp(6, 180000, true, idr));
assert_eq!(f2.len(), 2, "SPS 更新后应重新输出 seq_header");
}
/// 测试AAC 空负载返回 None
#[test]
fn test_aac_empty_payload_returns_none() {
let rtp = make_rtp(1, 0, true, vec![]);
assert!(depacketize_aac(&rtp).is_none());
}
/// 测试AAC 负载太短返回 None
#[test]
fn test_aac_payload_too_short_returns_none() {
let rtp = make_rtp(1, 0, true, vec![0x00, 0x10]);
assert!(depacketize_aac(&rtp).is_none());
}
/// 测试时间戳转换正确90kHz → ms归一化后首帧为 0
#[test]
fn test_video_timestamp_conversion_correct() {
let mut depacketizer = RtpDepacketizer::new();
let nalu = vec![0x65, 0x01]; // IDR
// 首帧RTP ts=90000归一化后 ts=00/90=0ms
let rtp = make_rtp(1, 90000, true, nalu.clone());
let frames = depacketizer.push_rtp(&rtp);
assert_eq!(frames.last().unwrap().timestamp_ms, 0);
// 第二帧RTP ts=99000归一化后 ts=90009000/90=100ms
let rtp2 = make_rtp(2, 99000, true, nalu);
let frames2 = depacketizer.push_rtp(&rtp2);
assert_eq!(frames2.last().unwrap().timestamp_ms, 100);
}
/// 测试:完整 FU-A 重组后数据与原始 NALU 一致(大 NALU 场景)
#[test]
fn test_fu_a_large_nalu_reassembly_matches_original() {
let mut depacketizer = RtpDepacketizer::new();
// 构造 3000 字节的原始 NALUIDR, type=5, ref_idc=3
let mut original_nalu = vec![0x65];
original_nalu.extend_from_slice(&[0xAB; 2999]);
// 模拟 MTU=1400 的 FU-A 分片
let nri = 3;
let ntype = 5;
let mtu = 1400;
let fu_payload_max = mtu - 2; // 1398
let mut seq = 0u16;
let timestamp = 90000u32;
let mut offset = 1; // 跳过 NALU header
while offset < original_nalu.len() {
let end_pos = std::cmp::min(offset + fu_payload_max, original_nalu.len());
let is_first = offset == 1;
let is_last = end_pos == original_nalu.len();
let fragment = &original_nalu[offset..end_pos];
let rtp = make_rtp(
seq,
timestamp,
is_last,
make_fu_a_payload(nri, ntype, is_first, is_last, fragment),
);
let frames = depacketizer.push_rtp(&rtp);
if is_last {
assert!(!frames.is_empty(), "最后一个分片应输出帧");
let frame = frames.last().unwrap();
// 提取重组后的 NALU
let avcc_data = &frame.data[5..];
let nalu_len = u32::from_be_bytes([
avcc_data[0],
avcc_data[1],
avcc_data[2],
avcc_data[3],
]) as usize;
let reassembled = &avcc_data[4..4 + nalu_len];
assert_eq!(
reassembled,
original_nalu.as_slice(),
"重组后的 NALU 应与原始一致"
);
} else {
assert!(frames.is_empty(), "中间分片不应输出帧");
}
seq = seq.wrapping_add(1);
offset = end_pos;
}
}
/// 测试set_sps_pps 预设 SPS/PPS 到解包器
#[test]
fn test_set_sps_pps_presets_pending() {
let mut dep = RtpDepacketizer::new();
let sps = vec![0x67, 0x42, 0xC0];
let pps = vec![0x68, 0xCE, 0x38];
dep.set_sps_pps(sps.clone(), pps.clone());
assert_eq!(dep.pending_sps, Some(sps), "pending_sps 应被预设");
assert_eq!(dep.pending_pps, Some(pps), "pending_pps 应被预设");
assert!(!dep.seq_header_emitted, "seq_header_emitted 应被重置为 false");
}
/// 测试set_sps_pps 后 IDR 帧触发 seq_header 输出
#[test]
fn test_set_sps_pps_then_idr_emits_seq_header() {
let mut dep = RtpDepacketizer::new();
// 预设 SPS/PPS模拟从 SDP 获取)
let sps = vec![0x67, 0x42, 0xC0, 0x1E, 0xD9, 0x00, 0xA0, 0x47, 0xFE, 0x88];
let pps = vec![0x68, 0xCE, 0x38, 0x80];
dep.set_sps_pps(sps, pps);
// 发送 IDR应输出 seq_header + IDR 两帧)
let idr = vec![0x65, 0x88, 0x84, 0x00, 0x40];
let rtp = make_rtp(1, 90000, true, idr);
let frames = dep.push_rtp(&rtp);
assert_eq!(frames.len(), 2, "应输出 seq_header + IDR 两帧");
assert_eq!(frames[0].data[1], 0x00, "第一帧应为 seq_header");
assert_eq!(frames[1].data[1], 0x01, "第二帧应为 AVC NALU");
}
// ── 多 NALU 合并测试 ────────────────────────────────────
/// 测试:多个相同时间戳的 NALU 合并为一个 AVFrame
#[test]
fn test_multiple_nalus_same_timestamp_grouped_into_one_frame() {
let mut dep = RtpDepacketizer::new();
// 预设 SPS/PPS 以避免 seq_header 干扰
dep.set_sps_pps(vec![0x67, 0x42], vec![0x68, 0xCE]);
let ts = 90000u32;
// 第一个 non-IDR NALUmarker=false — 不应输出帧
let nalu1 = vec![0x41, 0x9A, 0x24];
let rtp1 = make_rtp(1, ts, false, nalu1);
let frames1 = dep.push_rtp(&rtp1);
assert!(
frames1.is_empty(),
"第一个 NALU无 marker不应输出帧"
);
// 第二个 non-IDR NALU相同时间戳marker=false — 继续累积
let nalu2 = vec![0x41, 0x9B, 0x25];
let rtp2 = make_rtp(2, ts, false, nalu2);
let frames2 = dep.push_rtp(&rtp2);
assert!(
frames2.is_empty(),
"第二个 NALU无 marker不应输出帧"
);
// 第三个 non-IDR NALU相同时间戳marker=true — 输出合并帧
let nalu3 = vec![0x41, 0x9C, 0x26];
let rtp3 = make_rtp(3, ts, true, nalu3);
let frames3 = dep.push_rtp(&rtp3);
assert_eq!(frames3.len(), 1, "marker=true 时应输出 1 个合并帧");
// 验证帧包含所有 3 个 NALUAVCC 格式)
let frame = &frames3[0];
assert_eq!(frame.data[0], 0x27, "应为 inter+H264");
assert_eq!(frame.data[1], 0x01, "应为 AVC NALU");
// 解析 AVCC 区域中的 NALU
let avcc_data = &frame.data[5..];
let parsed_nalus = crate::codec::h264::split_avcc(avcc_data);
assert_eq!(parsed_nalus.len(), 3, "应包含 3 个 NALU");
}
/// 测试:时间戳跳变时先输出已累积的 NALU
#[test]
fn test_timestamp_change_flushes_pending_nalus() {
let mut dep = RtpDepacketizer::new();
dep.set_sps_pps(vec![0x67, 0x42], vec![0x68, 0xCE]);
let ts1 = 90000u32;
let nalu1 = vec![0x41, 0x9A];
let rtp1 = make_rtp(1, ts1, false, nalu1);
let _ = dep.push_rtp(&rtp1);
// 新时间戳 — 应先刷新之前累积的 NALU
// 首次遇到 slice NALU 时会先输出 seq_header
let ts2 = 90360u32; // +40ms90kHz × 40ms ≈ 3600 ticks
let nalu2 = vec![0x41, 0x9B];
let rtp2 = make_rtp(2, ts2, true, nalu2);
let frames = dep.push_rtp(&rtp2);
// 应得到 3 帧seq_header + 刷新的旧帧 + 新帧
assert_eq!(frames.len(), 3, "应先输出 seq_header再刷新旧帧并输出新帧");
assert_eq!(frames[0].data[1], 0x00, "第一帧应为 seq_header");
assert_eq!(frames[1].data[0], 0x27, "第二帧应为 inter+H264旧帧");
assert_eq!(frames[2].data[0], 0x27, "第三帧应为 inter+H264新帧");
}
/// 测试:多 NALU 帧中包含 IDR 时正确标记为关键帧
#[test]
fn test_multi_nalu_with_idr_marked_as_keyframe() {
let mut dep = RtpDepacketizer::new();
dep.set_sps_pps(vec![0x67, 0x42], vec![0x68, 0xCE]);
let ts = 90000u32;
// IDR NALU + non-IDR NALU 合并
let idr_nalu = vec![0x65, 0x88, 0x84];
let rtp1 = make_rtp(1, ts, false, idr_nalu);
let _ = dep.push_rtp(&rtp1);
let slice_nalu = vec![0x65, 0x01, 0x02]; // IDR 多 slice
let rtp2 = make_rtp(2, ts, true, slice_nalu);
let frames = dep.push_rtp(&rtp2);
assert_eq!(frames.len(), 1);
assert_eq!(
frames[0].frame_type,
FrameType::KeyFrame,
"包含 IDR 的合并帧应为关键帧"
);
assert_eq!(frames[0].data[0], 0x17, "应为 keyframe+H264");
}
/// 测试SPS 到达时先刷新已累积的 NALU
#[test]
fn test_sps_flushes_pending_nalus() {
let mut dep = RtpDepacketizer::new();
dep.set_sps_pps(vec![0x67, 0x42], vec![0x68, 0xCE]);
let ts = 90000u32;
let nalu = vec![0x41, 0x9A];
let rtp1 = make_rtp(1, ts, false, nalu);
let _ = dep.push_rtp(&rtp1);
// SPS 到达 — 应先输出累积的帧
let new_sps = vec![0x67, 0x4D, 0x00];
let rtp2 = make_rtp(2, 0, false, new_sps);
let frames = dep.push_rtp(&rtp2);
assert_eq!(frames.len(), 1, "SPS 到达时应先刷新累积的帧");
assert_eq!(frames[0].data[0], 0x27, "应为 inter+H264");
}
/// 测试flush() 方法在无累积 NALU 时返回 None
#[test]
fn test_flush_empty_returns_none() {
let mut dep = RtpDepacketizer::new();
assert!(dep.flush().is_none(), "无累积 NALU 时 flush 应返回 None");
}
/// 测试flush() 输出累积的 NALU 并清空状态
#[test]
fn test_flush_outputs_and_clears_pending() {
let mut dep = RtpDepacketizer::new();
dep.set_sps_pps(vec![0x67, 0x42], vec![0x68, 0xCE]);
let ts = 90000u32;
let nalu = vec![0x41, 0x9A, 0x24];
let rtp = make_rtp(1, ts, false, nalu);
let _ = dep.push_rtp(&rtp);
let frame = dep.flush().expect("应有累积的帧");
assert_eq!(frame.data[0], 0x27);
assert!(dep.pending_nalus.is_empty(), "flush 后应清空 pending_nalus");
// 再次 flush 应返回 None
assert!(dep.flush().is_none());
}
/// 测试:单个 NALU + marker=true 仍然正常工作(向后兼容)
#[test]
fn test_single_nalu_with_marker_still_works() {
let mut dep = RtpDepacketizer::new();
dep.set_sps_pps(vec![0x67, 0x42], vec![0x68, 0xCE]);
let nalu = vec![0x41, 0x9A, 0x24];
let rtp = make_rtp(1, 180000, true, nalu.clone());
let frames = dep.push_rtp(&rtp);
assert_eq!(frames.len(), 1, "单个 NALU + marker 应输出 1 帧");
// 验证 AVCC 解析后只有一个 NALU
let avcc_data = &frames[0].data[5..];
let parsed = crate::codec::h264::split_avcc(avcc_data);
assert_eq!(parsed.len(), 1, "应只包含 1 个 NALU");
assert_eq!(parsed[0], nalu.as_slice(), "NALU 数据应一致");
}
/// 测试:多 NALU IDR 帧正确触发 seq_header 输出
#[test]
fn test_multi_nalu_idr_emits_seq_header() {
let mut dep = RtpDepacketizer::new();
let sps = vec![0x67, 0x42, 0xC0];
let pps = vec![0x68, 0xCE, 0x38];
dep.push_rtp(&make_rtp(1, 0, false, sps));
dep.push_rtp(&make_rtp(2, 0, false, pps));
// 两个 IDR NALU 组成一个访问单元
let idr1 = vec![0x65, 0x88, 0x84];
let idr2 = vec![0x65, 0x01, 0x02];
// 第一个 IDR 到达时输出 seq_header因为 pending_nalus 为空且是 IDR
let frames1 = dep.push_rtp(&make_rtp(3, 90000, false, idr1));
assert_eq!(frames1.len(), 1, "第一个 IDR 应触发 seq_header");
assert_eq!(frames1[0].data[1], 0x00, "应为 seq_header");
// 第二个 IDRmarker=true输出合并帧
let frames2 = dep.push_rtp(&make_rtp(4, 90000, true, idr2));
assert_eq!(frames2.len(), 1, "应输出合并 IDR 帧");
assert_eq!(frames2[0].data[0], 0x17, "合并 IDR 应为 keyframe+H264");
// 验证合并帧包含 2 个 NALU
let avcc_data = &frames2[0].data[5..];
let parsed = crate::codec::h264::split_avcc(avcc_data);
assert_eq!(parsed.len(), 2, "应包含 2 个 IDR NALU");
}
}