use std::sync::Arc;
use std::time::{Duration, SystemTime};
use bytes::Bytes;
use hang::catalog::{AudioConfig, MOQ_EPOCH_UNIX_MILLIS, Timeline, VideoConfig};
use moq_mux::container::fmp4::Muxer;
use super::playlist::{Segment, Snapshot};
use super::segments;
use super::upstream::Upstream;
use crate::Result;
const DEFAULT_VIDEO_BITRATE: u64 = 2_000_000;
const DEFAULT_AUDIO_BITRATE: u64 = 128_000;
const MAX_SEGMENT_GROUPS: u64 = 1024;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Kind {
Video,
Audio,
}
impl Kind {
pub fn as_str(self) -> &'static str {
match self {
Kind::Video => "video",
Kind::Audio => "audio",
}
}
}
impl std::str::FromStr for Kind {
type Err = ();
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"video" => Ok(Kind::Video),
"audio" => Ok(Kind::Audio),
_ => Err(()),
}
}
}
#[derive(PartialEq)]
enum Config {
Video(VideoConfig),
Audio(AudioConfig),
}
pub struct Rendition {
pub name: String,
pub kind: Kind,
pub bandwidth: u64,
pub width: Option<u32>,
pub height: Option<u32>,
pub codec: String,
config: Config,
section: Timeline,
live: Arc<segments::Producer>,
target_duration: std::sync::atomic::AtomicU64,
init: tokio::sync::Mutex<Option<Bytes>>,
watcher: tokio::task::JoinHandle<()>,
}
impl Rendition {
pub(crate) fn matches_video(&self, config: &VideoConfig) -> bool {
self.config == Config::Video(config.clone())
}
pub(crate) fn matches_audio(&self, config: &AudioConfig) -> bool {
self.config == Config::Audio(config.clone())
}
pub(crate) fn video(name: String, config: &VideoConfig, upstream: &Upstream, window: Duration) -> Option<Self> {
let section = config.timeline.clone()?;
let live = Arc::new(segments::Producer::new(upstream.local(config.broadcast.as_ref())));
let watcher = spawn_watcher(
upstream.clone(),
config.broadcast.clone(),
section.clone(),
live.clone(),
window,
);
Some(Self {
name,
kind: Kind::Video,
bandwidth: config.bitrate.unwrap_or(DEFAULT_VIDEO_BITRATE),
width: config.coded_width,
height: config.coded_height,
codec: config.codec.to_string(),
config: Config::Video(config.clone()),
section,
live,
target_duration: std::sync::atomic::AtomicU64::new(0),
init: tokio::sync::Mutex::new(None),
watcher,
})
}
pub(crate) fn audio(name: String, config: &AudioConfig, upstream: &Upstream, window: Duration) -> Option<Self> {
let section = config.timeline.clone()?;
let live = Arc::new(segments::Producer::new(upstream.local(config.broadcast.as_ref())));
let watcher = spawn_watcher(
upstream.clone(),
config.broadcast.clone(),
section.clone(),
live.clone(),
window,
);
Some(Self {
name,
kind: Kind::Audio,
bandwidth: config.bitrate.unwrap_or(DEFAULT_AUDIO_BITRATE),
width: None,
height: None,
codec: config.codec.to_string(),
config: Config::Audio(config.clone()),
section,
live,
target_duration: std::sync::atomic::AtomicU64::new(0),
init: tokio::sync::Mutex::new(None),
watcher,
})
}
pub fn segments(self: &Arc<Self>) -> segments::Consumer {
self.live.subscribe(self.clone())
}
pub(crate) fn close(&self) {
self.live.close();
self.watcher.abort();
}
pub async fn playable(&self) {
kio::wait(|waiter| self.live.poll_playable(waiter)).await;
}
pub fn media_playlist(&self, query: Option<&str>) -> Option<String> {
self.is_playable().then(|| super::render_media(&self.playlist(), query))
}
pub(crate) fn playlist(&self) -> Snapshot {
let window = self.live.window();
let longest = window.segments.iter().map(|s| s.duration).fold(0.0f64, f64::max);
let want = (longest.ceil() as u64).max(1);
let target_duration = self
.target_duration
.fetch_max(want, std::sync::atomic::Ordering::Relaxed)
.max(want);
let program_date_time = window.segments.first().and_then(|first| self.wall_clock(first.pts));
Snapshot {
target_duration,
media_sequence: window.sequence,
segments: window
.segments
.into_iter()
.map(|s| Segment {
group: s.group,
duration: s.duration,
})
.collect(),
finished: window.ended,
program_date_time,
}
}
pub(crate) fn is_playable(&self) -> bool {
self.live.is_playable()
}
pub(crate) fn wall_clock(&self, pts: moq_net::Timestamp) -> Option<SystemTime> {
let wall = self.section.wall?;
let timescale = moq_net::Timescale::new(u64::from(self.section.timescale)).ok()?;
let units = u128::from(wall) + pts.as_scale(timescale);
let unix_ms = u128::from(MOQ_EPOCH_UNIX_MILLIS) + units * 1000 / u128::from(timescale.as_u64());
Some(SystemTime::UNIX_EPOCH + Duration::from_millis(u64::try_from(unix_ms).ok()?))
}
fn muxer(&self) -> Result<Muxer> {
Ok(match &self.config {
Config::Video(config) => Muxer::video(config)?,
Config::Audio(config) => Muxer::audio(config)?,
})
}
fn track(&self) -> Option<moq_net::track::Consumer> {
let broadcast = self.live.broadcast.get()?;
broadcast.track(&self.name).ok()
}
pub async fn init(&self) -> Result<Option<Bytes>> {
let mut cache = self.init.lock().await;
if let Some(bytes) = cache.as_ref() {
return Ok(Some(bytes.clone()));
}
let mut muxer = self.muxer()?;
let bytes = match muxer.init()? {
Some(bytes) => bytes,
None => {
let Some(sequence) = self.live.latest_group() else {
return Ok(None);
};
let Some(track) = self.track() else {
return Ok(None);
};
let Ok(mut group) = fetch(&track, sequence).await? else {
return Ok(None);
};
if read_group(&mut muxer, &mut group).await?.is_err() {
return Ok(None);
}
let Some(bytes) = muxer.init()? else {
return Ok(None);
};
bytes
}
};
*cache = Some(bytes.clone());
Ok(Some(bytes))
}
pub async fn segment(&self, sequence: u64) -> Result<Option<Bytes>> {
let Some((start, end)) = self.live.segment_groups(sequence) else {
return Ok(None);
};
let Some(track) = self.track() else {
return Ok(None);
};
let bounded = end.is_some();
if let Some(end) = end
&& end.saturating_sub(start) > MAX_SEGMENT_GROUPS
{
tracing::warn!(
start,
end,
"segment spans more than MAX_SEGMENT_GROUPS; refusing to truncate"
);
return Ok(None);
}
let end = end.unwrap_or(u64::MAX).min(start.saturating_add(MAX_SEGMENT_GROUPS));
let mut muxer = self.muxer()?;
let mut frames = Vec::new();
for sequence in start..end {
let miss = match fetch(&track, sequence).await? {
Ok(mut group) => match read_group(&mut muxer, &mut group).await? {
Ok(mut group_frames) => {
frames.append(&mut group_frames);
continue;
}
Err(err) => err,
},
Err(err) => err,
};
if !bounded {
break;
}
tracing::warn!(
segment = start,
group = sequence,
err = %miss,
"abandoning an advertised segment: a group is unavailable"
);
return Ok(None);
}
if frames.is_empty() {
return Ok(None);
}
Ok(Some(muxer.fragment(start as u32, &frames)?))
}
}
impl Drop for Rendition {
fn drop(&mut self) {
self.watcher.abort();
}
}
async fn fetch(
track: &moq_net::track::Consumer,
sequence: u64,
) -> Result<std::result::Result<moq_net::group::Consumer, moq_net::Error>> {
match track.fetch_group(sequence, None).await {
Ok(group) => Ok(Ok(group)),
Err(err) if is_cache_miss(&err) => Ok(Err(err)),
Err(err) => Err(err.into()),
}
}
async fn read_group(
muxer: &mut Muxer,
group: &mut moq_net::group::Consumer,
) -> Result<std::result::Result<Vec<moq_mux::container::Frame>, moq_net::Error>> {
match muxer.read(group).await {
Ok(frames) => Ok(Ok(frames)),
Err(err) => {
let miss = net_errors(&err).find(|err| is_cache_miss(err)).cloned();
match miss {
Some(miss) => Ok(Err(miss)),
None => Err(err.into()),
}
}
}
}
fn is_cache_miss(err: &moq_net::Error) -> bool {
let code = err.to_code();
code == moq_net::Error::NotFound.to_code()
|| code == moq_net::Error::Old.to_code()
|| code == moq_net::Error::Evicted.to_code()
}
fn net_errors(err: &moq_mux::Error) -> impl Iterator<Item = &moq_net::Error> {
std::iter::successors(Some(err as &(dyn std::error::Error + 'static)), |err| err.source())
.filter_map(|err| err.downcast_ref::<moq_net::Error>())
}
fn spawn_watcher(
upstream: Upstream,
broadcast: Option<moq_net::PathRelativeOwned>,
section: Timeline,
live: Arc<segments::Producer>,
window: Duration,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
match watch(upstream, broadcast, §ion, &live, window).await {
Ok(()) => live.end(),
Err(err) => {
tracing::warn!(track = %section.track, %err, "timeline watcher error; leaving the playlist live")
}
}
live.close();
})
}
async fn watch(
upstream: Upstream,
broadcast: Option<moq_net::PathRelativeOwned>,
section: &Timeline,
live: &segments::Producer,
window: Duration,
) -> Result<()> {
let broadcast = match live.broadcast.get() {
Some(broadcast) => broadcast.clone(),
None => {
let resolved = upstream.source.resolve(broadcast.as_ref()).await?;
let _ = live.broadcast.set(resolved.clone());
resolved
}
};
let mut timeline = moq_mux::timeline::Consumer::<()>::subscribe(&broadcast, section).await?;
while let Some(entry) = timeline.next().await? {
live.push(entry, window);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_cache_miss_that_crossed_a_session_is_still_a_cache_miss() {
for local in [moq_net::Error::NotFound, moq_net::Error::Old, moq_net::Error::Evicted] {
assert!(is_cache_miss(&local), "{local:?} locally");
let remote = moq_net::Error::Remote(local.to_code());
assert!(is_cache_miss(&remote), "{local:?} over the wire ({remote:?})");
}
}
#[test]
fn a_real_failure_is_not_a_cache_miss() {
for err in [
moq_net::Error::Unauthorized,
moq_net::Error::ProtocolViolation,
moq_net::Error::Timeout,
] {
assert!(!is_cache_miss(&err), "{err:?} locally");
let remote = moq_net::Error::Remote(err.to_code());
assert!(!is_cache_miss(&remote), "{err:?} over the wire ({remote:?})");
}
}
#[test]
fn a_cache_miss_is_recognised_through_the_mux_error_layers() {
let remote = moq_net::Error::Remote(moq_net::Error::NotFound.to_code());
assert!(net_errors(&moq_mux::Error::Moq(remote.clone())).any(is_cache_miss));
assert!(net_errors(&moq_mux::Error::Hang(hang::Error::Moq(remote.clone()))).any(is_cache_miss));
assert!(net_errors(&moq_mux::Error::Cmaf(moq_mux::container::fmp4::Error::Moq(remote))).any(is_cache_miss));
let denied = moq_net::Error::Unauthorized;
assert!(!net_errors(&moq_mux::Error::Moq(denied.clone())).any(is_cache_miss));
assert!(!net_errors(&moq_mux::Error::Hang(hang::Error::Moq(denied))).any(is_cache_miss));
assert!(!net_errors(&moq_mux::Error::UnknownFormat("nope".to_string())).any(is_cache_miss));
}
}