use std::io;
use std::io::{Cursor, SeekFrom};
use scuffle_av1::seq::SequenceHeaderObu;
use scuffle_av1::{ObuHeader, ObuType};
use scuffle_bytes_util::BitReader;
use crate::{ChromaSubsamplingModes, DetectGopStartError, GopStartDetection, VideoEncodingDetails};
pub fn detect_av1_keyframe_start(data: &[u8]) -> Result<GopStartDetection, DetectGopStartError> {
let mut keyframe_found = false;
let mut video_encoding_details: Option<VideoEncodingDetails> = None;
let mut cursor = Cursor::new(data);
loop {
let Ok(header) = ObuHeader::parse(&mut cursor) else {
if keyframe_found && video_encoding_details.is_some() {
break;
}
return Ok(GopStartDetection::NotStartOfGop);
};
let obu_size = header.size.unwrap_or_default();
match header.obu_type {
ObuType::SequenceHeader => {
let seq = SequenceHeaderObu::parse(header, &mut cursor)
.map_err(DetectGopStartError::Av1ParserError)?;
let profile = seq.seq_profile;
let bit_depth = seq.color_config.bit_depth as u8;
let (level, tier) = seq
.operating_points
.first()
.map(|op| (op.seq_level_idx, if op.seq_tier { "H" } else { "M" }))
.unwrap_or((1, "M"));
video_encoding_details = Some(VideoEncodingDetails {
codec_string: format!("av01.{profile}.{level:02}{tier}.{bit_depth:02}"),
coded_dimensions: [seq.max_frame_width as u16, seq.max_frame_height as u16],
bit_depth: Some(bit_depth),
chroma_subsampling: Some(chroma_mode_from_color_config(&seq.color_config)),
stsd: None,
});
continue;
}
ObuType::Frame | ObuType::FrameHeader
if is_keyframe(&mut cursor).map_err(DetectGopStartError::Av1ParserError)? =>
{
keyframe_found = true;
}
_ => {
}
}
skip_obu(&mut cursor, obu_size).map_err(DetectGopStartError::Av1ParserError)?;
}
if keyframe_found && let Some(details) = video_encoding_details {
Ok(GopStartDetection::StartOfGop(details))
} else {
Ok(GopStartDetection::NotStartOfGop)
}
}
fn skip_obu<R: io::Read + io::Seek>(reader: &mut R, obu_size: u64) -> io::Result<()> {
let offset = i64::try_from(obu_size).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("payload size exceeds seek limits: {err}"),
)
})?;
reader.seek(SeekFrom::Current(offset))?;
Ok(())
}
#[inline]
fn is_keyframe<R: io::Read>(reader: &mut R) -> io::Result<bool> {
let mut reader = BitReader::new(reader);
let show_existing_frame = reader.read_bit()?;
if show_existing_frame {
return Ok(false);
}
let frame_type = reader.read_bits(2)?;
Ok(frame_type == 0)
}
#[inline]
fn chroma_mode_from_color_config(config: &scuffle_av1::seq::ColorConfig) -> ChromaSubsamplingModes {
let subsampling_x = config.subsampling_x;
let subsampling_y = config.subsampling_y;
if config.mono_chrome {
ChromaSubsamplingModes::Monochrome
} else if !subsampling_x && !subsampling_y {
ChromaSubsamplingModes::Yuv444
} else if subsampling_x && !subsampling_y {
ChromaSubsamplingModes::Yuv422
} else {
ChromaSubsamplingModes::Yuv420
}
}
pub const AV1_TEST_KEYFRAME: &[u8] = &[
0x12, 0x00, 0x0A, 0x0A, 0x00, 0x00, 0x00, 0x02, 0xAF, 0xFF, 0x9F, 0xFF, 0x30, 0x08, 0x32, 0x14,
0x10, 0x00, 0xC0, 0x00, 0x00, 0x02, 0x80, 0x00, 0x00, 0x0A, 0x05, 0x76, 0xA4, 0xD6, 0x2F, 0x1F,
0xFA, 0x1E, 0x3C, 0xD8,
];
pub const AV1_TEST_INTER_FRAME: &[u8] = &[
0x12, 0x00, 0x32, 0x12, 0x30, 0x03, 0xC0, 0x80, 0x00, 0x00, 0x06, 0xC0, 0x00, 0x00, 0x02, 0x80,
0x00, 0x00, 0x80, 0x00, 0x99, 0x10,
];
#[cfg(test)]
mod test {
use super::{GopStartDetection, detect_av1_keyframe_start};
#[test]
fn test_detect_av1_keyframe_start() {
let result = detect_av1_keyframe_start(super::AV1_TEST_KEYFRAME);
match result {
Ok(GopStartDetection::StartOfGop(details)) => {
assert_eq!(details.codec_string, "av01.0.00M.08");
assert_eq!(details.coded_dimensions, [64, 64]);
assert_eq!(details.bit_depth, Some(8));
}
Err(err) => panic!("Failed to parse valid AV1 data: {err}"),
Ok(GopStartDetection::NotStartOfGop) => {
panic!("Expected to detect GOP start but got `NotStartOfGop`")
}
}
}
#[test]
fn test_detect_av1_empty_data() {
let result = detect_av1_keyframe_start(&[]);
assert!(matches!(result, Ok(GopStartDetection::NotStartOfGop)));
}
#[test]
fn test_detect_av1_invalid_data() {
let invalid_data = &[0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
let result = detect_av1_keyframe_start(invalid_data);
assert!(matches!(result, Ok(GopStartDetection::NotStartOfGop)));
}
#[test]
fn test_detect_av1_non_keyframe() {
let result = detect_av1_keyframe_start(super::AV1_TEST_INTER_FRAME);
match result {
Ok(GopStartDetection::StartOfGop(_)) => {
panic!("Should not detect GOP start without keyframe");
}
Err(_) | Ok(GopStartDetection::NotStartOfGop) => {
}
}
}
}