use std::fmt;
use std::str::FromStr;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AudioFormat {
Aac,
Opus,
Flac,
Mp3,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum VideoFormat {
Avc1,
Avc3,
Hvc1,
Hev1,
Av01,
Vp8,
Vp9,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ContainerFormat {
Fmp4,
Mkv,
Ts,
Flv,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Format {
Audio(AudioFormat),
Video(VideoFormat),
Container(ContainerFormat),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Kind {
Audio,
Video,
Container,
}
impl Kind {
pub fn name(self) -> &'static str {
match self {
Kind::Audio => "audio",
Kind::Video => "video",
Kind::Container => "container",
}
}
}
impl Format {
pub fn kind(self) -> Kind {
match self {
Format::Audio(_) => Kind::Audio,
Format::Video(_) => Kind::Video,
Format::Container(_) => Kind::Container,
}
}
}
impl FromStr for Format {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"aac" => Format::Audio(AudioFormat::Aac),
"opus" => Format::Audio(AudioFormat::Opus),
"flac" => Format::Audio(AudioFormat::Flac),
"mp3" => Format::Audio(AudioFormat::Mp3),
"avc1" | "avcc" => Format::Video(VideoFormat::Avc1),
"avc3" | "h264" => Format::Video(VideoFormat::Avc3),
"hvc1" | "hvcc" => Format::Video(VideoFormat::Hvc1),
"hev1" => Format::Video(VideoFormat::Hev1),
"av01" | "av1" | "av1c" | "av1C" => Format::Video(VideoFormat::Av01),
"vp8" | "vp08" => Format::Video(VideoFormat::Vp8),
"vp9" | "vp09" => Format::Video(VideoFormat::Vp9),
"fmp4" | "cmaf" => Format::Container(ContainerFormat::Fmp4),
"mkv" | "webm" | "matroska" => Format::Container(ContainerFormat::Mkv),
"ts" | "mpegts" | "mpeg2ts" | "m2ts" => Format::Container(ContainerFormat::Ts),
"flv" => Format::Container(ContainerFormat::Flv),
_ => return Err(crate::Error::UnknownFormat(s.to_string())),
})
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Format::Audio(format) => format.fmt(f),
Format::Video(format) => format.fmt(f),
Format::Container(format) => format.fmt(f),
}
}
}
fn parse_kind(s: &str, wanted: Kind) -> Result<Format, crate::Error> {
let format: Format = s.parse()?;
if format.kind() != wanted {
return Err(crate::Error::WrongKind {
format: s.to_string(),
actual: format.kind().name(),
wanted: wanted.name(),
});
}
Ok(format)
}
macro_rules! kind_format {
($ty:ident, $kind:ident, $($variant:ident => $name:literal),+ $(,)?) => {
impl fmt::Display for $ty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
$($ty::$variant => $name,)+
})
}
}
impl FromStr for $ty {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match parse_kind(s, Kind::$kind)? {
Format::$kind(format) => Ok(format),
_ => unreachable!(),
}
}
}
};
}
kind_format!(AudioFormat, Audio,
Aac => "aac", Opus => "opus", Flac => "flac", Mp3 => "mp3");
kind_format!(VideoFormat, Video,
Avc1 => "avc1", Avc3 => "avc3", Hvc1 => "hvc1", Hev1 => "hev1",
Av01 => "av01", Vp8 => "vp8", Vp9 => "vp9");
kind_format!(ContainerFormat, Container,
Fmp4 => "fmp4", Mkv => "mkv", Ts => "ts", Flv => "flv");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aliases_resolve_to_one_canonical_name() {
for (aliases, canonical) in [
(vec!["avc3", "h264"], "avc3"),
(vec!["avc1", "avcc"], "avc1"),
(vec!["hvc1", "hvcc"], "hvc1"),
(vec!["av01", "av1", "av1c", "av1C"], "av01"),
(vec!["vp8", "vp08"], "vp8"),
(vec!["vp9", "vp09"], "vp9"),
(vec!["fmp4", "cmaf"], "fmp4"),
(vec!["mkv", "webm", "matroska"], "mkv"),
(vec!["ts", "mpegts", "mpeg2ts", "m2ts"], "ts"),
(vec!["opus"], "opus"),
] {
for alias in aliases {
let format: Format = alias.parse().unwrap_or_else(|err| panic!("{alias}: {err}"));
assert_eq!(format.to_string(), canonical, "{alias} should canonicalize");
}
}
}
#[test]
fn a_format_of_another_kind_names_the_kind_that_takes_it() {
let err = "avc3".parse::<AudioFormat>().unwrap_err();
assert!(
matches!(
&err,
crate::Error::WrongKind {
actual: "video",
wanted: "audio",
..
}
),
"got {err:?}"
);
let err = "fmp4".parse::<VideoFormat>().unwrap_err();
assert!(
matches!(
&err,
crate::Error::WrongKind {
actual: "container",
wanted: "video",
..
}
),
"got {err:?}"
);
let err = "opus".parse::<ContainerFormat>().unwrap_err();
assert!(
matches!(
&err,
crate::Error::WrongKind {
actual: "audio",
wanted: "container",
..
}
),
"got {err:?}"
);
}
#[test]
fn an_unrecognized_name_is_unknown_rather_than_wrong_kind() {
let err = "prores".parse::<VideoFormat>().unwrap_err();
assert!(
matches!(&err, crate::Error::UnknownFormat(f) if f == "prores"),
"got {err:?}"
);
}
}