use anyhow::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(u8)]
pub enum VideoCodec {
H264 = 1,
Hevc = 2,
Av1 = 3,
Av2 = 4,
}
pub const RESERVED: [VideoCodec; 4] = [
VideoCodec::H264,
VideoCodec::Hevc,
VideoCodec::Av1,
VideoCodec::Av2,
];
impl VideoCodec {
pub fn id(self) -> u8 {
self as u8
}
pub fn from_id(id: u8) -> Result<Self> {
RESERVED
.into_iter()
.find(|c| c.id() == id)
.ok_or_else(|| anyhow::anyhow!("Unknown video codec id: {id}"))
}
pub fn name(self) -> &'static str {
match self {
VideoCodec::H264 => "h264",
VideoCodec::Hevc => "hevc",
VideoCodec::Av1 => "av1",
VideoCodec::Av2 => "av2",
}
}
pub fn web_codec_string(self) -> &'static str {
match self {
VideoCodec::Av1 => "av01.0.04M.08",
VideoCodec::H264 => "avc1.42E01E",
VideoCodec::Hevc => "hev1.1.6.L93.B0",
VideoCodec::Av2 => "av02.0.04M.08",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Capabilities {
codecs: Vec<VideoCodec>,
}
impl Capabilities {
pub fn none() -> Self {
Self::default()
}
pub fn of(codecs: impl IntoIterator<Item = VideoCodec>) -> Self {
let mut list: Vec<VideoCodec> = codecs.into_iter().collect();
list.sort();
list.dedup();
Self { codecs: list }
}
pub fn codecs(&self) -> &[VideoCodec] {
&self.codecs
}
pub fn can_decode(&self, codec: VideoCodec) -> bool {
self.codecs.contains(&codec)
}
pub fn is_empty(&self) -> bool {
self.codecs.is_empty()
}
}
pub fn probe_local() -> Capabilities {
Capabilities::none()
}
pub trait VideoEncoder: Send {
fn codec(&self) -> VideoCodec;
fn encode(&mut self, rgb: &[u8], width: u32, height: u32, keyframe: bool) -> Result<Vec<u8>>;
fn reset(&mut self);
}
pub fn encoder_for(
codec: VideoCodec,
_width: u32,
_height: u32,
_speed: u8,
) -> Result<Box<dyn VideoEncoder>> {
anyhow::bail!(
"No {} encoder is linked into this build. Available encoders: {}. \
See src/codec/mod.rs for what was tried and why it was rejected.",
codec.name(),
if probe_local().is_empty() {
"none".to_string()
} else {
probe_local()
.codecs()
.iter()
.map(|c| c.name())
.collect::<Vec<_>>()
.join(", ")
}
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_reserved_identifier_round_trips() {
for codec in RESERVED {
assert_eq!(VideoCodec::from_id(codec.id()).unwrap(), codec);
}
}
#[test]
fn an_unknown_identifier_is_refused() {
let err = VideoCodec::from_id(99).unwrap_err().to_string();
assert!(
err.contains("Unknown video codec id: 99"),
"unhelpful: {err}"
);
}
#[test]
fn probing_reports_nothing_because_nothing_is_linked() {
let caps = probe_local();
assert!(caps.is_empty());
for codec in RESERVED {
assert!(
!caps.can_decode(codec),
"{} must not be reported without a linked encoder",
codec.name()
);
}
}
#[test]
fn requesting_an_encoder_fails_with_the_reason() {
for codec in RESERVED {
let err = match encoder_for(codec, 64, 64, 8) {
Ok(_) => panic!("{} must not silently resolve to an encoder", codec.name()),
Err(e) => e.to_string(),
};
assert!(
err.contains(&format!("No {} encoder is linked", codec.name())),
"unhelpful for {}: {err}",
codec.name()
);
assert!(err.contains("src/codec/mod.rs"), "unhelpful: {err}");
}
}
#[test]
fn capabilities_can_be_declared_even_though_none_are_produced() {
let caps = Capabilities::of([VideoCodec::Av1, VideoCodec::Av1, VideoCodec::H264]);
assert_eq!(caps.codecs(), &[VideoCodec::H264, VideoCodec::Av1]);
assert!(caps.can_decode(VideoCodec::Av1));
assert!(!caps.can_decode(VideoCodec::Av2));
}
}