Skip to main content

moq_video/encode/
sink.rs

1//! An [`Encoder`](super::Encoder) that owns the thread it runs on, so any
2//! thread (or task) can drive it.
3//!
4//! Off macOS the encoder runs on a dedicated OS thread (mirroring the capture
5//! pump): the Windows hardware encoder is a Media
6//! Foundation MFT whose COM handles must be created, driven, and dropped all on
7//! one thread (COM apartments are per-thread), and whose encode call blocks on
8//! MFT events. Driving it inline on a tokio worker would unbalance the
9//! per-thread COM refcount as the future migrates between workers and park a
10//! worker on a stalled MFT. A synchronous caller has the same problem for the
11//! same reason: an FFI object shared between threads opens the apartment on
12//! whichever thread built it and closes it on whichever thread drops it.
13//! Confining the whole encoder lifetime to one thread fixes both; frames are
14//! `Send` there (Windows D3D11 textures and CPU I420 both are) and packets come
15//! back over a channel.
16//!
17//! macOS keeps encoding inline: VideoToolbox has no COM apartment to balance and
18//! doesn't block on an event loop, so a thread would only add a hop, and its
19//! zero-copy `CVPixelBuffer` surface is `!Send` and couldn't cross to one anyway.
20
21use std::sync::Arc;
22
23use super::Encoded;
24use super::encoder::Config;
25use crate::{Error, Frame};
26
27#[cfg(target_os = "macos")]
28use inline::Inner;
29#[cfg(not(target_os = "macos"))]
30use threaded::Inner;
31
32/// An [`Encoder`](super::Encoder) confined to one thread, driven from anywhere.
33///
34/// Same shape as [`Encoder`](super::Encoder), one method at a time, except that
35/// the calls are `async` and [`encode`](Self::encode) takes the frame by value
36/// (it may cross a thread). Reach for this instead of an `Encoder` whenever the
37/// encoder outlives a single thread's stack: an object shared across threads, an
38/// FFI handle, a task that migrates between executor workers. An `Encoder` you
39/// build, drive, and drop inside one function needs none of it.
40///
41/// Awaiting rather than blocking is the point: the codec runs on its own thread,
42/// so the executor keeps its worker while a slow hardware encoder works through
43/// a frame. A caller with no executor to yield to (an FFI boundary that must
44/// return a result synchronously) blocks on these futures itself.
45///
46/// # Cancellation
47///
48/// These futures are not cancel-safe, and the sink says so rather than letting
49/// it slide. The codec runs on its own thread, so a request that has been queued
50/// runs whether or not anyone is still waiting: dropping the future (racing it in
51/// a `select!`, giving it a timeout) leaves the codec a step ahead of the stream,
52/// holding output nobody received. Rather than let the next call carry on and
53/// publish a track quietly missing those frames, the sink refuses every call
54/// after a cancelled one. Drop it and open another.
55///
56/// Racing an encode against a shutdown signal is fine, since the sink is on its
57/// way out anyway. What does not work is cancelling one and carrying on.
58///
59/// macOS never refuses, because there is no thread to run ahead: the encoder
60/// runs inline, so a dropped future either had not started the call or had
61/// already finished it. Write to the contract above regardless, or the same code
62/// loses frames off macOS.
63pub struct Sink(Inner);
64
65impl Sink {
66	/// Open an encoder for `config` on its own thread. Returns once the encoder
67	/// is built (or its construction fails), so a bad config or a missing backend
68	/// surfaces here rather than on the first frame.
69	pub async fn open(config: &Config) -> Result<Self, Error> {
70		Ok(Self(Inner::open(config).await?))
71	}
72
73	/// The encoder name in use, e.g. `"mediafoundation"`.
74	pub fn name(&self) -> &str {
75		self.0.name()
76	}
77
78	/// Ask for the next frame to be encoded as a keyframe, like
79	/// [`Encoder::keyframe`](super::Encoder::keyframe).
80	///
81	/// Queued behind the frames already in flight rather than applied to
82	/// whichever one the codec happens to be on, so it keys the next frame you
83	/// pass to [`encode`](Self::encode). Only queues the request, so unlike the
84	/// rest there is nothing to await.
85	pub fn keyframe(&mut self) {
86		self.0.keyframe();
87	}
88
89	/// Encode one frame, waiting for its access units.
90	///
91	/// Otherwise [`Encoder::encode`](super::Encoder::encode): zero or more access
92	/// units, each stamped with the frame it came from.
93	///
94	/// Takes ownership, since the frame may be moved to the encode thread, but
95	/// takes it as anything that can become an [`Arc`] so a caller fanning one
96	/// frame out to several encoders (a transcode ladder) hands over a clone of
97	/// the handle rather than a copy of the pixels. Pass a [`Frame`] and it is
98	/// wrapped for you.
99	pub async fn encode(&mut self, frame: impl Into<Arc<Frame>>) -> Result<Vec<Encoded>, Error> {
100		self.0.encode(frame.into()).await
101	}
102
103	/// Retune the encoder, waiting for the backend's verdict. See
104	/// [`Encoder::set_bitrate`](super::Encoder::set_bitrate) for what a failure
105	/// means (not fatal: stop adapting, keep encoding).
106	pub async fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
107		self.0.set_bitrate(bitrate).await
108	}
109
110	/// Empty the codec at a boundary the output has to respect, leaving it ready
111	/// for the frames that follow. See [`Encoder::flush`](super::Encoder::flush).
112	///
113	/// A live track needs this at every group boundary: a backend that pipelines
114	/// is still holding the last frames of a group when it ends, and they would
115	/// otherwise surface in the next group ahead of its keyframe, where a
116	/// subscriber joining there cannot decode them. Publishing frame by frame
117	/// with no group structure needs none of it.
118	pub async fn flush(&mut self) -> Result<Vec<Encoded>, Error> {
119		self.0.flush().await
120	}
121
122	/// Drain the codec, returning every access unit it was still holding, and
123	/// shut the encoder down.
124	///
125	/// Consumes the sink, like [`Encoder::finish`](super::Encoder::finish).
126	/// Dropping a sink without this is fine and tears down just as cleanly, it
127	/// just discards the tail: publish the returned frames before ending a track,
128	/// or its last pictures never reach a subscriber.
129	pub async fn finish(self) -> Result<Vec<Encoded>, Error> {
130		self.0.finish().await
131	}
132}
133
134#[cfg(not(target_os = "macos"))]
135mod threaded {
136	use std::sync::Arc;
137
138	use tokio::sync::{mpsc, oneshot};
139
140	use super::super::Encoded;
141	use super::super::encoder::{Config, Encoder};
142	use crate::worker::{Ready, Worker};
143	use crate::{Error, Frame};
144
145	/// Work for the encode thread. Every variant goes down the same channel so a
146	/// keyframe request or a bitrate change lands in order with the frames around
147	/// it, rather than racing them.
148	enum Request {
149		/// A frame to encode, plus a oneshot to return the resulting access units
150		/// (or an error) in order.
151		Encode {
152			frame: Arc<Frame>,
153			resp: oneshot::Sender<Result<Vec<Encoded>, Error>>,
154		},
155		/// Key the next frame. No reply: the encoder only records the request, so
156		/// there is nothing to report and nothing to wait for.
157		Keyframe,
158		/// Retune to a new bitrate, reporting whether the backend took it so the
159		/// caller can stop adapting against an encoder that can't. The round trip
160		/// is affordable because the rate control policy only sends one of these
161		/// when the target moves meaningfully, not per frame.
162		SetBitrate {
163			bitrate: u64,
164			resp: oneshot::Sender<Result<(), Error>>,
165		},
166		/// Empty the codec at a group boundary, leaving it running. Unlike
167		/// `Finish` the encoder survives, so this is served like any other
168		/// request.
169		Flush {
170			resp: oneshot::Sender<Result<Vec<Encoded>, Error>>,
171		},
172		/// Drain the codec and shut down, returning the tail. Last request the
173		/// thread serves: `Encoder::finish` consumes the encoder, so the loop has
174		/// to break out rather than come back round for another frame.
175		Finish {
176			resp: oneshot::Sender<Result<Vec<Encoded>, Error>>,
177		},
178	}
179
180	/// Build an encoder for `config` and serve requests until the channel closes.
181	/// Runs entirely on the encode thread; see [`crate::worker`].
182	fn run(config: Config, ready: Ready, mut requests: mpsc::UnboundedReceiver<Request>) {
183		let mut encoder = match Encoder::new(&config) {
184			Ok(encoder) => encoder,
185			Err(err) => return ready.err(err),
186		};
187		// If the awaiting `open` was cancelled, give up before encoding.
188		if !ready.ok(encoder.name()) {
189			return;
190		}
191
192		// Serve each request in arrival order. The encoder and its COM / MFT
193		// handles are created, used, and dropped only on this thread. `finish`
194		// consumes the encoder, so it breaks out and drains below rather than
195		// serving another request.
196		let mut draining = None;
197		while let Some(req) = requests.blocking_recv() {
198			match req {
199				Request::Encode { frame, resp } => {
200					let _ = resp.send(encoder.encode(&frame));
201				}
202				Request::Keyframe => encoder.keyframe(),
203				Request::SetBitrate { bitrate, resp } => {
204					let _ = resp.send(encoder.set_bitrate(bitrate));
205				}
206				Request::Flush { resp } => {
207					let _ = resp.send(encoder.flush());
208				}
209				Request::Finish { resp } => {
210					draining = Some(resp);
211					break;
212				}
213			}
214		}
215		// The drain runs here, on this thread, and consumes the encoder; otherwise
216		// `encoder` drops here. Either way the COM apartment it opened closes on
217		// the thread that opened it.
218		if let Some(resp) = draining {
219			let _ = resp.send(encoder.finish());
220		}
221	}
222
223	/// An [`Encoder`] running on its own thread. See the module docs.
224	pub struct Inner(Worker<Request>);
225
226	impl Inner {
227		pub async fn open(config: &Config) -> Result<Self, Error> {
228			let config = config.clone();
229			let worker = Worker::open("moq-video-encode", move |ready, requests| run(config, ready, requests)).await?;
230			Ok(Self(worker))
231		}
232
233		pub fn name(&self) -> &str {
234			self.0.name()
235		}
236
237		pub fn keyframe(&mut self) {
238			// Nothing to report: a dead encode thread surfaces on the next encode,
239			// which is where the caller is already handling one.
240			let _ = self.0.send(Request::Keyframe);
241		}
242
243		pub async fn encode(&mut self, frame: Arc<Frame>) -> Result<Vec<Encoded>, Error> {
244			self.0.request(|resp| Request::Encode { frame, resp }).await
245		}
246
247		pub async fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
248			self.0.request(|resp| Request::SetBitrate { bitrate, resp }).await
249		}
250
251		pub async fn flush(&mut self) -> Result<Vec<Encoded>, Error> {
252			self.0.request(|resp| Request::Flush { resp }).await
253		}
254
255		pub async fn finish(mut self) -> Result<Vec<Encoded>, Error> {
256			// `self` drops on the way out, which drops the sender and joins the
257			// thread that just drained and released the encoder.
258			self.0.request(|resp| Request::Finish { resp }).await
259		}
260	}
261}
262
263#[cfg(target_os = "macos")]
264mod inline {
265	use std::sync::Arc;
266
267	use super::super::Encoded;
268	use super::super::encoder::{Config, Encoder};
269	use crate::{Error, Frame};
270
271	/// An [`Encoder`] driven inline on the calling thread (see the module docs).
272	pub struct Inner(Encoder);
273
274	impl Inner {
275		pub async fn open(config: &Config) -> Result<Self, Error> {
276			Ok(Self(Encoder::new(config)?))
277		}
278
279		pub fn name(&self) -> &str {
280			self.0.name()
281		}
282
283		pub fn keyframe(&mut self) {
284			self.0.keyframe();
285		}
286
287		/// Async only to match the threaded `Inner`; there's no thread to hand this
288		/// to, so it encodes inline. The same holds for the two below.
289		pub async fn encode(&mut self, frame: Arc<Frame>) -> Result<Vec<Encoded>, Error> {
290			self.0.encode(&frame)
291		}
292
293		pub async fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
294			self.0.set_bitrate(bitrate)
295		}
296
297		pub async fn flush(&mut self) -> Result<Vec<Encoded>, Error> {
298			self.0.flush()
299		}
300
301		pub async fn finish(self) -> Result<Vec<Encoded>, Error> {
302			self.0.finish()
303		}
304	}
305}
306
307/// macOS is exempt by design: the inline sink encodes on the calling thread, so
308/// there is no confinement to assert (see the module docs).
309#[cfg(all(test, not(target_os = "macos")))]
310mod tests {
311	use std::collections::HashSet;
312	use std::sync::{Arc, Mutex};
313	use std::thread::ThreadId;
314
315	use super::super::backend::probe;
316	use super::super::{Codec, Kind};
317	use super::*;
318	use crate::{I420, Surface};
319
320	/// A mid-gray frame at the probe backend's resolution, stamped as the
321	/// `index`th frame of a 30fps stream.
322	fn gray(index: u64) -> Frame {
323		let i420 = I420::new(320, 240, vec![0x80u8; I420::len(320, 240)]).unwrap();
324		Frame::new(
325			Surface::I420(i420),
326			moq_net::Timestamp::from_micros(index * 33_333).unwrap(),
327		)
328	}
329
330	fn probe_config() -> Config {
331		let mut config = Config::new(320, 240, 30);
332		config.codec = Codec::H264;
333		config.kind = Kind::Named(probe::NAME.into());
334		config
335	}
336
337	/// Regression: a queued request runs on the encode thread whether or not the
338	/// caller is still waiting, so a cancelled `encode` leaves the codec a step
339	/// ahead of the stream with output nobody received. Carrying on would publish
340	/// a track quietly missing those frames, which is worse than an error: only
341	/// the publisher could ever tell, and only by decoding its own output.
342	#[test]
343	fn a_cancelled_call_poisons_the_sink() {
344		let _probe = probe::exclusive();
345
346		let mut sink = pollster::block_on(Sink::open(&probe_config())).unwrap();
347
348		// Cancel an encode the moment it starts waiting, the shape a `select!` or a
349		// timeout produces. Holding the codec inside the call is what makes the
350		// cancel land mid-flight rather than race the encode thread for it.
351		let gate = probe::hold();
352		pollster::block_on(async {
353			let mut encode = Box::pin(sink.encode(gray(0)));
354			assert!(
355				futures::poll!(encode.as_mut()).is_pending(),
356				"the encode should still be waiting on the held codec"
357			);
358			// Dropped here, with the request queued and the reply still to come.
359		});
360		drop(gate);
361
362		// The codec really did run, so the stream is missing whatever came back.
363		let err = pollster::block_on(sink.encode(gray(1))).expect_err("the sink should refuse");
364		assert!(err.to_string().contains("cancelled"), "unexpected error: {err}");
365		// ...and it stays refused rather than recovering on the call after.
366		assert!(pollster::block_on(sink.flush()).is_err());
367
368		drop(sink);
369		let log = probe::take();
370		assert!(
371			log.iter().any(|(event, _)| *event == "encode"),
372			"the cancelled request should still have reached the codec: {log:?}"
373		);
374	}
375
376	/// Regression: the Windows backend opens a COM apartment on the thread that
377	/// builds the codec and closes it on the thread that drops it, so a codec
378	/// reachable from more than one thread has to own a thread of its own. Both
379	/// FFI bindings held a bare `Encoder` and drove it from whichever thread
380	/// called in, which leaked the opening thread's initialization and ran
381	/// `CoUninitialize` on a thread that never initialized COM.
382	///
383	/// Asserted on every platform rather than only Windows: the confinement is
384	/// what the bindings now rely on, so it should fail here rather than on a
385	/// machine none of CI has.
386	#[test]
387	fn the_codec_stays_on_one_thread_however_it_is_driven() {
388		let _probe = probe::exclusive();
389
390		let sink = Arc::new(Mutex::new(Some(
391			pollster::block_on(Sink::open(&probe_config())).unwrap(),
392		)));
393
394		// Drive it the way an FFI handle gets driven: a fresh caller thread every
395		// time, none of them the thread that opened it.
396		let mut callers = vec![std::thread::current().id()];
397		let mut flushed = Vec::new();
398		for index in 0..3u64 {
399			let sink = sink.clone();
400			let caller = std::thread::spawn(move || {
401				let mut guard = sink.lock().unwrap();
402				let sink = guard.as_mut().unwrap();
403				sink.keyframe();
404				pollster::block_on(sink.encode(gray(index))).unwrap();
405				pollster::block_on(sink.set_bitrate(500_000 + index)).unwrap();
406				// Only the first frame closes a group, so the two after it stay in
407				// the codec and leave the drain below something to find.
408				let flushed = match index {
409					0 => pollster::block_on(sink.flush()).unwrap(),
410					_ => Vec::new(),
411				};
412				(std::thread::current().id(), flushed)
413			});
414			let (caller, drained) = caller.join().unwrap();
415			callers.push(caller);
416			flushed.extend(drained);
417		}
418
419		// The probe holds each frame back by one, so a flush that reached the codec
420		// hands back the frame the group ended on. An inherited no-op would return
421		// nothing here and silently drop it into the next group.
422		let flushed: Vec<_> = flushed.iter().map(|frame| frame.timestamp.as_micros()).collect();
423		assert_eq!(flushed, vec![0], "the group boundary did not empty the codec");
424
425		// ...and finished, so dropped, from yet another.
426		let closer = std::thread::spawn(move || {
427			let sink = sink.lock().unwrap().take().unwrap();
428			let tail = pollster::block_on(sink.finish()).unwrap();
429			(std::thread::current().id(), tail)
430		});
431		let (closer, tail) = closer.join().unwrap();
432		callers.push(closer);
433
434		// Frame 2 never came back from an encode call and no flush claimed it, so
435		// the drain has to. Dropping the sink instead would lose it silently.
436		let tail: Vec<_> = tail.iter().map(|frame| frame.timestamp.as_micros()).collect();
437		assert_eq!(tail, vec![2 * 33_333], "the drain lost the codec's tail");
438
439		let log = probe::take();
440		for what in ["open", "encode", "set_bitrate", "flush", "finish", "drop"] {
441			assert!(log.iter().any(|(event, _)| *event == what), "no {what} in {log:?}");
442		}
443
444		let threads: HashSet<ThreadId> = log.iter().map(|(_, id)| *id).collect();
445		assert_eq!(threads.len(), 1, "the codec ran on more than one thread: {log:?}");
446
447		let codec = threads.into_iter().next().unwrap();
448		assert!(
449			!callers.contains(&codec),
450			"the codec ran on a caller's thread rather than its own: {log:?}"
451		);
452	}
453}