use std::fmt;
use serde::{Deserialize, Serialize};
use crate::{Container, VideoCodec};
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum BitDepth {
#[default]
#[serde(rename = "8")]
Eight,
#[serde(rename = "10")]
Ten,
}
impl BitDepth {
#[must_use]
pub const fn bits(self) -> u8 {
match self {
Self::Eight => 8,
Self::Ten => 10,
}
}
#[must_use]
pub const fn from_bits(bits: u8) -> Option<Self> {
match bits {
8 => Some(Self::Eight),
10 => Some(Self::Ten),
_ => None,
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct OutputPixels {
pub bit_depth: Option<BitDepth>,
pub alpha: bool,
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum Chroma {
Yuv420,
Yuv422,
Yuv444,
}
impl Chroma {
#[must_use]
pub const fn subsampling(self) -> (u32, u32) {
match self {
Self::Yuv420 => (2, 2),
Self::Yuv422 => (2, 1),
Self::Yuv444 => (1, 1),
}
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum PlaneLayout {
Planar,
P010,
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub struct PixelLayout {
pub chroma: Chroma,
pub bit_depth: BitDepth,
pub alpha: bool,
pub planes: PlaneLayout,
}
impl PixelLayout {
pub const YUV420P: Self = Self {
chroma: Chroma::Yuv420,
bit_depth: BitDepth::Eight,
alpha: false,
planes: PlaneLayout::Planar,
};
#[must_use]
pub fn is_yuv420p(self) -> bool {
self == Self::YUV420P
}
#[must_use]
pub const fn as_p010(self) -> Self {
match (self.chroma, self.bit_depth, self.alpha) {
(Chroma::Yuv420, BitDepth::Ten, false) => Self {
planes: PlaneLayout::P010,
..self
},
_ => self,
}
}
#[must_use]
pub const fn ffmpeg_name(self) -> &'static str {
match (self.planes, self.chroma, self.bit_depth, self.alpha) {
(PlaneLayout::P010, ..) => "p010le",
(PlaneLayout::Planar, Chroma::Yuv420, BitDepth::Eight, false) => "yuv420p",
(PlaneLayout::Planar, Chroma::Yuv420, BitDepth::Eight, true) => "yuva420p",
(PlaneLayout::Planar, Chroma::Yuv420, BitDepth::Ten, false) => "yuv420p10le",
(PlaneLayout::Planar, Chroma::Yuv420, BitDepth::Ten, true) => "yuva420p10le",
(PlaneLayout::Planar, Chroma::Yuv422, BitDepth::Eight, false) => "yuv422p",
(PlaneLayout::Planar, Chroma::Yuv422, BitDepth::Eight, true) => "yuva422p",
(PlaneLayout::Planar, Chroma::Yuv422, BitDepth::Ten, false) => "yuv422p10le",
(PlaneLayout::Planar, Chroma::Yuv422, BitDepth::Ten, true) => "yuva422p10le",
(PlaneLayout::Planar, Chroma::Yuv444, BitDepth::Eight, false) => "yuv444p",
(PlaneLayout::Planar, Chroma::Yuv444, BitDepth::Eight, true) => "yuva444p",
(PlaneLayout::Planar, Chroma::Yuv444, BitDepth::Ten, false) => "yuv444p10le",
(PlaneLayout::Planar, Chroma::Yuv444, BitDepth::Ten, true) => "yuva444p10le",
}
}
#[must_use]
pub fn from_ffmpeg_name(name: &str) -> Option<Self> {
let chromas = [Chroma::Yuv420, Chroma::Yuv422, Chroma::Yuv444];
let depths = [BitDepth::Eight, BitDepth::Ten];
chromas
.into_iter()
.flat_map(|chroma| depths.into_iter().map(move |bit_depth| (chroma, bit_depth)))
.flat_map(|(chroma, bit_depth)| {
[false, true].map(|alpha| Self {
chroma,
bit_depth,
alpha,
planes: PlaneLayout::Planar,
})
})
.flat_map(|layout| [layout, layout.as_p010()])
.find(|layout| layout.ffmpeg_name() == name)
}
#[must_use]
pub fn planes(self, width: u32, height: u32) -> Vec<(u32, u32, u32)> {
let (x, y) = self.chroma.subsampling();
let (chroma_width, chroma_height) = (width.div_ceil(x), height.div_ceil(y));
let bytes = match self.bit_depth {
BitDepth::Eight => 1,
BitDepth::Ten => 2,
};
let mut planes = vec![(width, height, bytes)];
match self.planes {
PlaneLayout::Planar => {
planes.push((chroma_width, chroma_height, bytes));
planes.push((chroma_width, chroma_height, bytes));
}
PlaneLayout::P010 => planes.push((chroma_width * 2, chroma_height, 2)),
}
if self.alpha {
planes.push((width, height, bytes));
}
planes
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OutputPixelsError {
AlphaUnsupported {
codec: VideoCodec,
},
AlphaUnsupportedInContainer {
codec: VideoCodec,
container: Container,
},
BitDepthUnsupported {
codec: VideoCodec,
bit_depth: BitDepth,
alpha: bool,
},
}
impl fmt::Display for OutputPixelsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AlphaUnsupported { codec } => write!(
formatter,
"video codec {} cannot keep alpha; use vp9 (webm, mkv) or prores (mov)",
codec.as_str()
),
Self::AlphaUnsupportedInContainer { codec, container } => write!(
formatter,
"{container} cannot carry {} alpha; use webm or mkv",
codec.as_str()
),
Self::BitDepthUnsupported {
codec,
bit_depth,
alpha,
} => write!(
formatter,
"video codec {} cannot be written at {} bits{}",
codec.as_str(),
bit_depth.bits(),
if *alpha { " with alpha" } else { "" }
),
}
}
}
impl std::error::Error for OutputPixelsError {}
pub fn resolve_pixels(
codec: VideoCodec,
container: Container,
pixels: OutputPixels,
) -> Result<PixelLayout, OutputPixelsError> {
let default_depth = match codec {
VideoCodec::ProRes => BitDepth::Ten,
_ => BitDepth::Eight,
};
let bit_depth = pixels.bit_depth.unwrap_or(default_depth);
let alpha = pixels.alpha;
let refuse_depth = Err(OutputPixelsError::BitDepthUnsupported {
codec,
bit_depth,
alpha,
});
let chroma = match codec {
VideoCodec::H264 | VideoCodec::Mpeg4Part2 | VideoCodec::Hevc | VideoCodec::Av1 => {
if alpha {
return Err(OutputPixelsError::AlphaUnsupported { codec });
}
let ten_bits = matches!(codec, VideoCodec::Hevc | VideoCodec::Av1);
if bit_depth == BitDepth::Ten && !ten_bits {
return refuse_depth;
}
Chroma::Yuv420
}
VideoCodec::Vp9 => {
if alpha && container == Container::Mp4 {
return Err(OutputPixelsError::AlphaUnsupportedInContainer { codec, container });
}
if alpha && bit_depth == BitDepth::Ten {
return refuse_depth;
}
Chroma::Yuv420
}
VideoCodec::ProRes => {
if bit_depth == BitDepth::Eight {
return refuse_depth;
}
if alpha {
Chroma::Yuv444
} else {
Chroma::Yuv422
}
}
};
Ok(PixelLayout {
chroma,
bit_depth,
alpha,
planes: PlaneLayout::Planar,
})
}
#[cfg(test)]
#[path = "pixels_tests.rs"]
mod tests;