hang 0.21.12

Media over QUIC
Documentation
mod aac;
mod codec;

pub use aac::*;
pub use codec::*;

use std::collections::{BTreeMap, btree_map};

use bytes::Bytes;

use serde::{Deserialize, Serialize};
use serde_with::DisplayFromStr;

use crate::catalog::Container;
use crate::catalog::hex::Hex;
use crate::catalog::millis::MillisCeil;

/// Information about an audio track in the catalog.
///
/// This struct contains a map of renditions (different quality/codec options)
///
/// Marked `#[non_exhaustive]` so additional optional fields can be added without
/// bumping the major version. External callers start from [`Audio::default`] and
/// fill in what they need ([`insert`](Self::insert) for renditions); struct-literal
/// construction (with or without `..base`) is not available outside this crate.
#[serde_with::serde_as]
#[serde_with::skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Audio {
	/// A map of track name to rendition configuration.
	/// This is not an array so it will work with JSON Merge Patch.
	/// We use a BTreeMap so keys are sorted alphabetically for *some* deterministic behavior.
	pub renditions: BTreeMap<String, AudioConfig>,
}

impl Audio {
	/// Insert a track config, returning an error if the name already exists.
	pub fn insert(&mut self, name: &str, config: AudioConfig) -> crate::Result<()> {
		let btree_map::Entry::Vacant(entry) = self.renditions.entry(name.to_string()) else {
			return Err(crate::Error::Duplicate(name.to_string()));
		};
		entry.insert(config);
		Ok(())
	}

	/// Remove the track from the catalog and return the configuration if found.
	pub fn remove(&mut self, name: &str) -> Option<AudioConfig> {
		self.renditions.remove(name)
	}
}

/// Audio decoder configuration based on WebCodecs AudioDecoderConfig.
///
/// This struct contains all the information needed to initialize an audio decoder,
/// including codec-specific parameters, sample rate, and channel configuration.
///
/// Reference: <https://www.w3.org/TR/webcodecs/#audio-decoder-config>
///
/// Marked `#[non_exhaustive]` so additional optional fields can be added
/// without bumping the major version. External callers build a config with
/// [`AudioConfig::new`] and then assign whichever optional fields they need;
/// struct-literal construction (with or without `..base`) is not available
/// outside this crate.
#[serde_with::serde_as]
#[serde_with::skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AudioConfig {
	/// Optional reference to another broadcast that publishes this track, expressed
	/// relative to the broadcast that served this catalog (e.g. `./source`). If unset,
	/// the track lives in the same broadcast as the catalog.
	///
	/// Resolve it with [`Path::resolve`](moq_net::Path::resolve): a reference that walks
	/// above the root names no broadcast, so the catalog is rejected.
	#[serde(default)]
	pub broadcast: Option<moq_net::path::RelativeOwned>,

	/// Human-readable rendition name for track pickers.
	#[serde(default)]
	pub label: Option<String>,

	// The codec, see the registry for details:
	// https://w3c.github.io/webcodecs/codec_registry.html
	#[serde_as(as = "DisplayFromStr")]
	pub codec: AudioCodec,

	// The sample rate of the audio in Hz
	pub sample_rate: u32,

	// The number of channels in the audio
	#[serde(rename = "numberOfChannels")]
	pub channel_count: u32,

	// The bitrate of the audio track in bits per second
	#[serde(default)]
	pub bitrate: Option<u64>,

	// Some codecs include a description so the decoder can be initialized without extra data.
	// If not provided, there may be in-band metadata (marginally higher overhead).
	#[serde(default)]
	#[serde_as(as = "Option<Hex>")]
	pub description: Option<Bytes>,

	/// Container format for frame encoding.
	/// Defaults to "legacy" for backward compatibility.
	#[serde(default)]
	pub container: Container,

	/// The maximum delay between a frame being ready and the publisher flushing it.
	/// The player's jitter buffer should be larger than this value.
	/// If not provided, the player should assume each frame is flushed immediately.
	///
	/// This is measured at the publisher (encoder latency, packet packing, reordering),
	/// never on the network a consumer sees. It only ever grows over the life of a stream.
	///
	/// Serialized as a whole number of milliseconds, rounded up, so an upper bound never
	/// rounds down into a promise the publisher can't keep.
	///
	/// NOTE: The audio "frame" duration depends on the codec, sample rate, etc.
	/// ex: AAC often uses 1024 samples per frame, so at 44100Hz, this would be 1024/44100 = 24ms
	#[serde_as(as = "MillisCeil")]
	#[serde(default)]
	pub jitter: Option<std::time::Duration>,

	/// How far this rendition's frames reach the transport behind the broadcast's earliest
	/// rendition, measured at the publisher from each rendition's minimum flush lateness.
	/// Absent on the earliest rendition and on any rendition the publisher did not measure.
	///
	/// A consumer holds `delay + jitter` for this rendition and MUST NOT subtract one rendition's
	/// `delay` from another's: each is a lifetime maximum, so two need not share an origin. It only
	/// ever grows over the life of a stream, and is serialized like [`jitter`](Self::jitter).
	#[serde_as(as = "MillisCeil")]
	#[serde(default)]
	pub delay: Option<std::time::Duration>,
}

impl AudioConfig {
	/// Construct a config with the required fields set and every optional
	/// field cleared. `container` defaults to [`Container::default`]. Fields
	/// are `pub`, so callers set whatever they need by assignment afterwards.
	///
	/// This is the only path external crates have to build an `AudioConfig`
	/// since the type is `#[non_exhaustive]`.
	pub fn new(codec: impl Into<AudioCodec>, sample_rate: u32, channel_count: u32) -> Self {
		Self {
			broadcast: None,
			label: None,
			codec: codec.into(),
			sample_rate,
			channel_count,
			bitrate: None,
			description: None,
			container: Container::default(),
			jitter: None,
			delay: None,
		}
	}
}

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

	#[test]
	fn label_round_trips() {
		let mut config = AudioConfig::new(AudioCodec::Opus, 48_000, 2);
		config.label = Some("English".to_string());

		let encoded = serde_json::to_value(&config).expect("failed to encode");
		assert_eq!(encoded["label"], "English");
		let decoded: AudioConfig = serde_json::from_value(encoded).expect("failed to decode");
		assert_eq!(decoded.label.as_deref(), Some("English"));
	}
}