use super::Encoded;
use super::encoder::{Codec, Kind, Settings};
use crate::Error;
mod libopus;
mod pcm;
#[cfg(test)]
pub(crate) mod stub;
pub(crate) trait Backend: Send + std::panic::UnwindSafe + std::panic::RefUnwindSafe {
fn encode(&mut self, pcm: &[f32]) -> Result<Encoded, Error>;
fn reset(&mut self);
fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error>;
fn bitrate(&self) -> u64;
fn delay(&self) -> usize;
fn name(&self) -> &str;
}
struct Candidate {
name: &'static str,
codecs: &'static [Codec],
open: fn(&Settings) -> Result<Box<dyn Backend>, Error>,
}
const PLATFORM: &[Candidate] = &[];
const SOFTWARE: &[Candidate] = &[
Candidate {
name: libopus::NAME,
codecs: &[Codec::Opus],
open: libopus::Libopus::open,
},
Candidate {
name: pcm::NAME,
codecs: &[Codec::Pcm],
open: pcm::Pcm::open,
},
];
#[cfg(test)]
const STUB_PLATFORM: &[Candidate] = &[Candidate {
name: stub::NAME,
codecs: &[Codec::Aac],
open: stub::Stub::open,
}];
pub(crate) fn open(settings: &Settings) -> Result<Box<dyn Backend>, Error> {
if let Kind::Named(requested) = &settings.kind
&& requested != settings.codec.as_str()
{
return Err(Error::Unsupported(format!(
"audio encoder {requested:?} is unavailable for {}",
settings.codec
)));
}
#[cfg(test)]
let platform = match stub::installed() {
true => STUB_PLATFORM,
false => PLATFORM,
};
#[cfg(not(test))]
let platform = PLATFORM;
select(settings, candidates(&settings.kind, platform, SOFTWARE))
}
fn candidates<'a>(kind: &Kind, platform: &'a [Candidate], software: &'a [Candidate]) -> Vec<&'a Candidate> {
match kind {
Kind::Software => software.iter().collect(),
Kind::Auto | Kind::Named(_) => platform.iter().chain(software).collect(),
}
}
fn select(settings: &Settings, candidates: Vec<&Candidate>) -> Result<Box<dyn Backend>, Error> {
let codec = settings.codec;
let mut refused = Vec::new();
for candidate in candidates {
if !candidate.codecs.contains(&codec) {
continue;
}
match (candidate.open)(settings) {
Ok(backend) => return Ok(backend),
Err(err) => refused.push((candidate.name, err)),
}
}
if refused.len() == 1 {
let (_, err) = refused.remove(0);
return Err(err);
}
if !refused.is_empty() {
let reasons: Vec<String> = refused.iter().map(|(name, err)| format!("{name}: {err}")).collect();
return Err(Error::Unsupported(reasons.join(", ")));
}
let available: Vec<&str> = PLATFORM
.iter()
.chain(SOFTWARE)
.filter(|c| c.codecs.contains(&codec))
.map(|c| c.name)
.collect();
let available = match available.is_empty() {
true => "none".to_owned(),
false => available.join(", "),
};
Err(Error::Unsupported(match &settings.kind {
Kind::Software => format!("no software {codec} audio encoder (this build has: {available})"),
Kind::Auto | Kind::Named(_) => format!("no {codec} audio encoder (this build has: {available})"),
}))
}
#[cfg(test)]
mod tests {
use super::*;
struct Fake(&'static str);
impl Backend for Fake {
fn encode(&mut self, _pcm: &[f32]) -> Result<Encoded, Error> {
Ok(Encoded::new(bytes::Bytes::new()))
}
fn reset(&mut self) {}
fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
Ok(())
}
fn bitrate(&self) -> u64 {
0
}
fn delay(&self) -> usize {
0
}
fn name(&self) -> &str {
self.0
}
}
const PLATFORM_STUB: Candidate = Candidate {
name: "platform",
codecs: &[Codec::Aac],
open: |_| Ok(Box::new(Fake("platform"))),
};
const REFUSING: Candidate = Candidate {
name: "refusing",
codecs: &[Codec::Aac],
open: |_| Err(Error::Unsupported("not these settings".into())),
};
const SOFTWARE_STUB: Candidate = Candidate {
name: "software",
codecs: &[Codec::Aac],
open: |_| Ok(Box::new(Fake("software"))),
};
const PCM_ONLY: Candidate = Candidate {
name: "pcm-only",
codecs: &[Codec::Pcm],
open: |_| panic!("opened for a codec it does not emit"),
};
fn aac(kind: Kind) -> Settings {
Settings {
kind,
..Settings::from_input(Codec::Aac, &Default::default())
}
}
fn pick(kind: Kind, platform: &[Candidate], software: &[Candidate]) -> Result<String, Error> {
let settings = aac(kind);
let backend = select(&settings, candidates(&settings.kind, platform, software))?;
Ok(backend.name().to_owned())
}
#[test]
fn auto_prefers_platform() {
let name = pick(Kind::Auto, &[PCM_ONLY, PLATFORM_STUB], &[SOFTWARE_STUB]).unwrap();
assert_eq!(name, "platform");
}
#[test]
fn auto_falls_back_to_software() {
let name = pick(Kind::Auto, &[REFUSING], &[SOFTWARE_STUB]).unwrap();
assert_eq!(name, "software");
}
#[test]
fn software_skips_platform() {
let name = pick(Kind::Software, &[PLATFORM_STUB], &[SOFTWARE_STUB]).unwrap();
assert_eq!(name, "software");
}
#[test]
fn named_codec_picks_like_auto() {
let name = pick(Kind::Named("aac".into()), &[PLATFORM_STUB], &[SOFTWARE_STUB]).unwrap();
assert_eq!(name, "platform");
}
#[test]
fn every_refusal_is_reported() {
const ALSO_REFUSING: Candidate = Candidate {
name: "also-refusing",
..REFUSING
};
let err = pick(Kind::Auto, &[REFUSING], &[ALSO_REFUSING]).unwrap_err();
let message = err.to_string();
assert!(
message.contains("refusing: ") && message.contains("also-refusing: "),
"{message}"
);
}
#[test]
fn aac_without_a_platform_encoder_is_refused() {
let err = open(&aac(Kind::Auto)).err().expect("no AAC encoder on this host");
let message = err.to_string();
assert!(message.contains("aac") && message.contains("none"), "{message}");
}
#[test]
fn stub_is_not_software() {
let _stub = stub::install();
assert_eq!(open(&aac(Kind::Auto)).unwrap().name(), stub::NAME);
let err = open(&aac(Kind::Software)).err().expect("no software AAC encoder");
assert!(err.to_string().contains("no software aac"), "{err}");
}
fn named(codec: Codec, name: &str) -> Settings {
Settings {
codec,
kind: Kind::Named(name.into()),
..Settings::default()
}
}
#[test]
fn codec_names_open_their_codec() {
let _stub = stub::install();
assert_eq!(open(&named(Codec::Opus, "opus")).unwrap().name(), libopus::NAME);
assert_eq!(open(&named(Codec::Pcm, "pcm")).unwrap().name(), pcm::NAME);
assert_eq!(open(&named(Codec::Aac, "aac")).unwrap().name(), stub::NAME);
}
#[test]
fn another_codecs_name_is_refused() {
assert!(matches!(open(&named(Codec::Opus, "pcm")), Err(Error::Unsupported(_))));
}
#[test]
fn backend_names_are_refused() {
let message = open(&named(Codec::Opus, libopus::NAME))
.err()
.expect("libopus is a backend, not a codec")
.to_string();
assert!(message.contains("\"libopus\"") && message.contains("opus"), "{message}");
}
}