use std::time::Duration;
use clap::ValueEnum;
use hang::catalog::{AudioCodecKind, VideoCodecKind};
use moq_mux::catalog::{self, CatalogFormat, Stream};
use moq_mux::select;
use tokio::io::AsyncWriteExt;
#[derive(Clone, Copy)]
pub enum SubscribeFormat {
Fmp4,
Mkv,
H264,
H265,
Ts,
Flv,
}
#[derive(ValueEnum, Clone, Copy)]
pub enum CatalogFormatArg {
Hang,
#[value(name = "hangz")]
HangZ,
Msf,
}
impl From<CatalogFormatArg> for CatalogFormat {
fn from(format: CatalogFormatArg) -> Self {
match format {
CatalogFormatArg::Hang => Self::Hang,
CatalogFormatArg::HangZ => Self::HangZ,
CatalogFormatArg::Msf => Self::Msf,
}
}
}
#[derive(ValueEnum, Clone, Copy)]
pub enum VideoCodecArg {
H264,
H265,
Vp8,
Vp9,
Av1,
}
impl From<VideoCodecArg> for VideoCodecKind {
fn from(value: VideoCodecArg) -> Self {
match value {
VideoCodecArg::H264 => Self::H264,
VideoCodecArg::H265 => Self::H265,
VideoCodecArg::Vp8 => Self::VP8,
VideoCodecArg::Vp9 => Self::VP9,
VideoCodecArg::Av1 => Self::AV1,
}
}
}
#[derive(ValueEnum, Clone, Copy)]
pub enum AudioCodecArg {
Aac,
Opus,
}
impl From<AudioCodecArg> for AudioCodecKind {
fn from(value: AudioCodecArg) -> Self {
match value {
AudioCodecArg::Aac => Self::AAC,
AudioCodecArg::Opus => Self::Opus,
}
}
}
#[derive(clap::Args, Clone, Default)]
pub struct SelectArgs {
#[arg(long)]
pub video_name: Option<String>,
#[arg(long)]
pub video_codec: Option<VideoCodecArg>,
#[arg(long)]
pub audio_name: Option<String>,
#[arg(long)]
pub audio_codec: Option<AudioCodecArg>,
}
#[derive(Clone)]
pub struct SubscribeArgs {
pub format: SubscribeFormat,
pub max_latency: Duration,
pub fragment_duration: Option<Duration>,
pub catalog: Option<CatalogFormatArg>,
pub select: SelectArgs,
}
impl SubscribeArgs {
pub fn catalog_format(&self, broadcast: &str) -> CatalogFormat {
self.catalog
.map(Into::into)
.or_else(|| CatalogFormat::detect(broadcast))
.unwrap_or_default()
}
fn format_codec(&self) -> Option<VideoCodecKind> {
match self.format {
SubscribeFormat::H264 => Some(VideoCodecKind::H264),
SubscribeFormat::H265 => Some(VideoCodecKind::H265),
SubscribeFormat::Fmp4 | SubscribeFormat::Mkv | SubscribeFormat::Ts | SubscribeFormat::Flv => None,
}
}
fn selection(&self) -> anyhow::Result<select::Broadcast> {
let user_codec = self.select.video_codec.map(VideoCodecKind::from);
let codec = match (self.format_codec(), user_codec) {
(Some(fmt), Some(user)) if fmt != user => {
anyhow::bail!(
"the output format implies video codec {fmt:?}, but --video-codec {user:?} was passed; \
remove --video-codec or pick a matching format"
);
}
(Some(fmt), _) => Some(fmt),
(None, user) => user,
};
let mut video = select::Video::default();
if let Some(name) = &self.select.video_name {
video = video.name(name);
}
if let Some(codec) = codec {
video = video.codec(codec);
}
let mut audio = select::Audio::default();
if let Some(name) = &self.select.audio_name {
audio = audio.name(name);
}
if let Some(codec) = self.select.audio_codec {
audio = audio.codec(codec.into());
}
Ok(select::Broadcast::default().video(video).audio(audio))
}
}
pub struct Subscribe {
source: moq_mux::Source,
catalog: CatalogFormat,
args: SubscribeArgs,
}
impl Subscribe {
pub fn new(source: moq_mux::Source, catalog: CatalogFormat, args: SubscribeArgs) -> Self {
Self { source, catalog, args }
}
async fn stream(&self) -> anyhow::Result<catalog::Select<catalog::Consumer>> {
let broadcast = self.source.broadcast().await?;
let consumer = catalog::Consumer::new(&broadcast, self.catalog).await?;
Ok(consumer.select(self.args.selection()?))
}
pub async fn run(self) -> anyhow::Result<()> {
match self.args.format {
SubscribeFormat::Fmp4 => self.run_fmp4().await,
SubscribeFormat::Mkv => self.run_mkv().await,
SubscribeFormat::H264 => self.run_h264().await,
SubscribeFormat::H265 => self.run_h265().await,
SubscribeFormat::Ts => self.run_ts().await,
SubscribeFormat::Flv => self.run_flv().await,
}
}
async fn run_fmp4(self) -> anyhow::Result<()> {
let mut stdout = tokio::io::stdout();
let stream = self.stream().await?;
let mut fmp4 = moq_mux::container::fmp4::Export::new(self.source, stream)
.with_latency(self.args.max_latency)
.with_fragment_duration(self.args.fragment_duration);
while let Some(chunk) = fmp4.next().await? {
stdout.write_all(&chunk).await?;
stdout.flush().await?;
}
Ok(())
}
async fn run_mkv(self) -> anyhow::Result<()> {
let mut stdout = tokio::io::stdout();
let stream = self.stream().await?;
let mut mkv = moq_mux::container::mkv::Export::new(self.source, stream)
.with_latency(self.args.max_latency)
.with_fragment_duration(self.args.fragment_duration);
while let Some(chunk) = mkv.next().await? {
stdout.write_all(&chunk).await?;
stdout.flush().await?;
}
Ok(())
}
async fn run_h264(self) -> anyhow::Result<()> {
let mut stdout = tokio::io::stdout();
let stream = self.stream().await?;
let mut h264 = moq_mux::codec::h264::Export::new(self.source, stream).with_latency(self.args.max_latency);
while let Some(chunk) = h264.next().await? {
stdout.write_all(&chunk).await?;
stdout.flush().await?;
}
Ok(())
}
async fn run_h265(self) -> anyhow::Result<()> {
let mut stdout = tokio::io::stdout();
let stream = self.stream().await?;
let mut h265 = moq_mux::codec::h265::Export::new(self.source, stream).with_latency(self.args.max_latency);
while let Some(chunk) = h265.next().await? {
stdout.write_all(&chunk).await?;
stdout.flush().await?;
}
Ok(())
}
async fn run_ts(self) -> anyhow::Result<()> {
let mut stdout = tokio::io::stdout();
let mut ts = moq_mux::container::ts::Export::with_ts(self.source, self.catalog)
.await?
.with_latency(self.args.max_latency);
while let Some(frame) = ts.next().await? {
stdout.write_all(&frame.payload).await?;
stdout.flush().await?;
}
Ok(())
}
async fn run_flv(self) -> anyhow::Result<()> {
let mut stdout = tokio::io::stdout();
let mut flv = moq_mux::container::flv::Export::with_catalog_format(self.source, self.catalog)
.await?
.with_latency(self.args.max_latency);
while let Some(chunk) = flv.next().await? {
stdout.write_all(&chunk).await?;
stdout.flush().await?;
}
Ok(())
}
}