use crate::error::{Error, Result};
pub const VIDEO_FRAME_KEYFRAME: u8 = 1; pub const VIDEO_FRAME_INTER: u8 = 2;
pub const VIDEO_FRAME_DISPOSABLE: u8 = 3; pub const VIDEO_FRAME_GENERATED_KEY: u8 = 4;
pub const VIDEO_FRAME_INFO: u8 = 5;
pub const VIDEO_CODEC_H263: u8 = 2;
pub const VIDEO_CODEC_SCREEN: u8 = 3;
pub const VIDEO_CODEC_VP6: u8 = 4;
pub const VIDEO_CODEC_VP6A: u8 = 5;
pub const VIDEO_CODEC_SCREEN_V2: u8 = 6;
pub const VIDEO_CODEC_AVC: u8 = 7;
pub const AVC_PACKET_TYPE_SEQUENCE_HEADER: u8 = 0;
pub const AVC_PACKET_TYPE_NALU: u8 = 1;
pub const AVC_PACKET_TYPE_END_OF_SEQUENCE: u8 = 2;
pub const VIDEO_IS_EX_HEADER: u8 = 0x80;
pub const EX_PACKET_TYPE_SEQUENCE_START: u8 = 0;
pub const EX_PACKET_TYPE_CODED_FRAMES: u8 = 1;
pub const EX_PACKET_TYPE_SEQUENCE_END: u8 = 2;
pub const EX_PACKET_TYPE_CODED_FRAMES_X: u8 = 3;
pub const EX_PACKET_TYPE_METADATA: u8 = 4;
pub const EX_PACKET_TYPE_MPEG2TS_SEQUENCE_START: u8 = 5;
pub const EX_PACKET_TYPE_MULTITRACK: u8 = 6;
pub const EX_PACKET_TYPE_MOD_EX: u8 = 7;
pub const MOD_EX_TYPE_TIMESTAMP_OFFSET_NANO: u8 = 0;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModEx {
pub mod_ex_type: u8,
pub data: Vec<u8>,
}
impl ModEx {
pub fn timestamp_offset_nano(&self) -> Option<u32> {
if self.mod_ex_type != MOD_EX_TYPE_TIMESTAMP_OFFSET_NANO || self.data.len() < 3 {
return None;
}
Some(((self.data[0] as u32) << 16) | ((self.data[1] as u32) << 8) | (self.data[2] as u32))
}
pub fn timestamp_offset_nano_entry(nano: u32) -> ModEx {
ModEx {
mod_ex_type: MOD_EX_TYPE_TIMESTAMP_OFFSET_NANO,
data: vec![(nano >> 16) as u8, (nano >> 8) as u8, nano as u8],
}
}
}
fn parse_mod_ex_chain(
payload: &[u8],
start: usize,
mod_ex_value: u8,
what: &str,
) -> Result<(Vec<ModEx>, u8, usize)> {
let mut pos = start;
let mut chain = Vec::new();
loop {
if pos >= payload.len() {
return Err(Error::Other(format!(
"Enhanced RTMP {what} ModEx: truncated reading modExDataSize"
)));
}
let mut size = payload[pos] as usize + 1;
pos += 1;
if size == 256 {
if pos + 2 > payload.len() {
return Err(Error::Other(format!(
"Enhanced RTMP {what} ModEx: truncated reading 16-bit modExDataSize"
)));
}
size = (((payload[pos] as usize) << 8) | (payload[pos + 1] as usize)) + 1;
pos += 2;
}
if pos + size > payload.len() {
return Err(Error::Other(format!(
"Enhanced RTMP {what} ModEx: truncated reading {size}-byte modExData"
)));
}
let data = payload[pos..pos + size].to_vec();
pos += size;
if pos >= payload.len() {
return Err(Error::Other(format!(
"Enhanced RTMP {what} ModEx: truncated reading modExType/packetType nibble"
)));
}
let nibble = payload[pos];
pos += 1;
let mod_ex_type = (nibble >> 4) & 0x0F;
let next_packet_type = nibble & 0x0F;
chain.push(ModEx { mod_ex_type, data });
if next_packet_type != mod_ex_value {
return Ok((chain, next_packet_type, pos));
}
}
}
fn build_mod_ex_chain(out: &mut Vec<u8>, chain: &[ModEx], mod_ex_value: u8, real_packet_type: u8) {
for (i, entry) in chain.iter().enumerate() {
let len = entry.data.len();
if (1..=255).contains(&len) {
out.push((len - 1) as u8);
} else {
out.push(0xFF);
let v16 = (len.saturating_sub(1)).min(0xFFFF) as u16;
out.push((v16 >> 8) as u8);
out.push(v16 as u8);
}
out.extend_from_slice(&entry.data);
let next = if i + 1 < chain.len() {
mod_ex_value
} else {
real_packet_type
};
out.push(((entry.mod_ex_type & 0x0F) << 4) | (next & 0x0F));
}
}
pub const FOURCC_AV1: [u8; 4] = *b"av01";
pub const FOURCC_VP9: [u8; 4] = *b"vp09";
pub const FOURCC_HEVC: [u8; 4] = *b"hvc1";
pub const FOURCC_VP8: [u8; 4] = *b"vp08";
pub const FOURCC_AVC: [u8; 4] = *b"avc1";
pub const FOURCC_VVC: [u8; 4] = *b"vvc1";
pub const AUDIO_FORMAT_PCM_LE: u8 = 0;
pub const AUDIO_FORMAT_ADPCM: u8 = 1;
pub const AUDIO_FORMAT_MP3: u8 = 2;
pub const AUDIO_FORMAT_PCM_LE_8BIT: u8 = 3;
pub const AUDIO_FORMAT_NELLYMOSER_16K_MONO: u8 = 4;
pub const AUDIO_FORMAT_NELLYMOSER_8K_MONO: u8 = 5;
pub const AUDIO_FORMAT_NELLYMOSER: u8 = 6;
pub const AUDIO_FORMAT_G711_ALAW: u8 = 7;
pub const AUDIO_FORMAT_G711_MULAW: u8 = 8;
pub const AUDIO_FORMAT_AAC: u8 = 10;
pub const AUDIO_FORMAT_SPEEX: u8 = 11;
pub const AUDIO_FORMAT_EX_HEADER: u8 = 9;
pub const AUDIO_PACKET_TYPE_SEQUENCE_START: u8 = 0;
pub const AUDIO_PACKET_TYPE_CODED_FRAMES: u8 = 1;
pub const AUDIO_PACKET_TYPE_SEQUENCE_END: u8 = 2;
pub const AUDIO_PACKET_TYPE_MULTICHANNEL_CONFIG: u8 = 4;
pub const AUDIO_PACKET_TYPE_MULTITRACK: u8 = 5;
pub const AUDIO_PACKET_TYPE_MOD_EX: u8 = 7;
pub const FOURCC_AC3: [u8; 4] = *b"ac-3";
pub const FOURCC_EAC3: [u8; 4] = *b"ec-3";
pub const FOURCC_OPUS: [u8; 4] = *b"Opus";
pub const FOURCC_MP3: [u8; 4] = *b".mp3";
pub const FOURCC_FLAC: [u8; 4] = *b"fLaC";
pub const FOURCC_AAC: [u8; 4] = *b"mp4a";
pub const AAC_PACKET_TYPE_SEQUENCE_HEADER: u8 = 0;
pub const AAC_PACKET_TYPE_RAW: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VideoTag {
pub frame_type: u8,
pub codec_id: u8,
pub avc_packet_type: Option<u8>,
pub composition_time: i32,
pub body: Vec<u8>,
pub ex_packet_type: Option<u8>,
pub fourcc: Option<[u8; 4]>,
pub mod_ex: Vec<ModEx>,
}
impl VideoTag {
pub fn is_keyframe(&self) -> bool {
self.frame_type == VIDEO_FRAME_KEYFRAME || self.frame_type == VIDEO_FRAME_GENERATED_KEY
}
pub fn is_avc_sequence_header(&self) -> bool {
self.codec_id == VIDEO_CODEC_AVC
&& self.avc_packet_type == Some(AVC_PACKET_TYPE_SEQUENCE_HEADER)
}
pub fn is_ex_sequence_header(&self) -> bool {
self.fourcc.is_some() && self.ex_packet_type == Some(EX_PACKET_TYPE_SEQUENCE_START)
}
pub fn is_ex_metadata(&self) -> bool {
self.fourcc.is_some() && self.ex_packet_type == Some(EX_PACKET_TYPE_METADATA)
}
pub fn timestamp_offset_nano(&self) -> u32 {
self.mod_ex
.iter()
.filter_map(ModEx::timestamp_offset_nano)
.fold(0u32, |acc, n| acc.saturating_add(n))
}
}
fn sign_extend_si24(raw: i32) -> i32 {
if raw & 0x0080_0000 != 0 {
raw | -0x0100_0000i32
} else {
raw
}
}
pub fn parse_video(payload: &[u8]) -> Result<VideoTag> {
if payload.is_empty() {
return Err(Error::Other("FLV video tag: empty".into()));
}
let b0 = payload[0];
if (b0 & VIDEO_IS_EX_HEADER) != 0 {
let frame_type = (b0 >> 4) & 0b0111;
let mut packet_type = b0 & 0x0F;
let mut pos = 1;
let mut mod_ex = Vec::new();
if packet_type == EX_PACKET_TYPE_MOD_EX {
let (chain, real_pt, next) =
parse_mod_ex_chain(payload, pos, EX_PACKET_TYPE_MOD_EX, "video")?;
mod_ex = chain;
packet_type = real_pt;
pos = next;
}
if pos + 4 > payload.len() {
return Err(Error::Other(
"Enhanced RTMP video tag: need 4 bytes for FourCC after header/ModEx".into(),
));
}
let mut fcc = [0u8; 4];
fcc.copy_from_slice(&payload[pos..pos + 4]);
pos += 4;
let needs_cts = packet_type == EX_PACKET_TYPE_CODED_FRAMES
&& (fcc == FOURCC_HEVC || fcc == FOURCC_AVC || fcc == FOURCC_VVC);
let (cts, body_start) = if needs_cts {
if pos + 3 > payload.len() {
return Err(Error::Other(
"Enhanced RTMP / HEVC CodedFrames: need 3 bytes for SI24 CTS".into(),
));
}
let raw = ((payload[pos] as i32) << 16)
| ((payload[pos + 1] as i32) << 8)
| (payload[pos + 2] as i32);
(sign_extend_si24(raw), pos + 3)
} else {
(0, pos)
};
Ok(VideoTag {
frame_type,
codec_id: 0, avc_packet_type: None,
composition_time: cts,
body: payload[body_start..].to_vec(),
ex_packet_type: Some(packet_type),
fourcc: Some(fcc),
mod_ex,
})
} else {
let frame_type = b0 >> 4;
let codec_id = b0 & 0x0F;
if codec_id == VIDEO_CODEC_AVC {
if payload.len() < 5 {
return Err(Error::Other("FLV/AVC tag: need 5+ bytes".into()));
}
let apt = payload[1];
let cts_raw =
((payload[2] as i32) << 16) | ((payload[3] as i32) << 8) | (payload[4] as i32);
Ok(VideoTag {
frame_type,
codec_id,
avc_packet_type: Some(apt),
composition_time: sign_extend_si24(cts_raw),
body: payload[5..].to_vec(),
ex_packet_type: None,
fourcc: None,
mod_ex: Vec::new(),
})
} else {
Ok(VideoTag {
frame_type,
codec_id,
avc_packet_type: None,
composition_time: 0,
body: payload[1..].to_vec(),
ex_packet_type: None,
fourcc: None,
mod_ex: Vec::new(),
})
}
}
}
pub fn build_video(tag: &VideoTag) -> Vec<u8> {
if let Some(fcc) = tag.fourcc {
let packet_type = tag.ex_packet_type.unwrap_or(EX_PACKET_TYPE_CODED_FRAMES);
let header_pt = if tag.mod_ex.is_empty() {
packet_type
} else {
EX_PACKET_TYPE_MOD_EX
};
let head = VIDEO_IS_EX_HEADER | ((tag.frame_type & 0x07) << 4) | (header_pt & 0x0F);
let mut out = Vec::with_capacity(tag.body.len() + 8);
out.push(head);
build_mod_ex_chain(&mut out, &tag.mod_ex, EX_PACKET_TYPE_MOD_EX, packet_type);
out.extend_from_slice(&fcc);
let cts_on_wire = packet_type == EX_PACKET_TYPE_CODED_FRAMES
&& (fcc == FOURCC_HEVC || fcc == FOURCC_AVC || fcc == FOURCC_VVC);
if cts_on_wire {
let cts = tag.composition_time & 0x00FF_FFFF;
out.extend_from_slice(&[(cts >> 16) as u8, (cts >> 8) as u8, cts as u8]);
}
out.extend_from_slice(&tag.body);
out
} else {
let head = (tag.frame_type << 4) | (tag.codec_id & 0x0F);
let mut out = Vec::with_capacity(tag.body.len() + 5);
out.push(head);
if tag.codec_id == VIDEO_CODEC_AVC {
out.push(tag.avc_packet_type.unwrap_or(AVC_PACKET_TYPE_NALU));
let cts = tag.composition_time & 0x00FF_FFFF;
out.extend_from_slice(&[(cts >> 16) as u8, (cts >> 8) as u8, cts as u8]);
}
out.extend_from_slice(&tag.body);
out
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AudioTag {
pub sound_format: u8,
pub sound_rate: u8,
pub sound_size_16bit: bool,
pub stereo: bool,
pub aac_packet_type: Option<u8>,
pub ex_packet_type: Option<u8>,
pub audio_fourcc: Option<[u8; 4]>,
pub body: Vec<u8>,
pub mod_ex: Vec<ModEx>,
}
impl AudioTag {
pub fn is_enhanced(&self) -> bool {
self.audio_fourcc.is_some()
}
pub fn is_aac_sequence_header(&self) -> bool {
self.sound_format == AUDIO_FORMAT_AAC
&& self.aac_packet_type == Some(AAC_PACKET_TYPE_SEQUENCE_HEADER)
}
pub fn is_ex_sequence_header(&self) -> bool {
self.audio_fourcc.is_some() && self.ex_packet_type == Some(AUDIO_PACKET_TYPE_SEQUENCE_START)
}
pub fn timestamp_offset_nano(&self) -> u32 {
self.mod_ex
.iter()
.filter_map(ModEx::timestamp_offset_nano)
.fold(0u32, |acc, n| acc.saturating_add(n))
}
}
pub fn parse_audio(payload: &[u8]) -> Result<AudioTag> {
if payload.is_empty() {
return Err(Error::Other("FLV audio tag: empty".into()));
}
let b0 = payload[0];
let sound_format = b0 >> 4;
if sound_format == AUDIO_FORMAT_EX_HEADER {
let mut packet_type = b0 & 0x0F;
let mut pos = 1;
let mut mod_ex = Vec::new();
if packet_type == AUDIO_PACKET_TYPE_MOD_EX {
let (chain, real_pt, next) =
parse_mod_ex_chain(payload, pos, AUDIO_PACKET_TYPE_MOD_EX, "audio")?;
mod_ex = chain;
packet_type = real_pt;
pos = next;
}
if pos + 4 > payload.len() {
return Err(Error::Other(
"Enhanced RTMP audio tag: need 4 bytes for FourCC after header/ModEx".into(),
));
}
let mut fcc = [0u8; 4];
fcc.copy_from_slice(&payload[pos..pos + 4]);
pos += 4;
Ok(AudioTag {
sound_format,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(packet_type),
audio_fourcc: Some(fcc),
body: payload[pos..].to_vec(),
mod_ex,
})
} else {
let sound_rate = (b0 >> 2) & 0x03;
let sound_size_16bit = (b0 & 0x02) != 0;
let stereo = (b0 & 0x01) != 0;
if sound_format == AUDIO_FORMAT_AAC {
if payload.len() < 2 {
return Err(Error::Other("FLV/AAC tag: need 2+ bytes".into()));
}
Ok(AudioTag {
sound_format,
sound_rate,
sound_size_16bit,
stereo,
aac_packet_type: Some(payload[1]),
ex_packet_type: None,
audio_fourcc: None,
body: payload[2..].to_vec(),
mod_ex: Vec::new(),
})
} else {
Ok(AudioTag {
sound_format,
sound_rate,
sound_size_16bit,
stereo,
aac_packet_type: None,
ex_packet_type: None,
audio_fourcc: None,
body: payload[1..].to_vec(),
mod_ex: Vec::new(),
})
}
}
}
pub fn build_audio(tag: &AudioTag) -> Vec<u8> {
if let Some(fcc) = tag.audio_fourcc {
let packet_type = tag.ex_packet_type.unwrap_or(AUDIO_PACKET_TYPE_CODED_FRAMES);
let header_pt = if tag.mod_ex.is_empty() {
packet_type
} else {
AUDIO_PACKET_TYPE_MOD_EX
};
let head = (AUDIO_FORMAT_EX_HEADER << 4) | (header_pt & 0x0F);
let mut out = Vec::with_capacity(tag.body.len() + 5);
out.push(head);
build_mod_ex_chain(&mut out, &tag.mod_ex, AUDIO_PACKET_TYPE_MOD_EX, packet_type);
out.extend_from_slice(&fcc);
out.extend_from_slice(&tag.body);
out
} else {
let b0 = (tag.sound_format << 4)
| ((tag.sound_rate & 0x03) << 2)
| (if tag.sound_size_16bit { 0x02 } else { 0 })
| (if tag.stereo { 0x01 } else { 0 });
let mut out = Vec::with_capacity(tag.body.len() + 2);
out.push(b0);
if tag.sound_format == AUDIO_FORMAT_AAC {
out.push(tag.aac_packet_type.unwrap_or(AAC_PACKET_TYPE_RAW));
}
out.extend_from_slice(&tag.body);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn video_tag_avc_nalu_roundtrip() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: VIDEO_CODEC_AVC,
avc_packet_type: Some(AVC_PACKET_TYPE_NALU),
composition_time: 42,
body: b"\x00\x00\x00\x05hello".to_vec(),
ex_packet_type: None,
fourcc: None,
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0x17); let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn video_tag_negative_cts_sign_extends() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_INTER,
codec_id: VIDEO_CODEC_AVC,
avc_packet_type: Some(AVC_PACKET_TYPE_NALU),
composition_time: -5,
body: vec![0x01],
ex_packet_type: None,
fourcc: None,
};
let payload = build_video(&tag);
let back = parse_video(&payload).unwrap();
assert_eq!(back.composition_time, -5);
}
#[test]
fn ex_video_tag_hevc_sequence_start_roundtrip() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"\x01dummy-hvcc".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_SEQUENCE_START),
fourcc: Some(FOURCC_HEVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0x90);
assert_eq!(&payload[1..5], b"hvc1");
assert_eq!(&payload[5..], b"\x01dummy-hvcc");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert!(back.is_ex_sequence_header());
assert!(back.is_keyframe());
}
#[test]
fn ex_video_tag_hevc_coded_frames_carries_cts() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_INTER,
codec_id: 0,
avc_packet_type: None,
composition_time: -33,
body: b"\x00\x00\x00\x04NALU".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES),
fourcc: Some(FOURCC_HEVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0xA1);
assert_eq!(&payload[1..5], b"hvc1");
assert_eq!(&payload[5..8], &[0xFF, 0xFF, 0xDF]);
assert_eq!(&payload[8..], b"\x00\x00\x00\x04NALU");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert_eq!(back.composition_time, -33);
}
#[test]
fn ex_video_tag_hevc_coded_frames_x_omits_cts() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_INTER,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"\x00\x00\x00\x04NALU".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES_X),
fourcc: Some(FOURCC_HEVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0xA3);
assert_eq!(&payload[1..5], b"hvc1");
assert_eq!(&payload[5..], b"\x00\x00\x00\x04NALU");
assert_eq!(payload.len(), 1 + 4 + 8);
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_video_tag_av1_sequence_start_no_cts() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"\x81\x05\x0c\x00".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_SEQUENCE_START),
fourcc: Some(FOURCC_AV1),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0x90);
assert_eq!(&payload[1..5], b"av01");
assert_eq!(&payload[5..], b"\x81\x05\x0c\x00");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert!(back.is_ex_sequence_header());
}
#[test]
fn ex_video_tag_av1_coded_frames_obus() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"\x0a\x0b\x0cobu-stub".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES),
fourcc: Some(FOURCC_AV1),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0x91);
assert_eq!(&payload[1..5], b"av01");
assert_eq!(&payload[5..], b"\x0a\x0b\x0cobu-stub");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_video_tag_vp9_coded_frames_full_frame() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"vp9-frame-bytes".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES),
fourcc: Some(FOURCC_VP9),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0x91);
assert_eq!(&payload[1..5], b"vp09");
assert_eq!(&payload[5..], b"vp9-frame-bytes");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_video_tag_sequence_end_empty_body() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: vec![],
ex_packet_type: Some(EX_PACKET_TYPE_SEQUENCE_END),
fourcc: Some(FOURCC_HEVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0x92);
assert_eq!(&payload[1..5], b"hvc1");
assert_eq!(payload.len(), 5);
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_video_tag_metadata_carries_amf_body() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_INFO, codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"amf-stub".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_METADATA),
fourcc: Some(FOURCC_HEVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0xD4);
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert!(back.is_ex_metadata());
}
#[test]
fn legacy_avc_high_frame_type_bit_was_always_zero() {
for ft in [
VIDEO_FRAME_KEYFRAME,
VIDEO_FRAME_INTER,
VIDEO_FRAME_DISPOSABLE,
VIDEO_FRAME_GENERATED_KEY,
VIDEO_FRAME_INFO,
] {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: ft,
codec_id: VIDEO_CODEC_AVC,
avc_packet_type: Some(AVC_PACKET_TYPE_NALU),
composition_time: 0,
body: vec![0x00],
ex_packet_type: None,
fourcc: None,
};
let payload = build_video(&tag);
assert_eq!(payload[0] & VIDEO_IS_EX_HEADER, 0, "ft={ft}");
}
}
#[test]
fn ex_video_tag_vp8_sequence_start_carries_vp_config_record() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: vec![
0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
],
ex_packet_type: Some(EX_PACKET_TYPE_SEQUENCE_START),
fourcc: Some(FOURCC_VP8),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0x90);
assert_eq!(&payload[1..5], b"vp08");
assert_eq!(&payload[5..], &tag.body[..]);
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert!(back.is_ex_sequence_header());
}
#[test]
fn ex_video_tag_vp8_coded_frames_no_cts() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_INTER,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"vp8-frame-bytes".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES),
fourcc: Some(FOURCC_VP8),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0xA1);
assert_eq!(&payload[1..5], b"vp08");
assert_eq!(&payload[5..], b"vp8-frame-bytes");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_video_tag_avc_fourcc_sequence_start_carries_avcc() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"\x01\x42\xc0\x1edummy-avcc".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_SEQUENCE_START),
fourcc: Some(FOURCC_AVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0x90);
assert_eq!(&payload[1..5], b"avc1");
assert_eq!(&payload[5..], b"\x01\x42\xc0\x1edummy-avcc");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert!(back.is_ex_sequence_header());
}
#[test]
fn ex_video_tag_avc_fourcc_coded_frames_carries_si24_cts() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_INTER,
codec_id: 0,
avc_packet_type: None,
composition_time: -100,
body: b"\x00\x00\x00\x05nalu1".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES),
fourcc: Some(FOURCC_AVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0xA1);
assert_eq!(&payload[1..5], b"avc1");
assert_eq!(&payload[5..8], &[0xFF, 0xFF, 0x9C]);
assert_eq!(&payload[8..], b"\x00\x00\x00\x05nalu1");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert_eq!(back.composition_time, -100);
}
#[test]
fn ex_video_tag_avc_fourcc_coded_frames_x_omits_cts() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_INTER,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"\x00\x00\x00\x05nalu2".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES_X),
fourcc: Some(FOURCC_AVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0xA3);
assert_eq!(&payload[1..5], b"avc1");
assert_eq!(&payload[5..], b"\x00\x00\x00\x05nalu2");
assert_eq!(payload.len(), 1 + 4 + 9);
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_video_tag_vvc_sequence_start_carries_vvcc() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"\xff\xfcdummy-vvcc".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_SEQUENCE_START),
fourcc: Some(FOURCC_VVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0x90);
assert_eq!(&payload[1..5], b"vvc1");
assert_eq!(&payload[5..], b"\xff\xfcdummy-vvcc");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert!(back.is_ex_sequence_header());
}
#[test]
fn ex_video_tag_vvc_coded_frames_carries_si24_cts() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 17,
body: b"\x00\x00\x00\x06h266ku".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES),
fourcc: Some(FOURCC_VVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0x91);
assert_eq!(&payload[1..5], b"vvc1");
assert_eq!(&payload[5..8], &[0x00, 0x00, 0x11]);
assert_eq!(&payload[8..], b"\x00\x00\x00\x06h266ku");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert_eq!(back.composition_time, 17);
}
#[test]
fn ex_video_tag_vvc_coded_frames_x_omits_cts() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_INTER,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"\x00\x00\x00\x03vvc".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES_X),
fourcc: Some(FOURCC_VVC),
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0xA3);
assert_eq!(&payload[1..5], b"vvc1");
assert_eq!(&payload[5..], b"\x00\x00\x00\x03vvc");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_video_tag_avc_fourcc_coded_frames_truncated_si24_errors() {
let truncated = [
0xA1, b'a', b'v', b'c', b'1', 0xFF, 0xFF, ];
assert!(parse_video(&truncated).is_err());
}
#[test]
fn ex_video_tag_v2_fourccs_are_distinct_from_v1_set() {
for &fcc in &[FOURCC_VP8, FOURCC_AVC, FOURCC_VVC] {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: vec![0xDE, 0xAD, 0xBE, 0xEF],
ex_packet_type: Some(EX_PACKET_TYPE_SEQUENCE_END),
fourcc: Some(fcc),
};
let payload = build_video(&tag);
assert_eq!(&payload[1..5], &fcc[..]);
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert!(!matches!(fcc, FOURCC_AV1 | FOURCC_VP9 | FOURCC_HEVC));
}
}
#[test]
fn audio_tag_aac_sequence_header_roundtrip() {
let tag = AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_AAC,
sound_rate: 3,
sound_size_16bit: true,
stereo: true,
aac_packet_type: Some(AAC_PACKET_TYPE_SEQUENCE_HEADER),
body: vec![0x12, 0x10], ex_packet_type: None,
audio_fourcc: None,
};
let payload = build_audio(&tag);
assert_eq!(payload[0], 0xAF); assert_eq!(payload[1], 0); let back = parse_audio(&payload).unwrap();
assert_eq!(back, tag);
assert!(back.is_aac_sequence_header());
assert!(!back.is_enhanced());
}
#[test]
fn ex_audio_tag_opus_sequence_start_roundtrip() {
let tag = AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_EX_HEADER,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(AUDIO_PACKET_TYPE_SEQUENCE_START),
audio_fourcc: Some(FOURCC_OPUS),
body: b"OpusHead\x01\x02".to_vec(),
};
let payload = build_audio(&tag);
assert_eq!(payload[0], 0x90);
assert_eq!(&payload[1..5], b"Opus");
assert_eq!(&payload[5..], b"OpusHead\x01\x02");
let back = parse_audio(&payload).unwrap();
assert_eq!(back, tag);
assert!(back.is_ex_sequence_header());
assert!(back.is_enhanced());
assert_eq!(back.sound_rate, 0);
assert!(!back.sound_size_16bit);
assert!(!back.stereo);
}
#[test]
fn ex_audio_tag_opus_coded_frames_carries_self_delimited_packets() {
let tag = AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_EX_HEADER,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(AUDIO_PACKET_TYPE_CODED_FRAMES),
audio_fourcc: Some(FOURCC_OPUS),
body: b"opus-frame-bytes".to_vec(),
};
let payload = build_audio(&tag);
assert_eq!(payload[0], 0x91);
assert_eq!(&payload[1..5], b"Opus");
assert_eq!(&payload[5..], b"opus-frame-bytes");
let back = parse_audio(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_audio_tag_flac_sequence_start_roundtrip() {
let tag = AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_EX_HEADER,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(AUDIO_PACKET_TYPE_SEQUENCE_START),
audio_fourcc: Some(FOURCC_FLAC),
body: b"fLaC\x80\x00\x00\x22streaminfo".to_vec(),
};
let payload = build_audio(&tag);
assert_eq!(payload[0], 0x90);
assert_eq!(&payload[1..5], b"fLaC");
assert_eq!(&payload[5..], b"fLaC\x80\x00\x00\x22streaminfo");
let back = parse_audio(&payload).unwrap();
assert_eq!(back, tag);
assert!(back.is_ex_sequence_header());
}
#[test]
fn ex_audio_tag_ac3_coded_frames_roundtrip() {
let tag = AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_EX_HEADER,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(AUDIO_PACKET_TYPE_CODED_FRAMES),
audio_fourcc: Some(FOURCC_AC3),
body: vec![0x0B, 0x77, 0x12, 0x34, 0x56, 0x78], };
let payload = build_audio(&tag);
assert_eq!(payload[0], 0x91);
assert_eq!(&payload[1..5], b"ac-3");
assert_eq!(&payload[5..], &[0x0B, 0x77, 0x12, 0x34, 0x56, 0x78]);
let back = parse_audio(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_audio_tag_eac3_coded_frames_roundtrip() {
let tag = AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_EX_HEADER,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(AUDIO_PACKET_TYPE_CODED_FRAMES),
audio_fourcc: Some(FOURCC_EAC3),
body: vec![0x0B, 0x77, 0xAB, 0xCD],
};
let payload = build_audio(&tag);
assert_eq!(payload[0], 0x91);
assert_eq!(&payload[1..5], b"ec-3");
let back = parse_audio(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_audio_tag_mp3_coded_frames_roundtrip() {
let tag = AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_EX_HEADER,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(AUDIO_PACKET_TYPE_CODED_FRAMES),
audio_fourcc: Some(FOURCC_MP3),
body: vec![0xFF, 0xFB, 0x90, 0x00], };
let payload = build_audio(&tag);
assert_eq!(payload[0], 0x91);
assert_eq!(&payload[1..5], b".mp3");
let back = parse_audio(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_audio_tag_aac_fourcc_sequence_start() {
let tag = AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_EX_HEADER,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(AUDIO_PACKET_TYPE_SEQUENCE_START),
audio_fourcc: Some(FOURCC_AAC),
body: vec![0x12, 0x10], };
let payload = build_audio(&tag);
assert_eq!(payload[0], 0x90);
assert_eq!(&payload[1..5], b"mp4a");
assert_eq!(&payload[5..], &[0x12, 0x10]);
let back = parse_audio(&payload).unwrap();
assert_eq!(back, tag);
assert!(back.is_ex_sequence_header());
assert!(!back.is_aac_sequence_header());
}
#[test]
fn ex_audio_tag_sequence_end_empty_body() {
let tag = AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_EX_HEADER,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(AUDIO_PACKET_TYPE_SEQUENCE_END),
audio_fourcc: Some(FOURCC_OPUS),
body: vec![],
};
let payload = build_audio(&tag);
assert_eq!(payload[0], 0x92);
assert_eq!(&payload[1..5], b"Opus");
assert_eq!(payload.len(), 5);
let back = parse_audio(&payload).unwrap();
assert_eq!(back, tag);
}
#[test]
fn ex_audio_tag_truncated_fourcc_errors() {
let truncated = [0x90, b'O', b'p', b'u']; assert!(parse_audio(&truncated).is_err());
let just_header = [0x90];
assert!(parse_audio(&just_header).is_err());
}
#[test]
fn legacy_audio_high_nibble_never_collides_with_ex_header() {
for sf in [
AUDIO_FORMAT_PCM_LE,
AUDIO_FORMAT_ADPCM,
AUDIO_FORMAT_MP3,
AUDIO_FORMAT_PCM_LE_8BIT,
AUDIO_FORMAT_NELLYMOSER_16K_MONO,
AUDIO_FORMAT_NELLYMOSER_8K_MONO,
AUDIO_FORMAT_NELLYMOSER,
AUDIO_FORMAT_G711_ALAW,
AUDIO_FORMAT_G711_MULAW,
AUDIO_FORMAT_AAC,
AUDIO_FORMAT_SPEEX,
] {
assert_ne!(sf, AUDIO_FORMAT_EX_HEADER, "sf={sf}");
}
}
#[test]
fn ex_video_mod_ex_timestamp_offset_nano_roundtrip() {
let nano = 999_999u32; let tag = VideoTag {
frame_type: VIDEO_FRAME_INTER,
codec_id: 0,
avc_packet_type: None,
composition_time: 7,
body: b"\x00\x00\x00\x05nalu!".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES),
fourcc: Some(FOURCC_VVC),
mod_ex: vec![ModEx::timestamp_offset_nano_entry(nano)],
};
let payload = build_video(&tag);
assert_eq!(payload[0], 0xA7);
assert_eq!(payload[1], 2);
assert_eq!(&payload[2..5], &[0x0F, 0x42, 0x3F]);
assert_eq!(payload[5], 0x01);
assert_eq!(&payload[6..10], b"vvc1");
assert_eq!(&payload[10..13], &[0x00, 0x00, 0x07]);
assert_eq!(&payload[13..], b"\x00\x00\x00\x05nalu!");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert_eq!(back.timestamp_offset_nano(), nano);
assert_eq!(back.mod_ex[0].timestamp_offset_nano(), Some(nano));
}
#[test]
fn ex_video_mod_ex_chain_multiple_entries_roundtrip() {
let tag = VideoTag {
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"av1cfg".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_SEQUENCE_START),
fourcc: Some(FOURCC_AV1),
mod_ex: vec![
ModEx::timestamp_offset_nano_entry(500_000),
ModEx {
mod_ex_type: 3, data: vec![0xAA, 0xBB],
},
],
};
let payload = build_video(&tag);
assert_eq!(payload[1], 2);
assert_eq!(&payload[2..5], &[0x07, 0xA1, 0x20]); assert_eq!(payload[5], 0x07);
assert_eq!(payload[6], 1);
assert_eq!(&payload[7..9], &[0xAA, 0xBB]);
assert_eq!(payload[9], 0x30);
assert_eq!(&payload[10..14], b"av01");
assert_eq!(&payload[14..], b"av1cfg");
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert_eq!(back.timestamp_offset_nano(), 500_000);
}
#[test]
fn ex_video_mod_ex_ui16_size_escape_roundtrip() {
let big = vec![0x5A; 300];
let tag = VideoTag {
frame_type: VIDEO_FRAME_INTER,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"hevc-frame".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES_X),
fourcc: Some(FOURCC_HEVC),
mod_ex: vec![ModEx {
mod_ex_type: MOD_EX_TYPE_TIMESTAMP_OFFSET_NANO,
data: big.clone(),
}],
};
let payload = build_video(&tag);
assert_eq!(payload[1], 0xFF);
assert_eq!(&payload[2..4], &[0x01, 0x2B]);
assert_eq!(&payload[4..4 + 300], &big[..]);
assert_eq!(payload[4 + 300], 0x03);
let back = parse_video(&payload).unwrap();
assert_eq!(back, tag);
assert_eq!(back.mod_ex[0].data.len(), 300);
}
#[test]
fn ex_audio_mod_ex_timestamp_offset_nano_roundtrip() {
let nano = 250_000u32;
let tag = AudioTag {
sound_format: AUDIO_FORMAT_EX_HEADER,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(AUDIO_PACKET_TYPE_CODED_FRAMES),
audio_fourcc: Some(FOURCC_OPUS),
body: b"opus-pkt".to_vec(),
mod_ex: vec![ModEx::timestamp_offset_nano_entry(nano)],
};
let payload = build_audio(&tag);
assert_eq!(payload[0], 0x97);
assert_eq!(payload[1], 2); assert_eq!(&payload[2..5], &[0x03, 0xD0, 0x90]); assert_eq!(payload[5], 0x01);
assert_eq!(&payload[6..10], b"Opus");
assert_eq!(&payload[10..], b"opus-pkt");
let back = parse_audio(&payload).unwrap();
assert_eq!(back, tag);
assert_eq!(back.timestamp_offset_nano(), nano);
}
#[test]
fn mod_ex_accessor_rejects_wrong_type_and_short_data() {
let wrong_type = ModEx {
mod_ex_type: 1,
data: vec![0, 0, 0],
};
assert_eq!(wrong_type.timestamp_offset_nano(), None);
let too_short = ModEx {
mod_ex_type: MOD_EX_TYPE_TIMESTAMP_OFFSET_NANO,
data: vec![0x00, 0x01],
};
assert_eq!(too_short.timestamp_offset_nano(), None);
}
#[test]
fn ex_video_mod_ex_truncated_chain_fails_controlled() {
let truncated = [0x97u8, 0x02];
assert!(parse_video(&truncated).is_err());
let no_nibble = [0x97u8, 0x02, 0x00, 0x00, 0x00];
assert!(parse_video(&no_nibble).is_err());
let no_fourcc = [0x97u8, 0x02, 0x00, 0x00, 0x00, 0x01];
assert!(parse_video(&no_fourcc).is_err());
}
#[test]
fn ex_audio_mod_ex_truncated_chain_fails_controlled() {
let truncated = [0x97u8, 0x02];
assert!(parse_audio(&truncated).is_err());
let no_fourcc = [0x97u8, 0x02, 0x00, 0x00, 0x00, 0x01];
assert!(parse_audio(&no_fourcc).is_err());
}
#[test]
fn ex_video_without_mod_ex_emits_no_prelude() {
let tag = VideoTag {
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: b"\x01cfg".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_SEQUENCE_START),
fourcc: Some(FOURCC_HEVC),
mod_ex: Vec::new(),
};
let payload = build_video(&tag);
assert_eq!(payload[0] & 0x0F, EX_PACKET_TYPE_SEQUENCE_START);
assert_eq!(&payload[1..5], b"hvc1");
assert_eq!(&payload[5..], b"\x01cfg");
assert_eq!(parse_video(&payload).unwrap(), tag);
}
}