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 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,
source: &moq_mux::Source,
window: Duration,
) -> Option<Self> {
let section = config.timeline.clone()?;
let live = Arc::new(segments::Producer::new());
let watcher = spawn_watcher(
source.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,
source: &moq_mux::Source,
window: Duration,
) -> Option<Self> {
let section = config.timeline.clone()?;
let live = Arc::new(segments::Producer::new());
let watcher = spawn_watcher(
source.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(self.section.timescale as u64).ok()?;
let units = wall as u128 + pts.as_scale(timescale);
let unix_ms = MOQ_EPOCH_UNIX_MILLIS as u128 + units * 1000 / timescale.as_u64() as u128;
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(crate) 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 Some(mut group) = fetch(&track, sequence).await? else {
return Ok(None);
};
if read_group(&mut muxer, &mut group).await?.is_none() {
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 Some(mut group) = fetch(&track, sequence).await? else {
if bounded {
return Ok(None);
}
break;
};
let Some(mut group_frames) = read_group(&mut muxer, &mut group).await? else {
if bounded {
return Ok(None);
}
break;
};
frames.append(&mut group_frames);
}
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<Option<moq_net::group::Consumer>> {
match track.fetch_group(sequence, None).await {
Ok(group) => Ok(Some(group)),
Err(err) if is_cache_miss(&err) => Ok(None),
Err(err) => Err(err.into()),
}
}
async fn read_group(
muxer: &mut Muxer,
group: &mut moq_net::group::Consumer,
) -> Result<Option<Vec<moq_mux::container::Frame>>> {
match muxer.read(group).await {
Ok(frames) => Ok(Some(frames)),
Err(err) if is_cache_miss_mux(&err) => Ok(None),
Err(err) => Err(err.into()),
}
}
fn is_cache_miss(err: &moq_net::Error) -> bool {
matches!(
err,
moq_net::Error::NotFound | moq_net::Error::Old | moq_net::Error::Evicted
)
}
fn is_cache_miss_mux(err: &moq_mux::Error) -> bool {
match err {
moq_mux::Error::Moq(err) => is_cache_miss(err),
moq_mux::Error::Cmaf(moq_mux::container::fmp4::Error::Moq(err)) => is_cache_miss(err),
_ => false,
}
}
fn spawn_watcher(
source: moq_mux::Source,
broadcast: Option<moq_net::PathRelativeOwned>,
section: Timeline,
live: Arc<segments::Producer>,
window: Duration,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
match watch(source, 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(
source: moq_mux::Source,
broadcast: Option<moq_net::PathRelativeOwned>,
section: &Timeline,
live: &segments::Producer,
window: Duration,
) -> Result<()> {
let broadcast = source.resolve(broadcast.as_ref()).await?;
let _ = live.broadcast.set(broadcast.clone());
let mut timeline = moq_mux::timeline::Consumer::<()>::subscribe(&broadcast, section).await?;
while let Some(entry) = timeline.next().await? {
live.push(entry, window);
}
Ok(())
}