Skip to main content

moq/
video.rs

1//! Native video encode/decode via [`moq_video`].
2//!
3//! The video counterpart to [`audio`](crate::audio): publish raw pictures as an
4//! encoded video track, and subscribe to one and hand back decoded raw frames,
5//! with the codec running inside the FFI boundary (VideoToolbox on macOS, Media
6//! Foundation on Windows, NVENC/NVDEC on Linux, openh264 as the software
7//! fallback; no ffmpeg). Siblings to `moq_publish_media_*` /
8//! `moq_consume_video`, which carry already-encoded frames for a caller that
9//! brings its own codec.
10//!
11//! Decode is H.264 only; a non-H.264 rendition fails the subscribe with a
12//! terminal error on the callback. Encode covers H.264 and H.265 (see
13//! [`moq_video_codec`]).
14
15use std::ffi::{c_char, c_void};
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, Ordering};
18use std::time::{Duration, Instant};
19
20use tokio::sync::oneshot;
21
22use crate::ffi::OnStatus;
23use crate::{Error, Id, NonZeroSlab, Shared, State, ffi};
24
25// ---- C-visible types ----
26
27/// Pixel layout of the raw frames handed to [`moq_encode_video_frame`].
28///
29/// The enum is exposed in the C header for readability, but ABI fields that
30/// carry it are typed `u32`. A C caller passing an unknown discriminant gets
31/// `Error::InvalidCode` instead of UB.
32#[repr(C)]
33#[allow(non_camel_case_types)]
34#[derive(Clone, Copy, Debug)]
35pub enum moq_video_pixel_format {
36	/// Tightly-packed planar I420: Y, then U, then V, no row padding.
37	/// `width * height * 3 / 2` bytes, the same layout [`moq_decode_video`]
38	/// hands back.
39	MOQ_VIDEO_PIXEL_FORMAT_I420 = 0,
40	/// Tightly-packed RGBA, `width * height * 4` bytes, no row padding.
41	MOQ_VIDEO_PIXEL_FORMAT_RGBA = 1,
42}
43
44/// Output video codec for [`moq_encode_video`].
45///
46/// Not every codec has a backend on every machine: H.265 is hardware-only, so
47/// publishing it fails where no hardware encoder is available.
48#[repr(C)]
49#[allow(non_camel_case_types)]
50#[derive(Clone, Copy, Debug)]
51pub enum moq_video_codec {
52	/// H.264 / AVC, published as an `avc3` track.
53	MOQ_VIDEO_CODEC_H264 = 0,
54	/// H.265 / HEVC, published as a `hev1` track.
55	MOQ_VIDEO_CODEC_H265 = 1,
56}
57
58/// Which encoder implementation [`moq_encode_video`] should use.
59#[repr(C)]
60#[allow(non_camel_case_types)]
61#[derive(Clone, Copy, Debug)]
62pub enum moq_video_encoder_kind {
63	/// Prefer a platform hardware encoder, falling back to software.
64	MOQ_VIDEO_ENCODER_KIND_AUTO = 0,
65	/// Hardware only; fails if none is available.
66	MOQ_VIDEO_ENCODER_KIND_HARDWARE = 1,
67	/// Software only (openh264, H.264 only).
68	MOQ_VIDEO_ENCODER_KIND_SOFTWARE = 2,
69	/// A specific backend, named by `moq_video_encoder_output::encoder`.
70	MOQ_VIDEO_ENCODER_KIND_NAMED = 3,
71}
72
73/// Raw frame layout the caller hands to [`moq_encode_video_frame`], plus
74/// the resolution and rate the encoder is opened at. Every published frame must
75/// match `width` x `height`; scale before publishing if your source moves.
76#[repr(C)]
77#[allow(non_camel_case_types)]
78pub struct moq_video_encoder_input {
79	/// `moq_video_pixel_format` discriminant.
80	pub format: u32,
81	/// Encoded width in pixels. Must be even (I420 chroma is subsampled 2x2).
82	pub width: u32,
83	/// Encoded height in pixels. Must be even.
84	pub height: u32,
85	/// Nominal frames per second, used for the codec time base and the default
86	/// bitrate and keyframe interval. Must be non-zero.
87	pub framerate: u32,
88}
89
90/// Codec-side configuration for [`moq_encode_video`]. Every knob spells
91/// "unset" as 0.
92#[repr(C)]
93#[allow(non_camel_case_types)]
94pub struct moq_video_encoder_output {
95	/// `moq_video_codec` discriminant.
96	pub codec: u32,
97	/// Target bitrate in bits per second. 0 derives one from the resolution and
98	/// framerate.
99	pub bitrate: u64,
100	/// Keyframe interval in frames: a subscriber joining mid-stream waits at
101	/// most this many frames before it can decode. 0 uses ~2 seconds.
102	pub gop: u32,
103	/// `moq_video_encoder_kind` discriminant.
104	pub kind: u32,
105	/// Backend name, UTF-8, e.g. `"videotoolbox"`, `"nvenc"`, `"mediafoundation"`,
106	/// `"openh264"`. Read only when `kind` is `MOQ_VIDEO_ENCODER_KIND_NAMED`.
107	pub encoder: *const c_char,
108	pub encoder_len: usize,
109}
110
111/// One raw frame handed to [`moq_encode_video_frame`].
112///
113/// Pixel format and resolution are fixed by [`moq_video_encoder_input`] at
114/// publish time, so a frame carries neither: `data` is exactly one picture in
115/// that layout, borrowed for the duration of the call (the encoder copies before
116/// returning). The decode side has its own [`moq_video_frame`], which does carry
117/// dimensions, since there they are what the stream turned out to be.
118#[repr(C)]
119#[allow(non_camel_case_types)]
120pub struct moq_video_encoder_frame {
121	/// Presentation timestamp, in microseconds.
122	pub timestamp_us: u64,
123	pub data: *const u8,
124	pub data_size: usize,
125}
126
127/// Decode-side configuration the caller passes to [`moq_decode_video`].
128///
129/// `format` selects the CPU pixel layout of each [`moq_video_frame`] (`I420`
130/// is `width * height * 3 / 2` bytes, `RGBA` is `width * height * 4` bytes),
131/// and `width`/`height` select its size: zero both for the stream's native
132/// size, otherwise both must be even and non-zero.
133///
134/// This struct is versioned by recompilation, not by reserved fields: adding a
135/// field changes its layout, so rebuild callers against the `moq.h` that ships
136/// with the `libmoq.a` they link.
137#[repr(C)]
138#[allow(non_camel_case_types)]
139pub struct moq_video_decoder_output {
140	/// Upper bound on buffering before skipping a stalled group, in
141	/// microseconds. Same congestion-control knob as
142	/// `moq_consume_video`'s `max_age_us`. 0 = skip aggressively
143	/// (the moq-mux default); set to your playout buffer for a softer skip.
144	pub max_age_us: u64,
145	/// `moq_video_pixel_format` discriminant. Unknown values fail
146	/// [`moq_decode_video`] rather than decoding into an assumed layout.
147	pub format: u32,
148	/// Target width in pixels. 0 with `height` 0 means the native size.
149	pub width: u32,
150	/// Target height in pixels. 0 with `width` 0 means the native size.
151	pub height: u32,
152}
153
154/// One decoded video frame from [`moq_decode_video`]: pixels plus a
155/// presentation timestamp.
156///
157/// The pixel layout is what [`moq_video_decoder_output`]'s `format` asked
158/// for: I420 is the Y plane (`width * height`), then U, then V (`width/2 *
159/// height/2` each), no row padding, BT.601 limited range; RGBA is tightly
160/// packed `width * height * 4` bytes, no row padding.
161///
162/// `data` is owned by the consume slab and stays valid until the same id is
163/// released with [`moq_decode_video_frame_free`].
164///
165/// The publish side has its own [`moq_video_encoder_frame`], which carries no
166/// dimensions because the encoder already fixed them.
167#[repr(C)]
168#[allow(non_camel_case_types)]
169pub struct moq_video_frame {
170	pub timestamp_us: u64,
171	pub width: u32,
172	pub height: u32,
173	pub data: *const u8,
174	pub data_size: usize,
175}
176
177// ---- State extension (used internally by lib.rs) ----
178
179/// Raw-video state: encoders being published, plus decoder tasks and their
180/// buffered decoded frames.
181#[derive(Default)]
182pub struct Video {
183	producers: NonZeroSlab<Shared<VideoEncoder>>,
184	consumer_tasks: NonZeroSlab<Option<VideoTaskEntry>>,
185	frames: NonZeroSlab<VideoFrame>,
186}
187
188/// Wait out an encode-thread round trip from a C entry point.
189///
190/// The C ABI hands back a status code, so there is no executor to yield to and
191/// this is where [`Sink`](moq_video::encode::Sink)'s futures stop. Blocking is
192/// also what paces the caller: a raw frame is megabytes, so a publish free to run
193/// ahead of the codec would queue pictures without bound.
194///
195/// `pollster` rather than a tokio helper because those panic when the calling
196/// thread is driving a runtime, which the one dispatching a callback is.
197fn block_on<T>(future: impl std::future::Future<Output = T>) -> T {
198	pollster::block_on(future)
199}
200
201fn video_ceiling(
202	config: &moq_video::encode::Config,
203	rendition: &hang::catalog::VideoConfig,
204) -> moq_net::bandwidth::Rate {
205	config
206		.bitrate
207		.or_else(|| rendition.bitrate.map(moq_net::bandwidth::Rate::from_bps))
208		.unwrap_or_else(|| {
209			moq_net::bandwidth::Rate::from_bps(
210				(config.size().pixels() as f64 * config.framerate.as_f64() * 0.07) as u64,
211			)
212		})
213}
214
215async fn follow_reservation(
216	inner: Shared<VideoEncoder>,
217	mut consumer: moq_net::bandwidth::Consumer,
218	ceiling: Arc<AtomicU64>,
219) {
220	use moq_mux::rate::{Control, Policy};
221
222	let mut max = moq_net::bandwidth::Rate::from_bps(ceiling.load(Ordering::SeqCst));
223	let mut control = Control::new(Policy::new(max));
224	loop {
225		let estimate = match consumer.changed().await {
226			Ok(estimate) => estimate,
227			Err(_) => return,
228		};
229		let next = moq_net::bandwidth::Rate::from_bps(ceiling.load(Ordering::SeqCst));
230		if next != max {
231			max = next;
232			control = Control::new(Policy::new(max));
233		}
234		let Some(bitrate) = control.update(estimate, Instant::now()) else {
235			continue;
236		};
237
238		let mut guard = inner.lock();
239		let Some(producer) = guard.as_mut() else {
240			return;
241		};
242		match block_on(producer.encoder.set_bitrate(bitrate)) {
243			Ok(()) => tracing::debug!(bitrate = bitrate.as_bps(), "adjusted encoder bitrate"),
244			Err(moq_video::Error::BitrateUnsupported(name)) => {
245				tracing::warn!(encoder = name, "encoder cannot follow the bandwidth estimate");
246				return;
247			}
248			Err(err) => {
249				tracing::warn!(error = %err, bitrate = bitrate.as_bps(), "failed to adjust encoder bitrate");
250			}
251		}
252	}
253}
254
255/// An encoder paired with the track publishing its output, plus the pixel format
256/// its caller feeds it (fixed at publish time, so a frame carries only pixels and
257/// a timestamp).
258///
259/// The encoder is a [`Sink`](moq_video::encode::Sink) rather than a bare
260/// `Encoder` because a C caller drives a handle from whichever thread it likes,
261/// so a bare `Encoder` would be built on one thread and dropped on another,
262/// unbalancing the per-thread COM apartment the Windows backend opens. The sink
263/// owns the thread instead, so every caller is welcome.
264pub(crate) struct VideoEncoder {
265	encoder: moq_video::encode::Sink,
266	producer: moq_video::encode::Producer<moq_mux::catalog::hang::Extra>,
267	format: moq_video_pixel_format,
268	/// The encoded resolution, from the publish config. Frames carry only pixels,
269	/// so this is what says how to read them.
270	size: moq_video::Size,
271	reservation: Option<Arc<moq_net::bandwidth::Reservation>>,
272	follow: Option<oneshot::Sender<()>>,
273	ceiling: Option<Arc<AtomicU64>>,
274}
275
276/// A delivered frame, flattened to CPU bytes at delivery time in the layout
277/// [`moq_decode_video`] was asked for: the C ABI hands out a stable byte
278/// pointer, so a GPU-decoded frame (e.g. NVDEC) is downloaded exactly once here.
279struct VideoFrame {
280	timestamp_us: u64,
281	width: u32,
282	height: u32,
283	data: bytes::Bytes,
284}
285
286/// What [`moq_decode_video`] delivers per frame: the requested CPU pixel format
287/// and target size, validated up front so the delivery loop never second-guesses.
288#[derive(Clone, Copy)]
289pub struct DecoderOutput {
290	format: moq_video_pixel_format,
291	size: Option<moq_video::Size>,
292}
293
294/// End a video track, given the result of draining its encoder into it.
295///
296/// A clean finish is a promise that the track holds everything the publisher
297/// produced, so a lost tail has to end the track as an abort instead. Finishing
298/// anyway would leave a truncated stream indistinguishable from a complete one,
299/// and only the local caller would ever learn otherwise.
300fn finalize(
301	mut producer: moq_video::encode::Producer<moq_mux::catalog::hang::Extra>,
302	drained: Result<(), moq_video::Error>,
303) -> Result<(), Error> {
304	match drained {
305		Ok(()) => Ok(producer.finish()?),
306		Err(err) => {
307			producer.abort(moq_net::Error::Transport(err.to_string()));
308			Err(err.into())
309		}
310	}
311}
312
313/// A spawned task entry: `close` signals shutdown, `callback` delivers status.
314///
315/// Same lifetime contract as the audio decoder: the task delivers one final
316/// terminal callback and then removes itself, so `user_data` stays valid until
317/// that callback fires. `close` is an `Option` so `consume_close` can drop just
318/// the sender without removing the entry.
319struct VideoTaskEntry {
320	close: Option<oneshot::Sender<()>>,
321	callback: OnStatus,
322}
323
324impl VideoEncoder {
325	fn publish_frame(&mut self, timestamp_us: u64, data: &[u8]) -> Result<(), Error> {
326		// A buffer that isn't one picture at the configured size is rejected here,
327		// by the surface constructors, rather than reinterpreted.
328		let size = self.size;
329		let surface = match self.format {
330			moq_video_pixel_format::MOQ_VIDEO_PIXEL_FORMAT_I420 => {
331				moq_video::Surface::I420(moq_video::I420::new(size, data.to_vec())?)
332			}
333			moq_video_pixel_format::MOQ_VIDEO_PIXEL_FORMAT_RGBA => moq_video::Surface::rgba(data, size)?,
334		};
335
336		let frame = moq_video::Frame::new(surface, moq_net::Timestamp::from_micros(timestamp_us)?);
337		// A backend that pipelines hands back an earlier frame's output, so this is
338		// zero or more access units rather than one per call.
339		let encoded = block_on(self.encoder.encode(frame))?;
340		self.producer.publish(&encoded)?;
341		Ok(())
342	}
343
344	fn publish_cut(&mut self) -> Result<(), Error> {
345		// A keyframe is what a cut is on the wire: the importer closes the open
346		// group and starts a new one at it.
347		Ok(block_on(self.encoder.cut())?)
348	}
349
350	fn publish_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
351		block_on(self.encoder.set_bitrate(moq_net::bandwidth::Rate::from_bps(bitrate)))?;
352		// Ceiling first: `update` wakes the follower, which must not read the old
353		// floor and retune above this cap before parking on an unchanged grant.
354		if let Some(ceiling) = &self.ceiling {
355			ceiling.store(bitrate, Ordering::SeqCst);
356		}
357		if let Some(reservation) = &self.reservation {
358			reservation.update(moq_net::bandwidth::Rate::from_bps(bitrate));
359		}
360		Ok(())
361	}
362
363	fn publish_finish(self) -> Result<(), Error> {
364		let VideoEncoder {
365			encoder, mut producer, ..
366		} = self;
367		// Drain the codec into the track before ending it, so the last frames land
368		// in it rather than being dropped with the encoder.
369		let drained = block_on(encoder.finish()).and_then(|encoded| producer.publish(&encoded));
370		finalize(producer, drained)
371	}
372}
373
374impl Video {
375	/// Advertise a track for an already-opened encoder.
376	///
377	/// The encoder and the rendition it will emit are both resolved by the caller,
378	/// and before this, so a config this machine can't encode fails without leaving
379	/// a track advertised that will never carry frames.
380	pub fn publish(
381		&mut self,
382		broadcast: &moq_net::broadcast::Producer,
383		catalog: moq_mux::catalog::Producer<moq_mux::catalog::hang::Extra>,
384		format: moq_video_pixel_format,
385		config: &moq_video::encode::Config,
386		rendition: hang::catalog::VideoConfig,
387		encoder: moq_video::encode::Sink,
388	) -> Result<Id, Error> {
389		let producer = moq_video::encode::Producer::new(broadcast.clone(), catalog, rendition)?;
390		self.producers.insert(Shared::new(VideoEncoder {
391			encoder,
392			producer,
393			format,
394			size: config.size(),
395			reservation: None,
396			follow: None,
397			ceiling: None,
398		}))
399	}
400
401	pub(crate) fn follow(
402		&self,
403		id: Id,
404		allocator: &moq_net::bandwidth::Allocator,
405		ceiling: moq_net::bandwidth::Rate,
406	) -> Result<(), Error> {
407		let shared = self.producer(id)?;
408		let max = ceiling;
409		let ceiling = Arc::new(AtomicU64::new(max.as_bps()));
410		let reservation = {
411			let mut guard = shared.lock();
412			let encoder = guard.as_mut().ok_or(Error::MediaNotFound)?;
413			let reservation = Arc::new(allocator.reserve(&encoder.producer.demand(), max));
414			encoder.reservation = Some(reservation.clone());
415			encoder.ceiling = Some(ceiling.clone());
416			reservation
417		};
418		let (close, closed) = oneshot::channel();
419		shared.lock().as_mut().ok_or(Error::MediaNotFound)?.follow = Some(close);
420		let follower = shared.clone();
421		tokio::spawn(async move {
422			tokio::select! {
423				biased;
424				_ = closed => {}
425				_ = follow_reservation(follower, reservation.consumer(), ceiling) => {}
426			}
427		});
428		Ok(())
429	}
430
431	pub(crate) fn reservation(&self, id: Id) -> Result<Option<Arc<moq_net::bandwidth::Reservation>>, Error> {
432		Ok(self
433			.producer(id)?
434			.lock()
435			.as_ref()
436			.ok_or(Error::MediaNotFound)?
437			.reservation
438			.clone())
439	}
440
441	/// A watch-only handle to the encoded track's subscriber demand.
442	pub(crate) fn demand(&self, id: Id) -> Result<moq_net::track::Demand, Error> {
443		Ok(self
444			.producer(id)?
445			.lock()
446			.as_ref()
447			.ok_or(Error::MediaNotFound)?
448			.producer
449			.demand())
450	}
451
452	/// Resolve a producer handle, so the caller can encode with the global lock
453	/// released.
454	///
455	/// Bind the result before locking it: a temporary [`State`] guard lives to the
456	/// end of the statement that created it, so resolving and locking in one
457	/// expression would put the encode back under the global lock.
458	pub(crate) fn producer(&self, id: Id) -> Result<Shared<VideoEncoder>, Error> {
459		self.producers.get(id).cloned().ok_or(Error::MediaNotFound)
460	}
461
462	/// Resolve a producer and drop its id, so nothing can be published to it after.
463	pub(crate) fn remove(&mut self, id: Id) -> Result<Shared<VideoEncoder>, Error> {
464		self.producers.remove(id).ok_or(Error::MediaNotFound)
465	}
466
467	pub fn consume(
468		&mut self,
469		broadcast: &moq_net::broadcast::Consumer,
470		catalog: &hang::catalog::VideoConfig,
471		name: &str,
472		options: moq_video::decode::Options,
473		output: DecoderOutput,
474		on_frame: OnStatus,
475	) -> Result<Id, Error> {
476		let broadcast = broadcast.clone();
477		let catalog = catalog.clone();
478		let name = name.to_string();
479
480		let channel = oneshot::channel();
481		let entry = VideoTaskEntry {
482			close: Some(channel.0),
483			callback: on_frame,
484		};
485		let id = self.consumer_tasks.insert(Some(entry))?;
486
487		// `Consumer::new` subscribes (blocking on SUBSCRIBE_OK), so run it inside
488		// the task to keep this entrypoint non-blocking.
489		tokio::spawn(async move {
490			let res = async move {
491				let consumer = moq_video::decode::Consumer::new(&broadcast, &catalog, name, options).await?;
492				Self::run(on_frame, consumer, channel.1, output).await
493			}
494			.await;
495
496			// Deliver one final terminal callback (code <= 0), then drop the entry.
497			// Pull it out from under the lock so the callback never runs while held.
498			let entry = State::lock().video.consumer_tasks.remove(id).flatten();
499			if let Some(entry) = entry {
500				entry.callback.call(res);
501			}
502		});
503
504		Ok(id)
505	}
506
507	async fn run(
508		callback: OnStatus,
509		mut consumer: moq_video::decode::Consumer,
510		mut close: oneshot::Receiver<()>,
511		output: DecoderOutput,
512	) -> Result<(), Error> {
513		loop {
514			// `biased` so a pending close always wins over a ready frame.
515			let frame = tokio::select! {
516				biased;
517				_ = &mut close => return Ok(()),
518				frame = consumer.read() => match frame? {
519					Some(frame) => frame,
520					None => return Ok(()),
521				},
522			};
523
524			// The decoder's scale hint is best effort: a backend without a
525			// scaler ignores it, so enforce the requested size here rather
526			// than trusting it. The frame is already CPU pixels, so this
527			// scales on the CPU; convert outside the lock, then hold the lock
528			// only to buffer it, and release before the callback.
529			let mut frame = frame;
530			if let Some(size) = output.size
531				&& frame.size() != size
532			{
533				frame = frame.resize(size, &moq_video::resize::Config::default())?;
534			}
535			let size = frame.size();
536			let data = match output.format {
537				moq_video_pixel_format::MOQ_VIDEO_PIXEL_FORMAT_I420 => {
538					bytes::Bytes::from(frame.surface.into_i420()?.into_data())
539				}
540				moq_video_pixel_format::MOQ_VIDEO_PIXEL_FORMAT_RGBA => bytes::Bytes::from(
541					frame
542						.surface
543						.to_rgba(&moq_video::convert::Config::default())?
544						.into_data(),
545				),
546			};
547			let frame = VideoFrame {
548				// The C ABI carries microseconds; the decoded frame's Timestamp is
549				// constrained to a QUIC VarInt, so the microsecond value fits a u64.
550				timestamp_us: frame.timestamp.as_micros() as u64,
551				width: size.width,
552				height: size.height,
553				data,
554			};
555			let frame_id = State::lock().video.frames.insert(frame)?;
556			callback.call(Ok(frame_id));
557		}
558	}
559
560	pub fn consume_close(&mut self, id: Id) -> Result<(), Error> {
561		// Signal shutdown; the task delivers a final callback and removes itself.
562		self.consumer_tasks
563			.get_mut(id)
564			.and_then(|entry| entry.as_mut())
565			.ok_or(Error::TrackNotFound)?
566			.close
567			.take()
568			.ok_or(Error::TrackNotFound)?;
569		Ok(())
570	}
571
572	pub fn frame_info(&self, id: Id, dst: &mut moq_video_frame) -> Result<(), Error> {
573		let frame = self.frames.get(id).ok_or(Error::FrameNotFound)?;
574		*dst = moq_video_frame {
575			timestamp_us: frame.timestamp_us,
576			width: frame.width,
577			height: frame.height,
578			data: frame.data.as_ptr(),
579			data_size: frame.data.len(),
580		};
581		Ok(())
582	}
583
584	pub fn frame_free(&mut self, id: Id) -> Result<(), Error> {
585		self.frames.remove(id).ok_or(Error::FrameNotFound)?;
586		Ok(())
587	}
588}
589
590// ---- C entry points ----
591
592fn pixel_format_from_u32(value: u32) -> Result<moq_video_pixel_format, Error> {
593	Ok(match value {
594		v if v == moq_video_pixel_format::MOQ_VIDEO_PIXEL_FORMAT_I420 as u32 => {
595			moq_video_pixel_format::MOQ_VIDEO_PIXEL_FORMAT_I420
596		}
597		v if v == moq_video_pixel_format::MOQ_VIDEO_PIXEL_FORMAT_RGBA as u32 => {
598			moq_video_pixel_format::MOQ_VIDEO_PIXEL_FORMAT_RGBA
599		}
600		_ => return Err(Error::InvalidCode),
601	})
602}
603
604/// Parse [`moq_video_decoder_output`]'s `width`/`height` into the target size:
605/// 0x0 is the stream's native size, anything else must be even and non-zero
606/// (I420 chroma is subsampled 2x2) and small enough for the packed byte math.
607fn decoder_size(width: u32, height: u32) -> Result<Option<moq_video::Size>, Error> {
608	if width == 0 && height == 0 {
609		return Ok(None);
610	}
611	if width == 0 || height == 0 || !width.is_multiple_of(2) || !height.is_multiple_of(2) {
612		return Err(Error::InvalidConfig(format!(
613			"decode size {width}x{height}: use 0x0 for the native size or even non-zero dimensions"
614		)));
615	}
616	let size = moq_video::Size::new(width, height);
617	if size
618		.pixels()
619		.checked_mul(4)
620		.is_none_or(|bytes| usize::try_from(bytes).is_err())
621	{
622		return Err(Error::InvalidConfig(format!(
623			"decode size {width}x{height}: dimensions too large to represent"
624		)));
625	}
626	Ok(Some(size))
627}
628
629fn codec_from_u32(value: u32) -> Result<moq_video::encode::Codec, Error> {
630	use moq_video::encode::Codec;
631	Ok(match value {
632		v if v == moq_video_codec::MOQ_VIDEO_CODEC_H264 as u32 => Codec::H264,
633		v if v == moq_video_codec::MOQ_VIDEO_CODEC_H265 as u32 => Codec::H265,
634		_ => return Err(Error::InvalidCode),
635	})
636}
637
638/// # Safety
639/// - `output->encoder` must point to `output->encoder_len` bytes of UTF-8 when
640///   `output->kind` is `MOQ_VIDEO_ENCODER_KIND_NAMED`.
641unsafe fn encoder_kind(output: &moq_video_encoder_output) -> Result<moq_video::encode::Kind, Error> {
642	use moq_video::encode::Kind;
643	Ok(match output.kind {
644		v if v == moq_video_encoder_kind::MOQ_VIDEO_ENCODER_KIND_AUTO as u32 => Kind::Auto,
645		v if v == moq_video_encoder_kind::MOQ_VIDEO_ENCODER_KIND_HARDWARE as u32 => Kind::Hardware,
646		v if v == moq_video_encoder_kind::MOQ_VIDEO_ENCODER_KIND_SOFTWARE as u32 => Kind::Software,
647		v if v == moq_video_encoder_kind::MOQ_VIDEO_ENCODER_KIND_NAMED as u32 => {
648			Kind::Named(unsafe { ffi::parse_str(output.encoder, output.encoder_len)? }.to_string())
649		}
650		_ => return Err(Error::InvalidCode),
651	})
652}
653
654/// Open a video track on a broadcast, encoding the raw frames you publish to it.
655///
656/// The encoder is opened here, so an unsupported codec, resolution, or backend
657/// fails now rather than on the first frame. The track is named after the codec
658/// (`.avc3` / `.hev1`) and its catalog rendition is published immediately, read
659/// out of the encoder rather than guessed, so a subscriber can find the track
660/// before a frame is written to it.
661///
662/// Returns a non-zero handle on success or a negative error code.
663///
664/// # Safety
665/// - `input` / `output` must point to fully populated structs.
666/// - `output->encoder` must point to `output->encoder_len` bytes of UTF-8 when
667///   `output->kind` is `MOQ_VIDEO_ENCODER_KIND_NAMED`.
668/// - `bandwidth` is a handle from [`crate::moq_session_bandwidth`], or 0 to hold the
669///   configured bitrate regardless of congestion.
670#[unsafe(no_mangle)]
671pub unsafe extern "C" fn moq_encode_video(
672	broadcast: u32,
673	input: *const moq_video_encoder_input,
674	output: *const moq_video_encoder_output,
675	bandwidth: u32,
676) -> i32 {
677	ffi::enter(move || {
678		let broadcast = ffi::parse_id(broadcast)?;
679		let raw_input = unsafe { input.as_ref() }.ok_or(Error::InvalidPointer)?;
680		let raw_output = unsafe { output.as_ref() }.ok_or(Error::InvalidPointer)?;
681
682		let format = pixel_format_from_u32(raw_input.format)?;
683
684		let framerate = moq_video::Rate::new(raw_input.framerate, 1)
685			.map_err(|_| Error::Video(moq_video::Error::InvalidFramerate(raw_input.framerate).into()))?;
686		let mut config = moq_video::encode::Config::new(raw_input.width, raw_input.height, framerate);
687		config.codec = codec_from_u32(raw_output.codec)?;
688		config.kind = unsafe { encoder_kind(raw_output)? };
689		// The C ABI spells an unset knob as 0, which neither field accepts as a real
690		// value: a zero bitrate or GOP is the default, not a request.
691		config.bitrate = (raw_output.bitrate != 0).then(|| moq_net::bandwidth::Rate::from_bps(raw_output.bitrate));
692		if raw_output.gop != 0 {
693			config.gop = moq_video::encode::Gop::Keyframe {
694				interval: raw_output.gop,
695			};
696		}
697
698		// Both before the global lock is taken: bringing up a hardware encoder is slow
699		// enough that every other call would wait behind it. The probe runs first and
700		// closes its encoder before this one opens, so only one codec session is live.
701		let rendition = block_on(config.probe())?;
702		let encoder = block_on(moq_video::encode::Sink::open(&config))?;
703
704		let bandwidth = ffi::parse_id_optional(bandwidth)?;
705		let mut state = State::lock();
706		let allocator = bandwidth.map(|id| state.bandwidth.allocator(id)).transpose()?;
707		let State { publish, video, .. } = &mut *state;
708		let (broadcast_producer, catalog) = publish.pair_mut(broadcast)?;
709
710		let id = video.publish(
711			broadcast_producer,
712			catalog.clone(),
713			format,
714			&config,
715			rendition.clone(),
716			encoder,
717		)?;
718		if let Some(allocator) = allocator.as_ref() {
719			video.follow(id, allocator, video_ceiling(&config, &rendition))?;
720		}
721		Ok(id)
722	})
723}
724
725/// This encoder's bandwidth reservation, or 0 if it was published without one.
726///
727/// Closing the returned handle does not release the encoder's claim; that lasts
728/// until [moq_encode_video_finish].
729#[unsafe(no_mangle)]
730pub extern "C" fn moq_encode_video_reservation(producer: u32) -> i32 {
731	ffi::enter(move || {
732		let producer = ffi::parse_id(producer)?;
733		let mut state = State::lock();
734		match state.video.reservation(producer)? {
735			Some(reservation) => Ok(i32::from(state.bandwidth.hold(reservation)?)),
736			None => Ok(0),
737		}
738	})
739}
740
741/// Watch whether the encoded video track has subscribers, so the camera and encoder
742/// run only while someone watches. See [`crate::moq_publish_media_demand`] for the
743/// callback contract.
744///
745/// Returns a non-zero watcher handle on success, or a negative code on failure.
746///
747/// # Safety
748/// - `on_demand` must be non-NULL.
749/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_demand` callback.
750#[unsafe(no_mangle)]
751pub unsafe extern "C" fn moq_encode_video_demand(
752	producer: u32,
753	on_demand: crate::moq_status_callback,
754	user_data: *mut c_void,
755) -> i32 {
756	ffi::enter(move || {
757		let producer = ffi::parse_id(producer)?;
758		let on_demand = unsafe { OnStatus::new(user_data, on_demand)? };
759		let mut state = State::lock();
760		let demand = state.video.demand(producer)?;
761		state.publish.demand(demand, on_demand)
762	})
763}
764
765/// Encode and publish one raw frame.
766///
767/// `frame->data` is borrowed for the duration of the call and must be exactly one
768/// picture in the pixel format and at the resolution declared by
769/// [`moq_video_encoder_input`].
770/// A backend that pipelines publishes an earlier frame's output here, so a call
771/// that emits nothing is normal rather than an error.
772///
773/// # Safety
774/// - `frame` must point to a valid [`moq_video_encoder_frame`].
775/// - `frame->data` must point to `frame->data_size` bytes.
776#[unsafe(no_mangle)]
777pub unsafe extern "C" fn moq_encode_video_frame(producer: u32, frame: *const moq_video_encoder_frame) -> i32 {
778	ffi::enter(move || {
779		let producer = ffi::parse_id(producer)?;
780		let frame = unsafe { frame.as_ref() }.ok_or(Error::InvalidPointer)?;
781		let data = unsafe { ffi::parse_slice(frame.data, frame.data_size)? };
782
783		let producer = State::lock().video.producer(producer)?;
784		producer
785			.lock()
786			.as_mut()
787			.ok_or(Error::MediaNotFound)?
788			.publish_frame(frame.timestamp_us, data)
789	})
790}
791
792/// Cut a new group at the next published frame.
793///
794/// Optional. The encoder already keyframes every `moq_video_encoder_output.gop`
795/// frames, and each of those cuts a group, so a subscriber can always join
796/// without you calling this. Reach for it only to place the boundaries yourself:
797/// aligning groups with something the encoder cannot see, such as a scene change,
798/// a source switch, or resuming after an idle gap.
799///
800/// The next frame is encoded as a keyframe, which closes the open group and
801/// starts a new one at it. Calling this repeatedly before that frame arrives cuts
802/// once, not several times.
803///
804/// Fails when the selected encoder cannot force a keyframe (a V4L2 driver
805/// without the control): nothing is queued, and groups keep falling every
806/// `gop` frames.
807#[unsafe(no_mangle)]
808pub extern "C" fn moq_encode_video_cut(producer: u32) -> i32 {
809	ffi::enter(move || {
810		let producer = ffi::parse_id(producer)?;
811		let producer = State::lock().video.producer(producer)?;
812		producer.lock().as_mut().ok_or(Error::MediaNotFound)?.publish_cut()
813	})
814}
815
816/// Retune a live encoder to `bitrate` bits per second, taking effect from
817/// roughly the next frame. No keyframe is forced, so this is cheap enough to
818/// drive from a congestion controller.
819///
820/// The configured bitrate is a ceiling on some backends (openh264 rejects a raise
821/// above the rate it opened at), so set `bitrate` to the highest you will ask
822/// for and adapt downwards from there.
823///
824/// When this encoder was published against a bandwidth allocator, the reservation
825/// and follower ceiling move with it, so a later grant cannot retune above this
826/// value.
827///
828/// Returns a negative code if this backend cannot retune while running. That is
829/// not fatal: the encoder keeps running at its current rate, so stop adapting
830/// rather than stop publishing.
831#[unsafe(no_mangle)]
832pub extern "C" fn moq_encode_video_bitrate(producer: u32, bitrate: u64) -> i32 {
833	ffi::enter(move || {
834		let producer = ffi::parse_id(producer)?;
835		let producer = State::lock().video.producer(producer)?;
836		producer
837			.lock()
838			.as_mut()
839			.ok_or(Error::MediaNotFound)?
840			.publish_bitrate(bitrate)
841	})
842}
843
844/// Flush any frames the codec is still holding and finalize the video track.
845///
846/// The handle is released, so nothing can be published to it afterwards.
847#[unsafe(no_mangle)]
848pub extern "C" fn moq_encode_video_finish(producer: u32) -> i32 {
849	ffi::enter(move || {
850		let producer = ffi::parse_id(producer)?;
851		// The id is dropped first, so nothing new queues behind the drain; whatever
852		// is mid-encode still finishes before this takes the encoder.
853		let producer = State::lock().video.remove(producer)?;
854		producer.take().ok_or(Error::MediaNotFound)?.publish_finish()
855	})
856}
857
858/// Subscribe to a video track and decode it into raw frames in the requested
859/// CPU pixel format and size (see [`moq_video_decoder_output`]).
860///
861/// The catalog `index` selects which video rendition to subscribe to, matching
862/// the existing `moq_consume_video` selection model. Only H.264 is
863/// supported; a non-H.264 rendition fails on the terminal callback.
864///
865/// An unknown `output->format` or an invalid `output->width`/`height` fails
866/// here, before subscribing: an accepted request always produces the requested
867/// layout or fails on the terminal callback instead of delivering it silently.
868///
869/// Returns a non-zero handle on success or a negative error code.
870///
871/// `on_frame` is called with a positive frame id per decoded frame, then exactly
872/// once more with a terminal code: `0` (closed cleanly) or a negative error.
873/// After the terminal (`<= 0`) callback, `on_frame` is never called again and
874/// `user_data` is never touched again, so release `user_data` there. The terminal
875/// callback fires even after [`moq_decode_video_cancel`].
876///
877/// Starts at the newest cached group so reopening live playback skips the backlog.
878///
879/// # Safety
880/// - `output` must point to a valid [`moq_video_decoder_output`].
881/// - `user_data` must stay valid until the terminal (`<= 0`) `on_frame` callback.
882#[unsafe(no_mangle)]
883pub unsafe extern "C" fn moq_decode_video(
884	catalog: u32,
885	index: u32,
886	output: *const moq_video_decoder_output,
887	on_frame: crate::moq_status_callback,
888	user_data: *mut c_void,
889) -> i32 {
890	ffi::enter(move || {
891		let raw = unsafe { output.as_ref() }.ok_or(Error::InvalidPointer)?;
892
893		// Validate the request before resolving anything: a C caller can probe
894		// support without a live track, and a bad request never opens a
895		// subscription it would immediately drop.
896		let format = pixel_format_from_u32(raw.format)?;
897		let size = decoder_size(raw.width, raw.height)?;
898		let catalog = ffi::parse_id(catalog)?;
899
900		let mut options = moq_video::decode::Options::new();
901		options.start = moq_video::decode::Start::Latest;
902		options.max_age = Duration::from_micros(raw.max_age_us);
903		// The C caller takes packed pixels, so let a backend that can decode
904		// straight to the CPU do that rather than downloading afterwards.
905		options.decoder.output = moq_video::Output::Cpu;
906		// A backend with a hardware scaler (NVDEC) honors this for free; the
907		// delivery loop still enforces it, since other backends ignore it.
908		options.decoder.scale_hint = size;
909		let output = DecoderOutput { format, size };
910		let on_frame = unsafe { OnStatus::new(user_data, on_frame)? };
911
912		let mut state = State::lock();
913		let (broadcast, video_cfg, name) = state.consume.video_rendition(catalog, index as usize)?;
914
915		let State { video, .. } = &mut *state;
916		video.consume(&broadcast, &video_cfg, &name, options, output, on_frame)
917	})
918}
919
920/// Stop a video (raw) consumer's background task.
921///
922/// Returns immediately: zero on success, or a negative code if already closed.
923/// Does NOT free `user_data`; the on-frame callback still fires once more with a
924/// terminal `0` (or a negative error), which is where `user_data` should be
925/// released. Frame ids already delivered are likewise not freed; release each
926/// with [`moq_decode_video_frame_free`].
927#[unsafe(no_mangle)]
928pub extern "C" fn moq_decode_video_cancel(consumer: u32) -> i32 {
929	ffi::enter(move || {
930		let consumer = ffi::parse_id(consumer)?;
931		State::lock().video.consume_close(consumer)
932	})
933}
934
935/// Copy a delivered frame's metadata into `dst`.
936///
937/// The written `dst->data` pointer remains valid until the same `id` is released
938/// with [`moq_decode_video_frame_free`].
939///
940/// # Safety
941/// - `dst` must point to a writable [`moq_video_frame`].
942#[unsafe(no_mangle)]
943pub unsafe extern "C" fn moq_decode_video_frame(id: u32, dst: *mut moq_video_frame) -> i32 {
944	ffi::enter(move || {
945		let id = ffi::parse_id(id)?;
946		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
947		State::lock().video.frame_info(id, dst)
948	})
949}
950
951/// Free a frame previously delivered through the consume callback. Required for
952/// every delivered frame id; closing the parent consumer is not enough.
953#[unsafe(no_mangle)]
954pub extern "C" fn moq_decode_video_frame_free(id: u32) -> i32 {
955	ffi::enter(move || {
956		let id = ffi::parse_id(id)?;
957		State::lock().video.frame_free(id)
958	})
959}
960#[cfg(test)]
961mod tests {
962	use super::*;
963
964	/// A video track wired up without an encoder, plus a subscriber on it: enough
965	/// to pin what [`finalize`] shows the far end.
966	async fn track_under_test() -> (
967		moq_video::encode::Producer<moq_mux::catalog::hang::Extra>,
968		moq_net::track::Subscriber,
969	) {
970		let mut broadcast = moq_net::broadcast::Info::new().produce();
971		let config = moq_mux::catalog::Config::default()
972			.with_catalog(moq_mux::catalog::hang::Catalog::<moq_mux::catalog::hang::Extra>::default());
973		let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap();
974		let consumer = broadcast.consume();
975		// Probed rather than hand-built, so the test track carries what a real one would.
976		let rendition = moq_video::encode::Config::new(320, 240, moq_video::Rate::new(30, 1).unwrap())
977			.probe()
978			.await
979			.unwrap();
980		let producer = moq_video::encode::Producer::new(broadcast, catalog, rendition).unwrap();
981
982		let name = producer.demand().name().to_string();
983		let track = consumer.track(&name).unwrap().subscribe(None).await.unwrap();
984		(producer, track)
985	}
986
987	/// A clean finish reaches the subscriber as the end of the track, which is what
988	/// makes the abort case below meaningful rather than vacuous.
989	#[tokio::test]
990	async fn a_successful_drain_ends_the_track_cleanly() {
991		let (producer, mut track) = track_under_test().await;
992		finalize(producer, Ok(())).unwrap();
993		assert!(matches!(track.recv_group().await, Ok(None)), "expected a clean end");
994	}
995
996	/// Regression: a lost tail must reach the subscriber as an abort. Finishing the
997	/// track anyway would report a truncated stream as a complete one, and only the
998	/// publisher would ever know otherwise.
999	#[tokio::test]
1000	async fn a_failed_drain_aborts_the_track() {
1001		let (producer, mut track) = track_under_test().await;
1002		let err = moq_video::Error::Codec(anyhow::anyhow!("the codec lost the tail"));
1003		finalize(producer, Err(err)).unwrap_err();
1004
1005		let Err(err) = track.recv_group().await else {
1006			panic!("expected an abort, not a clean end");
1007		};
1008		assert!(
1009			err.to_string().contains("the codec lost the tail"),
1010			"the abort should carry the drain failure: {err}"
1011		);
1012	}
1013}