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;
pub const AUDIO_CHANNEL_ORDER_UNSPECIFIED: u8 = 0;
pub const AUDIO_CHANNEL_ORDER_NATIVE: u8 = 1;
pub const AUDIO_CHANNEL_ORDER_CUSTOM: u8 = 2;
pub mod audio_channel {
pub const FRONT_LEFT: u8 = 0;
pub const FRONT_RIGHT: u8 = 1;
pub const FRONT_CENTER: u8 = 2;
pub const LOW_FREQUENCY1: u8 = 3;
pub const BACK_LEFT: u8 = 4;
pub const BACK_RIGHT: u8 = 5;
pub const FRONT_LEFT_CENTER: u8 = 6;
pub const FRONT_RIGHT_CENTER: u8 = 7;
pub const BACK_CENTER: u8 = 8;
pub const SIDE_LEFT: u8 = 9;
pub const SIDE_RIGHT: u8 = 10;
pub const TOP_CENTER: u8 = 11;
pub const TOP_FRONT_LEFT: u8 = 12;
pub const TOP_FRONT_CENTER: u8 = 13;
pub const TOP_FRONT_RIGHT: u8 = 14;
pub const TOP_BACK_LEFT: u8 = 15;
pub const TOP_BACK_CENTER: u8 = 16;
pub const TOP_BACK_RIGHT: u8 = 17;
pub const LOW_FREQUENCY2: u8 = 18;
pub const TOP_SIDE_LEFT: u8 = 19;
pub const TOP_SIDE_RIGHT: u8 = 20;
pub const BOTTOM_FRONT_CENTER: u8 = 21;
pub const BOTTOM_FRONT_LEFT: u8 = 22;
pub const BOTTOM_FRONT_RIGHT: u8 = 23;
pub const UNUSED: u8 = 0xfe;
pub const UNKNOWN: u8 = 0xff;
}
pub mod audio_channel_mask {
pub const FRONT_LEFT: u32 = 0x000001;
pub const FRONT_RIGHT: u32 = 0x000002;
pub const FRONT_CENTER: u32 = 0x000004;
pub const LOW_FREQUENCY1: u32 = 0x000008;
pub const BACK_LEFT: u32 = 0x000010;
pub const BACK_RIGHT: u32 = 0x000020;
pub const FRONT_LEFT_CENTER: u32 = 0x000040;
pub const FRONT_RIGHT_CENTER: u32 = 0x000080;
pub const BACK_CENTER: u32 = 0x000100;
pub const SIDE_LEFT: u32 = 0x000200;
pub const SIDE_RIGHT: u32 = 0x000400;
pub const TOP_CENTER: u32 = 0x000800;
pub const TOP_FRONT_LEFT: u32 = 0x001000;
pub const TOP_FRONT_CENTER: u32 = 0x002000;
pub const TOP_FRONT_RIGHT: u32 = 0x004000;
pub const TOP_BACK_LEFT: u32 = 0x008000;
pub const TOP_BACK_CENTER: u32 = 0x010000;
pub const TOP_BACK_RIGHT: u32 = 0x020000;
pub const LOW_FREQUENCY2: u32 = 0x040000;
pub const TOP_SIDE_LEFT: u32 = 0x080000;
pub const TOP_SIDE_RIGHT: u32 = 0x100000;
pub const BOTTOM_FRONT_CENTER: u32 = 0x200000;
pub const BOTTOM_FRONT_LEFT: u32 = 0x400000;
pub const BOTTOM_FRONT_RIGHT: u32 = 0x800000;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultichannelConfig {
pub order: MultichannelConfigOrder,
pub channel_count: u8,
pub extra: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MultichannelConfigOrder {
Unspecified,
Native { flags: u32 },
Custom { mapping: Vec<u8> },
Reserved(u8),
}
impl MultichannelConfigOrder {
pub fn as_u8(&self) -> u8 {
match self {
MultichannelConfigOrder::Unspecified => AUDIO_CHANNEL_ORDER_UNSPECIFIED,
MultichannelConfigOrder::Native { .. } => AUDIO_CHANNEL_ORDER_NATIVE,
MultichannelConfigOrder::Custom { .. } => AUDIO_CHANNEL_ORDER_CUSTOM,
MultichannelConfigOrder::Reserved(v) => *v,
}
}
}
impl MultichannelConfig {
pub fn parse(body: &[u8]) -> Result<MultichannelConfig> {
if body.len() < 2 {
return Err(Error::Other(
"MultichannelConfig: need 2 bytes (order + channelCount)".into(),
));
}
let order_byte = body[0];
let channel_count = body[1];
match order_byte {
AUDIO_CHANNEL_ORDER_UNSPECIFIED => {
if body.len() != 2 {
return Err(Error::Other(
"MultichannelConfig.Unspecified: trailing bytes after channelCount".into(),
));
}
Ok(MultichannelConfig {
order: MultichannelConfigOrder::Unspecified,
channel_count,
extra: Vec::new(),
})
}
AUDIO_CHANNEL_ORDER_NATIVE => {
if body.len() != 6 {
return Err(Error::Other(
"MultichannelConfig.Native: need 6 bytes (order + count + UI32 flags)"
.into(),
));
}
let flags = u32::from_be_bytes([body[2], body[3], body[4], body[5]]);
Ok(MultichannelConfig {
order: MultichannelConfigOrder::Native { flags },
channel_count,
extra: Vec::new(),
})
}
AUDIO_CHANNEL_ORDER_CUSTOM => {
let need = 2 + channel_count as usize;
if body.len() != need {
return Err(Error::Other(format!(
"MultichannelConfig.Custom: need {need} bytes for channelCount={channel_count}, got {}",
body.len()
)));
}
Ok(MultichannelConfig {
order: MultichannelConfigOrder::Custom {
mapping: body[2..need].to_vec(),
},
channel_count,
extra: Vec::new(),
})
}
other => Ok(MultichannelConfig {
order: MultichannelConfigOrder::Reserved(other),
channel_count,
extra: body[2..].to_vec(),
}),
}
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(8);
out.push(self.order.as_u8());
out.push(self.channel_count);
match &self.order {
MultichannelConfigOrder::Unspecified => {}
MultichannelConfigOrder::Native { flags } => {
out.extend_from_slice(&flags.to_be_bytes());
}
MultichannelConfigOrder::Custom { mapping } => {
out.extend_from_slice(mapping);
}
MultichannelConfigOrder::Reserved(_) => {
out.extend_from_slice(&self.extra);
}
}
out
}
}
pub const AV_MULTITRACK_TYPE_ONE_TRACK: u8 = 0;
pub const AV_MULTITRACK_TYPE_MANY_TRACKS: u8 = 1;
pub const AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS: u8 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Multitrack {
pub multitrack_type: u8,
pub tracks: Vec<MultitrackTrack>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultitrackTrack {
pub fourcc: Option<[u8; 4]>,
pub track_id: u8,
pub body: Vec<u8>,
}
impl Multitrack {
pub fn parse(body: &[u8], multitrack_type: u8) -> Result<Multitrack> {
let many_codecs = multitrack_type == AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS;
let one_track = multitrack_type == AV_MULTITRACK_TYPE_ONE_TRACK;
let mut pos = 0usize;
let mut tracks = Vec::new();
loop {
if pos >= body.len() {
if tracks.is_empty() {
return Err(Error::Other(
"Multitrack: empty track list (need at least one track)".into(),
));
}
break;
}
let track_fourcc = if many_codecs {
if pos + 4 > body.len() {
return Err(Error::Other(
"Multitrack: truncated reading per-track FourCC".into(),
));
}
let mut fcc = [0u8; 4];
fcc.copy_from_slice(&body[pos..pos + 4]);
pos += 4;
Some(fcc)
} else {
None
};
if pos >= body.len() {
return Err(Error::Other("Multitrack: truncated reading trackId".into()));
}
let track_id = body[pos];
pos += 1;
let track_body = if one_track {
let rest = body[pos..].to_vec();
pos = body.len();
rest
} else {
if pos + 3 > body.len() {
return Err(Error::Other(
"Multitrack: truncated reading sizeOfTrack UI24".into(),
));
}
let size = ((body[pos] as usize) << 16)
| ((body[pos + 1] as usize) << 8)
| (body[pos + 2] as usize);
pos += 3;
if pos + size > body.len() {
return Err(Error::Other(format!(
"Multitrack: sizeOfTrack={size} overruns remaining {} bytes",
body.len() - pos
)));
}
let slice = body[pos..pos + size].to_vec();
pos += size;
slice
};
tracks.push(MultitrackTrack {
fourcc: track_fourcc,
track_id,
body: track_body,
});
if one_track {
break;
}
}
Ok(Multitrack {
multitrack_type,
tracks,
})
}
pub fn encode(&self) -> Vec<u8> {
let many_codecs = self.multitrack_type == AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS;
let one_track = self.multitrack_type == AV_MULTITRACK_TYPE_ONE_TRACK;
let mut out = Vec::new();
for (i, track) in self.tracks.iter().enumerate() {
if one_track && i > 0 {
break;
}
if many_codecs {
let fcc = track.fourcc.unwrap_or([0; 4]);
out.extend_from_slice(&fcc);
}
out.push(track.track_id);
if !one_track {
let size = track.body.len() & 0x00FF_FFFF;
out.extend_from_slice(&[(size >> 16) as u8, (size >> 8) as u8, size as u8]);
}
out.extend_from_slice(&track.body);
}
out
}
}
#[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>,
pub multitrack: Option<Multitrack>,
}
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))
}
pub fn is_multitrack(&self) -> bool {
self.multitrack.is_some()
}
pub fn multitrack_tag(
frame_type: u8,
real_packet_type: u8,
shared_fourcc: Option<[u8; 4]>,
mt: Multitrack,
) -> VideoTag {
VideoTag {
frame_type,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: Vec::new(),
ex_packet_type: Some(real_packet_type),
fourcc: shared_fourcc,
mod_ex: Vec::new(),
multitrack: Some(mt),
}
}
}
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;
}
let mut multitrack_type: Option<u8> = None;
if packet_type == EX_PACKET_TYPE_MULTITRACK {
if pos >= payload.len() {
return Err(Error::Other(
"Enhanced RTMP video Multitrack: truncated reading multitrackType nibble"
.into(),
));
}
let nibble = payload[pos];
pos += 1;
let mt_type = (nibble >> 4) & 0x0F;
let inner_pt = nibble & 0x0F;
if inner_pt == EX_PACKET_TYPE_MULTITRACK {
return Err(Error::Other(
"Enhanced RTMP video Multitrack: inner PacketType MUST NOT be Multitrack"
.into(),
));
}
multitrack_type = Some(mt_type);
packet_type = inner_pt;
}
let need_shared_fourcc = match multitrack_type {
Some(t) => t != AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS,
None => true,
};
let fcc_opt = if need_shared_fourcc {
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;
Some(fcc)
} else {
None
};
let fcc = fcc_opt.unwrap_or([0; 4]);
if let Some(mt_type) = multitrack_type {
let mt = Multitrack::parse(&payload[pos..], mt_type)?;
return Ok(VideoTag {
frame_type,
codec_id: 0,
avc_packet_type: None,
composition_time: 0,
body: Vec::new(),
ex_packet_type: Some(packet_type),
fourcc: fcc_opt,
mod_ex,
multitrack: Some(mt),
});
}
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,
multitrack: None,
})
} 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(),
multitrack: None,
})
} 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(),
multitrack: None,
})
}
}
}
pub fn build_video(tag: &VideoTag) -> Vec<u8> {
if tag.fourcc.is_some() || tag.multitrack.is_some() {
let real_packet_type = tag.ex_packet_type.unwrap_or(EX_PACKET_TYPE_CODED_FRAMES);
let multitrack_outer_pt = if tag.multitrack.is_some() {
Some(EX_PACKET_TYPE_MULTITRACK)
} else {
None
};
let post_mod_ex_pt = multitrack_outer_pt.unwrap_or(real_packet_type);
let header_pt = if tag.mod_ex.is_empty() {
post_mod_ex_pt
} 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, post_mod_ex_pt);
if let Some(mt) = &tag.multitrack {
out.push(((mt.multitrack_type & 0x0F) << 4) | (real_packet_type & 0x0F));
if mt.multitrack_type != AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS {
let fcc = tag.fourcc.unwrap_or([0; 4]);
out.extend_from_slice(&fcc);
}
out.extend_from_slice(&mt.encode());
return out;
}
let fcc = tag
.fourcc
.expect("Enhanced-RTMP non-Multitrack tag requires fourcc");
out.extend_from_slice(&fcc);
let cts_on_wire = real_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>,
pub multitrack: Option<Multitrack>,
}
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 is_multichannel_config(&self) -> bool {
self.audio_fourcc.is_some()
&& self.ex_packet_type == Some(AUDIO_PACKET_TYPE_MULTICHANNEL_CONFIG)
}
pub fn multichannel_config(&self) -> Result<Option<MultichannelConfig>> {
if self.is_multichannel_config() {
Ok(Some(MultichannelConfig::parse(&self.body)?))
} else {
Ok(None)
}
}
pub fn multichannel_config_tag(fourcc: [u8; 4], cfg: &MultichannelConfig) -> AudioTag {
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_MULTICHANNEL_CONFIG),
audio_fourcc: Some(fourcc),
body: cfg.encode(),
mod_ex: Vec::new(),
multitrack: None,
}
}
pub fn is_multitrack(&self) -> bool {
self.multitrack.is_some()
}
pub fn multitrack_tag(
real_packet_type: u8,
shared_fourcc: Option<[u8; 4]>,
mt: Multitrack,
) -> AudioTag {
AudioTag {
sound_format: AUDIO_FORMAT_EX_HEADER,
sound_rate: 0,
sound_size_16bit: false,
stereo: false,
aac_packet_type: None,
ex_packet_type: Some(real_packet_type),
audio_fourcc: shared_fourcc,
body: Vec::new(),
mod_ex: Vec::new(),
multitrack: Some(mt),
}
}
}
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;
}
let mut multitrack_type: Option<u8> = None;
if packet_type == AUDIO_PACKET_TYPE_MULTITRACK {
if pos >= payload.len() {
return Err(Error::Other(
"Enhanced RTMP audio Multitrack: truncated reading multitrackType nibble"
.into(),
));
}
let nibble = payload[pos];
pos += 1;
let mt_type = (nibble >> 4) & 0x0F;
let inner_pt = nibble & 0x0F;
if inner_pt == AUDIO_PACKET_TYPE_MULTITRACK {
return Err(Error::Other(
"Enhanced RTMP audio Multitrack: inner PacketType MUST NOT be Multitrack"
.into(),
));
}
multitrack_type = Some(mt_type);
packet_type = inner_pt;
}
let need_shared_fourcc = match multitrack_type {
Some(t) => t != AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS,
None => true,
};
let fcc_opt = if need_shared_fourcc {
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;
Some(fcc)
} else {
None
};
if let Some(mt_type) = multitrack_type {
let mt = Multitrack::parse(&payload[pos..], mt_type)?;
return 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: fcc_opt,
body: Vec::new(),
mod_ex,
multitrack: Some(mt),
});
}
let fcc = fcc_opt.expect("non-Multitrack audio tag requires shared FourCC slot");
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,
multitrack: None,
})
} 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(),
multitrack: None,
})
} 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(),
multitrack: None,
})
}
}
}
pub fn build_audio(tag: &AudioTag) -> Vec<u8> {
if tag.audio_fourcc.is_some() || tag.multitrack.is_some() {
let real_packet_type = tag.ex_packet_type.unwrap_or(AUDIO_PACKET_TYPE_CODED_FRAMES);
let multitrack_outer_pt = if tag.multitrack.is_some() {
Some(AUDIO_PACKET_TYPE_MULTITRACK)
} else {
None
};
let post_mod_ex_pt = multitrack_outer_pt.unwrap_or(real_packet_type);
let header_pt = if tag.mod_ex.is_empty() {
post_mod_ex_pt
} 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,
post_mod_ex_pt,
);
if let Some(mt) = &tag.multitrack {
out.push(((mt.multitrack_type & 0x0F) << 4) | (real_packet_type & 0x0F));
if mt.multitrack_type != AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS {
let fcc = tag.audio_fourcc.unwrap_or([0; 4]);
out.extend_from_slice(&fcc);
}
out.extend_from_slice(&mt.encode());
return out;
}
let fcc = tag
.audio_fourcc
.expect("Enhanced-RTMP non-Multitrack audio tag requires audio_fourcc");
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,
multitrack: 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,
multitrack: 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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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,
multitrack: 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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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),
multitrack: None,
};
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,
multitrack: 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(),
multitrack: None,
};
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(),
multitrack: None,
};
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(),
multitrack: None,
};
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],
multitrack: None,
};
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],
multitrack: None,
};
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],
multitrack: None,
};
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],
multitrack: None,
};
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![],
multitrack: None,
};
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)],
multitrack: None,
};
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],
},
],
multitrack: None,
};
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(),
}],
multitrack: None,
};
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)],
multitrack: None,
};
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(),
multitrack: None,
};
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);
}
#[test]
fn multichannel_config_unspecified_roundtrip() {
let cfg = MultichannelConfig {
order: MultichannelConfigOrder::Unspecified,
channel_count: 2,
extra: Vec::new(),
};
let bytes = cfg.encode();
assert_eq!(bytes, [0x00, 0x02]);
let back = MultichannelConfig::parse(&bytes).unwrap();
assert_eq!(back, cfg);
}
#[test]
fn multichannel_config_native_5_1_layout() {
let mask = audio_channel_mask::FRONT_LEFT
| audio_channel_mask::FRONT_RIGHT
| audio_channel_mask::FRONT_CENTER
| audio_channel_mask::LOW_FREQUENCY1
| audio_channel_mask::BACK_LEFT
| audio_channel_mask::BACK_RIGHT;
assert_eq!(mask, 0x0000_003F);
let cfg = MultichannelConfig {
order: MultichannelConfigOrder::Native { flags: mask },
channel_count: 6,
extra: Vec::new(),
};
let bytes = cfg.encode();
assert_eq!(bytes, [0x01, 0x06, 0x00, 0x00, 0x00, 0x3F]);
let back = MultichannelConfig::parse(&bytes).unwrap();
assert_eq!(back, cfg);
if let MultichannelConfigOrder::Native { flags } = back.order {
assert_eq!(flags & audio_channel_mask::LOW_FREQUENCY1, 0x08);
assert_eq!(flags & audio_channel_mask::TOP_CENTER, 0); } else {
panic!("expected Native order");
}
}
#[test]
fn multichannel_config_custom_mapping_roundtrip() {
let cfg = MultichannelConfig {
order: MultichannelConfigOrder::Custom {
mapping: vec![audio_channel::FRONT_LEFT, audio_channel::FRONT_RIGHT],
},
channel_count: 2,
extra: Vec::new(),
};
let bytes = cfg.encode();
assert_eq!(bytes, [0x02, 0x02, 0x00, 0x01]);
let back = MultichannelConfig::parse(&bytes).unwrap();
assert_eq!(back, cfg);
}
#[test]
fn multichannel_config_custom_22_2_layout() {
let mapping: Vec<u8> = (0..24).collect();
let cfg = MultichannelConfig {
order: MultichannelConfigOrder::Custom {
mapping: mapping.clone(),
},
channel_count: 24,
extra: Vec::new(),
};
let bytes = cfg.encode();
assert_eq!(bytes.len(), 2 + 24);
assert_eq!(bytes[0], AUDIO_CHANNEL_ORDER_CUSTOM);
assert_eq!(bytes[1], 24);
assert_eq!(&bytes[2..], mapping.as_slice());
let back = MultichannelConfig::parse(&bytes).unwrap();
assert_eq!(back, cfg);
}
#[test]
fn multichannel_config_custom_with_unused_unknown_sentinels() {
let cfg = MultichannelConfig {
order: MultichannelConfigOrder::Custom {
mapping: vec![
audio_channel::FRONT_LEFT,
audio_channel::FRONT_RIGHT,
audio_channel::UNUSED,
audio_channel::UNKNOWN,
],
},
channel_count: 4,
extra: Vec::new(),
};
let bytes = cfg.encode();
assert_eq!(bytes, [0x02, 0x04, 0x00, 0x01, 0xFE, 0xFF]);
assert_eq!(MultichannelConfig::parse(&bytes).unwrap(), cfg);
}
#[test]
fn multichannel_config_truncated_errors() {
assert!(MultichannelConfig::parse(&[]).is_err());
assert!(MultichannelConfig::parse(&[0x00]).is_err());
assert!(MultichannelConfig::parse(&[0x01, 0x06, 0x00]).is_err());
assert!(MultichannelConfig::parse(&[0x02, 0x03, 0x00, 0x01]).is_err());
assert!(MultichannelConfig::parse(&[0x00, 0x02, 0xff]).is_err());
}
#[test]
fn multichannel_config_reserved_order_preserves_extra_bytes() {
let body = vec![0x05, 0x04, 0xAA, 0xBB, 0xCC];
let cfg = MultichannelConfig::parse(&body).unwrap();
assert_eq!(cfg.order, MultichannelConfigOrder::Reserved(0x05));
assert_eq!(cfg.channel_count, 4);
assert_eq!(cfg.extra, vec![0xAA, 0xBB, 0xCC]);
assert_eq!(cfg.encode(), body);
}
#[test]
fn audio_tag_multichannel_config_full_roundtrip() {
let cfg = MultichannelConfig {
order: MultichannelConfigOrder::Native {
flags: audio_channel_mask::FRONT_LEFT
| audio_channel_mask::FRONT_RIGHT
| audio_channel_mask::FRONT_CENTER,
},
channel_count: 3,
extra: Vec::new(),
};
let tag = AudioTag::multichannel_config_tag(FOURCC_OPUS, &cfg);
assert!(tag.is_multichannel_config());
assert_eq!(tag.audio_fourcc, Some(FOURCC_OPUS));
assert_eq!(
tag.ex_packet_type,
Some(AUDIO_PACKET_TYPE_MULTICHANNEL_CONFIG)
);
let wire = build_audio(&tag);
assert_eq!(wire[0], (AUDIO_FORMAT_EX_HEADER << 4) | 0x04);
assert_eq!(&wire[1..5], b"Opus");
assert_eq!(wire.len(), 1 + 4 + 6);
let back = parse_audio(&wire).unwrap();
assert_eq!(back, tag);
let cfg_back = back.multichannel_config().unwrap().unwrap();
assert_eq!(cfg_back, cfg);
}
#[test]
fn audio_tag_multichannel_config_accessor_returns_none_for_other_packet_types() {
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_SEQUENCE_START),
audio_fourcc: Some(FOURCC_OPUS),
body: vec![b'O', b'p', b'u', b's', b'H', b'e', b'a', b'd'],
mod_ex: Vec::new(),
multitrack: None,
};
assert!(!tag.is_multichannel_config());
assert!(tag.multichannel_config().unwrap().is_none());
}
#[test]
fn multichannel_config_disjoint_from_legacy_audio() {
let tag = AudioTag {
sound_format: AUDIO_FORMAT_AAC,
sound_rate: 3,
sound_size_16bit: true,
stereo: true,
aac_packet_type: Some(AAC_PACKET_TYPE_RAW),
ex_packet_type: None,
audio_fourcc: None,
body: vec![0x01, 0x06, 0x00, 0x00, 0x00, 0x3F],
mod_ex: Vec::new(),
multitrack: None,
};
assert!(!tag.is_multichannel_config());
assert!(tag.multichannel_config().unwrap().is_none());
}
#[test]
fn audio_channel_mask_22_2_bit_assignments() {
let pairs = [
(audio_channel::FRONT_LEFT, audio_channel_mask::FRONT_LEFT),
(audio_channel::FRONT_RIGHT, audio_channel_mask::FRONT_RIGHT),
(
audio_channel::FRONT_CENTER,
audio_channel_mask::FRONT_CENTER,
),
(
audio_channel::LOW_FREQUENCY1,
audio_channel_mask::LOW_FREQUENCY1,
),
(audio_channel::BACK_LEFT, audio_channel_mask::BACK_LEFT),
(audio_channel::BACK_RIGHT, audio_channel_mask::BACK_RIGHT),
(
audio_channel::FRONT_LEFT_CENTER,
audio_channel_mask::FRONT_LEFT_CENTER,
),
(
audio_channel::FRONT_RIGHT_CENTER,
audio_channel_mask::FRONT_RIGHT_CENTER,
),
(audio_channel::BACK_CENTER, audio_channel_mask::BACK_CENTER),
(audio_channel::SIDE_LEFT, audio_channel_mask::SIDE_LEFT),
(audio_channel::SIDE_RIGHT, audio_channel_mask::SIDE_RIGHT),
(audio_channel::TOP_CENTER, audio_channel_mask::TOP_CENTER),
(
audio_channel::TOP_FRONT_LEFT,
audio_channel_mask::TOP_FRONT_LEFT,
),
(
audio_channel::TOP_FRONT_CENTER,
audio_channel_mask::TOP_FRONT_CENTER,
),
(
audio_channel::TOP_FRONT_RIGHT,
audio_channel_mask::TOP_FRONT_RIGHT,
),
(
audio_channel::TOP_BACK_LEFT,
audio_channel_mask::TOP_BACK_LEFT,
),
(
audio_channel::TOP_BACK_CENTER,
audio_channel_mask::TOP_BACK_CENTER,
),
(
audio_channel::TOP_BACK_RIGHT,
audio_channel_mask::TOP_BACK_RIGHT,
),
(
audio_channel::LOW_FREQUENCY2,
audio_channel_mask::LOW_FREQUENCY2,
),
(
audio_channel::TOP_SIDE_LEFT,
audio_channel_mask::TOP_SIDE_LEFT,
),
(
audio_channel::TOP_SIDE_RIGHT,
audio_channel_mask::TOP_SIDE_RIGHT,
),
(
audio_channel::BOTTOM_FRONT_CENTER,
audio_channel_mask::BOTTOM_FRONT_CENTER,
),
(
audio_channel::BOTTOM_FRONT_LEFT,
audio_channel_mask::BOTTOM_FRONT_LEFT,
),
(
audio_channel::BOTTOM_FRONT_RIGHT,
audio_channel_mask::BOTTOM_FRONT_RIGHT,
),
];
for (ch, mask) in pairs {
assert_eq!(
1u32 << ch as u32,
mask,
"channel {ch} should map to mask bit (1 << {ch})"
);
}
}
#[test]
fn multitrack_one_track_video_roundtrip() {
let mt = Multitrack {
multitrack_type: AV_MULTITRACK_TYPE_ONE_TRACK,
tracks: vec![MultitrackTrack {
fourcc: None,
track_id: 0,
body: b"\x00\x00\x00\x05hello".to_vec(),
}],
};
let tag = VideoTag::multitrack_tag(
VIDEO_FRAME_KEYFRAME,
EX_PACKET_TYPE_CODED_FRAMES,
Some(FOURCC_AVC),
mt.clone(),
);
assert!(tag.is_multitrack());
let wire = build_video(&tag);
assert_eq!(wire[0], 0x96);
assert_eq!(wire[1], 0x01);
assert_eq!(&wire[2..6], b"avc1");
assert_eq!(wire[6], 0x00);
assert_eq!(&wire[7..], b"\x00\x00\x00\x05hello");
let back = parse_video(&wire).unwrap();
assert_eq!(back, tag);
assert_eq!(back.multitrack.as_ref().unwrap(), &mt);
assert_eq!(back.ex_packet_type, Some(EX_PACKET_TYPE_CODED_FRAMES));
assert_eq!(back.fourcc, Some(FOURCC_AVC));
}
#[test]
fn multitrack_many_tracks_video_roundtrip() {
let mt = Multitrack {
multitrack_type: AV_MULTITRACK_TYPE_MANY_TRACKS,
tracks: vec![
MultitrackTrack {
fourcc: None,
track_id: 0,
body: b"hevc-track-0".to_vec(),
},
MultitrackTrack {
fourcc: None,
track_id: 1,
body: b"hevc-track-1-longer".to_vec(),
},
],
};
let tag = VideoTag::multitrack_tag(
VIDEO_FRAME_INTER,
EX_PACKET_TYPE_CODED_FRAMES,
Some(FOURCC_HEVC),
mt.clone(),
);
let wire = build_video(&tag);
assert_eq!(wire[0], 0xA6);
assert_eq!(wire[1], 0x11);
assert_eq!(&wire[2..6], b"hvc1");
assert_eq!(wire[6], 0x00);
assert_eq!(&wire[7..10], &[0, 0, 12]);
assert_eq!(&wire[10..22], b"hevc-track-0");
assert_eq!(wire[22], 0x01);
assert_eq!(&wire[23..26], &[0, 0, 19]);
assert_eq!(&wire[26..45], b"hevc-track-1-longer");
let back = parse_video(&wire).unwrap();
assert_eq!(back, tag);
}
#[test]
fn multitrack_many_tracks_many_codecs_video_roundtrip() {
let mt = Multitrack {
multitrack_type: AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS,
tracks: vec![
MultitrackTrack {
fourcc: Some(FOURCC_HEVC),
track_id: 0,
body: b"hevc-data".to_vec(),
},
MultitrackTrack {
fourcc: Some(FOURCC_AV1),
track_id: 1,
body: b"av1-obu-bytes".to_vec(),
},
],
};
let tag = VideoTag::multitrack_tag(
VIDEO_FRAME_KEYFRAME,
EX_PACKET_TYPE_CODED_FRAMES,
None,
mt.clone(),
);
let wire = build_video(&tag);
assert_eq!(wire[0], 0x96);
assert_eq!(wire[1], 0x21);
assert_eq!(&wire[2..6], b"hvc1");
assert_eq!(wire[6], 0x00);
assert_eq!(&wire[7..10], &[0, 0, 9]);
assert_eq!(&wire[10..19], b"hevc-data");
assert_eq!(&wire[19..23], b"av01");
assert_eq!(wire[23], 0x01);
assert_eq!(&wire[24..27], &[0, 0, 13]);
assert_eq!(&wire[27..40], b"av1-obu-bytes");
let back = parse_video(&wire).unwrap();
assert_eq!(back, tag);
assert_eq!(back.fourcc, None);
}
#[test]
fn multitrack_audio_one_track_roundtrip() {
let mt = Multitrack {
multitrack_type: AV_MULTITRACK_TYPE_ONE_TRACK,
tracks: vec![MultitrackTrack {
fourcc: None,
track_id: 0,
body: b"opus-packet-bytes".to_vec(),
}],
};
let tag = AudioTag::multitrack_tag(
AUDIO_PACKET_TYPE_CODED_FRAMES,
Some(FOURCC_OPUS),
mt.clone(),
);
assert!(tag.is_multitrack());
let wire = build_audio(&tag);
assert_eq!(wire[0], 0x95);
assert_eq!(wire[1], 0x01);
assert_eq!(&wire[2..6], b"Opus");
assert_eq!(wire[6], 0x00); assert_eq!(&wire[7..], b"opus-packet-bytes");
let back = parse_audio(&wire).unwrap();
assert_eq!(back, tag);
assert_eq!(back.ex_packet_type, Some(AUDIO_PACKET_TYPE_CODED_FRAMES));
assert_eq!(back.audio_fourcc, Some(FOURCC_OPUS));
}
#[test]
fn multitrack_audio_many_tracks_many_codecs_roundtrip() {
let mt = Multitrack {
multitrack_type: AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS,
tracks: vec![
MultitrackTrack {
fourcc: Some(FOURCC_OPUS),
track_id: 0,
body: b"opus-bytes".to_vec(),
},
MultitrackTrack {
fourcc: Some(FOURCC_AAC),
track_id: 7,
body: b"aac-raw-frame".to_vec(),
},
],
};
let tag = AudioTag::multitrack_tag(AUDIO_PACKET_TYPE_CODED_FRAMES, None, mt.clone());
let wire = build_audio(&tag);
assert_eq!(wire[0], 0x95);
assert_eq!(wire[1], 0x21);
assert_eq!(&wire[2..6], b"Opus");
assert_eq!(wire[6], 0x00);
assert_eq!(&wire[7..10], &[0, 0, 10]);
assert_eq!(&wire[10..20], b"opus-bytes");
assert_eq!(&wire[20..24], b"mp4a");
assert_eq!(wire[24], 0x07);
assert_eq!(&wire[25..28], &[0, 0, 13]);
assert_eq!(&wire[28..41], b"aac-raw-frame");
let back = parse_audio(&wire).unwrap();
assert_eq!(back, tag);
assert_eq!(back.audio_fourcc, None);
}
#[test]
fn multitrack_video_inner_packet_type_must_not_be_multitrack() {
let wire = [0x96u8, 0x06, b'a', b'v', b'c', b'1', 0x00];
let err = parse_video(&wire).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("MUST NOT"), "got: {msg}");
}
#[test]
fn multitrack_audio_inner_packet_type_must_not_be_multitrack() {
let wire = [0x95u8, 0x05, b'O', b'p', b'u', b's', 0x00];
let err = parse_audio(&wire).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("MUST NOT"), "got: {msg}");
}
#[test]
fn multitrack_video_truncated_size_overruns_error() {
let mut wire = vec![0x96u8, 0x11];
wire.extend_from_slice(b"avc1");
wire.push(0x00); wire.extend_from_slice(&[0x00, 0x00, 100]); wire.extend_from_slice(b"short"); let err = parse_video(&wire).unwrap_err();
let msg = format!("{err:?}");
assert!(
msg.contains("overruns"),
"expected size-overrun error, got: {msg}"
);
}
#[test]
fn multitrack_truncation_paths_audio_video() {
assert!(parse_video(&[0x96]).is_err());
assert!(parse_audio(&[0x95]).is_err());
assert!(parse_video(&[0x96, 0x01]).is_err());
assert!(parse_audio(&[0x95, 0x01]).is_err());
assert!(parse_video(&[0x96, 0x01, b'a', b'v', b'c', b'1']).is_err());
assert!(parse_video(&[0x96, 0x21]).is_err());
}
#[test]
fn multitrack_video_roundtrips_through_mod_ex_prelude() {
let mt = Multitrack {
multitrack_type: AV_MULTITRACK_TYPE_ONE_TRACK,
tracks: vec![MultitrackTrack {
fourcc: None,
track_id: 0,
body: b"hevc-nalus".to_vec(),
}],
};
let mut tag = VideoTag::multitrack_tag(
VIDEO_FRAME_KEYFRAME,
EX_PACKET_TYPE_CODED_FRAMES,
Some(FOURCC_HEVC),
mt.clone(),
);
tag.mod_ex = vec![ModEx::timestamp_offset_nano_entry(123_456)];
let wire = build_video(&tag);
let back = parse_video(&wire).unwrap();
assert_eq!(back, tag);
assert_eq!(back.timestamp_offset_nano(), 123_456);
assert!(back.is_multitrack());
}
#[test]
fn multitrack_helpers_round_trip_through_body_encode_parse() {
let mt = Multitrack {
multitrack_type: 4,
tracks: vec![
MultitrackTrack {
fourcc: None,
track_id: 2,
body: vec![0xDE, 0xAD],
},
MultitrackTrack {
fourcc: None,
track_id: 3,
body: vec![0xBE, 0xEF, 0xCA, 0xFE],
},
],
};
let bytes = mt.encode();
let back = Multitrack::parse(&bytes, 4).unwrap();
assert_eq!(back, mt);
}
}