pub mod catchup;
pub mod dash;
pub mod ll_dash;
pub mod llhls;
pub mod smooth;
pub mod ts_hls;
#[cfg(feature = "whep")]
pub mod whep;
use std::sync::Arc;
use axum::Router;
use crate::route::RouteHandle;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum OutputKind {
#[serde(rename = "llhls")]
LlHls,
#[serde(rename = "dash")]
Dash,
#[serde(rename = "ll_dash")]
LlDash,
#[serde(rename = "smooth")]
Smooth,
#[serde(rename = "ts_hls")]
TsHls,
#[serde(rename = "catchup")]
Catchup,
#[serde(rename = "srt_push")]
SrtPush {
url: String,
#[serde(default)]
format: Option<crate::config::PushFormat>,
#[serde(default)]
reconnect: Option<crate::config::ReconnectPolicy>,
},
#[serde(rename = "rtmp_push")]
RtmpPush {
url: String,
#[serde(default)]
format: Option<crate::config::PushFormat>,
#[serde(default)]
reconnect: Option<crate::config::ReconnectPolicy>,
},
#[serde(rename = "rtsp_push")]
RtspPush {
url: String,
#[serde(default)]
format: Option<crate::config::PushFormat>,
#[serde(default)]
reconnect: Option<crate::config::ReconnectPolicy>,
},
#[serde(rename = "custom")]
Custom {
type_tag: String,
#[serde(default)]
params: serde_json::Value,
},
#[cfg(feature = "whep")]
#[serde(rename = "whep")]
Whep {
listen: String,
},
}
impl OutputKind {
pub fn name(&self) -> &str {
match self {
OutputKind::LlHls => "llhls",
OutputKind::Dash => "dash",
OutputKind::LlDash => "ll_dash",
OutputKind::Smooth => "smooth",
OutputKind::TsHls => "ts_hls",
OutputKind::Catchup => "catchup",
OutputKind::SrtPush { .. } => "srt_push",
OutputKind::RtmpPush { .. } => "rtmp_push",
OutputKind::RtspPush { .. } => "rtsp_push",
OutputKind::Custom { type_tag, .. } => type_tag,
#[cfg(feature = "whep")]
OutputKind::Whep { .. } => "whep",
}
}
pub fn is_push(&self) -> bool {
matches!(
self,
OutputKind::SrtPush { .. } | OutputKind::RtmpPush { .. } | OutputKind::RtspPush { .. }
)
}
pub fn is_whep(&self) -> bool {
#[cfg(feature = "whep")]
{
matches!(self, OutputKind::Whep { .. })
}
#[cfg(not(feature = "whep"))]
{
false
}
}
pub fn build(&self) -> Arc<dyn Output> {
self.build_with_playlist_name(llhls::DEFAULT_PLAYLIST_NAME)
}
pub fn build_with_playlist_name(&self, playlist_name: &str) -> Arc<dyn Output> {
match self {
OutputKind::LlHls => Arc::new(llhls::LlHlsOutput::new(playlist_name)),
OutputKind::Dash => Arc::new(dash::DashOutput),
OutputKind::LlDash => Arc::new(ll_dash::LlDashOutput),
OutputKind::Smooth => Arc::new(smooth::SmoothOutput),
OutputKind::TsHls => Arc::new(ts_hls::TsHlsOutput::new(playlist_name)),
OutputKind::Catchup => Arc::new(catchup::CatchupOutput),
OutputKind::SrtPush { .. }
| OutputKind::RtmpPush { .. }
| OutputKind::RtspPush { .. } => {
unreachable!(
"OutputKind::SrtPush/RtmpPush/RtspPush produce no Arc<dyn Output> — \
push outputs are driven by crate::push::drive_push"
)
}
OutputKind::Custom { .. } => unreachable!(
"OutputKind::Custom cannot be built without a SchemeRegistry — \
crate::origin::serve_with_registry resolves it via \
`registry.output(type_tag)` instead of this method"
),
#[cfg(feature = "whep")]
OutputKind::Whep { .. } => unreachable!(
"OutputKind::Whep produces no Arc<dyn Output> — WHEP egress is driven by \
crate::output::whep::run_whep, a raw listen socket, exactly like WHIP \
ingest on the source side"
),
}
}
}
broadcast_common::impl_spec_display!(OutputKind);
pub trait Output: Send + Sync + 'static {
fn kind(&self) -> OutputKind;
fn manifest_routes(&self, route: Arc<RouteHandle>) -> Router;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn output_kind_name_and_display_agree() {
for (kind, label) in [
(OutputKind::LlHls, "llhls"),
(OutputKind::Dash, "dash"),
(OutputKind::LlDash, "ll_dash"),
(OutputKind::Smooth, "smooth"),
(OutputKind::TsHls, "ts_hls"),
(OutputKind::Catchup, "catchup"),
] {
assert_eq!(kind.name(), label);
assert_eq!(kind.to_string(), label);
}
}
#[test]
fn every_output_kind_merges_without_panicking() {
let route = Arc::new(RouteHandle::new(1.0, 250, 8));
let mut merged = Router::new();
for kind in [
OutputKind::LlHls,
OutputKind::Dash,
OutputKind::LlDash,
OutputKind::Smooth,
OutputKind::Catchup,
] {
merged = merged.merge(kind.build().manifest_routes(route.clone()));
}
let _: Router = merged;
let _: Router = Router::new().merge(OutputKind::TsHls.build().manifest_routes(route));
}
#[test]
fn output_kind_serde_round_trips() {
for kind in [
OutputKind::LlHls,
OutputKind::Dash,
OutputKind::LlDash,
OutputKind::Smooth,
OutputKind::TsHls,
OutputKind::Catchup,
] {
let json = serde_json::to_string(&kind).unwrap();
let back: OutputKind = serde_json::from_str(&json).unwrap();
assert_eq!(back.name(), kind.name());
}
assert_eq!(
serde_json::to_string(&OutputKind::LlHls).unwrap(),
"\"llhls\""
);
assert_eq!(
serde_json::to_string(&OutputKind::TsHls).unwrap(),
"\"ts_hls\""
);
}
#[test]
fn output_kind_build_matches_kind() {
assert!(matches!(
OutputKind::LlHls.build().kind(),
OutputKind::LlHls
));
assert!(matches!(OutputKind::Dash.build().kind(), OutputKind::Dash));
assert!(matches!(
OutputKind::LlDash.build().kind(),
OutputKind::LlDash
));
assert!(matches!(
OutputKind::TsHls.build().kind(),
OutputKind::TsHls
));
assert!(matches!(
OutputKind::Catchup.build().kind(),
OutputKind::Catchup
));
}
#[test]
fn output_kind_custom_deserializes_with_type_tag_and_params() {
let json = r#"{ "custom": { "type_tag": "webrtc", "params": { "k": "v" } } }"#;
let kind: OutputKind = serde_json::from_str(json).unwrap();
match &kind {
OutputKind::Custom { type_tag, params } => {
assert_eq!(type_tag, "webrtc");
assert_eq!(params.get("k").and_then(|v| v.as_str()), Some("v"));
}
other => panic!("expected OutputKind::Custom, got {other:?}"),
}
assert_eq!(kind.name(), "webrtc");
}
#[test]
#[should_panic(expected = "SchemeRegistry")]
fn output_kind_custom_build_panics() {
let kind = OutputKind::Custom {
type_tag: "webrtc".into(),
params: serde_json::Value::Null,
};
let _ = kind.build();
}
}