use alloc::collections::VecDeque;
use alloc::vec::Vec;
use crate::annexb::find_start_code_prefix;
use crate::error::{Error, Result as AuResult};
use crate::nal::{NalCodec, nal_unit_type};
const AVC_VCL_MIN: u8 = 1;
const AVC_VCL_MAX: u8 = 5;
const AVC_AUD: u8 = 9;
const AVC_END_OF_SEQ: u8 = 10;
const AVC_END_OF_STREAM: u8 = 11;
const AVC_FILLER_DATA: u8 = 12;
const HEVC_VCL_MAX: u8 = 31;
const HEVC_AUD: u8 = 35;
const HEVC_EOS: u8 = 36;
const HEVC_EOB: u8 = 37;
const HEVC_FD: u8 = 38;
const HEVC_SUFFIX_SEI: u8 = 40;
const HEVC_RSV_NVCL_SUFFIX_MIN: u8 = 45;
const HEVC_RSV_NVCL_SUFFIX_MAX: u8 = 47;
const HEVC_UNSPEC_SUFFIX_MIN: u8 = 56;
const VVC_VCL_MAX: u8 = 11;
const VVC_SUFFIX_APS: u8 = 18;
const VVC_AUD: u8 = 20;
const VVC_EOS: u8 = 21;
const VVC_EOB: u8 = 22;
const VVC_SUFFIX_SEI: u8 = 24;
const VVC_FD: u8 = 25;
const MAX_OPEN_NAL_BYTES: usize = 64 * 1024 * 1024;
fn header_len(codec: NalCodec) -> usize {
match codec {
NalCodec::Avc => 1,
NalCodec::Hevc | NalCodec::Vvc => 2,
}
}
fn is_vcl(codec: NalCodec, t: u8) -> bool {
match codec {
NalCodec::Avc => (AVC_VCL_MIN..=AVC_VCL_MAX).contains(&t),
NalCodec::Hevc => t <= HEVC_VCL_MAX,
NalCodec::Vvc => t <= VVC_VCL_MAX,
}
}
fn is_aud(codec: NalCodec, t: u8) -> bool {
match codec {
NalCodec::Avc => t == AVC_AUD,
NalCodec::Hevc => t == HEVC_AUD,
NalCodec::Vvc => t == VVC_AUD,
}
}
fn is_suffix(codec: NalCodec, t: u8) -> bool {
match codec {
NalCodec::Avc => matches!(t, AVC_END_OF_SEQ | AVC_END_OF_STREAM | AVC_FILLER_DATA),
NalCodec::Hevc => {
matches!(t, HEVC_EOS | HEVC_EOB | HEVC_FD | HEVC_SUFFIX_SEI)
|| (HEVC_RSV_NVCL_SUFFIX_MIN..=HEVC_RSV_NVCL_SUFFIX_MAX).contains(&t)
|| t >= HEVC_UNSPEC_SUFFIX_MIN
}
NalCodec::Vvc => matches!(
t,
VVC_EOS | VVC_EOB | VVC_SUFFIX_APS | VVC_SUFFIX_SEI | VVC_FD
),
}
}
fn first_slice_of_picture(codec: NalCodec, nal_body: &[u8]) -> bool {
let hl = header_len(codec);
match codec {
NalCodec::Avc | NalCodec::Hevc => nal_body.get(hl).is_some_and(|b| b & 0x80 != 0),
NalCodec::Vvc => false,
}
}
pub(crate) fn first_nal_start(data: &[u8]) -> Option<usize> {
let p = find_start_code_prefix(data, 0)?;
let mut s = p;
while s > 0 && data[s - 1] == 0 {
s -= 1;
}
Some(s)
}
pub struct AccessUnitSplitter {
codec: NalCodec,
buf: Vec<u8>,
primed: bool,
scanned: usize,
au: Vec<u8>,
au_has_vcl: bool,
ready: VecDeque<Vec<u8>>,
#[cfg(test)]
scanned_bytes: usize,
}
impl AccessUnitSplitter {
pub fn new(codec: NalCodec) -> Self {
Self {
codec,
buf: Vec::new(),
primed: false,
scanned: 0,
au: Vec::new(),
au_has_vcl: false,
ready: VecDeque::new(),
#[cfg(test)]
scanned_bytes: 0,
}
}
pub fn push(&mut self, bytes: &[u8]) -> AuResult<()> {
self.buf.extend_from_slice(bytes);
self.drain_complete_nals()
}
pub fn pop(&mut self) -> Option<Vec<u8>> {
self.ready.pop_front()
}
pub fn finish(&mut self) {
if self.primed && !self.buf.is_empty() {
let range = core::mem::take(&mut self.buf);
self.process_nal(&range);
}
if !self.au.is_empty() {
self.ready.push_back(core::mem::take(&mut self.au));
self.au_has_vcl = false;
}
}
fn drain_complete_nals(&mut self) -> AuResult<()> {
if !self.primed {
match first_nal_start(&self.buf) {
Some(first) => {
if first > 0 {
self.buf.drain(..first);
}
self.primed = true;
self.scanned = 0;
}
None => {
return self.check_open_nal_cap();
}
}
}
let buf = core::mem::take(&mut self.buf);
let keep_from = self.scan(&buf);
self.buf.extend_from_slice(&buf[keep_from..]);
self.scanned = self.scanned.saturating_sub(keep_from);
self.check_open_nal_cap()
}
fn check_open_nal_cap(&self) -> AuResult<()> {
for (len, what) in [
(self.buf.len(), "Annex B NAL"),
(self.au.len(), "access unit"),
] {
if len > MAX_OPEN_NAL_BYTES {
return Err(Error::InvalidValue {
field: what,
value: len as u64,
reason: "exceeds the maximum buffered Annex B NAL/access unit size",
});
}
}
Ok(())
}
fn scan(&mut self, buf: &[u8]) -> usize {
let mut starts: Vec<usize> = Vec::new();
let n = buf.len();
let mut p = self.scanned.min(n.saturating_sub(3));
#[cfg(test)]
let scan_start = p;
while let Some(at) = find_start_code_prefix(buf, p) {
starts.push(at);
p = at + 3;
}
p = p.max(n.saturating_sub(2));
#[cfg(test)]
{
self.scanned_bytes += p.saturating_sub(scan_start);
}
self.scanned = self.scanned.max(p.min(n.saturating_sub(2)));
if starts.is_empty() {
return 0;
}
let mut boundaries: Vec<usize> = Vec::with_capacity(starts.len() + 1);
boundaries.push(0);
for &cp in &starts {
if cp == 0 {
continue;
}
let mut s = cp;
while s > 0 && buf[s - 1] == 0 {
s -= 1;
}
boundaries.push(s);
}
for i in 0..boundaries.len() - 1 {
self.process_nal(&buf[boundaries[i]..boundaries[i + 1]]);
}
boundaries[boundaries.len() - 1]
}
fn process_nal(&mut self, nal_with_code: &[u8]) {
let body = &nal_with_code[start_code_len(nal_with_code)..];
let Some(t) = nal_unit_type(self.codec, body) else {
self.au.extend_from_slice(nal_with_code);
return;
};
let vcl = is_vcl(self.codec, t);
let starts_new_au = if is_aud(self.codec, t) {
true
} else if vcl {
self.au_has_vcl && first_slice_of_picture(self.codec, body)
} else {
self.au_has_vcl && !is_suffix(self.codec, t)
};
if starts_new_au && !self.au.is_empty() {
self.ready.push_back(core::mem::take(&mut self.au));
self.au_has_vcl = false;
}
self.au.extend_from_slice(nal_with_code);
if vcl {
self.au_has_vcl = true;
}
}
}
fn start_code_len(nal_with_code: &[u8]) -> usize {
find_start_code_prefix(nal_with_code, 0).map_or(nal_with_code.len(), |i| i + 3)
}
pub fn split_access_units(codec: NalCodec, annexb: &[u8]) -> Vec<Vec<u8>> {
let mut s = AccessUnitSplitter::new(codec);
if s.push(annexb).is_err() {
return Vec::new();
}
s.finish();
let mut out = Vec::new();
while let Some(au) = s.pop() {
out.push(au);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn avc_sps() -> Vec<u8> {
vec![0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1e]
}
fn avc_pps() -> Vec<u8> {
vec![0x00, 0x00, 0x00, 0x01, 0x68, 0xce, 0x38, 0x80]
}
fn avc_aud() -> Vec<u8> {
vec![0x00, 0x00, 0x00, 0x01, 0x09, 0xf0]
}
fn avc_idr_first() -> Vec<u8> {
vec![0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00]
}
fn avc_p_first() -> Vec<u8> {
vec![0x00, 0x00, 0x01, 0x41, 0x9a, 0x00]
}
fn avc_p_cont() -> Vec<u8> {
vec![0x00, 0x00, 0x01, 0x41, 0x00, 0x11]
}
fn concat(parts: &[Vec<u8>]) -> Vec<u8> {
let mut v = Vec::new();
for p in parts {
v.extend_from_slice(p);
}
v
}
#[test]
fn splits_aud_delimited_stream() {
let stream = concat(&[
avc_aud(),
avc_sps(),
avc_pps(),
avc_idr_first(),
avc_aud(),
avc_p_first(),
]);
let aus = split_access_units(NalCodec::Avc, &stream);
assert_eq!(aus.len(), 2, "two AUDs → two access units");
assert_eq!(concat(&aus), stream, "AU concatenation is byte-exact");
}
#[test]
fn splits_audless_stream_on_first_slice_and_config() {
let stream = concat(&[
avc_sps(),
avc_pps(),
avc_idr_first(),
avc_p_first(),
avc_p_first(),
]);
let aus = split_access_units(NalCodec::Avc, &stream);
assert_eq!(aus.len(), 3);
assert_eq!(concat(&aus), stream);
assert!(aus[0].windows(1).any(|b| b[0] == 0x67));
}
#[test]
fn multi_slice_picture_stays_one_au() {
let stream = concat(&[avc_sps(), avc_idr_first(), avc_p_cont()]);
let aus = split_access_units(NalCodec::Avc, &stream);
assert_eq!(aus.len(), 1, "continuation slice stays in the same AU");
assert_eq!(concat(&aus), stream);
}
#[test]
fn streaming_matches_whole_buffer_at_every_split_point() {
let stream = concat(&[
avc_aud(),
avc_sps(),
avc_pps(),
avc_idr_first(),
avc_aud(),
avc_p_first(),
avc_p_cont(),
avc_aud(),
avc_p_first(),
]);
let whole = split_access_units(NalCodec::Avc, &stream);
let mut s = AccessUnitSplitter::new(NalCodec::Avc);
let mut chunked = Vec::new();
for &b in &stream {
s.push(&[b]).unwrap();
while let Some(au) = s.pop() {
chunked.push(au);
}
}
s.finish();
while let Some(au) = s.pop() {
chunked.push(au);
}
assert_eq!(chunked, whole, "byte-by-byte streaming == whole-buffer");
assert_eq!(concat(&chunked), stream);
}
#[test]
fn hevc_aud_boundaries() {
let aud = vec![0x00u8, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50];
let vps = vec![0x00u8, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c];
let idr = vec![0x00u8, 0x00, 0x01, 0x26, 0x01, 0x80]; let stream = concat(&[aud.clone(), vps, idr, aud]);
let aus = split_access_units(NalCodec::Hevc, &stream);
assert_eq!(aus.len(), 2);
assert_eq!(concat(&aus), stream);
}
#[test]
fn leading_junk_before_first_start_code_is_dropped() {
let mut stream = vec![0xaa, 0xbb, 0xcc];
stream.extend_from_slice(&concat(&[avc_aud(), avc_idr_first()]));
let aus = split_access_units(NalCodec::Avc, &stream);
assert_eq!(aus.len(), 1);
assert_eq!(&aus[0][..4], &[0x00, 0x00, 0x00, 0x01]);
}
#[test]
fn avc_filler_and_eos_trail_their_picture() {
let filler = vec![0x00u8, 0x00, 0x00, 0x01, 0x0c, 0xff, 0x80];
let eos = vec![0x00u8, 0x00, 0x00, 0x01, 0x0a];
let stream = concat(&[
avc_aud(),
avc_idr_first(),
filler.clone(),
eos,
avc_aud(),
avc_p_first(),
filler,
]);
let aus = split_access_units(NalCodec::Avc, &stream);
assert_eq!(aus.len(), 2, "filler/EOS must not open an access unit");
assert_eq!(concat(&aus), stream, "concatenation stays byte-exact");
for au in &aus {
assert!(
contains_nal_type(au, NalCodec::Avc, &[1, 5]),
"each AU must contain a VCL NAL"
);
}
}
#[test]
fn hevc_suffix_nals_trail_their_picture() {
let aud = vec![0x00u8, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50];
let idr = vec![0x00u8, 0x00, 0x01, 0x26, 0x01, 0x80]; let p = vec![0x00u8, 0x00, 0x01, 0x02, 0x01, 0x00]; let suffix_sei = vec![0x00u8, 0x00, 0x00, 0x01, 0x50, 0x01, 0xaa];
let fd = vec![0x00u8, 0x00, 0x00, 0x01, 0x4c, 0x01, 0xff];
let prefix_sei = vec![0x00u8, 0x00, 0x00, 0x01, 0x4e, 0x01, 0xbb];
let stream = concat(&[aud.clone(), idr, suffix_sei, fd, prefix_sei, p, aud]);
let aus = split_access_units(NalCodec::Hevc, &stream);
assert_eq!(aus.len(), 3, "only the prefix SEI starts the second AU");
assert_eq!(concat(&aus), stream);
let first = &aus[0];
assert_eq!(
&first[first.len() - 14..],
&[
0x00, 0x00, 0x00, 0x01, 0x50, 0x01, 0xaa, 0x00, 0x00, 0x00, 0x01, 0x4c, 0x01, 0xff, ][..]
);
assert!(contains_nal_type(first, NalCodec::Hevc, &[38, 40]));
assert!(contains_nal_type(&aus[1], NalCodec::Hevc, &[2, 39]));
}
fn contains_nal_type(annexb: &[u8], codec: NalCodec, types: &[u8]) -> bool {
for (i, w) in annexb.windows(3).enumerate() {
if w == [0x00, 0x00, 0x01] {
let body = &annexb[i + 3..];
if let Some(t) = nal_unit_type(codec, body)
&& types.contains(&t)
{
return true;
}
}
}
false
}
#[test]
fn chunked_pushes_keep_au_boundaries_after_a_drain() {
let mut stream = Vec::new();
for i in 0..4u8 {
stream.extend_from_slice(&avc_aud());
stream.extend_from_slice(&avc_idr_first());
let mut filler = alloc::vec![0x00, 0x00, 0x00, 0x01, 0x0c]; filler.extend_from_slice(&alloc::vec![0xffu8; 3000 + i as usize]);
stream.extend_from_slice(&filler);
}
let whole = split_access_units(NalCodec::Avc, &stream);
assert_eq!(whole.len(), 4, "four AUD-led access units");
for chunk_size in [1usize, 2, 3, 7, 64, 511, 512, 513, 1316] {
let mut s = AccessUnitSplitter::new(NalCodec::Avc);
let mut chunked = Vec::new();
for chunk in stream.chunks(chunk_size) {
s.push(chunk).unwrap();
while let Some(au) = s.pop() {
chunked.push(au);
}
}
s.finish();
while let Some(au) = s.pop() {
chunked.push(au);
}
assert_eq!(
chunked, whole,
"chunk size {chunk_size}: AU boundaries must match the one-shot split"
);
assert_eq!(concat(&chunked), stream, "chunk size {chunk_size}");
}
}
#[test]
fn scan_cursor_does_not_change_the_result() {
let mut big = avc_idr_first();
big.extend_from_slice(&vec![0x55u8; 200_000]);
let stream = concat(&[avc_aud(), big, avc_p_first(), avc_aud()]);
let whole = split_access_units(NalCodec::Avc, &stream);
let mut s = AccessUnitSplitter::new(NalCodec::Avc);
let mut chunked = Vec::new();
for chunk in stream.chunks(1316) {
s.push(chunk).unwrap();
while let Some(au) = s.pop() {
chunked.push(au);
}
}
s.finish();
while let Some(au) = s.pop() {
chunked.push(au);
}
assert_eq!(chunked, whole);
assert_eq!(concat(&chunked), stream);
}
#[test]
fn total_bytes_scanned_is_linear_in_the_stream_length() {
let mut big = avc_idr_first();
big.extend_from_slice(&vec![0x55u8; 200_000]);
let stream = concat(&[avc_aud(), big, avc_p_first(), avc_aud()]);
let mut s = AccessUnitSplitter::new(NalCodec::Avc);
for chunk in stream.chunks(1316) {
s.push(chunk).unwrap();
while s.pop().is_some() {}
}
s.finish();
assert!(
s.scanned_bytes <= 2 * stream.len(),
"scan walked {} bytes for a {}-byte stream; must stay O(n)",
s.scanned_bytes,
stream.len()
);
let mut s2 = AccessUnitSplitter::new(NalCodec::Avc);
let mut chunked = Vec::new();
for chunk in stream.chunks(1316) {
s2.push(chunk).unwrap();
while let Some(au) = s2.pop() {
chunked.push(au);
}
}
s2.finish();
while let Some(au) = s2.pop() {
chunked.push(au);
}
assert_eq!(chunked, split_access_units(NalCodec::Avc, &stream));
}
#[test]
fn an_unterminated_nal_is_rejected_at_the_cap() {
let mut s = AccessUnitSplitter::new(NalCodec::Avc);
s.push(&avc_aud()).unwrap();
let chunk = vec![0x55u8; 1 << 20];
let mut err = None;
for _ in 0..(MAX_OPEN_NAL_BYTES / chunk.len() + 4) {
if let Err(e) = s.push(&chunk) {
err = Some(e);
break;
}
}
let err = err.expect("an unterminated NAL must eventually be rejected");
assert!(
matches!(
err,
Error::InvalidValue {
field: "Annex B NAL",
..
}
),
"expected the buffered-NAL cap error, got {err:?}"
);
let mut ok = Vec::new();
for _ in 0..64 {
ok.extend_from_slice(&avc_aud());
ok.extend_from_slice(&avc_idr_first());
let mut filler = alloc::vec![0x00, 0x00, 0x00, 0x01, 0x0c];
filler.extend_from_slice(&alloc::vec![0xffu8; 2000]);
ok.extend_from_slice(&filler);
}
let mut s2 = AccessUnitSplitter::new(NalCodec::Avc);
for chunk in ok.chunks(4096) {
s2.push(chunk)
.expect("conformant stream must not be rejected");
}
s2.finish();
let mut n = 0;
while s2.pop().is_some() {
n += 1;
}
assert_eq!(n, 64);
}
}