Skip to main content

moq_video/decode/
sink.rs

1//! A [`Decoder`](super::Decoder) that owns the thread it runs on, so any thread
2//! (or task) can drive it.
3//!
4//! The decode-side mirror of [`encode::Sink`](crate::encode::Sink), for the same
5//! reason: the Windows backend is a Media Foundation transform whose COM handles
6//! must be created, driven, and dropped all on one thread (COM apartments are
7//! per-thread). `unsafe impl Send for MediaFoundation` holds only while that is
8//! true, and a decoder held across `.await` in a plain task does not keep it: the
9//! future migrates between executor workers, so the apartment is opened on one
10//! and closed on another.
11//!
12//! macOS keeps decoding inline: VideoToolbox has no COM apartment to balance, so
13//! a thread would only add a hop, and its zero-copy `CVPixelBuffer` surface is
14//! `!Send` and could not cross to one anyway.
15
16use bytes::Bytes;
17use hang::catalog::VideoConfig;
18use moq_net::Timestamp;
19
20use super::decoder::Config;
21use crate::{Error, Frame};
22
23#[cfg(target_os = "macos")]
24use inline::Inner;
25#[cfg(not(target_os = "macos"))]
26use threaded::Inner;
27
28/// A [`Decoder`](super::Decoder) confined to one thread, driven from anywhere.
29///
30/// Same shape as [`Decoder`](super::Decoder), except that
31/// [`decode`](Self::decode) is `async` and takes its payload by value (it may
32/// cross a thread). Reach for this instead of a `Decoder` whenever the decoder
33/// outlives a single thread's stack: a spawned task, an object shared across
34/// threads, an FFI handle. A `Decoder` you build, drive, and drop inside one
35/// function needs none of it.
36///
37/// [`Consumer`](super::Consumer) is built on one, so a caller reading a
38/// subscribed track from a task already gets this and needs nothing here.
39///
40/// # Cancellation
41///
42/// Not cancel-safe, and it says so rather than letting it slide. A queued request
43/// runs whether or not anyone is still waiting, so dropping the future (racing it
44/// in a `select!`, giving it a timeout) can leave the decoder a step ahead of the
45/// caller. Rather than let the next call carry on against a decoder that has moved,
46/// the sink refuses every call after a
47/// cancelled one. Drop it and open another.
48///
49/// macOS never refuses, because there is no thread to run ahead: the decoder runs
50/// inline, so a dropped future either had not started the call or had already
51/// finished it. Write to the contract above regardless, or the same code loses
52/// frames off macOS.
53pub struct Sink(Inner);
54
55impl Sink {
56	/// Open a decoder for `catalog` on its own thread. Returns once the decoder
57	/// is built (or its construction fails), so an unsupported codec surfaces
58	/// here rather than on the first frame.
59	pub async fn open(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
60		Ok(Self(Inner::open(catalog, config).await?))
61	}
62
63	/// The decoder name in use, e.g. `"mediafoundation"`.
64	pub fn name(&self) -> &str {
65		self.0.name()
66	}
67
68	/// Decode one access unit, waiting for whatever pictures it yields.
69	///
70	/// Otherwise [`Decoder::decode`](super::Decoder::decode): zero or more frames,
71	/// since a backend that reorders holds pictures back. `payload` is a [`Bytes`],
72	/// so handing it over is a refcount rather than a copy.
73	pub async fn decode(&mut self, payload: Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error> {
74		self.0.decode(payload, timestamp, keyframe).await
75	}
76
77	/// Drain the frames the decoder still holds once the stream has ended.
78	///
79	/// Otherwise [`Decoder::flush`](super::Decoder::flush), confined to the codec
80	/// thread like [`decode`](Self::decode).
81	pub async fn flush(&mut self) -> Result<Vec<Frame>, Error> {
82		self.0.flush().await
83	}
84}
85
86#[cfg(not(target_os = "macos"))]
87mod threaded {
88	use bytes::Bytes;
89	use hang::catalog::VideoConfig;
90	use moq_net::Timestamp;
91	use tokio::sync::{mpsc, oneshot};
92
93	use super::super::decoder::{Config, Decoder};
94	use crate::worker::{Ready, Worker};
95	use crate::{Error, Frame};
96
97	/// Work for the decode thread. One variant today; the enum is here so a
98	/// future request (a flush, a reset) lands in order with the frames around it
99	/// rather than racing them.
100	enum Request {
101		Decode {
102			payload: Bytes,
103			timestamp: Timestamp,
104			keyframe: bool,
105			resp: oneshot::Sender<Result<Vec<Frame>, Error>>,
106		},
107		Flush {
108			resp: oneshot::Sender<Result<Vec<Frame>, Error>>,
109		},
110	}
111
112	/// Build a decoder and serve requests until the channel closes. Runs entirely
113	/// on the decode thread; see [`crate::worker`].
114	fn run(catalog: VideoConfig, config: Config, ready: Ready, mut requests: mpsc::UnboundedReceiver<Request>) {
115		let mut decoder = match Decoder::new(&catalog, &config) {
116			Ok(decoder) => decoder,
117			Err(err) => return ready.err(err),
118		};
119		// If the awaiting `open` was cancelled, give up before decoding.
120		if !ready.ok(decoder.name()) {
121			return;
122		}
123
124		// Serve each request in arrival order. The decoder and its COM / MFT
125		// handles are created, used, and dropped only on this thread.
126		while let Some(req) = requests.blocking_recv() {
127			match req {
128				Request::Decode {
129					payload,
130					timestamp,
131					keyframe,
132					resp,
133				} => {
134					let _ = resp.send(decoder.decode(&payload, timestamp, keyframe));
135				}
136				Request::Flush { resp } => {
137					let _ = resp.send(decoder.flush());
138				}
139			}
140		}
141		// `decoder` drops here, on this thread, balancing the COM apartment.
142	}
143
144	/// A [`Decoder`] running on its own thread. See the module docs.
145	pub struct Inner(Worker<Request>);
146
147	impl Inner {
148		pub async fn open(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
149			let catalog = catalog.clone();
150			let config = config.clone();
151			let worker = Worker::open("moq-video-decode", move |ready, requests| {
152				run(catalog, config, ready, requests)
153			})
154			.await?;
155			Ok(Self(worker))
156		}
157
158		pub fn name(&self) -> &str {
159			self.0.name()
160		}
161
162		pub async fn decode(
163			&mut self,
164			payload: Bytes,
165			timestamp: Timestamp,
166			keyframe: bool,
167		) -> Result<Vec<Frame>, Error> {
168			self.0
169				.request(|resp| Request::Decode {
170					payload,
171					timestamp,
172					keyframe,
173					resp,
174				})
175				.await
176		}
177
178		pub async fn flush(&mut self) -> Result<Vec<Frame>, Error> {
179			self.0.request(|resp| Request::Flush { resp }).await
180		}
181	}
182}
183
184#[cfg(target_os = "macos")]
185mod inline {
186	use bytes::Bytes;
187	use hang::catalog::VideoConfig;
188	use moq_net::Timestamp;
189
190	use super::super::decoder::{Config, Decoder};
191	use crate::{Error, Frame};
192
193	/// A [`Decoder`] driven inline on the calling thread (see the module docs).
194	pub struct Inner(Decoder);
195
196	impl Inner {
197		pub async fn open(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
198			Ok(Self(Decoder::new(catalog, config)?))
199		}
200
201		pub fn name(&self) -> &str {
202			self.0.name()
203		}
204
205		/// Async only to match the threaded `Inner`; there's no thread to hand
206		/// this to, so it decodes inline.
207		pub async fn decode(
208			&mut self,
209			payload: Bytes,
210			timestamp: Timestamp,
211			keyframe: bool,
212		) -> Result<Vec<Frame>, Error> {
213			self.0.decode(&payload, timestamp, keyframe)
214		}
215
216		pub async fn flush(&mut self) -> Result<Vec<Frame>, Error> {
217			self.0.flush()
218		}
219	}
220}
221
222/// macOS is exempt by design: the inline sink decodes on the calling thread, so
223/// there is no confinement to assert (see the module docs).
224#[cfg(all(test, not(target_os = "macos")))]
225mod tests {
226	use std::collections::HashSet;
227	use std::sync::{Arc, Mutex};
228	use std::thread::ThreadId;
229
230	use super::super::Kind;
231	use super::super::backend::probe;
232	use super::*;
233
234	fn probe_catalog() -> VideoConfig {
235		let mut catalog = VideoConfig::new(hang::catalog::H264 {
236			inline: true,
237			profile: 0x42,
238			constraints: 0,
239			level: 30,
240		});
241		catalog.coded_width = Some(probe::SIZE.width);
242		catalog.coded_height = Some(probe::SIZE.height);
243		catalog
244	}
245
246	fn probe_config() -> Config {
247		let mut config = Config::new();
248		config.kind = Kind::Named(probe::NAME.into());
249		config
250	}
251
252	fn at(index: u64) -> Timestamp {
253		Timestamp::from_micros(index * 33_333).unwrap()
254	}
255
256	/// Regression: the Windows decoder opens a COM apartment on the thread that
257	/// builds it and closes it on the thread that drops it. Every owner holds the
258	/// codec across `.await` in a spawned task (`decode::Consumer`'s read loop,
259	/// which libmoq drives; moq-transcode's feed and fetch pipeline), so the
260	/// future migrates between executor workers and the apartment is opened on
261	/// one and closed on another.
262	#[test]
263	fn the_codec_stays_on_one_thread_however_it_is_driven() {
264		let _probe = probe::exclusive();
265
266		let sink = Arc::new(Mutex::new(Some(
267			pollster::block_on(Sink::open(&probe_catalog(), &probe_config())).unwrap(),
268		)));
269
270		// Drive it the way a migrating task does: a fresh caller thread every
271		// time, none of them the one that opened it.
272		let mut callers = vec![std::thread::current().id()];
273		for index in 0..3u64 {
274			let sink = sink.clone();
275			let caller = std::thread::spawn(move || {
276				let mut guard = sink.lock().unwrap();
277				let sink = guard.as_mut().unwrap();
278				let frames = pollster::block_on(sink.decode(Bytes::from_static(b"au"), at(index), index == 0)).unwrap();
279				// The frame really came back, so the assertions below are about a
280				// decoder that ran rather than one that no-opped.
281				assert_eq!(frames.len(), 1);
282				assert_eq!(frames[0].timestamp, at(index));
283				std::thread::current().id()
284			});
285			callers.push(caller.join().unwrap());
286		}
287
288		// ...and dropped from yet another.
289		let closer = std::thread::spawn(move || {
290			sink.lock().unwrap().take();
291			std::thread::current().id()
292		});
293		callers.push(closer.join().unwrap());
294
295		let log = probe::take();
296		for what in ["open", "decode", "drop"] {
297			assert!(log.iter().any(|(event, _)| *event == what), "no {what} in {log:?}");
298		}
299
300		let threads: HashSet<ThreadId> = log.iter().map(|(_, id)| *id).collect();
301		assert_eq!(threads.len(), 1, "the codec ran on more than one thread: {log:?}");
302
303		let codec = threads.into_iter().next().unwrap();
304		assert!(
305			!callers.contains(&codec),
306			"the codec ran on a caller's thread rather than its own: {log:?}"
307		);
308	}
309}