moq-mux 0.8.1

Media muxers and demuxers for MoQ
Documentation
use crate::catalog::hang::CatalogExt;
use crate::container::Frame;

use super::FrameHeader;

/// A frame-based importer for raw VP8.
///
/// A VP8 elementary stream isn't self-delimiting, so the caller must pass whole
/// frames, one per [`decode`](Self::decode). The first key frame's header supplies
/// the catalog dimensions, so the rendition isn't published until then. Build it
/// with [`new`](Self::new), passing the track producer and the
/// [`catalog::Reserved`](crate::catalog::Reserved) it reserves its rendition from.
pub struct Import<E: CatalogExt = ()> {
	// The track being produced.
	track: crate::container::Producer<crate::catalog::hang::Container>,

	// This importer's catalog rendition, published on the first key frame.
	rendition: crate::catalog::VideoTrack<E>,

	catalog: crate::codec::video::Catalog,
}

impl<E: CatalogExt> Import<E> {
	/// Publish on an existing track producer, seeding the rendition from `hint` (pass
	/// [`VideoHint::default`](crate::catalog::VideoHint) for none).
	///
	/// VP8 carries no out-of-band config, so a hint carrying a codec publishes the catalog rendition
	/// up front instead of waiting for the first key frame.
	pub fn new(
		track: moq_net::track::Producer,
		reserved: crate::catalog::Reserved<E>,
		hint: crate::catalog::VideoHint,
	) -> crate::Result<Self> {
		let rendition = reserved.video(track.name());
		let catalog = crate::codec::video::Catalog::new(&reserved, track.name(), hint)?;
		let mut import = Self {
			track: reserved
				.producer()
				.media_producer(track, crate::catalog::hang::Container::Legacy)?,
			rendition,
			catalog,
		};
		if let Some(config) = import.catalog.initial_config() {
			import.apply_config(config);
		}
		Ok(import)
	}

	/// Initialize the importer.
	///
	/// VP8 has no out-of-band configuration record, so this is normally called with
	/// an empty slice (gstreamer / ffi pass `&[]`) and the catalog is filled from the
	/// first key frame. If the caller does pass the first frame here, it's decoded so
	/// nothing is dropped.
	pub fn initialize(&mut self, buf: &[u8]) -> crate::Result<()> {
		if !buf.is_empty() {
			self.decode(buf, None)?;
		}
		Ok(())
	}

	fn init(&mut self, width: u16, height: u16) -> crate::Result<()> {
		let mut config = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8);
		config.coded_width = Some(width as u32);
		config.coded_height = Some(height as u32);
		config.container = hang::catalog::Container::Legacy;

		self.apply_config(config);
		Ok(())
	}

	/// Apply a resolved config, updating the catalog rendition in place.
	///
	/// A changed config just re-mirrors the rendition; there are no fixed tracks to reject a
	/// reconfiguration.
	fn apply_config(&mut self, config: hang::catalog::VideoConfig) {
		self.catalog.publish(&mut self.rendition, config);
	}

	/// Decode a single VP8 frame.
	pub fn decode<B: moq_net::IntoBytes>(&mut self, frame: B, pts: Option<moq_net::Timestamp>) -> crate::Result<()> {
		if frame.as_ref().is_empty() {
			return Err(super::Error::EmptyFrame.into());
		}

		let header = FrameHeader::parse(frame.as_ref())?;
		if let Some((width, height)) = header.dimensions {
			self.init(width, height)?;
		}

		let pts = self.rendition.timestamp(pts)?;
		// A key frame starts a new group: close the previous one for the bitrate detector.
		if header.keyframe {
			self.rendition.record_group_end(Some(pts));
		}
		let bytes = frame.as_ref().len();
		self.track.write(Frame {
			timestamp: pts,
			payload: frame.into_bytes(),
			keyframe: header.keyframe,
			duration: None,
		})?;

		self.rendition.record_frame(pts, bytes);

		Ok(())
	}

	/// A watch-only handle to this track's subscriber demand.
	pub fn demand(&self) -> moq_net::track::Demand {
		self.track.track().demand()
	}

	/// Finish the track, flushing the current group.
	pub fn finish(&mut self) -> crate::Result<()> {
		self.rendition.record_group_end(None);
		self.track.finish()?;
		Ok(())
	}

	/// Abort the track with `err` instead of finishing it cleanly, so subscribers
	/// see the real cause rather than [`moq_net::Error::Dropped`]. Consumes this importer.
	pub fn abort(self, err: moq_net::Error) {
		self.track.abort(err);
	}

	/// Cut the current group at `end` without finishing the track.
	pub fn cut(&mut self, end: Option<moq_net::Timestamp>) -> crate::Result<()> {
		self.rendition.record_group_end(end);
		self.track.cut(end)?;
		Ok(())
	}

	/// Close the current group and open the next one at `sequence`.
	pub fn seek(&mut self, sequence: u64) -> crate::Result<()> {
		self.rendition.record_group_end(None);
		self.track.seek(sequence)?;
		Ok(())
	}
}

#[cfg(test)]
mod tests {
	use bytes::Bytes;

	use moq_net::Timestamp;

	fn setup() -> (moq_net::track::Producer, crate::catalog::Producer) {
		let mut broadcast = moq_net::broadcast::Info::new().produce();
		let catalog = crate::catalog::Producer::new(&mut broadcast).unwrap();
		let track = broadcast.create_track("0.vp8", hang::container::track_info()).unwrap();
		(track, catalog)
	}

	/// A 320x240 key frame followed by an interframe should create a single VP8
	/// rendition with the right dimensions and emit both frames.
	#[tokio::test(start_paused = true)]
	async fn imports_keyframe_then_interframe() {
		let (track, catalog) = setup();
		let mut import = super::Import::new(track, catalog.reserve(), Default::default()).unwrap();

		// Empty init buffer: the catalog is filled on the first key frame.
		import.initialize(&[]).unwrap();
		assert!(catalog.snapshot().video.renditions.is_empty());

		let keyframe = Bytes::from_static(&[0x10, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0x40, 0x01, 0xf0, 0x00]);
		import
			.decode(&keyframe, Some(Timestamp::from_micros(0).unwrap()))
			.unwrap();

		let snapshot = catalog.snapshot();
		let config = snapshot.video.renditions.get("0.vp8").unwrap();
		assert_eq!(config.codec, hang::catalog::VideoCodec::VP8);
		assert_eq!(config.coded_width, Some(320));
		assert_eq!(config.coded_height, Some(240));

		// Interframe: no start code or dimensions, but still a valid frame.
		let interframe = Bytes::from_static(&[0x31, 0x00, 0x00, 0xaa, 0xbb]);
		import
			.decode(&interframe, Some(Timestamp::from_micros(33_000).unwrap()))
			.unwrap();

		import.finish().unwrap();
	}

	/// An interframe before any key frame has no dimensions, so the Producer
	/// rejects a non-keyframe as the first frame in a group.
	#[tokio::test(start_paused = true)]
	async fn rejects_interframe_first() {
		let (track, catalog) = setup();
		let mut import = super::Import::new(track, catalog.reserve(), Default::default()).unwrap();

		let interframe = Bytes::from_static(&[0x31, 0x00, 0x00, 0xaa, 0xbb]);
		assert!(
			import
				.decode(&interframe, Some(Timestamp::from_micros(0).unwrap()))
				.is_err()
		);
	}
}