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
5//! [`capture::pump`](crate::capture)): 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}