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 = "linux", feature = "nvidia"))]
mod nvdec;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) 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 name(&self) -> &str;
}
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 = "linux", feature = "nvidia"))]
Candidate {
name: nvdec::NAME,
supports: |c| matches!(c, Codec::H264 | Codec::H265 | Codec::Av1),
open: nvdec::Nvdec::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,
}];
#[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::new();
let mut refused = Vec::new();
for attempt in attempts {
if !(attempt.candidate.supports)(codec) {
continue;
}
let name = attempt.candidate.name;
tried.push(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");
if attempt.hardware {
refused.push(format!("{name}: {e}"));
}
}
}
}
if tried.is_empty() {
return Err(Error::NoDecoder(format!("none support {}", codec.label())));
}
Err(Error::NoDecoder(tried.join(", ")))
}
#[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 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"));
}
}