moq-mux 0.10.13

Media muxers and demuxers for MoQ
Documentation
//! Low Overhead Container.
//!
//! The IETF draft replacement for hang's Legacy format. Each moq frame
//! holds a small property block (timestamp, optional per-frame
//! timescale) followed by the codec bitstream. Defaults to microsecond
//! timestamps. See [draft-ietf-moq-loc-04](https://www.ietf.org/archive/id/draft-ietf-moq-loc-04.html).

use std::task::Poll;

use moq_net::{Timescale, Timestamp};

use crate::container::{Container, Frame, Kind};

/// LOC's catalog convention: timestamps are in microseconds when no per-frame
/// 0x08 timescale property is present.
const DEFAULT_TIMESCALE: Timescale = Timescale::MICRO;

/// LOC wire format configured for the track's media role.
pub struct Wire(
	/// The kind of content carried by the track.
	pub Kind,
);

impl Container for Wire {
	type Error = crate::Error;

	fn write(&self, group: &mut moq_net::group::Producer, frames: &[Frame]) -> Result<(), Self::Error> {
		for frame in frames {
			// LOC's wire format omits per-frame timescale by convention; the catalog
			// default is microseconds, so convert at the boundary.
			let timestamp = frame.timestamp.convert(DEFAULT_TIMESCALE).map_err(hang::Error::from)?;
			let data = moq_loc::encode(timestamp.value(), &frame.payload)?;

			// Carry the timestamp on the net frame too (converted to the track's
			// timescale), so a relay sees it without parsing the LOC payload.
			let mut chunked = group.create_frame(moq_net::frame::Info {
				size: data.len() as u64,
				timestamp: frame.timestamp,
			})?;
			chunked.write(data)?;
			chunked.finish()?;
		}
		Ok(())
	}

	fn poll_read(
		&self,
		group: &mut moq_net::group::Consumer,
		waiter: &kio::Waiter,
	) -> Poll<Result<Option<Vec<Frame>>, Self::Error>> {
		use std::task::ready;

		let Some(frame) = ready!(group.poll_read_frame(waiter)?) else {
			return Poll::Ready(Ok(None));
		};

		let loc = moq_loc::decode(frame.payload)?;
		// `loc.timescale == Some(0)` is a malformed wire (caught by moq_loc::decode itself),
		// so any Some(_) we see here is non-zero. Falling back to the catalog default
		// keeps this code path infallible.
		let scale = loc
			.timescale
			.and_then(|s| Timescale::new(s).ok())
			.unwrap_or(DEFAULT_TIMESCALE);
		let timestamp = Timestamp::new(loc.timestamp, scale).map_err(hang::Error::from)?;

		Poll::Ready(Ok(Some(vec![Frame {
			timestamp,
			payload: loc.payload,
			// LOC doesn't carry the keyframe bit on the wire; the
			// wrapping Consumer fills it in from group position.
			keyframe: false,
			// LOC carries no per-frame duration.
			duration: None,
		}])))
	}

	fn kind(&self) -> Kind {
		self.0
	}

	fn end(&self, frame: &Frame) -> Option<moq_net::Timestamp> {
		(self.0 != Kind::Data && frame.payload.is_empty()).then_some(frame.timestamp)
	}

	fn finish_group(
		&self,
		group: &mut moq_net::group::Producer,
		end: Option<moq_net::Timestamp>,
	) -> Result<(), Self::Error> {
		if self.0 == Kind::Video
			&& let Some(timestamp) = end
		{
			self.write(
				group,
				&[Frame {
					timestamp,
					payload: bytes::Bytes::new(),
					keyframe: false,
					duration: None,
				}],
			)?;
		}
		Ok(())
	}
}