use std::sync::{Arc, Mutex, RwLock};
use std::task::Poll;
use std::time::{Duration, SystemTime};
use bytes::Bytes;
use hang::catalog::{Archive, AudioConfig, Clock, VideoConfig};
use moq_mux::container::fmp4::Muxer;
use moq_mux::timeline::Entry;
use sha2::{Digest, Sha256};
use super::playlist::{Segment, Snapshot};
use super::upstream::Upstream;
use super::{mpd, segments};
use crate::Result;
const DEFAULT_VIDEO_BITRATE: u64 = 2_000_000;
const DEFAULT_AUDIO_BITRATE: u64 = 128_000;
fn default_bandwidth(kind: Kind) -> u64 {
match kind {
Kind::Video => DEFAULT_VIDEO_BITRATE,
Kind::Audio => DEFAULT_AUDIO_BITRATE,
}
}
const MAX_SEGMENT_GROUPS: u64 = 1024;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Kind {
Video,
Audio,
}
impl Kind {
pub fn parse(value: &str) -> Option<Self> {
match value {
"video" => Some(Self::Video),
"audio" => Some(Self::Audio),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Kind::Video => "video",
Kind::Audio => "audio",
}
}
}
const FEED_MAX_DELAY: Duration = Duration::from_secs(30);
struct Init {
bytes: Bytes,
hash: String,
}
impl Init {
fn new(bytes: Bytes) -> Self {
let hash = Sha256::digest(&bytes)[..8].iter().map(|b| format!("{b:02x}")).collect();
Self { bytes, hash }
}
}
#[derive(Clone, Default)]
struct Run {
generation: Option<Arc<str>>,
init: Option<Arc<Init>>,
epoch: u64,
}
enum Config {
Video(VideoConfig),
Audio(AudioConfig),
}
struct Media {
handle: Mutex<Handle>,
sibling: Option<(moq_mux::Source, moq_net::path::RelativeOwned)>,
}
struct Handle {
binding: Arc<moq_mux::Binding>,
waiting: bool,
}
impl Media {
fn bind(upstream: &Upstream, rel: Option<&moq_net::path::RelativeOwned>) -> moq_mux::Result<Self> {
Ok(Self {
handle: Mutex::new(Handle {
binding: Arc::new(upstream.bind(rel)?),
waiting: false,
}),
sibling: sibling(upstream, rel),
})
}
fn admits(&self, window: &segments::Producer) -> bool {
self.sync(window);
!self.handle.lock().expect("media lock poisoned").waiting
}
fn sync(&self, window: &segments::Producer) -> Arc<moq_mux::Binding> {
let Some((source, rel)) = &self.sibling else {
return self.handle.lock().expect("media lock poisoned").binding.clone();
};
let mut handle = self.handle.lock().expect("media lock poisoned");
match handle.binding.poll_broadcast(&kio::Waiter::noop()) {
Poll::Ready(Ok(broadcast)) if broadcast.is_closed() => {
window.clear();
if let Ok(next) = source.bind(Some(rel)) {
handle.binding = Arc::new(next);
}
handle.waiting = true;
}
Poll::Ready(Ok(_)) => handle.waiting = false,
Poll::Ready(Err(_)) => {
if let Ok(next) = source.bind(Some(rel)) {
handle.binding = Arc::new(next);
}
}
Poll::Pending => {}
}
handle.binding.clone()
}
}
fn sibling(
upstream: &Upstream,
rel: Option<&moq_net::path::RelativeOwned>,
) -> Option<(moq_mux::Source, moq_net::path::RelativeOwned)> {
let target = upstream.source.resolve_reference(rel)?;
if upstream.source.resolve_reference(None).as_ref() == Some(&target) {
return None;
}
Some((
upstream.source.clone(),
rel.cloned().unwrap_or_else(moq_net::path::Relative::empty),
))
}
fn normalize_video(config: &VideoConfig) -> VideoConfig {
let mut config = config.clone();
config.bitrate = None;
config.jitter = None;
config.delay = None;
config.label = None;
config.stalled = None;
config.framerate = None;
config
}
fn normalize_audio(config: &AudioConfig) -> AudioConfig {
let mut config = config.clone();
config.bitrate = None;
config.jitter = None;
config.delay = None;
config.label = None;
config
}
pub struct Rendition {
pub name: String,
pub kind: Kind,
bitrate: RwLock<Option<u64>>,
framerate: RwLock<Option<f64>>,
pub width: Option<u32>,
pub height: Option<u32>,
pub codec: String,
config: Config,
section: Archive,
clock: Option<Clock>,
live: Arc<segments::Producer>,
media: Media,
run: Mutex<Run>,
building: tokio::sync::Mutex<()>,
}
impl Rendition {
pub(crate) fn matches_video(&self, config: &VideoConfig) -> bool {
let Config::Video(current) = &self.config else {
return false;
};
normalize_video(current) == normalize_video(config)
}
pub(crate) fn matches_audio(&self, config: &AudioConfig) -> bool {
let Config::Audio(current) = &self.config else {
return false;
};
normalize_audio(current) == normalize_audio(config)
}
pub fn bandwidth(&self) -> u64 {
self.bitrate().unwrap_or(default_bandwidth(self.kind))
}
pub(crate) fn refresh(&self, bitrate: Option<u64>, framerate: Option<f64>) {
*self.bitrate.write().expect("bitrate lock poisoned") = bitrate;
*self.framerate.write().expect("framerate lock poisoned") = framerate;
}
fn bitrate(&self) -> Option<u64> {
*self.bitrate.read().expect("bitrate lock poisoned")
}
fn framerate(&self) -> Option<f64> {
*self.framerate.read().expect("framerate lock poisoned")
}
pub(crate) fn video(
name: String,
config: &VideoConfig,
upstream: &Upstream,
section: Archive,
clock: Option<Clock>,
) -> moq_mux::Result<Self> {
Ok(Self {
name,
kind: Kind::Video,
bitrate: RwLock::new(config.bitrate),
framerate: RwLock::new(config.framerate),
width: config.coded_width,
height: config.coded_height,
codec: config.codec.to_string(),
config: Config::Video(config.clone()),
section,
clock,
live: Arc::new(segments::Producer::new()),
media: Media::bind(upstream, config.broadcast.as_ref())?,
run: Mutex::default(),
building: tokio::sync::Mutex::new(()),
})
}
pub(crate) fn audio(
name: String,
config: &AudioConfig,
upstream: &Upstream,
section: Archive,
clock: Option<Clock>,
) -> moq_mux::Result<Self> {
Ok(Self {
name,
kind: Kind::Audio,
bitrate: RwLock::new(config.bitrate),
framerate: RwLock::new(None),
width: None,
height: None,
codec: config.codec.to_string(),
config: Config::Audio(config.clone()),
section,
clock,
live: Arc::new(segments::Producer::new()),
media: Media::bind(upstream, config.broadcast.as_ref())?,
run: Mutex::default(),
building: tokio::sync::Mutex::new(()),
})
}
pub(crate) fn push(&self, index: u64, entry: &Entry, discontinuity: u64, window: Duration) {
if !self.media.admits(&self.live) {
return;
}
let row = segments::Row {
index,
segment: entry.segment,
ranges: entry.tracks.get(&self.name).cloned().unwrap_or_default(),
duration: entry.duration,
pts: entry.pts,
end: Duration::from(entry.pts) + entry.duration,
discontinuity,
};
self.live.push(row, window);
}
pub(crate) fn pop(&self, range: std::ops::Range<u64>) {
self.live.pop(range);
}
pub(crate) fn clear(&self) {
self.live.clear();
}
pub(crate) fn label(&self, generation: Option<Arc<str>>) {
self.run.lock().expect("run lock poisoned").generation = generation;
}
pub(crate) fn restart(&self, generation: Option<Arc<str>>) {
self.live.clear();
let mut run = self.run.lock().expect("run lock poisoned");
*run = Run {
generation,
init: None,
epoch: run.epoch + 1,
};
}
fn run(&self) -> Run {
self.run.lock().expect("run lock poisoned").clone()
}
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub(crate) fn generation(&self) -> Option<Arc<str>> {
self.run().generation
}
pub(crate) fn end(&self) {
self.live.end();
}
pub fn segments(self: &Arc<Self>) -> segments::Consumer {
self.live.subscribe(self.clone())
}
pub(crate) fn close(&self) {
self.live.close();
}
pub async fn playable(&self) {
kio::wait(|waiter| self.poll_playable(waiter)).await;
}
pub(crate) fn poll_playable(&self, waiter: &kio::Waiter) -> Poll<()> {
self.media.sync(&self.live);
self.live.poll_playable(waiter)
}
pub fn media_playlist(&self, query: Option<&str>) -> Option<String> {
let run = self.run();
let init = self.built_init(&run)?;
self.is_playable()
.then(|| super::render_media(&self.snapshot_as(run.generation), &init.hash, query))
}
pub async fn playlist(&self, query: Option<&str>) -> Result<Option<String>> {
self.playable().await;
if self.init().await?.is_none() {
return Ok(None);
}
Ok(self.media_playlist(query))
}
#[cfg(test)]
pub(crate) fn snapshot(&self) -> Snapshot {
self.snapshot_as(self.generation())
}
fn snapshot_as(&self, generation: Option<Arc<str>>) -> Snapshot {
self.media.sync(&self.live);
let window = self.live.window();
let declared = self
.section
.duration_max
.map(|max| max.div_ceil((self.section.timescale as u64).max(1)))
.unwrap_or(0);
let observed = window
.segments
.iter()
.map(|s| s.duration.as_secs_f64().ceil() as u64)
.max()
.unwrap_or(0);
let target_duration = declared.max(observed).max(1);
let program_date_time = window.segments.first().and_then(|first| self.wall_clock(first.pts));
let mut previous: Option<u64> = None;
let segments = window
.segments
.into_iter()
.map(|s| {
let discontinuity = previous.replace(s.discontinuity).is_some_and(|p| p != s.discontinuity);
Segment {
segment: s.segment,
duration: s.duration,
gap: s.ranges.is_empty(),
discontinuity,
}
})
.collect();
Snapshot {
target_duration,
media_sequence: window.sequence,
segments,
finished: window.ended,
program_date_time,
generation,
}
}
pub(crate) fn is_playable(&self) -> bool {
self.media.sync(&self.live);
self.live.is_playable()
}
pub(crate) fn representation(&self) -> Option<mpd::Representation> {
let run = self.run();
let init = self.built_init(&run)?;
self.media.sync(&self.live);
let window = self.live.window();
let timescale = self.timescale();
let segments = window
.segments
.iter()
.filter(|row| !row.ranges.is_empty())
.filter_map(|row| {
let t = row.pts.as_scale(timescale) as u64;
let span = row.end.saturating_sub(row.pts.into());
let d = (span.as_nanos() * timescale.as_u64() as u128 / 1_000_000_000) as u64;
(d > 0).then_some((t, d))
})
.collect();
let (framerate, sample_rate, channel_count) = match &self.config {
Config::Video(_) => (self.framerate(), None, None),
Config::Audio(config) => (None, Some(config.sample_rate), Some(config.channel_count)),
};
Some(mpd::Representation {
name: self.name.clone(),
kind: self.kind,
bandwidth: self.bandwidth(),
codec: self.codec.clone(),
width: self.width,
height: self.height,
framerate,
sample_rate,
channel_count,
timescale: self.section.timescale.max(1),
segments,
ended: window.ended,
init: init.hash.clone(),
generation: run.generation,
})
}
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub(crate) async fn segment_at(&self, time: u64) -> Result<Option<Bytes>> {
self.media.sync(&self.live);
let Some(segment) = self.live.segment_number_at(time, self.timescale()) else {
return Ok(None);
};
self.segment(segment).await
}
fn timescale(&self) -> moq_net::Timescale {
moq_net::Timescale::new((self.section.timescale as u64).max(1)).expect("clamped nonzero timescale")
}
pub(crate) fn wall_clock(&self, pts: moq_net::Timestamp) -> Option<SystemTime> {
let clock = self.clock.as_ref()?;
clock.wall_clock(pts).ok()
}
pub(crate) fn muxer(&self) -> Result<Muxer> {
let bitrate = self.bitrate();
Ok(match &self.config {
Config::Video(config) => {
let mut config = config.clone();
config.bitrate = bitrate;
Muxer::video(&config)?.with_framerate(self.framerate())
}
Config::Audio(config) => {
let mut config = config.clone();
config.bitrate = bitrate;
Muxer::audio(&config)?
}
})
}
pub(crate) fn binding(&self) -> Arc<moq_mux::Binding> {
self.media.sync(&self.live)
}
pub(crate) fn poll_subscribe(
&self,
binding: &moq_mux::Binding,
waiter: &kio::Waiter,
) -> Poll<Option<moq_net::track::Subscribing>> {
let Ok(broadcast) = std::task::ready!(binding.poll_broadcast(waiter)) else {
return Poll::Ready(None);
};
let track = broadcast.track(&self.name).ok();
let subscription = moq_net::track::Subscription::default().with_max_age(FEED_MAX_DELAY);
Poll::Ready(track.map(|track| track.subscribe(subscription).into_inner()))
}
async fn track(
&self,
binding: &moq_mux::Binding,
) -> Option<(moq_net::broadcast::Consumer, moq_net::track::Consumer)> {
let broadcast = binding.broadcast().await.ok()?;
let track = broadcast.track(&self.name).ok()?;
Some((broadcast, track))
}
pub async fn init(&self) -> Result<Option<Bytes>> {
Ok(self.load_init().await?.map(|init| init.bytes.clone()))
}
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub(crate) async fn init_versioned(&self, hash: &str) -> Result<Option<Bytes>> {
let init = self.load_init().await?;
Ok(init.filter(|init| init.hash == hash).map(|init| init.bytes.clone()))
}
fn built_init(&self, run: &Run) -> Option<Arc<Init>> {
if let Some(init) = &run.init {
return Some(init.clone());
}
let bytes = self.muxer().ok()?.init().ok()??;
Some(self.cache_init(run.epoch, bytes))
}
fn cache_init(&self, epoch: u64, bytes: Bytes) -> Arc<Init> {
let mut run = self.run.lock().expect("run lock poisoned");
if run.epoch != epoch {
return Arc::new(Init::new(bytes));
}
run.init.get_or_insert_with(|| Arc::new(Init::new(bytes))).clone()
}
async fn load_init(&self) -> Result<Option<Arc<Init>>> {
let binding = self.media.sync(&self.live);
if let Some(init) = self.run().init {
return Ok(Some(init));
}
let _building = self.building.lock().await;
let run = self.run();
if let Some(init) = run.init {
return Ok(Some(init));
}
let mut muxer = self.muxer()?;
let bytes = match muxer.init()? {
Some(bytes) => bytes,
None => {
let Some(sequence) = self.live.latest_keyframe_group() else {
return Ok(None);
};
let Some((broadcast, track)) = self.track(&binding).await else {
return Ok(None);
};
let Ok(mut group) = fetch(&track, sequence, &broadcast).await? else {
return Ok(None);
};
if read_group(&mut muxer, &mut group, &broadcast).await?.is_err() {
return Ok(None);
}
let Some(bytes) = muxer.init()? else {
return Ok(None);
};
bytes
}
};
Ok(Some(self.cache_init(run.epoch, bytes)))
}
pub async fn segment(&self, segment: u64) -> Result<Option<Bytes>> {
let binding = self.media.sync(&self.live);
let run = self.run();
let Some(ranges) = self.live.segment_ranges(segment) else {
return Ok(None);
};
if ranges.is_empty() {
return Ok(None);
}
let total: u64 = ranges
.iter()
.map(|r| r.end.saturating_sub(r.start).saturating_add(1))
.sum();
if total > MAX_SEGMENT_GROUPS {
tracing::warn!(
total,
"segment spans more than MAX_SEGMENT_GROUPS; refusing to truncate"
);
return Ok(None);
}
let Some((broadcast, track)) = self.track(&binding).await else {
return Ok(None);
};
let mut muxer = self.muxer()?;
let mut frames = Vec::new();
for range in &ranges {
for sequence in range.start..=range.end {
let miss = match fetch(&track, sequence, &broadcast).await? {
Ok(mut group) => match read_group(&mut muxer, &mut group, &broadcast).await? {
Ok(mut group_frames) => {
frames.append(&mut group_frames);
continue;
}
Err(err) => err,
},
Err(err) => err,
};
tracing::warn!(
segment,
group = sequence,
err = %miss,
"abandoning an advertised segment: a group is unavailable"
);
return Ok(None);
}
}
if frames.is_empty() {
return Ok(None);
}
let fragment = muxer.fragment(segment as u32, &frames)?;
if let Some(bytes) = muxer.init()? {
self.cache_init(run.epoch, bytes);
}
Ok(Some(fragment))
}
}
async fn fetch(
track: &moq_net::track::Consumer,
sequence: u64,
broadcast: &moq_net::broadcast::Consumer,
) -> 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_unservable(&err, broadcast) => Ok(Err(err)),
Err(err) => Err(err.into()),
}
}
async fn read_group(
muxer: &mut Muxer,
group: &mut moq_net::group::Consumer,
broadcast: &moq_net::broadcast::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_unservable(err, broadcast)).cloned();
match miss {
Some(miss) => Ok(Err(miss)),
None => Err(err.into()),
}
}
}
}
fn is_unservable(err: &moq_net::Error, broadcast: &moq_net::broadcast::Consumer) -> bool {
is_cache_miss(err) || is_gone_publisher(err, broadcast)
}
fn is_cache_miss(err: &moq_net::Error) -> bool {
matches!(
err,
moq_net::Error::NotFound
| moq_net::Error::Old
| moq_net::Error::Evicted
| moq_net::Error::Stream(
moq_net::StreamError::NotFound | moq_net::StreamError::Old | moq_net::StreamError::Evicted
)
)
}
fn is_gone_publisher(err: &moq_net::Error, broadcast: &moq_net::broadcast::Consumer) -> bool {
broadcast.is_closed() || matches!(err, moq_net::Error::Dropped | moq_net::Error::Cancel)
}
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>())
}
#[cfg(test)]
mod tests {
use super::*;
fn over_lite(local: &moq_net::Error) -> moq_net::Error {
moq_net::Error::from(moq_net::StreamError::from_code(
moq_net::StreamError::from(local).to_code(),
))
}
fn over_ietf_miss() -> moq_net::Error {
moq_net::Error::from(moq_net::StreamError::from_code(0))
}
fn produce_origin() -> moq_net::origin::Producer {
let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default());
std::mem::forget(driver);
producer
}
#[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 lite = over_lite(&local);
assert!(is_cache_miss(&lite), "{local:?} over lite ({lite:?})");
assert!(!is_cache_miss(&over_ietf_miss()), "{local:?} over ietf must not 404");
}
}
#[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,
moq_net::Error::from(moq_net::SessionError::Unauthorized),
moq_net::Error::from(moq_net::StreamError::DeliveryTimeout),
] {
assert!(!is_cache_miss(&err), "{err:?} locally");
assert!(!is_cache_miss(&over_lite(&err)), "{err:?} over lite");
}
}
#[test]
fn a_cache_miss_is_recognised_through_the_mux_error_layers() {
let lite = over_lite(&moq_net::Error::NotFound);
assert!(net_errors(&moq_mux::Error::Moq(lite.clone())).any(is_cache_miss));
assert!(net_errors(&moq_mux::Error::Hang(hang::Error::Moq(lite.clone()))).any(is_cache_miss));
assert!(net_errors(&moq_mux::Error::Cmaf(moq_mux::container::fmp4::Error::Moq(lite))).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));
}
#[test]
fn a_gone_publisher_is_unservable_and_an_ietf_miss_is_not() {
let origin = produce_origin();
let producer = origin.create_broadcast("live").expect("publish allowed");
let live = producer.consume();
let ietf_miss = over_ietf_miss();
assert!(
!is_gone_publisher(&ietf_miss, &live),
"an IETF stream-reset miss on a live publisher must stay 500"
);
assert!(!is_unservable(&ietf_miss, &live));
drop(producer);
let gone = live;
assert!(is_gone_publisher(&ietf_miss, &gone));
assert!(is_unservable(&ietf_miss, &gone));
assert!(is_gone_publisher(&moq_net::Error::Dropped, &gone));
assert!(is_gone_publisher(&moq_net::Error::Cancel, &gone));
assert!(!is_cache_miss(&moq_net::Error::Dropped));
assert!(!is_cache_miss(&moq_net::Error::Remote(0)));
}
}