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, Default, PartialEq, Eq, Hash)]
pub enum Hardware {
#[default]
Prefer,
Require,
Off,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Backend {
VideoToolbox,
MediaFoundation,
MediaCodec,
VaApi,
Dav1d,
}
impl Backend {
pub fn is_hardware(self) -> bool {
self != Backend::Dav1d
}
pub fn feature(self) -> &'static str {
match self {
Backend::VideoToolbox
| Backend::MediaFoundation
| Backend::MediaCodec
| Backend::VaApi => "decode-platform",
Backend::Dav1d => "decode-av1",
}
}
pub fn handles(self, encoding: Encoding) -> bool {
match self {
Backend::VideoToolbox
| Backend::MediaFoundation
| Backend::MediaCodec
| Backend::VaApi => matches!(encoding, Encoding::H264 | Encoding::Av1),
Backend::Dav1d => encoding == Encoding::Av1,
}
}
pub fn is_available(self) -> bool {
let compiled = match self.feature() {
"decode-platform" => cfg!(feature = "decode-platform"),
"decode-av1" => cfg!(feature = "decode-av1"),
_ => 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 => 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",
};
formatter.write_str(name)
}
}
pub const BACKENDS: [Backend; 5] = [
Backend::VideoToolbox,
Backend::MediaFoundation,
Backend::MediaCodec,
Backend::VaApi,
Backend::Dav1d,
];
#[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>> {
open_with(config, Hardware::Prefer)
}
pub fn open_with(config: &DecoderConfig, hardware: Hardware) -> Result<Box<dyn Decoder>> {
if !config.encoding.is_in_scope() {
return Err(Error::NoDecoder {
encoding: config.encoding,
remedy: out_of_scope(config.encoding),
});
}
let candidates: Vec<Backend> = backends_for(config.encoding)
.into_iter()
.filter(|backend| hardware != Hardware::Require || backend.is_hardware())
.collect();
let Some(first) = candidates.first().copied() else {
if hardware == Hardware::Require && !backends_for(config.encoding).is_empty() {
return Err(Error::NoDecoder {
encoding: config.encoding,
remedy: "hardware decoding was required, and only a software decoder for it \
is available here"
.to_string(),
});
}
return Err(Error::NoDecoder {
encoding: config.encoding,
remedy: remedy_for(config.encoding),
});
};
let mut refusal = None;
for backend in candidates {
match instantiate(backend, config, hardware) {
Some(Ok(decoder)) => return Ok(decoder),
Some(Err(error)) => refusal = Some(error),
None => {}
}
}
Err(refusal.unwrap_or_else(|| Error::NoDecoder {
encoding: config.encoding,
remedy: format!(
"{first} would take this and is not implemented yet \
(planning/TODO.md §2); nothing decodes {} in this build",
config.encoding
),
}))
}
#[allow(unused_variables)]
fn instantiate(
backend: Backend,
config: &DecoderConfig,
hardware: Hardware,
) -> Option<Result<Box<dyn Decoder>>> {
match backend {
#[cfg(all(feature = "decode-platform", target_vendor = "apple"))]
Backend::VideoToolbox => Some(
crate::decode_videotoolbox::VideoToolboxDecoder::with_hardware(config, hardware)
.map(|decoder| Box::new(decoder) as Box<dyn Decoder>),
),
#[cfg(all(feature = "decode-platform", windows))]
Backend::MediaFoundation => Some(
crate::media_foundation::MediaFoundationDecoder::new(config, hardware)
.map(|decoder| Box::new(decoder) as Box<dyn Decoder>),
),
#[cfg(all(feature = "decode-platform", target_os = "android"))]
Backend::MediaCodec => Some(
crate::media_codec::MediaCodecDecoder::new(config, hardware)
.map(|decoder| Box::new(decoder) as Box<dyn Decoder>),
),
#[cfg(feature = "decode-av1")]
Backend::Dav1d => Some(
crate::decode_av1::Av1Decoder::new()
.map(|decoder| Box::new(decoder) as Box<dyn Decoder>),
),
_ => None,
}
}
pub(crate) fn out_of_scope(encoding: Encoding) -> String {
format!(
"{encoding} is outside vtome's scope: it reads H.264 and AV1 only. Convert it to \
one of those with another tool first"
)
}
fn remedy_for(encoding: Encoding) -> String {
if !encoding.is_in_scope() {
return out_of_scope(encoding);
}
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(", ")
)
}
pub struct StubDecoder;
impl Decoder for StubDecoder {
fn encoding(&self) -> Encoding {
Encoding::Av1 }
fn decode(&mut self, _packet: &Packet) -> Result<Option<Frame>> {
Ok(None)
}
fn flush(&mut self) -> Result<Vec<Frame>> {
Ok(Vec::new())
}
fn reset(&mut self) -> Result<()> {
Ok(())
}
fn is_hardware(&self) -> bool {
false
}
}
#[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());
}
#[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::VideoToolbox.handles(Encoding::H264));
for backend in BACKENDS {
assert!(!backend.handles(Encoding::H265), "{backend}");
assert!(!backend.handles(Encoding::Vp9), "{backend}");
}
}
#[test]
fn an_out_of_scope_encoding_is_refused_as_a_scope_decision() {
for encoding in [Encoding::Vp9, Encoding::H265, Encoding::Theora] {
let Err(Error::NoDecoder { remedy, .. }) = open(&config(encoding)) else {
panic!("{encoding} is outside vtome's scope");
};
assert!(remedy.contains("scope"), "{remedy}");
assert!(!remedy.contains("feature"), "{remedy}");
}
}
#[test]
fn a_missing_decoder_names_the_feature_that_would_have_supplied_it() {
if cfg!(feature = "decode-av1") {
return;
}
let remedy = remedy_for(Encoding::Av1);
assert!(remedy.contains("decode-av1"), "{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-platform")) {
assert!(backends_for(Encoding::Av1).is_empty());
assert!(backends_for(Encoding::H264).is_empty());
}
}
}