use mediaio_ffmpeg::MediaKind;
use smol_bytes::Utf8Bytes;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum License {
Lgpl,
Gpl,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Entry {
name: &'static str,
kind: MediaKind,
license: License,
hardware: bool,
}
impl Entry {
const fn new(name: &'static str, kind: MediaKind, license: License, hardware: bool) -> Self {
Self {
name,
kind,
license,
hardware,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn name(&self) -> &'static str {
self.name
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn kind(&self) -> MediaKind {
self.kind
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn license(&self) -> License {
self.license
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn is_hardware(&self) -> bool {
self.hardware
}
}
pub const CATALOGUE: &[Entry] = &[
Entry::new("h264_videotoolbox", MediaKind::Video, License::Lgpl, true),
Entry::new("hevc_videotoolbox", MediaKind::Video, License::Lgpl, true),
Entry::new("prores_videotoolbox", MediaKind::Video, License::Lgpl, true),
Entry::new("prores_ks", MediaKind::Video, License::Lgpl, false),
Entry::new("libopenh264", MediaKind::Video, License::Lgpl, false),
Entry::new("libx264", MediaKind::Video, License::Gpl, false),
Entry::new("libx264rgb", MediaKind::Video, License::Gpl, false),
Entry::new("libx265", MediaKind::Video, License::Gpl, false),
Entry::new("aac", MediaKind::Audio, License::Lgpl, false),
Entry::new("aac_at", MediaKind::Audio, License::Lgpl, false),
];
const GPL: bool = cfg!(feature = "gpl");
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Admitted {
entry: &'static Entry,
}
impl Admitted {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn entry(&self) -> &'static Entry {
self.entry
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn name(&self) -> &'static str {
self.entry.name
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum EncoderRefusal {
#[error("`{name}` is not an encoder this crate opens by name")]
Unknown {
name: Utf8Bytes,
},
#[error("`{name}` is a GPL encoder, reachable only with this crate's `gpl` feature")]
Gpl {
name: &'static str,
},
#[error("the linked FFmpeg has no `{name}` encoder")]
NotLinked {
name: &'static str,
},
}
pub fn lookup(name: &str) -> Result<&'static Entry, EncoderRefusal> {
let entry = CATALOGUE
.iter()
.find(|entry| entry.name == name)
.ok_or_else(|| EncoderRefusal::Unknown {
name: Utf8Bytes::from(name),
})?;
if entry.license == License::Gpl && !GPL {
return Err(EncoderRefusal::Gpl { name: entry.name });
}
Ok(entry)
}
pub fn encoder_by_name(name: &str) -> Result<Admitted, EncoderRefusal> {
let entry = lookup(name)?;
if ffmpeg_next::encoder::find_by_name(entry.name).is_none() {
return Err(EncoderRefusal::NotLinked { name: entry.name });
}
Ok(Admitted { entry })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_catalogues_names_are_unique_and_plain() {
for (i, entry) in CATALOGUE.iter().enumerate() {
assert!(!entry.name().is_empty());
assert!(
entry.name().bytes().all(|b| b.is_ascii_graphic()),
"{} must be a plain FFmpeg name",
entry.name()
);
assert!(
CATALOGUE[..i]
.iter()
.all(|other| other.name() != entry.name()),
"{} is listed twice",
entry.name()
);
assert!(matches!(entry.kind(), MediaKind::Video | MediaKind::Audio));
}
}
#[test]
fn an_unknown_name_is_refused_by_name() {
for name in ["libfdk_aac", "x264", "aac\0", ""] {
match encoder_by_name(name) {
Err(EncoderRefusal::Unknown { name: refused }) => assert_eq!(refused, name),
other => panic!("{name:?} should be unknown, got {other:?}"),
}
}
}
#[test]
fn every_lgpl_entry_passes_the_license_gate() {
for entry in CATALOGUE.iter().filter(|e| e.license() == License::Lgpl) {
assert_eq!(lookup(entry.name()), Ok(entry), "{}", entry.name());
}
}
#[cfg(not(feature = "gpl"))]
#[test]
fn every_gpl_entry_is_refused_by_name_without_the_feature() {
let gpl: Vec<&Entry> = CATALOGUE
.iter()
.filter(|e| e.license() == License::Gpl)
.collect();
assert!(!gpl.is_empty());
for entry in gpl {
assert_eq!(
lookup(entry.name()),
Err(EncoderRefusal::Gpl { name: entry.name() })
);
}
}
#[cfg(feature = "gpl")]
#[test]
fn every_gpl_entry_passes_the_license_gate_with_the_feature() {
for entry in CATALOGUE.iter().filter(|e| e.license() == License::Gpl) {
assert_eq!(lookup(entry.name()), Ok(entry), "{}", entry.name());
}
}
#[test]
fn aac_is_admitted_by_the_linked_libavcodec() {
let admitted = encoder_by_name("aac").expect("every libavcodec carries aac");
assert_eq!(admitted.name(), "aac");
assert_eq!(admitted.entry().kind(), MediaKind::Audio);
}
#[cfg(not(feature = "gpl"))]
#[test]
fn a_gpl_name_is_refused_before_libavcodec_is_asked() {
assert_eq!(
encoder_by_name("libx264"),
Err(EncoderRefusal::Gpl { name: "libx264" })
);
}
#[cfg(feature = "gpl")]
#[test]
fn a_gpl_name_reaches_libavcodec_with_the_feature() {
match encoder_by_name("libx264") {
Ok(admitted) => assert_eq!(admitted.name(), "libx264"),
Err(refusal) => assert_eq!(refusal, EncoderRefusal::NotLinked { name: "libx264" }),
}
}
}