use std::time::Duration;
use moq_mux::catalog::CatalogFormat;
use crate::subscribe::{CatalogFormatArg, SelectArgs};
const DELAY_MAX: Duration = Duration::from_secs(10);
#[derive(usage::Args, Clone)]
#[usage(unknown_flags = "error", args_override_self = false)]
pub struct Args {
#[usage(long, value_enum)]
pub catalog_format: Option<CatalogFormatArg>,
#[usage(long, default = "100ms")]
pub delay: crate::duration::Duration,
#[usage(flatten)]
pub select: SelectArgs,
}
impl Args {
pub(super) fn catalog_format(&self, broadcast: &str) -> CatalogFormat {
self.catalog_format
.map(Into::into)
.or_else(|| CatalogFormat::detect(broadcast))
.unwrap_or_default()
}
pub fn validate(&self) -> anyhow::Result<()> {
use crate::subscribe::VideoCodecArg;
anyhow::ensure!(
!matches!(self.select.video_codec, Some(VideoCodecArg::Vp8 | VideoCodecArg::Vp9)),
"`play` cannot decode vp8 or vp9; pass --video-codec h264, h265, or av1"
);
anyhow::ensure!(
self.delay.into_std() <= DELAY_MAX,
"--delay must be at most {DELAY_MAX:?}; it is the depth the speaker buffers"
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(usage::Cli)]
struct Cli {
#[usage(flatten)]
args: Args,
}
fn parse(flags: &[&str]) -> Args {
let argv: Vec<&std::ffi::OsStr> = flags.iter().map(std::ffi::OsStr::new).collect();
Cli::parse_from(&argv).expect("parse the play flags").args
}
#[test]
fn undecodable_video_codecs_are_refused() {
parse(&[]).validate().unwrap();
parse(&["--video-codec", "h264"]).validate().unwrap();
parse(&["--video-codec", "av1"]).validate().unwrap();
let err = parse(&["--video-codec", "vp9"]).validate().unwrap_err().to_string();
assert!(err.contains("vp8 or vp9"), "{err}");
assert!(parse(&["--video-codec", "vp8"]).validate().is_err());
}
#[test]
fn the_delay_sets_the_staleness_budget() {
assert_eq!(parse(&[]).delay.into_std(), std::time::Duration::from_millis(100));
assert_eq!(
parse(&["--delay", "500ms"]).delay.into_std(),
std::time::Duration::from_millis(500)
);
}
#[test]
fn the_playout_budget_has_one_flag() {
for removed in [["--max-age", "500ms"], ["--latency-max", "500ms"]] {
let argv: Vec<&std::ffi::OsStr> = removed.iter().map(std::ffi::OsStr::new).collect();
assert!(Cli::parse_from(&argv).is_err(), "{} still parsed", removed[0]);
}
}
#[test]
fn the_delay_is_bounded_by_the_speakers_ring() {
let err = parse(&["--delay", "11s"]).validate().unwrap_err().to_string();
assert!(err.contains("--delay must be at most"), "{err}");
parse(&["--delay", "10s"]).validate().unwrap();
#[cfg(feature = "play")]
assert_eq!(DELAY_MAX, moq_audio::playback::Input::LATENCY_MAX);
}
#[test]
fn the_catalog_format_follows_the_broadcast_suffix() {
assert_eq!(parse(&[]).catalog_format("room.hang"), CatalogFormat::Hang);
assert_eq!(parse(&[]).catalog_format("room.msf"), CatalogFormat::Msf);
assert_eq!(parse(&[]).catalog_format("room"), CatalogFormat::DEFAULT);
assert_eq!(
parse(&["--catalog-format", "hangz"]).catalog_format("room.msf"),
CatalogFormat::HangZ
);
}
}