use crate::color::ColorSpace;
use crate::error::{Error, Result};
use crate::frame::Frame;
use crate::identify::Encoding;
use crate::media::{Packet, TrackInfo};
pub trait Decoder: Send {
fn encoding(&self) -> Encoding;
fn decode(&mut self, packet: &Packet) -> Result<Option<Frame>>;
fn flush(&mut self) -> Result<Vec<Frame>>;
fn reset(&mut self) -> Result<()>;
fn is_hardware(&self) -> bool;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Backend {
VideoToolbox,
MediaFoundation,
MediaCodec,
VaApi,
Dav1d,
LibVpx,
}
impl Backend {
pub fn is_hardware(self) -> bool {
!matches!(self, Backend::Dav1d | Backend::LibVpx)
}
pub fn feature(self) -> &'static str {
match self {
Backend::VideoToolbox
| Backend::MediaFoundation
| Backend::MediaCodec
| Backend::VaApi => "decode-platform",
Backend::Dav1d => "decode-av1",
Backend::LibVpx => "decode-vp9",
}
}
pub fn handles(self, encoding: Encoding) -> bool {
match self {
Backend::VideoToolbox | Backend::MediaFoundation | Backend::MediaCodec => matches!(
encoding,
Encoding::H264 | Encoding::H265 | Encoding::Av1 | Encoding::Vp9
),
Backend::VaApi => matches!(
encoding,
Encoding::H264 | Encoding::H265 | Encoding::Av1 | Encoding::Vp9 | Encoding::Vp8
),
Backend::Dav1d => encoding == Encoding::Av1,
Backend::LibVpx => matches!(encoding, Encoding::Vp9 | Encoding::Vp8),
}
}
pub fn is_available(self) -> bool {
let compiled = match self.feature() {
"decode-platform" => cfg!(feature = "decode-platform"),
"decode-av1" => cfg!(feature = "decode-av1"),
"decode-vp9" => cfg!(feature = "decode-vp9"),
_ => false,
};
compiled && self.exists_here()
}
fn exists_here(self) -> bool {
match self {
Backend::VideoToolbox => cfg!(target_vendor = "apple"),
Backend::MediaFoundation => cfg!(windows),
Backend::MediaCodec => cfg!(target_os = "android"),
Backend::VaApi => cfg!(all(
unix,
not(target_vendor = "apple"),
not(target_os = "android")
)),
Backend::Dav1d | Backend::LibVpx => true,
}
}
}
impl std::fmt::Display for Backend {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Backend::VideoToolbox => "VideoToolbox",
Backend::MediaFoundation => "Media Foundation",
Backend::MediaCodec => "MediaCodec",
Backend::VaApi => "VA-API",
Backend::Dav1d => "dav1d",
Backend::LibVpx => "libvpx",
};
formatter.write_str(name)
}
}
pub const BACKENDS: [Backend; 6] = [
Backend::VideoToolbox,
Backend::MediaFoundation,
Backend::MediaCodec,
Backend::VaApi,
Backend::Dav1d,
Backend::LibVpx,
];
#[derive(Clone, Debug)]
pub struct DecoderConfig {
pub encoding: Encoding,
pub width: u32,
pub height: u32,
pub bit_depth: u32,
pub color: ColorSpace,
pub extra_data: Vec<u8>,
}
impl DecoderConfig {
pub fn from_track(track: &TrackInfo) -> Result<Self> {
let encoding = track.encoding.ok_or_else(|| {
Error::unsupported(format!(
"track {} is {:?}, which vtome does not recognise as a video encoding",
track.id, track.codec_id
))
})?;
Ok(DecoderConfig {
encoding,
width: track.width,
height: track.height,
bit_depth: track.bit_depth,
color: track.color,
extra_data: track.extra_data.clone(),
})
}
}
pub fn backends_for(encoding: Encoding) -> Vec<Backend> {
BACKENDS
.into_iter()
.filter(|backend| backend.handles(encoding) && backend.is_available())
.collect()
}
pub fn open(config: &DecoderConfig) -> Result<Box<dyn Decoder>> {
let candidates = backends_for(config.encoding);
let Some(backend) = candidates.first().copied() else {
return Err(Error::NoDecoder {
encoding: config.encoding,
remedy: remedy_for(config.encoding),
});
};
Err(Error::NoDecoder {
encoding: config.encoding,
remedy: format!(
"{backend} would take this and is not implemented yet \
(planning/TODO.md §2); nothing decodes {} in this build",
config.encoding
),
})
}
fn remedy_for(encoding: Encoding) -> String {
let missing: Vec<Backend> = BACKENDS
.into_iter()
.filter(|backend| backend.handles(encoding) && backend.exists_here())
.collect();
if missing.is_empty() {
return format!(
"nothing on this platform decodes {encoding}, and vtome has no software \
fallback for it — transcode to AV1 first"
);
}
let features: Vec<&str> = {
let mut features: Vec<&str> = missing.iter().map(|backend| backend.feature()).collect();
features.sort_unstable();
features.dedup();
features
};
format!(
"enable the {} feature{} ({} would handle it here)",
features.join(" or "),
if features.len() == 1 { "" } else { "s" },
missing
.iter()
.map(Backend::to_string)
.collect::<Vec<_>>()
.join(", ")
)
}
#[cfg(test)]
mod tests {
use super::*;
fn config(encoding: Encoding) -> DecoderConfig {
DecoderConfig {
encoding,
width: 1920,
height: 1080,
bit_depth: 8,
color: ColorSpace::default(),
extra_data: Vec::new(),
}
}
#[test]
fn the_platform_backends_are_mutually_exclusive() {
let platform = [
Backend::VideoToolbox,
Backend::MediaFoundation,
Backend::MediaCodec,
Backend::VaApi,
]
.into_iter()
.filter(|backend| backend.exists_here())
.count();
assert!(platform <= 1, "{platform} platform decoders on one machine");
assert!(Backend::Dav1d.exists_here() && Backend::LibVpx.exists_here());
}
#[test]
fn hardware_backends_are_marked_as_such() {
assert!(Backend::VideoToolbox.is_hardware());
assert!(!Backend::Dav1d.is_hardware());
}
#[test]
fn only_the_platform_backends_offer_the_patented_encodings() {
assert!(!Backend::Dav1d.handles(Encoding::H264));
assert!(!Backend::LibVpx.handles(Encoding::H265));
assert!(Backend::VideoToolbox.handles(Encoding::H264));
}
#[test]
fn a_missing_decoder_names_the_feature_that_would_have_supplied_it() {
let Err(Error::NoDecoder { remedy, .. }) = open(&config(Encoding::Av1)) else {
panic!("no AV1 decoder is implemented yet");
};
assert!(
remedy.contains("dav1d") || remedy.contains("decode-av1") || remedy.contains("§2"),
"{remedy}"
);
}
#[test]
fn an_encoding_nothing_here_can_decode_says_to_transcode_instead() {
let remedy = remedy_for(Encoding::Theora);
assert!(remedy.contains("transcode"), "{remedy}");
}
#[test]
fn a_track_with_no_recognised_encoding_is_a_different_error_from_no_decoder() {
let track = TrackInfo {
id: 3,
kind: crate::media::TrackKind::Video,
encoding: None,
codec_id: "V_SOMETHING_ELSE".to_string(),
width: 320,
height: 240,
frame_rate: None,
bit_depth: 8,
color: ColorSpace::default(),
rotation: Default::default(),
duration: std::time::Duration::ZERO,
extra_data: Vec::new(),
};
assert!(matches!(
DecoderConfig::from_track(&track),
Err(Error::Unsupported { .. })
));
}
#[test]
fn nothing_is_available_without_its_feature() {
if !cfg!(any(
feature = "decode-av1",
feature = "decode-vp9",
feature = "decode-platform"
)) {
assert!(backends_for(Encoding::Av1).is_empty());
assert!(backends_for(Encoding::H264).is_empty());
}
}
}