1095 lines
42 KiB
Rust
1095 lines
42 KiB
Rust
//! 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<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 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<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 头 + 每个 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<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 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<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();
|
||
|
||
// 发送 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");
|
||
}
|
||
}
|