//! 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-A(Fragmentation Unit)NALU 类型 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, /// 是否正在重组中(收到 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>, /// 缓存最近收到的 PPS NALU(不含起始码) pending_pps: Option>, /// 标记是否已向引擎发送过 sequence header seq_header_emitted: bool, /// 上一个视频帧的 RTP 时间戳(检测时间戳跳变以判断新帧) last_video_timestamp: u32, /// 当前访问单元(Access Unit)中累积的 NALU 列表 /// /// H.264 一帧画面可能包含多个 NALU(如多 slice), /// 它们共享相同的 RTP 时间戳,需要合并为一个 AVFrame。 pending_nalus: Vec>, /// 当前累积 NALU 的 RTP 时间戳 pending_timestamp: u32, /// 时间戳归一化基准:首个视频帧的 RTP 时间戳 /// RTP 时间戳从随机值开始,转换为毫秒后可能超过 RTMP 扩展时间戳阈值 (0xFFFFFF), /// 导致 ffmpeg 解析失败。记录首个帧的 RTP 时间戳,后续帧减去此基准值。 video_ts_base: Option, } 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, pps: Vec) { 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 { 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 { 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 indicator,Type 来自 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 { 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 { 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], timestamp_ms: u64, is_keyframe: bool, ) -> Option { if nalus.is_empty() { return None; } let frame_type_byte = if is_keyframe { 0x17 } else { 0x27 }; // 预分配空间:5 字节 FLV 头 + 每个 NALU(4 字节长度前缀 + 数据) 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 格式的 NALU(4 字节大端长度 + 数据) 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 { 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 { 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) -> 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 { 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 { 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 NALU(type=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 = 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(); // 发送 SPS(type=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 单独不应输出帧"); // 发送 PPS(type=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 单独不应输出帧"); // 发送 IDR(type=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 slice(type=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 + IDR(2 帧) let frames1 = depacketizer.push_rtp(&make_rtp(3, 90000, true, idr.clone())); assert_eq!(frames1.len(), 2, "首个 IDR 应输出 seq_header + IDR"); // 第二个 IDR:只输出 IDR(1 帧) 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=0,0/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=9000,9000/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 字节的原始 NALU(IDR, 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 NALU,marker=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 个 NALU(AVCC 格式) 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; // +40ms(90kHz × 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"); // 第二个 IDR(marker=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"); } }