use super::{CodecParser, Frame, PesPacket, pts_to_ns};
const SAMPLE_RATES: [u32; 4] = [48_000, 44_100, 32_000, 48_000];
const EAC3_REDUCED_RATES: [u32; 4] = [24_000, 22_050, 16_000, 48_000];
const MIN_FRAME_BYTES: usize = 6;
const AC3_SAMPLES_PER_FRAME: u32 = 1536;
const MAX_AC3_BUF: usize = 64 * 1024;
pub struct Ac3Parser {
buf: Vec<u8>,
flush_pts_ns: i64,
}
impl Default for Ac3Parser {
fn default() -> Self {
Self::new()
}
}
impl Ac3Parser {
pub fn new() -> Self {
Self {
buf: Vec::with_capacity(4096),
flush_pts_ns: 0,
}
}
}
impl CodecParser for Ac3Parser {
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
if pes.data.is_empty() {
return Vec::new();
}
let base_pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
self.buf.extend_from_slice(&pes.data);
let data = &self.buf;
let mut frames = Vec::new();
let mut pos = 0;
let mut frame_pts_ns = base_pts_ns;
while pos < data.len() {
let sync = find_ac3_sync(&data[pos..]);
let start = match sync {
Some(offset) => pos + offset,
None => break,
};
let remaining = &data[start..];
if remaining.len() < 6 {
break;
}
let bsid = get_bsid(remaining);
let frame_size = if bsid >= 11 {
eac3_frame_size(remaining)
} else {
ac3_frame_size(remaining)
};
if !(MIN_FRAME_BYTES..=8192).contains(&frame_size) {
pos = start + 2;
continue;
}
if start + frame_size > data.len() {
break;
}
let duration_ns = frame_duration_ns(remaining, bsid);
frames.push(Frame {
pts_ns: frame_pts_ns,
keyframe: true,
data: data[start..start + frame_size].to_vec(),
duration_ns: Some(duration_ns),
});
frame_pts_ns += duration_ns as i64;
pos = start + frame_size;
}
let keep_from = if pos < data.len() {
match find_ac3_sync(&data[pos..]) {
Some(o) => pos + o,
None if data.last() == Some(&0x0B) => data.len() - 1,
None => data.len(),
}
} else {
data.len()
};
if keep_from < data.len() {
let tail = &data[keep_from..];
if tail.len() > MAX_AC3_BUF {
tracing::debug!(
target: "mux",
"ac3: carry-over buffer exceeded {} bytes without a frame; dropping and resyncing",
MAX_AC3_BUF
);
self.buf.clear();
} else {
self.buf = tail.to_vec();
self.flush_pts_ns = frame_pts_ns;
}
} else {
self.buf.clear();
}
frames
}
fn flush(&mut self) -> Vec<Frame> {
let buf = std::mem::take(&mut self.buf);
let Some(off) = find_ac3_sync(&buf) else {
return Vec::new();
};
let frame = &buf[off..];
if frame.len() < 6 {
return Vec::new();
}
let bsid = get_bsid(frame);
let frame_size = if bsid >= 11 {
eac3_frame_size(frame)
} else {
ac3_frame_size(frame)
};
if !(MIN_FRAME_BYTES..=8192).contains(&frame_size) || off + frame_size > buf.len() {
return Vec::new();
}
let duration_ns = frame_duration_ns(frame, bsid);
vec![Frame {
pts_ns: self.flush_pts_ns,
keyframe: true,
data: buf[off..off + frame_size].to_vec(),
duration_ns: Some(duration_ns),
}]
}
fn codec_private(&self) -> Option<Vec<u8>> {
None
}
}
fn eac3_samples_per_frame(data: &[u8]) -> u32 {
if data.len() < 5 {
return AC3_SAMPLES_PER_FRAME;
}
let fscod = (data[4] >> 6) & 0x03;
if fscod == 0x03 {
return 6 * 256;
}
let numblkscod = (data[4] >> 4) & 0x03;
let numblks = match numblkscod {
0 => 1,
1 => 2,
2 => 3,
_ => 6,
};
numblks * 256
}
fn frame_sample_rate(data: &[u8], bsid: u8) -> u32 {
if data.len() < 5 {
return SAMPLE_RATES[0];
}
let fscod = (data[4] >> 6) & 0x03;
if fscod == 0x03 && bsid >= 11 {
let fscod2 = (data[4] >> 4) & 0x03;
return EAC3_REDUCED_RATES[fscod2 as usize];
}
SAMPLE_RATES[fscod as usize]
}
fn frame_duration_ns(data: &[u8], bsid: u8) -> u64 {
let samples = if bsid >= 11 {
eac3_samples_per_frame(data)
} else {
AC3_SAMPLES_PER_FRAME
} as u64;
let rate = frame_sample_rate(data, bsid) as u64;
(samples * 1_000_000_000 + rate / 2) / rate
}
fn find_ac3_sync(data: &[u8]) -> Option<usize> {
(0..data.len().saturating_sub(1)).find(|&i| data[i] == 0x0B && data[i + 1] == 0x77)
}
fn get_bsid(data: &[u8]) -> u8 {
if data.len() < 6 {
return 0;
}
(data[5] >> 3) & 0x1F
}
fn eac3_frame_size(data: &[u8]) -> usize {
if data.len() < 4 {
return 0;
}
let frmsiz = ((data[2] as usize & 0x07) << 8) | data[3] as usize;
(frmsiz + 1) * 2
}
pub(crate) fn ac3_frame_size(data: &[u8]) -> usize {
if data.len() < 5 {
return 0;
}
let fscod = (data[4] >> 6) & 0x03;
let frmsizecod = (data[4] & 0x3F) as usize;
if frmsizecod >= AC3_FRAME_SIZES.len() {
return 0;
}
let words = AC3_FRAME_SIZES[frmsizecod];
match fscod {
0 => words[0] * 2,
1 => words[1] * 2,
2 => words[2] * 2,
_ => 0,
}
}
const AC3_FRAME_SIZES: [[usize; 3]; 38] = [
[64, 69, 96],
[64, 70, 96],
[80, 87, 120],
[80, 88, 120],
[96, 104, 144],
[96, 105, 144],
[112, 121, 168],
[112, 122, 168],
[128, 139, 192],
[128, 140, 192],
[160, 174, 240],
[160, 175, 240],
[192, 208, 288],
[192, 209, 288],
[224, 243, 336],
[224, 244, 336],
[256, 278, 384],
[256, 279, 384],
[320, 348, 480],
[320, 349, 480],
[384, 417, 576],
[384, 418, 576],
[448, 487, 672],
[448, 488, 672],
[512, 557, 768],
[512, 558, 768],
[640, 696, 960],
[640, 697, 960],
[768, 835, 1152],
[768, 836, 1152],
[896, 975, 1344],
[896, 976, 1344],
[1024, 1114, 1536],
[1024, 1115, 1536],
[1152, 1253, 1728],
[1152, 1254, 1728],
[1280, 1393, 1920],
[1280, 1394, 1920],
];
#[cfg(test)]
mod tests {
use super::*;
fn make_ac3_frame(fscod: u8, frmsizecod: u8) -> Vec<u8> {
let size = AC3_FRAME_SIZES[frmsizecod as usize][fscod as usize] * 2;
let mut frame = vec![0u8; size];
frame[0] = 0x0B;
frame[1] = 0x77;
frame[4] = (fscod << 6) | frmsizecod;
frame[5] = 0x08 << 3; frame
}
#[test]
fn parse_empty_pes() {
let mut parser = Ac3Parser::new();
let pes = PesPacket {
pid: 0,
pts: None,
dts: None,
data: vec![],
};
assert!(parser.parse(&pes).is_empty());
}
#[test]
fn parse_single_frame() {
let mut parser = Ac3Parser::new();
let frame_data = make_ac3_frame(0, 2); let pes = PesPacket {
pid: 0,
pts: Some(90000),
dts: None,
data: frame_data.clone(),
};
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].data.len(), 160);
}
#[test]
fn parse_frame_spanning_two_pes() {
let mut parser = Ac3Parser::new();
let frame_data = make_ac3_frame(0, 2); let mid = 80;
let pes1 = PesPacket {
pid: 0,
pts: Some(90000),
dts: None,
data: frame_data[..mid].to_vec(),
};
let frames1 = parser.parse(&pes1);
assert!(frames1.is_empty(), "partial frame should not emit");
let pes2 = PesPacket {
pid: 0,
pts: Some(93000),
dts: None,
data: frame_data[mid..].to_vec(),
};
let frames2 = parser.parse(&pes2);
assert_eq!(frames2.len(), 1);
assert_eq!(frames2[0].data.len(), 160);
}
#[test]
fn skip_garbage_before_sync() {
let mut parser = Ac3Parser::new();
let frame_data = make_ac3_frame(0, 2);
let mut data = vec![0xDE, 0xAD, 0xBE, 0xEF]; data.extend_from_slice(&frame_data);
let pes = PesPacket {
pid: 0,
pts: None,
dts: None,
data,
};
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].data.len(), 160);
}
#[test]
fn sync_word_split_across_pes_is_preserved() {
let mut parser = Ac3Parser::new();
let frame_data = make_ac3_frame(0, 2);
let mut pes1_data = frame_data.clone();
pes1_data.push(0x0B);
let pes1 = PesPacket {
pid: 0,
pts: Some(90000),
dts: None,
data: pes1_data,
};
let frames1 = parser.parse(&pes1);
assert_eq!(frames1.len(), 1, "first complete frame emitted");
let mut pes2_data = vec![0x77];
pes2_data.extend_from_slice(&frame_data[2..]);
let pes2 = PesPacket {
pid: 0,
pts: Some(93000),
dts: None,
data: pes2_data,
};
let frames2 = parser.parse(&pes2);
assert_eq!(frames2.len(), 1, "split-sync frame must be recovered");
assert_eq!(frames2[0].data.len(), 160);
}
#[test]
fn buffer_stays_bounded_across_many_garbage_pes() {
let mut parser = Ac3Parser::new();
for i in 0..256 {
let mut data = vec![0x55u8; 8192];
if i % 3 == 0 {
*data.last_mut().unwrap() = 0x0B;
}
let pes = PesPacket {
pid: 0,
pts: None,
dts: None,
data,
};
let frames = parser.parse(&pes);
assert!(frames.is_empty());
assert!(
parser.buf.len() <= MAX_AC3_BUF,
"buffer grew to {} (cap {})",
parser.buf.len(),
MAX_AC3_BUF
);
}
assert!(parser.buf.len() <= 1, "retained {} bytes", parser.buf.len());
}
#[test]
fn split_sync_below_cap_is_still_retained() {
let mut parser = Ac3Parser::new();
let data = vec![0x00, 0x00, 0x0B];
let pes = PesPacket {
pid: 0,
pts: None,
dts: None,
data,
};
assert!(parser.parse(&pes).is_empty());
assert_eq!(parser.buf, vec![0x0B], "lone trailing 0x0B retained");
}
#[test]
fn flush_emits_complete_buffered_frame_at_eos() {
let mut parser = Ac3Parser::new();
let frame_data = make_ac3_frame(0, 2);
parser.buf = frame_data.clone();
parser.flush_pts_ns = pts_to_ns(99000);
let f = parser.flush();
assert_eq!(f.len(), 1, "complete buffered frame drained at EOS");
assert_eq!(f[0].data.len(), 160);
assert_eq!(f[0].pts_ns, pts_to_ns(99000), "flush uses carried PTS");
assert!(f[0].duration_ns.is_some(), "flush sets duration");
assert!(parser.buf.is_empty(), "buffer consumed by flush");
}
#[test]
fn flush_carries_running_pts_from_partial_tail() {
let mut parser = Ac3Parser::new();
let frame_data = make_ac3_frame(0, 2);
let mut data = frame_data.clone();
data.extend_from_slice(&frame_data[..40]); let pes = PesPacket {
pid: 0,
pts: Some(90000),
dts: None,
data,
};
let f = parser.parse(&pes);
assert_eq!(f.len(), 1, "frame 1 emitted in parse");
let dur = f[0].duration_ns.unwrap() as i64;
assert_eq!(parser.flush_pts_ns, pts_to_ns(90000) + dur);
}
#[test]
fn flush_drops_partial_tail() {
let mut parser = Ac3Parser::new();
let frame_data = make_ac3_frame(0, 2);
parser.buf = frame_data[..80].to_vec(); assert!(parser.flush().is_empty(), "partial tail dropped");
}
#[test]
fn per_frame_pts_increments_within_one_pes() {
let mut parser = Ac3Parser::new();
let frame_data = make_ac3_frame(0, 2); let mut data = frame_data.clone();
data.extend_from_slice(&frame_data);
let pes = PesPacket {
pid: 0,
pts: Some(90000),
dts: None,
data,
};
let f = parser.parse(&pes);
assert_eq!(f.len(), 2);
assert_eq!(f[0].pts_ns, pts_to_ns(90000), "frame 0 uses PES base PTS");
let expect = 1536u64 * 1_000_000_000 / 48_000;
assert_eq!(f[0].duration_ns, Some(expect));
assert_eq!(
f[1].pts_ns - f[0].pts_ns,
expect as i64,
"frame 1 PTS advances by one frame duration, not equal to frame 0"
);
}
#[test]
fn frame_duration_ac3_48khz() {
let frame = make_ac3_frame(0, 2);
let bsid = get_bsid(&frame);
assert!(bsid < 11, "test frame is legacy AC-3");
assert_eq!(frame_duration_ns(&frame, bsid), 32_000_000);
}
#[test]
fn eac3_subheader_sized_frame_is_rejected() {
let mut parser = Ac3Parser::new();
let mut data = vec![0x0B, 0x77, 0x00, 0x00, 0x00, 16 << 3, 0x00, 0x00];
let good = make_ac3_frame(0, 2);
data.extend_from_slice(&good);
let pes = PesPacket {
pid: 0,
pts: Some(90000),
dts: None,
data,
};
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1, "only the real AC-3 frame is emitted");
assert_eq!(frames[0].data.len(), 160);
}
#[test]
fn eac3_fscod2_reduced_rate_duration() {
let data = [0x0B, 0x77, 0x00, 0x00, 0xC0, 16 << 3];
let bsid = get_bsid(&data);
assert!(bsid >= 11, "test frame is E-AC-3");
assert_eq!(frame_duration_ns(&data, bsid), 64_000_000);
}
#[test]
fn ac3_frame_size_table() {
assert_eq!(ac3_frame_size(&[0x0B, 0x77, 0, 0, 0x00, 0x40]), 128);
assert_eq!(ac3_frame_size(&[0x0B, 0x77, 0, 0, 0x02, 0x40]), 160);
}
#[test]
fn ac3_frame_size_44100_uses_second_column() {
assert_eq!(
ac3_frame_size(&[0x0B, 0x77, 0, 0, 0x40, 0x00]),
69 * 2,
"44.1kHz column (index 1), 69 words = 138 bytes"
);
}
#[test]
fn ac3_frame_size_32000_uses_third_column() {
assert_eq!(
ac3_frame_size(&[0x0B, 0x77, 0, 0, 0x80, 0x00]),
96 * 2,
"32kHz column (index 2), 96 words = 192 bytes"
);
}
#[test]
fn ac3_frame_size_reserved_fscod3_is_unmappable() {
assert_eq!(ac3_frame_size(&[0x0B, 0x77, 0, 0, 0xC0, 0x00]), 0);
}
#[test]
fn ac3_frame_size_frmsizecod_out_of_range_is_zero() {
assert_eq!(ac3_frame_size(&[0x0B, 0x77, 0, 0, 0x26, 0x00]), 0);
assert_eq!(ac3_frame_size(&[0x0B, 0x77, 0, 0, 0x3F, 0x00]), 0);
}
#[test]
fn ac3_frame_size_short_input_is_zero() {
assert_eq!(ac3_frame_size(&[0x0B, 0x77, 0, 0]), 0);
assert_eq!(ac3_frame_size(&[]), 0);
}
#[test]
fn ac3_frame_size_max_frmsizecod_37() {
assert_eq!(ac3_frame_size(&[0x0B, 0x77, 0, 0, 0x25, 0x00]), 1280 * 2);
}
#[test]
fn eac3_frame_size_formula() {
assert_eq!(eac3_frame_size(&[0x0B, 0x77, 0x07, 0xFF]), 4096);
assert_eq!(eac3_frame_size(&[0x0B, 0x77, 0x00, 0x02]), 6);
}
#[test]
fn eac3_frame_size_short_input_zero() {
assert_eq!(eac3_frame_size(&[0x0B, 0x77, 0x00]), 0);
}
#[test]
fn eac3_frame_size_masks_byte2_to_three_bits() {
assert_eq!(eac3_frame_size(&[0x0B, 0x77, 0xFF, 0x00]), (1792 + 1) * 2);
}
#[test]
fn get_bsid_extracts_bits_7_3() {
assert_eq!(get_bsid(&[0x0B, 0x77, 0, 0, 0, 0xA8]), 21);
assert_eq!(get_bsid(&[0x0B, 0x77, 0, 0, 0, 0x0F]), 1);
}
#[test]
fn get_bsid_short_input_zero() {
assert_eq!(get_bsid(&[0x0B, 0x77, 0, 0, 0]), 0);
}
#[test]
fn bsid_11_is_first_eac3_value() {
assert_eq!(get_bsid(&[0x0B, 0x77, 0, 0, 0, 10 << 3]), 10);
assert_eq!(get_bsid(&[0x0B, 0x77, 0, 0, 0, 11 << 3]), 11);
}
#[test]
fn ac3_duration_44100() {
let frame = make_ac3_frame(1, 0); let bsid = get_bsid(&frame);
assert!(bsid < 11);
let expect = (1536u64 * 1_000_000_000 + 44_100 / 2) / 44_100;
assert_eq!(frame_duration_ns(&frame, bsid), expect);
}
#[test]
fn ac3_duration_32000() {
let frame = make_ac3_frame(2, 0); let bsid = get_bsid(&frame);
assert_eq!(frame_duration_ns(&frame, bsid), 48_000_000);
}
#[test]
fn eac3_fscod2_22050_reduced_rate() {
let data = [0x0B, 0x77, 0x00, 0x00, 0xD0, 16 << 3];
let bsid = get_bsid(&data);
assert!(bsid >= 11);
let expect = (1536u64 * 1_000_000_000 + 22_050 / 2) / 22_050;
assert_eq!(frame_duration_ns(&data, bsid), expect);
}
#[test]
fn eac3_fscod2_16000_reduced_rate() {
let data = [0x0B, 0x77, 0x00, 0x00, 0xE0, 16 << 3];
let bsid = get_bsid(&data);
let expect = 1536u64 * 1_000_000_000 / 16_000; assert_eq!(frame_duration_ns(&data, bsid), expect);
}
#[test]
fn eac3_fscod2_reserved_index3_falls_back_48k() {
let data = [0x0B, 0x77, 0x00, 0x00, 0xF0, 16 << 3];
let bsid = get_bsid(&data);
let expect = 1536u64 * 1_000_000_000 / 48_000; assert_eq!(frame_duration_ns(&data, bsid), expect);
}
#[test]
fn ac3_fscod3_does_not_use_fscod2_path() {
let data = [0x0B, 0x77, 0x00, 0x00, 0xC0, 8 << 3]; let bsid = get_bsid(&data);
assert!(bsid < 11);
assert_eq!(frame_duration_ns(&data, bsid), 32_000_000);
}
#[test]
fn frame_sample_rate_short_input_defaults_48k() {
let short = [0x0B, 0x77, 0x00, 0x00];
let expect = 1536u64 * 1_000_000_000 / 48_000;
assert_eq!(frame_duration_ns(&short, 8), expect);
}
#[test]
fn eac3_numblkscod_block_counts() {
let mk = |numblkscod: u8| [0x0B, 0x77, 0x00, 0x00, numblkscod << 4, 0x00];
assert_eq!(
eac3_samples_per_frame(&mk(0)),
256,
"numblkscod 0 → 1 block"
);
assert_eq!(
eac3_samples_per_frame(&mk(1)),
512,
"numblkscod 1 → 2 blocks"
);
assert_eq!(
eac3_samples_per_frame(&mk(2)),
768,
"numblkscod 2 → 3 blocks"
);
assert_eq!(
eac3_samples_per_frame(&mk(3)),
1536,
"numblkscod 3 → 6 blocks"
);
}
#[test]
fn eac3_samples_fscod3_fixed_at_six_blocks() {
let data = [0x0B, 0x77, 0x00, 0x00, 0xC0, 0x00];
assert_eq!(eac3_samples_per_frame(&data), 6 * 256);
}
#[test]
fn eac3_samples_short_input_defaults_1536() {
assert_eq!(eac3_samples_per_frame(&[0x0B, 0x77, 0x00, 0x00]), 1536);
}
#[test]
fn eac3_frame_at_min_frame_bytes_is_accepted() {
let mut parser = Ac3Parser::new();
let mut data = vec![0x0B, 0x77, 0x00, 0x02, 0x00, 16 << 3];
data.truncate(6);
data.extend_from_slice(&make_ac3_frame(0, 2));
let f = parser.parse(&make_eac3_pes(data));
assert_eq!(f.len(), 2, "6-byte E-AC-3 frame accepted + following AC-3");
assert_eq!(f[0].data.len(), 6);
}
#[test]
fn eac3_max_frmsiz_frame_within_window_accepted() {
let mut parser = Ac3Parser::new();
let mut frame = vec![0u8; 4096];
frame[0] = 0x0B;
frame[1] = 0x77;
frame[2] = 0x07; frame[3] = 0xFF; frame[5] = 16 << 3; let f = parser.parse(&make_eac3_pes(frame));
assert_eq!(f.len(), 1, "4096-byte E-AC-3 frame within window accepted");
assert_eq!(f[0].data.len(), 4096);
}
#[test]
fn undersized_sync_skips_two_bytes_and_resyncs() {
let mut parser = Ac3Parser::new();
let mut data = vec![0x0B, 0x77, 0x00, 0x00, 0x00, 16 << 3];
data.extend_from_slice(&make_ac3_frame(0, 2)); let f = parser.parse(&make_eac3_pes(data));
assert_eq!(f.len(), 1, "junk sync skipped, real frame found");
assert_eq!(f[0].data.len(), 160);
}
#[test]
fn find_ac3_sync_locates_0b77() {
assert_eq!(find_ac3_sync(&[0xFF, 0x0B, 0x77, 0x00]), Some(1));
assert_eq!(find_ac3_sync(&[0x0B, 0x77]), Some(0));
}
#[test]
fn find_ac3_sync_lone_0b_at_end_not_matched() {
assert_eq!(find_ac3_sync(&[0xFF, 0xFF, 0x0B]), None);
assert_eq!(find_ac3_sync(&[0x0B]), None);
assert_eq!(find_ac3_sync(&[]), None);
}
#[test]
fn find_ac3_sync_0b_without_77_no_false_positive() {
assert_eq!(find_ac3_sync(&[0x0B, 0x76, 0x0B, 0x78]), None);
}
#[test]
fn flush_rejects_frame_extending_past_buffer() {
let mut parser = Ac3Parser::new();
let frame = make_ac3_frame(0, 2); parser.buf = frame[..100].to_vec();
assert!(
parser.flush().is_empty(),
"incomplete frame must not be emitted truncated at flush"
);
}
#[test]
fn flush_with_no_sync_is_empty() {
let mut parser = Ac3Parser::new();
parser.buf = vec![0xAA, 0xBB, 0xCC];
assert!(parser.flush().is_empty());
}
fn make_eac3_pes(data: Vec<u8>) -> PesPacket {
PesPacket {
pid: 0,
pts: Some(90000),
dts: None,
data,
}
}
}