use crate::error::{CodecError, CodecResult};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum FrameType {
#[default]
Key = 0,
Inter = 1,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ColorSpace {
#[default]
Yuv = 0,
Reserved = 1,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ClampingType {
#[default]
Required = 0,
None = 1,
}
#[derive(Clone, Debug, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct FrameHeader {
pub frame_type: FrameType,
pub version: u8,
pub show_frame: bool,
pub first_partition_size: u32,
pub width: u16,
pub horizontal_scale: u8,
pub height: u16,
pub vertical_scale: u8,
pub color_space: ColorSpace,
pub clamping_type: ClampingType,
pub segmentation_enabled: bool,
pub filter_type: u8,
pub loop_filter_level: u8,
pub sharpness_level: u8,
pub mode_ref_lf_delta_enabled: bool,
pub log2_nbr_of_dct_partitions: u8,
pub quant_index: u8,
pub refresh_golden_frame: bool,
pub refresh_alternate_frame: bool,
pub copy_buffer_to_golden: u8,
pub copy_buffer_to_alternate: u8,
pub sign_bias_golden: bool,
pub sign_bias_alternate: bool,
pub refresh_entropy_probs: bool,
pub refresh_last: bool,
}
impl FrameHeader {
const SYNC_CODE: [u8; 3] = [0x9D, 0x01, 0x2A];
#[allow(clippy::cast_possible_truncation)]
pub fn parse(data: &[u8]) -> CodecResult<Self> {
if data.len() < 3 {
return Err(CodecError::InvalidBitstream(
"VP8 frame data too short for header".to_string(),
));
}
let mut header = Self::default();
let b0 = data[0];
let b1 = data[1];
let b2 = data[2];
header.frame_type = if b0 & 0x01 == 0 {
FrameType::Key
} else {
FrameType::Inter
};
header.version = (b0 >> 1) & 0x07;
header.show_frame = (b0 >> 4) & 0x01 != 0;
header.first_partition_size =
(u32::from(b0 >> 5) & 0x07) | (u32::from(b1) << 3) | (u32::from(b2) << 11);
let mut offset = 3;
if header.frame_type == FrameType::Key {
if data.len() < offset + 7 {
return Err(CodecError::InvalidBitstream(
"VP8 keyframe header too short for dimensions".to_string(),
));
}
if data[offset..offset + 3] != Self::SYNC_CODE {
return Err(CodecError::InvalidBitstream(
"Invalid VP8 keyframe sync code".to_string(),
));
}
offset += 3;
let w0 = u16::from(data[offset]);
let w1 = u16::from(data[offset + 1]);
header.width = (w0 | (w1 << 8)) & 0x3FFF;
header.horizontal_scale = (w1 >> 6) as u8;
let h0 = u16::from(data[offset + 2]);
let h1 = u16::from(data[offset + 3]);
header.height = (h0 | (h1 << 8)) & 0x3FFF;
header.vertical_scale = (h1 >> 6) as u8;
header.refresh_golden_frame = true;
header.refresh_alternate_frame = true;
header.refresh_last = true;
}
Ok(header)
}
#[must_use]
pub const fn is_keyframe(&self) -> bool {
matches!(self.frame_type, FrameType::Key)
}
#[must_use]
#[allow(clippy::match_same_arms)]
pub const fn scaled_width(&self) -> u32 {
let (scale, div) = match self.horizontal_scale {
0 => (1, 1), 1 => (5, 4), 2 => (5, 3), 3 => (2, 1), _ => (1, 1), };
(self.width as u32 * scale) / div
}
#[must_use]
#[allow(clippy::match_same_arms)]
pub const fn scaled_height(&self) -> u32 {
let (scale, div) = match self.vertical_scale {
0 => (1, 1), 1 => (5, 4), 2 => (5, 3), 3 => (2, 1), _ => (1, 1), };
(self.height as u32 * scale) / div
}
#[must_use]
#[allow(clippy::manual_div_ceil)]
pub const fn mb_width(&self) -> u32 {
(self.width as u32 + 15) / 16
}
#[must_use]
#[allow(clippy::manual_div_ceil)]
pub const fn mb_height(&self) -> u32 {
(self.height as u32 + 15) / 16
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keyframe_header() {
let data = [
0x00, 0x00, 0x00, 0x9D, 0x01, 0x2A, 0x40, 0x01, 0xF0, 0x00, ];
let header = FrameHeader::parse(&data).expect("should succeed");
assert!(header.is_keyframe());
assert_eq!(header.width, 320);
assert_eq!(header.height, 240);
assert!(header.refresh_golden_frame);
assert!(header.refresh_alternate_frame);
assert!(header.refresh_last);
}
#[test]
fn test_invalid_sync() {
let data = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
assert!(FrameHeader::parse(&data).is_err());
}
#[test]
fn test_inter_frame() {
let data = [
0x01, 0x00, 0x00, ];
let header = FrameHeader::parse(&data).expect("should succeed");
assert!(!header.is_keyframe());
assert_eq!(header.frame_type, FrameType::Inter);
}
#[test]
fn test_scaled_dimensions() {
let mut header = FrameHeader::default();
header.width = 320;
header.height = 240;
header.horizontal_scale = 0;
header.vertical_scale = 0;
assert_eq!(header.scaled_width(), 320);
assert_eq!(header.scaled_height(), 240);
header.horizontal_scale = 3;
header.vertical_scale = 3;
assert_eq!(header.scaled_width(), 640);
assert_eq!(header.scaled_height(), 480);
}
#[test]
fn test_macroblock_dimensions() {
let mut header = FrameHeader::default();
header.width = 320;
header.height = 240;
assert_eq!(header.mb_width(), 20);
assert_eq!(header.mb_height(), 15);
header.width = 321;
header.height = 241;
assert_eq!(header.mb_width(), 21);
assert_eq!(header.mb_height(), 16);
}
#[test]
fn test_too_short_data() {
let data: [u8; 2] = [0x00, 0x00];
assert!(FrameHeader::parse(&data).is_err());
}
#[test]
fn test_keyframe_too_short() {
let data = [
0x00, 0x00, 0x00, 0x9D, 0x01, 0x2A, ];
assert!(FrameHeader::parse(&data).is_err());
}
#[test]
fn test_frame_tag_parsing() {
let data = [
0x14, 0x10, 0x20, 0x9D, 0x01, 0x2A, 0x00, 0x01, 0x00, 0x01,
];
let header = FrameHeader::parse(&data).expect("should succeed");
assert!(header.is_keyframe());
assert!(header.show_frame);
assert_eq!(header.version, 2);
}
}