Skip to main content

moq_video/encode/
encoder.rs

1//! Video encoder front end.
2//!
3//! Accepts raw [`Frame`]s and delegates the actual encode to a
4//! [`Backend`](super::backend::Backend). The resulting frames carry Annex-B in
5//! the framing the catalog importer for [`Config::codec`] expects: H.264
6//! (`moq_mux::codec::h264`) or H.265 (`moq_mux::codec::h265`).
7
8use super::Encoded;
9use super::backend::{self, Backend};
10use crate::{Color, Error, Frame, Size};
11
12/// Output video codec. `#[non_exhaustive]` so new codecs can be added without
13/// breaking external `match`es.
14///
15/// Not every codec has a backend on every platform: H.265 is hardware-only
16/// (VideoToolbox on macOS today). Building an [`Encoder`] returns
17/// [`Error::NoEncoder`](crate::Error::NoEncoder) when nothing can encode the
18/// requested codec on this machine.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum Codec {
22	/// H.264 / AVC, Annex-B with in-band SPS/PPS (the "avc3" shape). The widest
23	/// support and the default.
24	#[default]
25	H264,
26	/// H.265 / HEVC, Annex-B with in-band VPS/SPS/PPS (the "hev1" shape).
27	H265,
28}
29
30/// Which encoder implementation to use. `#[non_exhaustive]` so new selection
31/// strategies can be added without breaking external `match`es.
32#[derive(Clone, Debug, Default, PartialEq, Eq)]
33#[non_exhaustive]
34pub enum Kind {
35	/// Prefer a platform hardware encoder, falling back to the openh264 software
36	/// encoder when none is available.
37	#[default]
38	Auto,
39	/// Hardware only; error if none is available.
40	Hardware,
41	/// Software only (openh264 for H.264).
42	Software,
43	/// A specific backend by name, e.g. `"videotoolbox"`, `"nvenc"`, `"vaapi"`,
44	/// or `"openh264"`.
45	Named(String),
46}
47
48/// Encoder configuration. `width` / `height` / `framerate` are the encoded
49/// output; input frames must already be at this resolution.
50///
51/// `#[non_exhaustive]`: build via [`Config::new`] and set the optional fields,
52/// so future knobs don't break callers.
53#[derive(Clone, Debug)]
54#[non_exhaustive]
55pub struct Config {
56	pub width: u32,
57	pub height: u32,
58	pub framerate: u32,
59	/// Target bitrate in bits per second. `None` derives a sane default
60	/// from resolution and framerate (~0.07 bits per pixel per second).
61	pub bitrate: Option<u64>,
62	/// Keyframe interval in frames. Subscribers joining mid-stream wait at
63	/// most this many frames before they can start decoding.
64	pub gop: u32,
65	/// Output codec. Defaults to [`Codec::H264`].
66	pub codec: Codec,
67	pub kind: Kind,
68	/// The color space of the input frames, written into the bitstream's VUI so a
69	/// decoder doesn't have to guess. `None` uses [`Color::infer`], which is both
70	/// what the crate's own RGB conversions produce and what a player falls back
71	/// to, so leaving it unset keeps the pixels and the label in agreement.
72	///
73	/// Set it only when feeding frames the crate did not convert and whose space
74	/// you know from elsewhere.
75	pub color: Option<Color>,
76}
77
78impl Config {
79	/// A config encoding `width` x `height` at `framerate`, with the default
80	/// codec, GOP, and bitrate.
81	pub fn new(width: u32, height: u32, framerate: u32) -> Self {
82		Self {
83			width,
84			height,
85			framerate,
86			bitrate: None,
87			// ~2 seconds at the configured framerate.
88			gop: framerate.saturating_mul(2).max(1),
89			codec: Codec::default(),
90			kind: Kind::Auto,
91			color: None,
92		}
93	}
94
95	/// The encoded resolution.
96	pub fn size(&self) -> Size {
97		Size::new(self.width, self.height)
98	}
99
100	/// Resolved input color space: explicit override, or the size-based guess
101	/// every player makes for an untagged stream. Backends write this into the
102	/// VUI; the crate's RGB conversions pick the same answer for the same size,
103	/// so the samples match what the bitstream claims.
104	pub(crate) fn resolved_color(&self) -> Color {
105		self.color.unwrap_or_else(|| Color::infer(self.size()))
106	}
107
108	/// Resolved bitrate: explicit override, or a pixels-per-second estimate.
109	pub(crate) fn resolved_bitrate(&self) -> u64 {
110		self.bitrate.unwrap_or_else(|| {
111			// 0.07 bits per pixel per second matches the JS publisher's
112			// default and lands ~4.4 Mbps for 1080p30.
113			((self.size().pixels() * self.framerate as u64) as f64 * 0.07) as u64
114		})
115	}
116}
117
118/// Video encoder. Build one with [`Encoder::new`], feed it raw [`Frame`]s via
119/// [`encode`](Self::encode), and publish the resulting [`Encoded`] access units
120/// through a [`Producer`](super::Producer) built for the same [`Codec`].
121pub struct Encoder {
122	backend: Box<dyn Backend>,
123	codec: Codec,
124	size: Size,
125	bitrate: u64,
126	/// What the backend wrote into the bitstream's VUI, kept so a frame declaring
127	/// a different space is caught rather than silently mislabeled.
128	color: Color,
129	/// A keyframe asked for by [`Encoder::keyframe`], applied to the next frame.
130	/// Held rather than applied immediately because the caller decides a group
131	/// boundary before it has the frame that opens it.
132	pending_keyframe: bool,
133}
134
135impl Encoder {
136	/// Open an encoder for `config`.
137	pub fn new(config: &Config) -> Result<Self, Error> {
138		// Validate at the construction boundary so both entry points (the
139		// capture loop and a bring-your-own-frames caller) reject a zero
140		// framerate, which would produce a degenerate codec time base.
141		if config.framerate == 0 {
142			return Err(Error::InvalidFramerate(0));
143		}
144		// I420 chroma is subsampled 2x2, so the encoded resolution must be even.
145		let size = config.size();
146		size.validate("encoder")?;
147
148		let backend = backend::open(config)?;
149		Ok(Self {
150			backend,
151			codec: config.codec,
152			size,
153			bitrate: config.resolved_bitrate(),
154			color: config.resolved_color(),
155			pending_keyframe: false,
156		})
157	}
158
159	/// The encoder name in use, e.g. `"videotoolbox"`.
160	pub fn name(&self) -> &str {
161		self.backend.name()
162	}
163
164	/// The resolution this encoder emits, which every frame fed to it must match.
165	pub fn size(&self) -> Size {
166		self.size
167	}
168
169	/// The current target bitrate in bits per second: what
170	/// [`Config::bitrate`] resolved to at open, or the last value
171	/// [`set_bitrate`](Self::set_bitrate) accepted.
172	pub fn bitrate(&self) -> u64 {
173		self.bitrate
174	}
175
176	/// Retune the live encoder to `bitrate` bits per second, taking effect from
177	/// roughly the next frame. No IDR is forced, so this is cheap enough to
178	/// drive from a congestion controller: pair it with
179	/// [`rate::Control`](super::rate::Control), which decides *when* the target
180	/// is worth moving.
181	///
182	/// Setting the rate the encoder is already at does nothing and succeeds.
183	///
184	/// # Errors
185	///
186	/// Returns [`Error::BitrateUnsupported`] if this backend can't retune while
187	/// running. That's not fatal: the encoder keeps running at its current rate,
188	/// so a caller driving a control loop should stop adapting rather than stop
189	/// encoding.
190	pub fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
191		if bitrate == self.bitrate {
192			return Ok(());
193		}
194		self.backend.set_bitrate(bitrate)?;
195		// Only after the backend accepts it, so a failed set doesn't leave the
196		// getter reporting a rate the encoder isn't using.
197		self.bitrate = bitrate;
198		Ok(())
199	}
200
201	/// The codec this encoder emits. A [`Producer`](super::Producer) must be
202	/// built for the same codec to publish its packets.
203	pub fn codec(&self) -> Codec {
204		self.codec
205	}
206
207	/// Ask for the next frame to be encoded as a keyframe (an IDR), on top of the
208	/// ones [`Config::gop`] already inserts on its own.
209	///
210	/// Rarely needed: the encoder keys frames automatically, so reach for this only
211	/// when something outside the encoder needs a decodable starting point at a
212	/// specific frame. Opening a new group is the usual reason (a subscriber has to
213	/// be able to start there); resuming after an idle gap is another.
214	///
215	/// The request waits for the next [`encode`](Self::encode) rather than applying
216	/// at once, so it is safe to call before the frame exists. Calling it repeatedly
217	/// before a frame arrives asks for one keyframe, not several.
218	pub fn keyframe(&mut self) {
219		self.pending_keyframe = true;
220	}
221
222	/// Encode one raw [`Frame`], whether it came from capture, a decoder (the
223	/// transcode input path), or your own pixels via
224	/// [`Surface::rgba`](crate::Surface::rgba).
225	///
226	/// Returns zero or more encoded access units, each carrying the timestamp of the
227	/// raw frame it came from: a backend that buffers hands back an earlier frame's
228	/// output, so the two don't always line up.
229	///
230	/// A GPU surface feeds a hardware encoder on the same device directly
231	/// (NVDEC -> NVENC never leaves the GPU, a `CVPixelBuffer` goes straight to
232	/// VideoToolbox); anything else falls back to a CPU I420 upload. The frame must
233	/// already be at the encoder's resolution: decode with
234	/// [`decode::Config::resize`](crate::decode::Config), or scale first with
235	/// [`Frame::resize`](crate::Frame::resize).
236	pub fn encode(&mut self, frame: &Frame) -> Result<Vec<Encoded>, Error> {
237		// A transposed frame is why this compares the shape rather than a byte
238		// count: 240x320 and 320x240 hold the same number of bytes.
239		let size = frame.size();
240		if size != self.size {
241			return Err(Error::Codec(anyhow::anyhow!(
242				"frame {size} does not match encoder {}",
243				self.size
244			)));
245		}
246		// The VUI is fixed when the session opens, so a frame in a different space
247		// gets encoded under the wrong label. Reached by resizing across the
248		// standard-definition boundary, where the pixels keep their space but the
249		// encoder was sized into another one; `Config::color` is the way to pin it.
250		//
251		// Warn rather than reject: a live gateway transcoding a source whose VUI
252		// disagrees with its resolution should keep serving a mislabeled stream
253		// rather than drop it, and this is no worse than the untagged stream that
254		// came before. Once, because it would otherwise fire every frame.
255		if let Some(color) = frame.surface.color()
256			&& color != self.color
257		{
258			static WARN_ONCE: std::sync::Once = std::sync::Once::new();
259			WARN_ONCE.call_once(|| {
260				tracing::warn!(
261					frame = ?color,
262					encoder = ?self.color,
263					"frame color space differs from the one written into the bitstream; set encode::Config::color"
264				);
265			});
266		}
267		let encoded = self.backend.encode(frame, self.pending_keyframe)?;
268		// Cleared only once the frame is through: a failed encode produced no
269		// picture, so the request still belongs to whatever comes next rather than
270		// being swallowed. The size check above returns early for the same reason.
271		self.pending_keyframe = false;
272		Ok(encoded)
273	}
274
275	/// Flush the encoder, returning any buffered frames. Each keeps the timestamp
276	/// of the raw frame it was encoded from, so a drained tail stays in step with
277	/// what was published before it.
278	///
279	/// Consumes the encoder: nothing can be encoded after a flush, so this is the
280	/// last call rather than one leaving a drained encoder in your hands.
281	pub fn finish(mut self) -> Result<Vec<Encoded>, Error> {
282		self.backend.finish()
283	}
284}
285
286#[cfg(test)]
287mod tests {
288	use super::*;
289
290	use crate::{I420, Surface};
291
292	/// A mid-gray RGBA buffer: encodable without a camera.
293	fn gray_rgba(width: u32, height: u32) -> Vec<u8> {
294		vec![0x80u8; width as usize * height as usize * 4]
295	}
296
297	/// The `index`th frame of a mid-gray 30fps stream, so a round-tripped
298	/// timestamp identifies the frame it came from.
299	fn gray_frame(width: u32, height: u32, index: u64) -> Frame {
300		let surface = Surface::rgba(&gray_rgba(width, height), Size::new(width, height)).unwrap();
301		Frame::new(surface, at(index))
302	}
303
304	/// The presentation time of frame `index` at 30fps.
305	fn at(index: u64) -> moq_net::Timestamp {
306		moq_net::Timestamp::from_micros(index * 33_333).unwrap()
307	}
308
309	/// The payloads of some encoded frames, for the Annex-B assertions.
310	fn payloads(frames: &[Encoded]) -> Vec<bytes::Bytes> {
311		frames.iter().map(|f| f.payload.clone()).collect()
312	}
313
314	#[test]
315	fn software_encoder_emits_annexb() {
316		let config = Config {
317			kind: Kind::Software,
318			..Config::new(320, 240, 30)
319		};
320		let mut encoder = Encoder::new(&config).expect("openh264 is vendored, always available");
321		assert_eq!(encoder.name(), "openh264");
322
323		let mut frames = Vec::new();
324		for i in 0..30 {
325			if i == 0 {
326				encoder.keyframe();
327			}
328			frames.extend(encoder.encode(&gray_frame(320, 240, i)).unwrap());
329		}
330		frames.extend(encoder.finish().unwrap());
331
332		assert!(!frames.is_empty(), "encoder produced no packets");
333
334		// Every encoded frame carries the timestamp of the raw frame it came from,
335		// so the stream stays in step even if a backend buffers.
336		let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
337		assert!(
338			micros.windows(2).all(|w| w[0] < w[1]),
339			"encoded timestamps not strictly increasing: {micros:?}"
340		);
341		assert!(
342			micros.iter().all(|&t| t % 33_333 == 0 && t < 30 * 33_333),
343			"encoded timestamp outside the fed set: {micros:?}"
344		);
345
346		// The first packet must start with an Annex-B start code so the avc3
347		// importer can find the inline SPS/PPS.
348		let packets = payloads(&frames);
349		let first = &packets[0];
350		let has_start_code = first.starts_with(&[0, 0, 0, 1]) || first.starts_with(&[0, 0, 1]);
351		assert!(
352			has_start_code,
353			"first packet is not Annex-B: {:02x?}",
354			&first[..first.len().min(8)]
355		);
356	}
357
358	/// The bring-your-own-pixels path: RGBA in through `Surface::rgba`, Annex-B out.
359	#[test]
360	fn encode_rgba_surface_emits_annexb() {
361		let config = Config {
362			kind: Kind::Software,
363			..Config::new(320, 240, 30)
364		};
365		let mut encoder = Encoder::new(&config).unwrap();
366
367		let mut frames = encoder.encode(&gray_frame(320, 240, 0)).unwrap();
368		frames.extend(encoder.finish().unwrap());
369		assert!(!frames.is_empty());
370		let packets = payloads(&frames);
371		assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
372	}
373
374	/// The same path starting from planar I420 the caller already has.
375	#[test]
376	fn encode_i420_surface_emits_annexb() {
377		let config = Config {
378			kind: Kind::Software,
379			..Config::new(320, 240, 30)
380		};
381		let mut encoder = Encoder::new(&config).unwrap();
382
383		// A mid-gray I420 frame: flat 0x80 across all three planes.
384		let i420 = I420::new(320, 240, vec![0x80u8; I420::len(320, 240)]).unwrap();
385		let frame = Frame::new(Surface::I420(i420), at(0));
386		let mut frames = encoder.encode(&frame).unwrap();
387		frames.extend(encoder.finish().unwrap());
388		assert!(!frames.is_empty());
389		let packets = payloads(&frames);
390		assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
391	}
392
393	/// A frame that isn't the encoder's size must error rather than encode its
394	/// top-left corner.
395	#[test]
396	fn encode_rejects_dimension_mismatch() {
397		let Ok(mut encoder) = Encoder::new(&Config::new(320, 240, 30)) else {
398			return;
399		};
400		assert!(matches!(encoder.encode(&gray_frame(640, 480, 0)), Err(Error::Codec(_))));
401	}
402
403	/// A transposed frame is why the encoder compares the shape rather than the
404	/// byte count: 240x320 and 320x240 hold the same number of bytes, so a length
405	/// check alone would accept this and encode garbage.
406	#[test]
407	fn encode_rejects_transposed_frame() {
408		let Ok(mut encoder) = Encoder::new(&Config::new(320, 240, 30)) else {
409			return;
410		};
411
412		let transposed = gray_frame(240, 320, 0);
413		assert_eq!(
414			gray_rgba(240, 320).len(),
415			gray_rgba(320, 240).len(),
416			"the byte counts must collide"
417		);
418		assert!(matches!(encoder.encode(&transposed), Err(Error::Codec(_))));
419	}
420
421	#[test]
422	fn new_rejects_zero_framerate() {
423		// Framerate is validated before any backend opens, so this holds on every
424		// platform regardless of which encoders are compiled in.
425		let config = Config::new(320, 240, 0);
426		assert!(matches!(Encoder::new(&config), Err(Error::InvalidFramerate(0))));
427	}
428
429	#[test]
430	fn unknown_named_encoder_errors() {
431		let config = Config {
432			kind: Kind::Named("definitely_not_a_codec".into()),
433			..Config::new(320, 240, 30)
434		};
435		assert!(matches!(Encoder::new(&config), Err(Error::NoEncoder(_))));
436	}
437
438	/// Exercises the hand-rolled VideoToolbox backend end to end on macOS:
439	/// synthetic frames through the real `VTCompressionSession`, asserting the
440	/// AVCC -> Annex-B conversion produces a self-contained IDR (SPS+PPS+slice).
441	#[cfg(target_os = "macos")]
442	#[test]
443	fn videotoolbox_emits_annexb_keyframe() {
444		let config = Config {
445			kind: Kind::Named("videotoolbox".into()),
446			..Config::new(320, 240, 30)
447		};
448		let mut encoder = Encoder::new(&config).expect("videotoolbox is available on macOS");
449		assert_eq!(encoder.name(), "videotoolbox");
450
451		let mut frames = Vec::new();
452		for i in 0..10 {
453			if i == 0 {
454				encoder.keyframe();
455			}
456			frames.extend(encoder.encode(&gray_frame(320, 240, i)).unwrap());
457		}
458		frames.extend(encoder.finish().unwrap());
459
460		assert!(!frames.is_empty(), "encoder produced no packets");
461		// VideoToolbox completes each frame before returning, so the timestamps come
462		// back one per input frame, in order.
463		let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
464		assert!(
465			micros.windows(2).all(|w| w[0] < w[1]),
466			"encoded timestamps not strictly increasing: {micros:?}"
467		);
468
469		let packets = payloads(&frames);
470		let first = &packets[0];
471		assert!(
472			first.starts_with(&[0, 0, 0, 1]) || first.starts_with(&[0, 0, 1]),
473			"first packet is not Annex-B"
474		);
475
476		// The first access unit must be a self-contained IDR: SPS (7), PPS (8),
477		// IDR slice (5), all spliced in-band by the AVCC -> Annex-B conversion.
478		let types = nal_types(first);
479		assert!(types.contains(&7), "no SPS in first packet: {types:?}");
480		assert!(types.contains(&8), "no PPS in first packet: {types:?}");
481		assert!(types.contains(&5), "first packet is not an IDR: {types:?}");
482	}
483
484	/// HEVC via VideoToolbox: synthetic frames through the real
485	/// `VTCompressionSession` with `kCMVideoCodecType_HEVC`, asserting the
486	/// HVCC -> Annex-B conversion produces a self-contained IRAP (VPS+SPS+PPS+IDR).
487	#[cfg(target_os = "macos")]
488	#[test]
489	fn videotoolbox_emits_annexb_keyframe_h265() {
490		let config = Config {
491			codec: Codec::H265,
492			kind: Kind::Named("videotoolbox".into()),
493			..Config::new(320, 240, 30)
494		};
495		let mut encoder = Encoder::new(&config).expect("videotoolbox HEVC is available on macOS");
496		assert_eq!(encoder.name(), "videotoolbox");
497		assert_eq!(encoder.codec(), Codec::H265);
498
499		let mut frames = Vec::new();
500		for i in 0..10 {
501			if i == 0 {
502				encoder.keyframe();
503			}
504			frames.extend(encoder.encode(&gray_frame(320, 240, i)).unwrap());
505		}
506		frames.extend(encoder.finish().unwrap());
507
508		assert!(!frames.is_empty(), "encoder produced no packets");
509		let packets = payloads(&frames);
510		let first = &packets[0];
511		assert!(
512			first.starts_with(&[0, 0, 0, 1]) || first.starts_with(&[0, 0, 1]),
513			"first packet is not Annex-B"
514		);
515
516		// The first access unit must be a self-contained IRAP: VPS (32), SPS (33),
517		// PPS (34), and an IDR slice (16..=23), spliced in-band by the conversion.
518		let types = hevc_nal_types(first);
519		assert!(types.contains(&32), "no VPS in first packet: {types:?}");
520		assert!(types.contains(&33), "no SPS in first packet: {types:?}");
521		assert!(types.contains(&34), "no PPS in first packet: {types:?}");
522		assert!(
523			types.iter().any(|t| (16..=23).contains(t)),
524			"first packet is not an IRAP: {types:?}"
525		);
526	}
527
528	/// HEVC NAL unit types in an Annex-B buffer (type = `(byte >> 1) & 0x3f`).
529	#[cfg(target_os = "macos")]
530	fn hevc_nal_types(annexb: &[u8]) -> Vec<u8> {
531		let mut types = Vec::new();
532		let mut i = 0;
533		while i + 3 < annexb.len() {
534			if annexb[i..i + 3] == [0, 0, 1] {
535				types.push((annexb[i + 3] >> 1) & 0x3f);
536				i += 3;
537			} else {
538				i += 1;
539			}
540		}
541		types
542	}
543
544	/// Feed a GPU surface (NV12 `CVPixelBuffer`) straight into VideoToolbox:
545	/// the zero-copy capture -> encode path, no I420 round-trip.
546	#[cfg(target_os = "macos")]
547	#[test]
548	fn videotoolbox_encodes_surface_zero_copy() {
549		let config = Config {
550			kind: Kind::Named("videotoolbox".into()),
551			..Config::new(320, 240, 30)
552		};
553		let mut encoder = Encoder::new(&config).unwrap();
554
555		let mut frames = Vec::new();
556		for i in 0..10 {
557			if i == 0 {
558				encoder.keyframe();
559			}
560			let frame = Frame::new(Surface::PixelBuffer(nv12_surface(320, 240)), at(i));
561			frames.extend(encoder.encode(&frame).unwrap());
562		}
563		frames.extend(encoder.finish().unwrap());
564
565		assert!(!frames.is_empty());
566		let packets = payloads(&frames);
567		let types = nal_types(&packets[0]);
568		assert!(
569			types.contains(&7) && types.contains(&8) && types.contains(&5),
570			"no IDR: {types:?}"
571		);
572	}
573
574	/// A software encoder must download a GPU surface to I420 first. Exercises
575	/// the NV12 -> I420 fallback path.
576	#[cfg(target_os = "macos")]
577	#[test]
578	fn openh264_downloads_surface() {
579		let config = Config {
580			kind: Kind::Software,
581			..Config::new(320, 240, 30)
582		};
583		let mut encoder = Encoder::new(&config).unwrap();
584
585		encoder.keyframe();
586		let frame = Frame::new(Surface::PixelBuffer(nv12_surface(320, 240)), at(0));
587		let mut frames = encoder.encode(&frame).unwrap();
588		frames.extend(encoder.finish().unwrap());
589
590		assert!(!frames.is_empty());
591		let packets = payloads(&frames);
592		assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
593	}
594
595	/// A mid-gray NV12 `CVPixelBuffer`, the format AVFoundation/ScreenCaptureKit
596	/// hand us. Y and interleaved UV planes filled with 128.
597	#[cfg(target_os = "macos")]
598	fn nv12_surface(width: u32, height: u32) -> crate::frame::macos::PixelBuffer {
599		use std::ptr::{self, NonNull};
600
601		use objc2_core_foundation::CFRetained;
602		use objc2_core_video::{
603			CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
604			CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
605			kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
606		};
607
608		let mut raw: *mut CVPixelBuffer = ptr::null_mut();
609		let status = unsafe {
610			CVPixelBufferCreate(
611				None,
612				width as usize,
613				height as usize,
614				kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
615				None,
616				NonNull::new(&mut raw).unwrap(),
617			)
618		};
619		assert_eq!(status, 0, "CVPixelBufferCreate failed");
620		let buffer = unsafe { CFRetained::from_raw(NonNull::new(raw).unwrap()) };
621
622		let flags = CVPixelBufferLockFlags(0);
623		assert_eq!(unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) }, 0);
624		for (plane, rows) in [(0usize, height as usize), (1usize, height as usize / 2)] {
625			let base = CVPixelBufferGetBaseAddressOfPlane(&buffer, plane) as *mut u8;
626			let stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, plane);
627			unsafe { ptr::write_bytes(base, 128, stride * rows) };
628		}
629		unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
630
631		crate::frame::macos::PixelBuffer::new(buffer, width, height)
632	}
633
634	/// NAL unit types in an Annex-B buffer, found via 3-byte start codes (a
635	/// 4-byte `00 00 00 01` code contains `00 00 01` too, so this catches both).
636	fn nal_types(annexb: &[u8]) -> Vec<u8> {
637		let mut types = Vec::new();
638		let mut i = 0;
639		while i + 3 < annexb.len() {
640			if annexb[i..i + 3] == [0, 0, 1] {
641				types.push(annexb[i + 3] & 0x1f);
642				i += 3;
643			} else {
644				i += 1;
645			}
646		}
647		types
648	}
649
650	/// CPU path: synthetic RGBA through the Media Foundation hardware encoder
651	/// (I420 -> system-memory NV12 upload). Ignored: needs a hardware encoder MFT,
652	/// which GPU-less CI runners lack. Run with `--ignored`.
653	#[cfg(target_os = "windows")]
654	#[test]
655	#[ignore]
656	fn mediafoundation_cpu_rgba() {
657		let config = Config {
658			kind: Kind::Named("mediafoundation".into()),
659			..Config::new(640, 480, 30)
660		};
661		let mut encoder = Encoder::new(&config).expect("hardware H.264 encoder available");
662		assert_eq!(encoder.name(), "mediafoundation");
663
664		let mut frames = Vec::new();
665		for i in 0..30 {
666			if i == 0 {
667				encoder.keyframe();
668			}
669			frames.extend(encoder.encode(&gray_frame(640, 480, i)).unwrap());
670		}
671		frames.extend(encoder.finish().unwrap());
672
673		assert!(!frames.is_empty(), "encoder produced no packets");
674		// The MFT buffers, so packets come back stamped with the frame they were
675		// encoded from rather than whichever frame was going in at the time.
676		let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
677		assert!(
678			micros.windows(2).all(|w| w[0] < w[1]),
679			"encoded timestamps not strictly increasing: {micros:?}"
680		);
681		assert!(
682			micros.iter().all(|&t| t % 33_333 == 0 && t < 30 * 33_333),
683			"encoded timestamp outside the fed set: {micros:?}"
684		);
685
686		let packets = payloads(&frames);
687		let types = nal_types(&packets[0]);
688		assert!(types.contains(&7), "no SPS in first packet: {types:?}");
689		assert!(types.contains(&8), "no PPS in first packet: {types:?}");
690		assert!(types.contains(&5), "first packet is not an IDR: {types:?}");
691	}
692
693	/// Full zero-copy path: real camera -> D3D11 NV12 texture -> hardware encoder
694	/// via the DXGI device manager, no CPU round-trip. Ignored: needs a camera and
695	/// a GPU. Run with `--ignored`.
696	#[cfg(target_os = "windows")]
697	#[tokio::test]
698	#[ignore]
699	async fn mediafoundation_camera_texture() {
700		let mut camera = crate::capture::open(&crate::capture::Config::default())
701			.await
702			.expect("open default camera");
703		let (w, h) = (camera.width(), camera.height());
704
705		let config = Config {
706			kind: Kind::Named("mediafoundation".into()),
707			..Config::new(w, h, camera.framerate().unwrap_or(30))
708		};
709		let mut encoder = Encoder::new(&config).expect("hardware H.264 encoder available");
710
711		let mut frames = Vec::new();
712		let mut textures = 0;
713		for i in 0..30 {
714			let surface = camera.read().await.expect("frame, not end of stream");
715			if matches!(surface, Surface::Texture(_)) {
716				textures += 1;
717			}
718			if i == 0 {
719				encoder.keyframe();
720			}
721			frames.extend(encoder.encode(&Frame::new(surface, at(i))).unwrap());
722		}
723		frames.extend(encoder.finish().unwrap());
724
725		// On a GPU this exercises the zero-copy texture path; the assert guards
726		// against silently testing only the CPU fallback.
727		assert!(textures > 0, "capture never produced a GPU texture");
728		assert!(!frames.is_empty(), "encoder produced no packets");
729		let packets = payloads(&frames);
730		let types = nal_types(&packets[0]);
731		assert!(
732			types.contains(&7) && types.contains(&8) && types.contains(&5),
733			"no IDR: {types:?}"
734		);
735	}
736
737	/// The openh264 retune goes through the raw `set_option` FFI, so this covers
738	/// both that the call is accepted and that the encoder keeps producing after
739	/// it. A wrong option id or a bad `SBitrateInfo` layout would fail here.
740	#[test]
741	fn set_bitrate_retunes_software_encoder() {
742		let config = Config {
743			kind: Kind::Software,
744			..Config::new(320, 240, 30)
745		};
746		let mut encoder = Encoder::new(&config).unwrap();
747
748		let opened = encoder.bitrate();
749		assert_eq!(opened, config.resolved_bitrate());
750
751		// Encode first: this is the live-retune path, once the encoder exists.
752		encoder.encode(&gray_frame(320, 240, 0)).unwrap();
753
754		let halved = opened / 2;
755		encoder.set_bitrate(halved).unwrap();
756		assert_eq!(encoder.bitrate(), halved);
757
758		// The retuned encoder must still emit a decodable keyframe, not wedge.
759		let frames = encoder.encode(&gray_frame(320, 240, 1)).unwrap();
760		assert!(!frames.is_empty(), "encoder produced nothing after a retune");
761		let packets = payloads(&frames);
762		assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
763	}
764
765	/// Regression: openh264 creates its encoder lazily on the first frame and
766	/// rejects `SetOption` with `cmInitExpected` until then. A retune before any
767	/// frame must be deferred to the first encode, not reported as a failure.
768	#[test]
769	fn set_bitrate_before_the_first_frame_is_deferred() {
770		let config = Config {
771			kind: Kind::Software,
772			..Config::new(320, 240, 30)
773		};
774		let mut encoder = Encoder::new(&config).unwrap();
775
776		let halved = encoder.bitrate() / 2;
777		encoder.set_bitrate(halved).expect("a retune before the first frame");
778		assert_eq!(encoder.bitrate(), halved);
779
780		// The deferred rate is applied during this encode, which must still work.
781		let frames = encoder.encode(&gray_frame(320, 240, 0)).unwrap();
782		assert!(!frames.is_empty());
783
784		// And the encoder is live now, so a further retune takes the direct path.
785		encoder.set_bitrate(halved / 2).unwrap();
786		assert!(encoder.encode(&gray_frame(320, 240, 1)).is_ok());
787	}
788
789	/// Setting the current rate must not reach the backend at all: the control
790	/// loop is allowed to be chatty, and the encoder shouldn't pay for it.
791	#[test]
792	fn set_bitrate_to_current_is_a_noop() {
793		let config = Config {
794			kind: Kind::Software,
795			..Config::new(320, 240, 30)
796		};
797		let mut encoder = Encoder::new(&config).unwrap();
798
799		let opened = encoder.bitrate();
800		encoder.set_bitrate(opened).unwrap();
801		assert_eq!(encoder.bitrate(), opened);
802	}
803
804	#[test]
805	fn default_bitrate_scales_with_resolution() {
806		let small = Config::new(320, 240, 30).resolved_bitrate();
807		let large = Config::new(1920, 1080, 30).resolved_bitrate();
808		assert!(large > small);
809		assert!(small > 0);
810	}
811
812	/// A backend that holds each frame back by one, like the Media Foundation MFT:
813	/// `encode` returns the *previous* frame's access unit and `finish` drains the
814	/// last. The payload is the frame's timestamp in microseconds, so a test can
815	/// tell which frame a packet came from independently of what it's stamped with.
816	struct Delayed {
817		pending: Option<Encoded>,
818	}
819
820	impl Backend for Delayed {
821		fn encode(&mut self, frame: &Frame, _keyframe: bool) -> Result<Vec<Encoded>, Error> {
822			let payload = bytes::Bytes::from(frame.timestamp.as_micros().to_string());
823			let previous = self.pending.replace(Encoded::new(payload, frame.timestamp));
824			Ok(previous.into_iter().collect())
825		}
826
827		fn finish(&mut self) -> Result<Vec<Encoded>, Error> {
828			Ok(self.pending.take().into_iter().collect())
829		}
830
831		fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
832			Ok(())
833		}
834
835		fn name(&self) -> &str {
836			"delayed"
837		}
838	}
839
840	/// An encoder over a hand-built backend, so a test can pick the buffering
841	/// behavior rather than take whatever this machine's hardware does.
842	fn encoder_with(backend: Box<dyn Backend>, config: &Config) -> Encoder {
843		Encoder {
844			backend,
845			codec: config.codec,
846			size: config.size(),
847			bitrate: config.resolved_bitrate(),
848			color: config.resolved_color(),
849			pending_keyframe: false,
850		}
851	}
852
853	/// Records the keyframe flag each frame reached the codec with, so a test can
854	/// check what the encoder actually asked for.
855	struct Recorder(std::sync::Arc<std::sync::Mutex<Vec<bool>>>);
856
857	impl Backend for Recorder {
858		fn encode(&mut self, _frame: &Frame, keyframe: bool) -> Result<Vec<Encoded>, Error> {
859			self.0.lock().unwrap().push(keyframe);
860			Ok(Vec::new())
861		}
862
863		fn finish(&mut self) -> Result<Vec<Encoded>, Error> {
864			Ok(Vec::new())
865		}
866
867		fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
868			Ok(())
869		}
870
871		fn name(&self) -> &str {
872			"recorder"
873		}
874	}
875
876	/// Keyframes are automatic, so an untouched encoder forces none. A request is
877	/// held until a frame arrives (callers decide a group boundary before they have
878	/// the frame that opens it), collapses if made twice, and clears afterwards
879	/// rather than keying every frame from then on.
880	#[test]
881	fn a_keyframe_request_waits_for_the_next_frame_then_clears() {
882		let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
883		let config = Config::new(320, 240, 30);
884		let mut encoder = encoder_with(Box::new(Recorder(log.clone())), &config);
885
886		// Nothing asked for: `Config::gop` keys the stream on its own.
887		encoder.encode(&gray_frame(320, 240, 0)).unwrap();
888
889		// Asked twice before a frame exists: one keyframe, on the next frame.
890		encoder.keyframe();
891		encoder.keyframe();
892		encoder.encode(&gray_frame(320, 240, 1)).unwrap();
893
894		// And it does not carry into the frame after.
895		encoder.encode(&gray_frame(320, 240, 2)).unwrap();
896
897		assert_eq!(*log.lock().unwrap(), vec![false, true, false]);
898	}
899
900	/// `keyframe()` has to reach the codec on a *warm* encoder, which is the case
901	/// that matters: a fresh one emits an IDR on its first frame regardless, so only
902	/// a mid-stream request proves the plumbing works. Runs on openh264, so this
903	/// holds on every platform rather than only where hardware exists.
904	#[test]
905	fn a_mid_stream_keyframe_request_emits_an_idr() {
906		let config = Config {
907			kind: Kind::Software,
908			..Config::new(320, 240, 30)
909		};
910		// A GOP far longer than the run, so any IDR here was asked for rather than
911		// inserted on schedule.
912		let mut encoder = Encoder::new(&Config { gop: 1000, ..config }).unwrap();
913
914		let mut per_frame = Vec::new();
915		for i in 0..6 {
916			// Frame 0 opens the stream; ask again at frame 3, mid-stream.
917			if i == 3 {
918				encoder.keyframe();
919			}
920			let encoded = encoder.encode(&gray_frame(320, 240, i)).unwrap();
921			let joined: Vec<u8> = encoded.iter().flat_map(|f| f.payload.iter()).copied().collect();
922			per_frame.push(nal_types(&joined));
923		}
924
925		// The requested frame is a self-contained IDR: SPS (7) and PPS (8) inline
926		// ahead of an IDR slice (5), which is what avc3 promises a joining subscriber.
927		let asked = &per_frame[3];
928		assert!(asked.contains(&5), "the requested frame is not an IDR: {asked:?}");
929		assert!(asked.contains(&7), "no SPS with the requested IDR: {asked:?}");
930		assert!(asked.contains(&8), "no PPS with the requested IDR: {asked:?}");
931
932		// And the frames around it stay delta frames, so the request keyed exactly
933		// one: keying every frame would pass the assertions above while destroying
934		// the bitrate.
935		for i in [1, 2, 4, 5] {
936			assert!(
937				!per_frame[i].contains(&5),
938				"frame {i} was keyed without being asked: {:?}",
939				per_frame[i]
940			);
941		}
942	}
943
944	/// A backend whose every encode fails, to pin what survives one.
945	struct Failing;
946
947	impl Backend for Failing {
948		fn encode(&mut self, _frame: &Frame, _keyframe: bool) -> Result<Vec<Encoded>, Error> {
949			Err(Error::Codec(anyhow::anyhow!("no")))
950		}
951
952		fn finish(&mut self) -> Result<Vec<Encoded>, Error> {
953			Ok(Vec::new())
954		}
955
956		fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
957			Ok(())
958		}
959
960		fn name(&self) -> &str {
961			"failing"
962		}
963	}
964
965	/// A request outlives an encode that produced no picture, whether it was
966	/// rejected up front (wrong size) or failed in the backend. Dropping it would
967	/// leave the next frame unkeyed, so a subscriber waits out a whole GOP for a
968	/// starting point the caller already asked for.
969	#[test]
970	fn a_failed_encode_keeps_the_keyframe_request() {
971		let config = Config::new(320, 240, 30);
972
973		let mut encoder = encoder_with(Box::new(Failing), &config);
974		encoder.keyframe();
975		assert!(encoder.encode(&gray_frame(320, 240, 0)).is_err());
976		assert!(encoder.pending_keyframe, "the backend error swallowed the request");
977
978		// Rejected before the backend ever sees it, for the same reason.
979		let mut encoder = encoder_with(Box::new(Delayed { pending: None }), &config);
980		encoder.keyframe();
981		assert!(encoder.encode(&gray_frame(640, 480, 0)).is_err());
982		assert!(encoder.pending_keyframe, "the size check swallowed the request");
983
984		// ...and the next frame that does go through claims it.
985		encoder.encode(&gray_frame(320, 240, 1)).unwrap();
986		assert!(!encoder.pending_keyframe);
987	}
988
989	/// Regression: a backend that buffers hands back an earlier frame's access
990	/// unit, so the timestamp has to ride through the encoder with the picture.
991	/// Stamping output with whatever frame is going in at the time (or the tail
992	/// with one arbitrary time) shifts every packet by the encoder delay and
993	/// collapses the drained tail onto a single instant.
994	#[test]
995	fn a_buffering_backend_keeps_each_frames_timestamp() {
996		let config = Config::new(320, 240, 30);
997		let mut encoder = encoder_with(Box::new(Delayed { pending: None }), &config);
998
999		// The packet handed back while frame `i` goes in belongs to frame `i - 1`, so
1000		// it has to be stamped one frame back. Stamping at the call site (the only
1001		// option when encode just returned bytes) would shift the whole stream.
1002		for i in 0..5 {
1003			let encoded = encoder.encode(&gray_frame(320, 240, i)).unwrap();
1004			if i == 0 {
1005				assert!(encoded.is_empty(), "the first frame is still buffered");
1006				continue;
1007			}
1008			assert_eq!(encoded.len(), 1);
1009			assert_eq!(encoded[0].timestamp, at(i - 1));
1010			// And the packet really is the earlier frame's, not a mis-stamped copy of
1011			// the one going in.
1012			assert_eq!(&encoded[0].payload[..], at(i - 1).as_micros().to_string().as_bytes());
1013		}
1014
1015		// The tail: frame 4 never came back from an encode call, so `finish` drains
1016		// it, still carrying its own time rather than one the caller picks.
1017		let tail = encoder.finish().unwrap();
1018		assert_eq!(tail.len(), 1);
1019		assert_eq!(tail[0].timestamp, at(4));
1020		assert!(
1021			encoder_with(Box::new(Delayed { pending: None }), &config)
1022				.finish()
1023				.unwrap()
1024				.is_empty()
1025		);
1026	}
1027
1028	/// The hardware encoder states the color space too, and states the one the
1029	/// pixels were actually converted into. VideoToolbox takes the three
1030	/// properties as a request, so read the SPS back rather than trusting that it
1031	/// honored them.
1032	#[cfg(target_os = "macos")]
1033	#[test]
1034	fn videotoolbox_sps_declares_the_color_space() {
1035		use super::backend::test_util::{BT601_DESCRIBED, BT709_DESCRIBED, declared_color};
1036
1037		for (size, described) in [
1038			(Size::new(640, 480), BT601_DESCRIBED),
1039			(Size::new(1920, 1080), BT709_DESCRIBED),
1040		] {
1041			let config = Config {
1042				kind: Kind::Named("videotoolbox".into()),
1043				..Config::new(size.width, size.height, 30)
1044			};
1045			let mut encoder = Encoder::new(&config).expect("videotoolbox is available on macOS");
1046
1047			let rgba = [255u8, 0, 0, 255].repeat(size.pixels() as usize);
1048			let surface = crate::frame::Surface::rgba(&rgba, size).unwrap();
1049			encoder.keyframe();
1050			let frames = encoder
1051				.encode(&Frame::new(surface, moq_net::Timestamp::from_micros(0).unwrap()))
1052				.unwrap();
1053
1054			let keyframe = frames.first().expect("a keyframe");
1055			assert_eq!(declared_color(&keyframe.payload), Some(described), "{size} SPS");
1056		}
1057	}
1058
1059	/// Resizing across the standard-definition boundary keeps the pixels' color
1060	/// space but moves the encoder into another one, which would emit BT.709
1061	/// pixels under a BT.601 label. `Config::color` pins the real space, and the
1062	/// bitstream then says so.
1063	#[test]
1064	fn config_color_pins_the_space_a_resize_carried() {
1065		use crate::Color;
1066
1067		let big = Size::new(1280, 720);
1068		let small = Size::new(640, 480);
1069
1070		// Converted at 720p, so the samples are BT.709.
1071		let rgba = vec![0x80u8; big.pixels() as usize * 4];
1072		let frame = Frame::new(
1073			crate::frame::Surface::rgba(&rgba, big).unwrap(),
1074			moq_net::Timestamp::from_micros(0).unwrap(),
1075		);
1076		let scaled = frame.resize(small).unwrap();
1077		assert_eq!(
1078			scaled.surface.color(),
1079			Some(Color::Bt709Limited),
1080			"resize keeps the space"
1081		);
1082
1083		// Left to infer, a 480p encoder writes BT.601 over those BT.709 samples. It
1084		// still encodes (a live gateway keeps serving) but the label is wrong, which
1085		// is what `Config::color` exists to fix.
1086		let config = Config {
1087			kind: Kind::Software,
1088			..Config::new(small.width, small.height, 30)
1089		};
1090		let mut encoder = Encoder::new(&config).unwrap();
1091		encoder.keyframe();
1092		let frames = encoder.encode(&scaled).expect("a mismatch warns rather than fails");
1093		use super::backend::test_util::{BT601_DESCRIBED, BT709_DESCRIBED, declared_color};
1094		assert_eq!(
1095			declared_color(&frames.first().expect("a keyframe").payload),
1096			Some(BT601_DESCRIBED),
1097			"the inferred label is the wrong one, which is the case Config::color covers"
1098		);
1099
1100		// Declaring the real space fixes the label.
1101		let config = Config {
1102			kind: Kind::Software,
1103			color: Some(Color::Bt709Limited),
1104			..Config::new(small.width, small.height, 30)
1105		};
1106		let mut encoder = Encoder::new(&config).unwrap();
1107		encoder.keyframe();
1108		let frames = encoder.encode(&scaled).expect("a declared space encodes");
1109
1110		let keyframe = frames.first().expect("a keyframe");
1111		assert_eq!(declared_color(&keyframe.payload), Some(BT709_DESCRIBED));
1112	}
1113}