use crate::av1::detect_av1_keyframe_start;
use crate::h264::detect_h264_annexb_gop;
use crate::h265::detect_h265_annexb_gop;
use crate::vp8::detect_vp8_gop;
use crate::vp9::detect_vp9_gop;
use crate::{VideoCodec, VideoEncodingDetails};
#[derive(thiserror::Error, Debug)]
pub enum DetectGopStartError {
#[error("Detection not supported for codec: {0:?}")]
UnsupportedCodec(VideoCodec),
#[error("NAL header error: {0:?}")]
NalHeaderError(h264_reader::nal::NalHeaderError),
#[error("AV1 parser error: {0}")]
Av1ParserError(std::io::Error),
#[error("Detected group of picture but failed to extract encoding details: {0:?}")]
FailedToExtractEncodingDetails(String),
}
impl PartialEq<Self> for DetectGopStartError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::UnsupportedCodec(a), Self::UnsupportedCodec(b)) => a == b,
(Self::NalHeaderError(_), Self::NalHeaderError(_)) => true, (Self::FailedToExtractEncodingDetails(a), Self::FailedToExtractEncodingDetails(b)) => {
a == b
}
_ => false,
}
}
}
impl Eq for DetectGopStartError {}
#[derive(Default, PartialEq, Eq, Debug)]
pub enum GopStartDetection {
StartOfGop(VideoEncodingDetails),
#[default]
NotStartOfGop,
}
impl GopStartDetection {
#[inline]
pub fn is_start_of_gop(&self) -> bool {
matches!(self, Self::StartOfGop(_))
}
}
#[inline]
pub fn detect_gop_start(
sample_data: &[u8],
codec: VideoCodec,
) -> Result<GopStartDetection, DetectGopStartError> {
match codec {
VideoCodec::H264 => detect_h264_annexb_gop(sample_data),
VideoCodec::H265 => detect_h265_annexb_gop(sample_data),
VideoCodec::AV1 => detect_av1_keyframe_start(sample_data),
VideoCodec::VP8 => Ok(detect_vp8_gop(sample_data)),
VideoCodec::VP9 => Ok(detect_vp9_gop(sample_data)),
VideoCodec::ImageSequence(codec) => {
let (codec_string, meta) = match codec {
Some(codec) if codec == "image/png" => (codec, png_meta(sample_data)?),
Some(codec) if codec == "image/jpeg" => (codec, jpeg_meta(sample_data)?),
None => guess_image_meta(sample_data)?,
Some(_) => {
return Err(DetectGopStartError::UnsupportedCodec(
VideoCodec::ImageSequence(codec),
));
}
};
Ok(GopStartDetection::StartOfGop(VideoEncodingDetails {
codec_string,
coded_dimensions: meta.coded_dimensions,
bit_depth: meta.bit_depth,
chroma_subsampling: None,
stsd: None,
}))
}
}
}
pub fn is_start_of_gop(sample_data: &[u8], codec: VideoCodec) -> Result<bool, DetectGopStartError> {
Ok(detect_gop_start(sample_data, codec)?.is_start_of_gop())
}
fn guess_image_meta(sample_data: &[u8]) -> Result<(String, ImageMeta), ImageSizeError> {
type MetaFn = fn(&[u8]) -> Result<ImageMeta, ImageSizeError>;
let formats: &[(&str, MetaFn)] = &[("image/png", png_meta), ("image/jpeg", jpeg_meta)];
for &(name, meta_fn) in formats {
match meta_fn(sample_data) {
Ok(meta) => return Ok((name.to_owned(), meta)),
Err(ImageSizeError::WrongFormat(_)) => {}
Err(err) => {
return Err(err);
}
}
}
Err(ImageSizeError::WrongFormat(String::new()))
}
struct ImageMeta {
coded_dimensions: [u16; 2],
bit_depth: Option<u8>,
}
enum ImageSizeError {
WrongFormat(String),
InvalidData(String),
}
impl From<ImageSizeError> for DetectGopStartError {
fn from(err: ImageSizeError) -> Self {
match err {
ImageSizeError::WrongFormat(for_format) => {
Self::FailedToExtractEncodingDetails(match for_format.as_str() {
"" => {
"Image data doesn't match any supported image format (image/png, image/jpeg)".to_owned()
}
_ => {
format!(
"Image didn't match the specified image format '{}'",
for_format.as_str()
)
}
})
}
ImageSizeError::InvalidData(msg) => Self::FailedToExtractEncodingDetails(msg),
}
}
}
fn png_meta(sample_data: &[u8]) -> Result<ImageMeta, ImageSizeError> {
const PNG_MAGIC_BYTES: &[u8] = b"\x89PNG\r\n\x1a\n";
if sample_data.get(..8) != Some(PNG_MAGIC_BYTES) {
return Err(ImageSizeError::WrongFormat("image/png".to_owned()));
}
let convert_size = |e: Option<&[u8]>| {
u32::from_be_bytes(
e.ok_or_else(|| {
ImageSizeError::InvalidData("Invalid PNG data, couldn't extract size".to_owned())
})?
.try_into()
.expect("This is 4 bytes"),
)
.try_into()
.map_err(|_err| ImageSizeError::InvalidData("PNG image dimension too large".to_owned()))
};
let w = convert_size(sample_data.get(16..20))?;
let h = convert_size(sample_data.get(20..24))?;
let bit_depth = sample_data.get(24).copied();
Ok(ImageMeta {
coded_dimensions: [w, h],
bit_depth,
})
}
fn jpeg_meta(data: &[u8]) -> Result<ImageMeta, ImageSizeError> {
const JPEG_MAGIC_BYTES: &[u8] = &[0xFF, 0xD8];
if data.get(..2).is_none_or(|b| b != JPEG_MAGIC_BYTES) {
return Err(ImageSizeError::WrongFormat("image/jpeg".to_owned()));
}
let invalid_data =
|| ImageSizeError::InvalidData("Invalid JPEG data, couldn't extract size".to_owned());
let mut i = 2;
loop {
if *data.get(i).ok_or_else(invalid_data)? != 0xFF {
return Err(invalid_data());
}
while *data.get(i).ok_or_else(invalid_data)? == 0xFF {
i += 1;
}
let tag = *data.get(i).ok_or_else(invalid_data)?;
i += 1;
let len = u16::from_be_bytes([
*data.get(i).ok_or_else(invalid_data)?,
*data.get(i + 1).ok_or_else(invalid_data)?,
]) as usize;
i += 2;
let is_sof = matches!(tag, 0xC0..=0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF);
if is_sof {
let bit_depth = *data.get(i).ok_or_else(invalid_data)?;
let s = data.get(i + 1..i + 5).ok_or_else(invalid_data)?;
let h = u16::from_be_bytes([s[0], s[1]]);
let w = u16::from_be_bytes([s[2], s[3]]);
return Ok(ImageMeta {
coded_dimensions: [w, h],
bit_depth: Some(bit_depth),
});
}
i += len - 2;
}
}