moq-net 0.2.1

The networking layer for Media over QUIC: real-time pub/sub with built-in caching, fan-out, and prioritization.
Documentation
use std::{borrow::Cow, time::Duration};

use crate::{
	Path, Timescale,
	coding::{Decode, DecodeError, Encode, EncodeError},
};

use super::{Message, Version};

/// Sent by the subscriber on a Track Stream (0x6) to request a track's immutable
/// publisher properties, without subscribing or fetching.
///
/// Lite05+ only.
#[derive(Clone, Debug)]
pub struct Track<'a> {
	pub broadcast: Path<'a>,
	pub track: Cow<'a, str>,
}

impl Message for Track<'_> {
	fn decode_msg<R: bytes::Buf>(r: &mut R, version: Version) -> Result<Self, DecodeError> {
		if !version.has_track_stream() {
			return Err(DecodeError::Version);
		}

		let broadcast = Path::decode(r, version)?;
		let track = Cow::<str>::decode(r, version)?;

		Ok(Self { broadcast, track })
	}

	fn encode_msg<W: bytes::BufMut>(&self, w: &mut W, version: Version) -> Result<(), EncodeError> {
		if !version.has_track_stream() {
			return Err(EncodeError::Version);
		}

		self.broadcast.encode(w, version)?;
		self.track.encode(w, version)?;
		Ok(())
	}
}

/// The publisher's sole reply on a Track Stream, carrying the track's immutable
/// properties. Every field is fixed for the lifetime of the track, so a subscriber
/// fetches this once and reuses it across every SUBSCRIBE and FETCH.
///
/// Lite05+ only.
#[derive(Clone, Debug)]
pub struct TrackInfo {
	/// The publisher's tie-break priority for this track.
	pub priority: u8,
	/// Whether groups are prioritized in sequence order. Groups may always arrive
	/// out-of-order (or not at all) over the network.
	pub ordered: bool,
	/// Publisher Max Latency: an upper bound on how long the publisher caches a
	/// non-latest group past the arrival of a newer one. Encoded as milliseconds.
	pub latency_max: Duration,
	/// Per-frame timestamp scale (units per second). Mandatory on Lite05+: every track
	/// is timed, so this is always a real scale on the wire (never zero).
	pub timescale: Timescale,
}

impl Message for TrackInfo {
	fn decode_msg<R: bytes::Buf>(r: &mut R, version: Version) -> Result<Self, DecodeError> {
		if !version.has_track_stream() {
			return Err(DecodeError::Version);
		}

		let priority = u8::decode(r, version)?;
		let ordered = u8::decode(r, version)? != 0;
		let latency_max = Duration::decode(r, version)?;
		let timescale = Timescale::new(u64::decode(r, version)?).map_err(|_| DecodeError::InvalidValue)?;

		Ok(Self {
			priority,
			ordered,
			latency_max,
			timescale,
		})
	}

	fn encode_msg<W: bytes::BufMut>(&self, w: &mut W, version: Version) -> Result<(), EncodeError> {
		if !version.has_track_stream() {
			return Err(EncodeError::Version);
		}

		self.priority.encode(w, version)?;
		(self.ordered as u8).encode(w, version)?;
		self.latency_max.encode(w, version)?;
		u64::from(self.timescale).encode(w, version)?;
		Ok(())
	}
}

#[cfg(test)]
mod test {
	use super::*;

	fn info_sample() -> TrackInfo {
		TrackInfo {
			priority: 7,
			ordered: false,
			latency_max: Duration::from_millis(2000),
			timescale: Timescale::MICRO,
		}
	}

	fn info_roundtrip(version: Version, info: &TrackInfo) -> TrackInfo {
		let mut buf = Vec::new();
		info.encode_msg(&mut buf, version).unwrap();
		let mut slice = buf.as_slice();
		TrackInfo::decode_msg(&mut slice, version).unwrap()
	}

	#[test]
	fn track_info_roundtrips_on_lite05() {
		let got = info_roundtrip(Version::Lite05, &info_sample());
		assert_eq!(got.priority, 7);
		assert!(!got.ordered);
		assert_eq!(got.latency_max, Duration::from_millis(2000));
		assert_eq!(got.timescale, Timescale::MICRO);
	}

	#[test]
	fn track_info_default_timescale_roundtrips() {
		let mut info = info_sample();
		info.timescale = Timescale::default();
		assert_eq!(info_roundtrip(Version::Lite05, &info).timescale, Timescale::MILLI);
	}

	#[test]
	fn track_info_defaults_match_cross_language_wire_bytes() {
		let info = crate::track::Info::default();
		let info = TrackInfo {
			priority: info.priority,
			ordered: info.ordered,
			latency_max: info.latency_max,
			timescale: info.timescale,
		};
		let mut buf = Vec::new();
		info.encode(&mut buf, Version::Lite05).unwrap();

		assert_eq!(buf, [0x06, 0x00, 0x00, 0x53, 0x88, 0x43, 0xe8]);
	}

	#[test]
	fn track_info_errors_before_lite05() {
		let mut buf = Vec::new();
		assert!(info_sample().encode_msg(&mut buf, Version::Lite04).is_err());
	}

	#[test]
	fn track_request_roundtrips_on_lite05() {
		let msg = Track {
			broadcast: Path::new("room").to_owned(),
			track: Cow::Borrowed("video"),
		};
		let mut buf = Vec::new();
		msg.encode_msg(&mut buf, Version::Lite05).unwrap();
		let mut slice = buf.as_slice();
		let got = Track::decode_msg(&mut slice, Version::Lite05).unwrap();
		assert_eq!(got.broadcast, Path::new("room"));
		assert_eq!(got.track, "video");
	}

	#[test]
	fn track_request_errors_before_lite05() {
		let msg = Track {
			broadcast: Path::new("room").to_owned(),
			track: Cow::Borrowed("video"),
		};
		let mut buf = Vec::new();
		assert!(msg.encode_msg(&mut buf, Version::Lite04).is_err());
	}
}