use super::encoder::{Codec, Config, Kind};
use crate::encode::Encoded;
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 nvenc;
#[cfg(all(target_os = "linux", feature = "vaapi"))]
mod vaapi;
pub(crate) trait Backend: Send {
fn encode(&mut self, frame: &Frame, keyframe: bool) -> Result<Vec<Encoded>, Error>;
fn flush(&mut self) -> Result<Vec<Encoded>, Error>;
fn finish(&mut self) -> Result<Vec<Encoded>, Error>;
fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error>;
fn name(&self) -> &str;
}
struct Candidate {
name: &'static str,
codecs: &'static [Codec],
open: fn(&Config) -> Result<Box<dyn Backend>, Error>,
}
const HARDWARE: &[Candidate] = &[
#[cfg(target_os = "macos")]
Candidate {
name: videotoolbox::NAME,
codecs: &[Codec::H264, Codec::H265],
open: videotoolbox::VideoToolbox::open,
},
#[cfg(target_os = "windows")]
Candidate {
name: mediafoundation::NAME,
codecs: &[Codec::H264, Codec::H265],
open: mediafoundation::MediaFoundation::open,
},
#[cfg(all(target_os = "linux", feature = "nvidia"))]
Candidate {
name: nvenc::NAME,
codecs: &[Codec::H264, Codec::H265],
open: nvenc::Nvenc::open,
},
#[cfg(all(target_os = "linux", feature = "vaapi"))]
Candidate {
name: vaapi::NAME,
codecs: &[Codec::H264],
open: vaapi::Vaapi::open,
},
];
const SOFTWARE: &[Candidate] = &[Candidate {
name: openh264::NAME,
codecs: &[Codec::H264],
open: openh264::Openh264::open,
}];
#[cfg(test)]
const NAMED_ONLY: &[Candidate] = &[Candidate {
name: probe::NAME,
codecs: &[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(config: &Config) -> Result<Box<dyn Backend>, Error> {
let codec = config.codec;
let supports = move |c: &Candidate| c.codecs.contains(&codec);
let hardware = HARDWARE.iter().filter(|c| supports(c)).map(Attempt::hardware);
let software = SOFTWARE.iter().filter(|c| supports(c)).map(Attempt::software);
let attempts: Vec<Attempt> = match &config.kind {
Kind::Auto => hardware.chain(software).collect(),
Kind::Hardware => hardware.collect(),
Kind::Software => software.collect(),
Kind::Named(name) => HARDWARE
.iter()
.map(Attempt::hardware)
.chain(SOFTWARE.iter().chain(NAMED_ONLY.iter()).map(Attempt::software))
.filter(|a| supports(a.candidate) && a.candidate.name == name)
.collect(),
};
select(attempts, config)
}
fn select(attempts: Vec<Attempt>, config: &Config) -> Result<Box<dyn Backend>, Error> {
let mut tried = Vec::new();
let mut refused = Vec::new();
for attempt in attempts {
let name = attempt.candidate.name;
tried.push(name);
match (attempt.candidate.open)(config) {
Ok(backend) => {
if !attempt.hardware && !refused.is_empty() {
tracing::warn!(
encoder = name,
refused = %refused.join(", "),
"no hardware encoder available, falling back to software"
);
}
return Ok(backend);
}
Err(e) => {
tracing::debug!(encoder = name, error = %e, "encoder unavailable, trying next");
if attempt.hardware {
refused.push(format!("{name}: {e}"));
}
}
}
}
Err(Error::NoEncoder(tried.join(", ")))
}
#[cfg(test)]
pub(crate) mod test_util {
use h264_reader::nal::sps::SeqParameterSet;
use h264_reader::nal::{Nal, RefNal, UnitType};
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Described {
pub primaries: u8,
pub transfer: u8,
pub matrix: u8,
pub full_range: bool,
}
pub(crate) const BT601_DESCRIBED: Described = Described {
primaries: 6,
transfer: 1,
matrix: 6,
full_range: false,
};
pub(crate) const BT709_DESCRIBED: Described = Described {
primaries: 1,
transfer: 1,
matrix: 1,
full_range: false,
};
pub(crate) fn declared_color(annexb: &[u8]) -> Option<Described> {
let starts: Vec<usize> = (0..annexb.len().saturating_sub(2))
.filter(|&i| annexb[i..i + 3] == [0, 0, 1])
.map(|i| i + 3)
.collect();
let sps = starts.iter().enumerate().find_map(|(n, &start)| {
let end = starts.get(n + 1).map_or(annexb.len(), |&next| next - 3);
let nal = RefNal::new(&annexb[start..end], &[], true);
match nal.header().ok()?.nal_unit_type() {
UnitType::SeqParameterSet => SeqParameterSet::from_bits(nal.rbsp_bits()).ok(),
_ => None,
}
})?;
let signal = sps.vui_parameters.as_ref()?.video_signal_type.as_ref()?;
let description = signal.colour_description.as_ref()?;
Some(Described {
primaries: description.colour_primaries,
transfer: description.transfer_characteristics,
matrix: description.matrix_coefficients,
full_range: signal.video_full_range_flag,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Stub;
impl Stub {
fn open(_config: &Config) -> Result<Box<dyn Backend>, Error> {
Ok(Box::new(Self))
}
}
impl Backend for Stub {
fn encode(&mut self, _frame: &Frame, _keyframe: bool) -> Result<Vec<Encoded>, Error> {
Ok(Vec::new())
}
fn flush(&mut self) -> Result<Vec<Encoded>, Error> {
Ok(Vec::new())
}
fn finish(&mut self) -> Result<Vec<Encoded>, Error> {
Ok(Vec::new())
}
fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
Ok(())
}
fn name(&self) -> &str {
"stub"
}
}
const WORKING: Candidate = Candidate {
name: "stub",
codecs: &[Codec::H264],
open: Stub::open,
};
const REFUSING: Candidate = Candidate {
name: "driverless",
codecs: &[Codec::H264],
open: |_| Err(Error::Codec(anyhow::anyhow!("driver libraries not found"))),
};
fn config() -> Config {
Config::new(320, 240, 30)
}
#[tracing_test::traced_test]
#[test]
fn falling_past_hardware_warns() {
let backend = select(
vec![Attempt::hardware(&REFUSING), Attempt::software(&WORKING)],
&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 asking_for_software_is_not_a_fallback() {
select(vec![Attempt::software(&WORKING)], &config()).unwrap();
assert!(!logs_contain("falling back to software"));
}
#[tracing_test::traced_test]
#[test]
fn hardware_that_opens_is_not_a_fallback() {
select(
vec![Attempt::hardware(&WORKING), Attempt::software(&WORKING)],
&config(),
)
.unwrap();
assert!(!logs_contain("falling back to software"));
}
}