use bytes::Bytes;
use moq_net::Timestamp;
use super::decoder::{Config, Kind};
use crate::{Error, Frame};
mod openh264;
#[cfg(test)]
pub(crate) mod probe;
#[cfg(target_os = "macos")]
mod videotoolbox;
#[cfg(target_os = "windows")]
mod mediafoundation;
#[cfg(all(target_os = "android", feature = "mediacodec"))]
mod mediacodec;
#[cfg(all(target_os = "linux", feature = "nvidia"))]
mod nvdec;
#[cfg(all(target_os = "linux", feature = "vaapi"))]
pub(crate) mod vaapi;
#[cfg(all(target_os = "linux", feature = "v4l2"))]
mod v4l2;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Codec {
H264,
H265,
Av1,
}
impl Codec {
fn label(self) -> &'static str {
match self {
Codec::H264 => "H.264",
Codec::H265 => "H.265",
Codec::Av1 => "AV1",
}
}
}
pub(crate) trait Backend: Send {
fn decode(&mut self, access_unit: Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error>;
fn flush(&mut self) -> Result<Vec<Frame>, Error>;
fn name(&self) -> &str;
}
pub const NAMES: &[&str] = &[
"videotoolbox",
"mediafoundation",
"mediacodec",
"nvdec",
"vaapi",
"v4l2",
"openh264",
];
type Open = fn(Codec, &Config) -> Result<Box<dyn Backend>, Error>;
struct Candidate {
name: &'static str,
supports: fn(Codec) -> bool,
open: Open,
}
const HARDWARE: &[Candidate] = &[
#[cfg(target_os = "macos")]
Candidate {
name: videotoolbox::NAME,
supports: |c| matches!(c, Codec::H264 | Codec::H265),
open: videotoolbox::VideoToolbox::open,
},
#[cfg(target_os = "windows")]
Candidate {
name: mediafoundation::NAME,
supports: |c| matches!(c, Codec::H264 | Codec::H265),
open: mediafoundation::MediaFoundation::open,
},
#[cfg(all(target_os = "android", feature = "mediacodec"))]
Candidate {
name: mediacodec::NAME,
supports: |c| matches!(c, Codec::H264 | Codec::H265 | Codec::Av1),
open: mediacodec::MediaCodec::open,
},
#[cfg(all(target_os = "linux", feature = "nvidia"))]
Candidate {
name: nvdec::NAME,
supports: |c| matches!(c, Codec::H264 | Codec::H265 | Codec::Av1),
open: nvdec::Nvdec::open,
},
#[cfg(all(target_os = "linux", feature = "vaapi"))]
Candidate {
name: vaapi::NAME,
supports: |c| matches!(c, Codec::H264),
open: vaapi::Vaapi::open,
},
#[cfg(all(target_os = "linux", feature = "v4l2"))]
Candidate {
name: v4l2::NAME,
supports: |c| matches!(c, Codec::H264),
open: v4l2::V4l2::open,
},
];
const SOFTWARE: Candidate = Candidate {
name: openh264::NAME,
supports: |c| matches!(c, Codec::H264),
open: openh264::Openh264::open,
};
#[cfg(test)]
const NAMED_ONLY: &[Candidate] = &[
Candidate {
name: probe::NAME,
supports: |c| matches!(c, Codec::H264),
open: probe::Probe::open,
},
Candidate {
name: probe::BUFFERED_NAME,
supports: |c| matches!(c, Codec::H264),
open: probe::Buffered::open,
},
#[cfg(not(target_os = "macos"))]
Candidate {
name: probe::BLOCKING_FLUSH_NAME,
supports: |c| matches!(c, Codec::H264),
open: probe::BlockingFlush::open,
},
];
#[cfg(not(test))]
const NAMED_ONLY: &[Candidate] = &[];
struct Attempt<'a> {
candidate: &'a Candidate,
hardware: bool,
}
impl<'a> Attempt<'a> {
fn hardware(candidate: &'a Candidate) -> Self {
Self {
candidate,
hardware: true,
}
}
fn software(candidate: &'a Candidate) -> Self {
Self {
candidate,
hardware: false,
}
}
}
pub(crate) fn open(codec: Codec, config: &Config) -> Result<Box<dyn Backend>, Error> {
let attempts: Vec<Attempt> = match &config.kind {
Kind::Auto => HARDWARE
.iter()
.map(Attempt::hardware)
.chain(std::iter::once(Attempt::software(&SOFTWARE)))
.collect(),
Kind::Hardware => HARDWARE.iter().map(Attempt::hardware).collect(),
Kind::Software => vec![Attempt::software(&SOFTWARE)],
Kind::Named(name) => HARDWARE
.iter()
.map(Attempt::hardware)
.chain(
std::iter::once(&SOFTWARE)
.chain(NAMED_ONLY.iter())
.map(Attempt::software),
)
.filter(|a| a.candidate.name == name)
.collect(),
};
select(codec, attempts, config)
}
fn select(codec: Codec, attempts: Vec<Attempt>, config: &Config) -> Result<Box<dyn Backend>, Error> {
let mut tried: Vec<String> = Vec::new();
let mut refused = Vec::new();
for attempt in attempts {
if !(attempt.candidate.supports)(codec) {
continue;
}
let name = attempt.candidate.name;
match (attempt.candidate.open)(codec, config) {
Ok(backend) => {
if !attempt.hardware && !refused.is_empty() {
tracing::warn!(
decoder = name,
refused = %refused.join(", "),
"no hardware decoder available, falling back to software"
);
}
return Ok(backend);
}
Err(e) => {
tracing::debug!(decoder = name, error = %e, "decoder unavailable, trying next");
tried.push(format!("{name}: {e}"));
if attempt.hardware {
refused.push(format!("{name}: {e}"));
}
}
}
}
if tried.is_empty() {
let available = available_names(codec);
return match &config.kind {
Kind::Named(name) => Err(Error::UnknownDecoder {
name: name.clone(),
codec,
available: available.join(", "),
}),
kind => Err(Error::NoDecoder(format!(
"nothing compiled in for {} at {kind:?} (this build has: {})",
codec.label(),
available.join(", "),
))),
};
}
Err(Error::NoDecoder(tried.join(", ")))
}
fn available_names(codec: Codec) -> Vec<&'static str> {
HARDWARE
.iter()
.chain(std::iter::once(&SOFTWARE))
.filter(|candidate| (candidate.supports)(codec))
.map(|candidate| candidate.name)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
struct Stub;
impl Stub {
fn open(_codec: Codec, _config: &Config) -> Result<Box<dyn Backend>, Error> {
Ok(Box::new(Self))
}
}
impl Backend for Stub {
fn decode(&mut self, _access_unit: Bytes, _timestamp: Timestamp, _keyframe: bool) -> Result<Vec<Frame>, Error> {
Ok(Vec::new())
}
fn flush(&mut self) -> Result<Vec<Frame>, Error> {
Ok(Vec::new())
}
fn name(&self) -> &str {
"stub"
}
}
const WORKING: Candidate = Candidate {
name: "stub",
supports: |c| matches!(c, Codec::H264),
open: Stub::open,
};
const REFUSING: Candidate = Candidate {
name: "driverless",
supports: |c| matches!(c, Codec::H264),
open: |_, _| Err(Error::Codec(anyhow::anyhow!("driver libraries not found"))),
};
#[tracing_test::traced_test]
#[test]
fn falling_past_hardware_warns() {
let config = Config::new();
let attempts = vec![Attempt::hardware(&REFUSING), Attempt::software(&WORKING)];
let backend = select(Codec::H264, attempts, &config).unwrap();
assert_eq!(backend.name(), "stub");
logs_assert(
|lines: &[&str]| match lines.iter().find(|line| line.contains("falling back to software")) {
Some(warning) if warning.contains("driverless") && warning.contains("driver libraries not found") => {
Ok(())
}
Some(warning) => Err(format!("warning does not name the refusal: {warning}")),
None => Err("no fallback warning".to_owned()),
},
);
}
#[tracing_test::traced_test]
#[test]
fn hardware_that_cannot_decode_the_codec_is_not_a_fallback() {
const H265_ONLY: Candidate = Candidate {
name: "driverless",
supports: |c| matches!(c, Codec::H265),
open: |_, _| Err(Error::Codec(anyhow::anyhow!("driver libraries not found"))),
};
let attempts = vec![Attempt::hardware(&H265_ONLY), Attempt::software(&WORKING)];
select(Codec::H264, attempts, &Config::new()).unwrap();
assert!(!logs_contain("no hardware decoder available"));
}
#[test]
fn an_unknown_name_names_itself_and_the_alternatives() {
let mut config = Config::new();
config.kind = Kind::Named("vappi".to_owned());
match open(Codec::H264, &config) {
Err(Error::UnknownDecoder { name, codec, available }) => {
assert_eq!(name, "vappi");
assert_eq!(codec, crate::decode::Codec::H264);
assert!(available.contains(openh264::NAME), "nothing offered: {available}");
}
Err(other) => panic!("expected UnknownDecoder, got {other:?}"),
Ok(backend) => panic!("expected UnknownDecoder, opened {}", backend.name()),
}
}
#[test]
fn every_candidate_refusing_reports_why() {
let mut config = Config::new();
config.kind = Kind::Named("driverless".to_owned());
match select(Codec::H264, vec![Attempt::hardware(&REFUSING)], &config) {
Err(Error::NoDecoder(tried)) => {
assert!(tried.contains("driverless"), "does not name the backend: {tried}");
assert!(
tried.contains("driver libraries not found"),
"does not carry the reason: {tried}"
);
}
Err(other) => panic!("expected NoDecoder, got {other:?}"),
Ok(backend) => panic!("expected NoDecoder, opened {}", backend.name()),
}
}
#[test]
fn every_compiled_backend_is_named_publicly() {
for candidate in HARDWARE.iter().chain(std::iter::once(&SOFTWARE)) {
assert!(
NAMES.contains(&candidate.name),
"{} is compiled in but missing from NAMES",
candidate.name,
);
}
}
}