#![allow(clippy::match_same_arms)]
#![allow(clippy::struct_excessive_bools)]
#![allow(clippy::field_reassign_with_default)]
#![allow(clippy::if_not_else)]
use crate::error::{CodecError, CodecResult};
use oximedia_io::BitReader;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Vp9FrameType {
#[default]
Key = 0,
Inter = 1,
}
impl Vp9FrameType {
#[must_use]
pub const fn is_keyframe(&self) -> bool {
matches!(self, Self::Key)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ColorSpace {
#[default]
Unknown = 0,
Bt601 = 1,
Bt709 = 2,
Smpte170 = 3,
Smpte240 = 4,
Bt2020 = 5,
Reserved = 6,
Srgb = 7,
}
impl From<u8> for ColorSpace {
fn from(value: u8) -> Self {
match value {
0 => Self::Unknown,
1 => Self::Bt601,
2 => Self::Bt709,
3 => Self::Smpte170,
4 => Self::Smpte240,
5 => Self::Bt2020,
6 => Self::Reserved,
7 => Self::Srgb,
_ => Self::Unknown,
}
}
}
impl ColorSpace {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Unknown => "unknown",
Self::Bt601 => "bt601",
Self::Bt709 => "bt709",
Self::Smpte170 => "smpte170",
Self::Smpte240 => "smpte240",
Self::Bt2020 => "bt2020",
Self::Reserved => "reserved",
Self::Srgb => "srgb",
}
}
}
#[derive(Clone, Debug, Default)]
pub struct UncompressedHeader {
pub frame_marker: u8,
pub profile: u8,
pub show_existing_frame: bool,
pub frame_to_show: u8,
pub frame_type: Vp9FrameType,
pub show_frame: bool,
pub error_resilient: bool,
pub width: u32,
pub height: u32,
pub render_width: u32,
pub render_height: u32,
pub intra_only: bool,
pub reset_frame_context: u8,
pub refresh_frame_flags: u8,
pub ref_frame_idx: [u8; 3],
pub ref_frame_sign_bias: [bool; 4],
pub allow_high_precision_mv: bool,
pub interp_filter: u8,
pub color_space: ColorSpace,
pub color_range: bool,
pub subsampling_x: bool,
pub subsampling_y: bool,
pub bit_depth: u8,
}
impl UncompressedHeader {
const SYNC_BYTES: [u8; 3] = [0x49, 0x83, 0x42];
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
pub fn parse(data: &[u8]) -> CodecResult<Self> {
let mut reader = BitReader::new(data);
let mut header = Self::default();
header.frame_marker = reader.read_bits(2).map_err(CodecError::Core)? as u8;
if header.frame_marker != 0b10 {
return Err(CodecError::InvalidBitstream(
"Invalid VP9 frame marker".into(),
));
}
let profile_low = reader.read_bit().map_err(CodecError::Core)?;
let profile_high = reader.read_bit().map_err(CodecError::Core)?;
header.profile = (profile_high << 1) | profile_low;
if header.profile == 3 {
let reserved = reader.read_bit().map_err(CodecError::Core)?;
if reserved != 0 {
return Err(CodecError::InvalidBitstream("Reserved bit not zero".into()));
}
}
header.show_existing_frame = reader.read_bit().map_err(CodecError::Core)? != 0;
if header.show_existing_frame {
header.frame_to_show = reader.read_bits(3).map_err(CodecError::Core)? as u8;
return Ok(header);
}
header.frame_type = if reader.read_bit().map_err(CodecError::Core)? != 0 {
Vp9FrameType::Inter
} else {
Vp9FrameType::Key
};
header.show_frame = reader.read_bit().map_err(CodecError::Core)? != 0;
header.error_resilient = reader.read_bit().map_err(CodecError::Core)? != 0;
if header.frame_type == Vp9FrameType::Key {
Self::parse_sync_bytes(&mut reader)?;
Self::parse_color_config(&mut reader, &mut header)?;
Self::parse_frame_size(&mut reader, &mut header)?;
Self::parse_render_size(&mut reader, &mut header)?;
header.refresh_frame_flags = 0xFF;
} else {
if !header.show_frame {
header.intra_only = reader.read_bit().map_err(CodecError::Core)? != 0;
}
if !header.error_resilient {
header.reset_frame_context = reader.read_bits(2).map_err(CodecError::Core)? as u8;
}
if header.intra_only {
Self::parse_sync_bytes(&mut reader)?;
if header.profile > 0 {
Self::parse_color_config(&mut reader, &mut header)?;
} else {
header.color_space = ColorSpace::Bt601;
header.subsampling_x = true;
header.subsampling_y = true;
header.bit_depth = 8;
}
header.refresh_frame_flags = reader.read_bits(8).map_err(CodecError::Core)? as u8;
Self::parse_frame_size(&mut reader, &mut header)?;
Self::parse_render_size(&mut reader, &mut header)?;
} else {
header.refresh_frame_flags = reader.read_bits(8).map_err(CodecError::Core)? as u8;
for i in 0..3 {
header.ref_frame_idx[i] = reader.read_bits(3).map_err(CodecError::Core)? as u8;
header.ref_frame_sign_bias[i + 1] =
reader.read_bit().map_err(CodecError::Core)? != 0;
}
let found_ref = Self::parse_frame_size_with_refs(&mut reader, &mut header)?;
if !found_ref {
Self::parse_frame_size(&mut reader, &mut header)?;
}
Self::parse_render_size(&mut reader, &mut header)?;
header.allow_high_precision_mv = reader.read_bit().map_err(CodecError::Core)? != 0;
Self::parse_interp_filter(&mut reader, &mut header)?;
}
}
Ok(header)
}
#[allow(clippy::cast_possible_truncation)]
fn parse_sync_bytes(reader: &mut BitReader<'_>) -> CodecResult<()> {
for expected in Self::SYNC_BYTES {
let byte = reader.read_bits(8).map_err(CodecError::Core)? as u8;
if byte != expected {
return Err(CodecError::InvalidBitstream(
"Invalid VP9 sync bytes".into(),
));
}
}
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
fn parse_color_config(reader: &mut BitReader<'_>, header: &mut Self) -> CodecResult<()> {
if header.profile >= 2 {
header.bit_depth = if reader.read_bit().map_err(CodecError::Core)? != 0 {
12
} else {
10
};
} else {
header.bit_depth = 8;
}
header.color_space = ColorSpace::from(reader.read_bits(3).map_err(CodecError::Core)? as u8);
if header.color_space != ColorSpace::Srgb {
header.color_range = reader.read_bit().map_err(CodecError::Core)? != 0;
if header.profile == 1 || header.profile == 3 {
header.subsampling_x = reader.read_bit().map_err(CodecError::Core)? != 0;
header.subsampling_y = reader.read_bit().map_err(CodecError::Core)? != 0;
reader.read_bit().map_err(CodecError::Core)?;
} else {
header.subsampling_x = true;
header.subsampling_y = true;
}
} else {
header.color_range = true;
if header.profile == 1 || header.profile == 3 {
header.subsampling_x = false;
header.subsampling_y = false;
reader.read_bit().map_err(CodecError::Core)?;
}
}
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
fn parse_frame_size(reader: &mut BitReader<'_>, header: &mut Self) -> CodecResult<()> {
header.width = reader.read_bits(16).map_err(CodecError::Core)? as u32 + 1;
header.height = reader.read_bits(16).map_err(CodecError::Core)? as u32 + 1;
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
fn parse_render_size(reader: &mut BitReader<'_>, header: &mut Self) -> CodecResult<()> {
let different = reader.read_bit().map_err(CodecError::Core)? != 0;
if different {
header.render_width = reader.read_bits(16).map_err(CodecError::Core)? as u32 + 1;
header.render_height = reader.read_bits(16).map_err(CodecError::Core)? as u32 + 1;
} else {
header.render_width = header.width;
header.render_height = header.height;
}
Ok(())
}
fn parse_frame_size_with_refs(
reader: &mut BitReader<'_>,
_header: &mut Self,
) -> CodecResult<bool> {
for _ in 0..3 {
if reader.read_bit().map_err(CodecError::Core)? != 0 {
return Ok(true);
}
}
Ok(false)
}
#[allow(clippy::cast_possible_truncation)]
fn parse_interp_filter(reader: &mut BitReader<'_>, header: &mut Self) -> CodecResult<()> {
let switchable = reader.read_bit().map_err(CodecError::Core)? != 0;
header.interp_filter = if switchable {
4
} else {
reader.read_bits(2).map_err(CodecError::Core)? as u8
};
Ok(())
}
#[must_use]
pub fn is_keyframe(&self) -> bool {
self.frame_type == Vp9FrameType::Key
}
#[must_use]
pub fn is_intra_only(&self) -> bool {
self.frame_type == Vp9FrameType::Key || self.intra_only
}
#[must_use]
pub const fn chroma_subsampling(&self) -> (u8, u8) {
let x = if self.subsampling_x { 2 } else { 1 };
let y = if self.subsampling_y { 2 } else { 1 };
(x, y)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_space_from() {
assert_eq!(ColorSpace::from(2), ColorSpace::Bt709);
assert_eq!(ColorSpace::from(7), ColorSpace::Srgb);
}
#[test]
fn test_vp9_frame_type() {
assert!(Vp9FrameType::Key.is_keyframe());
assert!(!Vp9FrameType::Inter.is_keyframe());
}
#[test]
fn test_invalid_frame_marker() {
let data = [0x00];
assert!(UncompressedHeader::parse(&data).is_err());
}
#[test]
fn test_chroma_subsampling() {
let mut header = UncompressedHeader::default();
header.subsampling_x = true;
header.subsampling_y = true;
assert_eq!(header.chroma_subsampling(), (2, 2));
}
}