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 decode
43/// runs whether or not anyone is still waiting, so dropping the future (racing it
44/// in a `select!`, giving it a timeout) leaves the decoder a step ahead of the
45/// stream, holding frames nobody received. Rather than let the next call carry on
46/// against a decoder that has moved, 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
78#[cfg(not(target_os = "macos"))]
79mod threaded {
80	use bytes::Bytes;
81	use hang::catalog::VideoConfig;
82	use moq_net::Timestamp;
83	use tokio::sync::{mpsc, oneshot};
84
85	use super::super::decoder::{Config, Decoder};
86	use crate::worker::{Ready, Worker};
87	use crate::{Error, Frame};
88
89	/// Work for the decode thread. One variant today; the enum is here so a
90	/// future request (a flush, a reset) lands in order with the frames around it
91	/// rather than racing them.
92	enum Request {
93		Decode {
94			payload: Bytes,
95			timestamp: Timestamp,
96			keyframe: bool,
97			resp: oneshot::Sender<Result<Vec<Frame>, Error>>,
98		},
99	}
100
101	/// Build a decoder and serve requests until the channel closes. Runs entirely
102	/// on the decode thread; see [`crate::worker`].
103	fn run(catalog: VideoConfig, config: Config, ready: Ready, mut requests: mpsc::UnboundedReceiver<Request>) {
104		let mut decoder = match Decoder::new(&catalog, &config) {
105			Ok(decoder) => decoder,
106			Err(err) => return ready.err(err),
107		};
108		// If the awaiting `open` was cancelled, give up before decoding.
109		if !ready.ok(decoder.name()) {
110			return;
111		}
112
113		// Serve each request in arrival order. The decoder and its COM / MFT
114		// handles are created, used, and dropped only on this thread.
115		while let Some(req) = requests.blocking_recv() {
116			match req {
117				Request::Decode {
118					payload,
119					timestamp,
120					keyframe,
121					resp,
122				} => {
123					let _ = resp.send(decoder.decode(&payload, timestamp, keyframe));
124				}
125			}
126		}
127		// `decoder` drops here, on this thread, balancing the COM apartment.
128	}
129
130	/// A [`Decoder`] running on its own thread. See the module docs.
131	pub struct Inner(Worker<Request>);
132
133	impl Inner {
134		pub async fn open(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
135			let catalog = catalog.clone();
136			let config = config.clone();
137			let worker = Worker::open("moq-video-decode", move |ready, requests| {
138				run(catalog, config, ready, requests)
139			})
140			.await?;
141			Ok(Self(worker))
142		}
143
144		pub fn name(&self) -> &str {
145			self.0.name()
146		}
147
148		pub async fn decode(
149			&mut self,
150			payload: Bytes,
151			timestamp: Timestamp,
152			keyframe: bool,
153		) -> Result<Vec<Frame>, Error> {
154			self.0
155				.request(|resp| Request::Decode {
156					payload,
157					timestamp,
158					keyframe,
159					resp,
160				})
161				.await
162		}
163	}
164}
165
166#[cfg(target_os = "macos")]
167mod inline {
168	use bytes::Bytes;
169	use hang::catalog::VideoConfig;
170	use moq_net::Timestamp;
171
172	use super::super::decoder::{Config, Decoder};
173	use crate::{Error, Frame};
174
175	/// A [`Decoder`] driven inline on the calling thread (see the module docs).
176	pub struct Inner(Decoder);
177
178	impl Inner {
179		pub async fn open(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
180			Ok(Self(Decoder::new(catalog, config)?))
181		}
182
183		pub fn name(&self) -> &str {
184			self.0.name()
185		}
186
187		/// Async only to match the threaded `Inner`; there's no thread to hand
188		/// this to, so it decodes inline.
189		pub async fn decode(
190			&mut self,
191			payload: Bytes,
192			timestamp: Timestamp,
193			keyframe: bool,
194		) -> Result<Vec<Frame>, Error> {
195			self.0.decode(&payload, timestamp, keyframe)
196		}
197	}
198}
199
200/// macOS is exempt by design: the inline sink decodes on the calling thread, so
201/// there is no confinement to assert (see the module docs).
202#[cfg(all(test, not(target_os = "macos")))]
203mod tests {
204	use std::collections::HashSet;
205	use std::sync::{Arc, Mutex};
206	use std::thread::ThreadId;
207
208	use super::super::Kind;
209	use super::super::backend::probe;
210	use super::*;
211
212	fn probe_catalog() -> VideoConfig {
213		let mut catalog = VideoConfig::new(hang::catalog::H264 {
214			inline: true,
215			profile: 0x42,
216			constraints: 0,
217			level: 30,
218		});
219		catalog.coded_width = Some(probe::SIZE.width);
220		catalog.coded_height = Some(probe::SIZE.height);
221		catalog
222	}
223
224	fn probe_config() -> Config {
225		let mut config = Config::new();
226		config.kind = Kind::Named(probe::NAME.into());
227		config
228	}
229
230	fn at(index: u64) -> Timestamp {
231		Timestamp::from_micros(index * 33_333).unwrap()
232	}
233
234	/// Regression: the Windows decoder opens a COM apartment on the thread that
235	/// builds it and closes it on the thread that drops it. Every owner holds the
236	/// codec across `.await` in a spawned task (`decode::Consumer`'s read loop,
237	/// which libmoq drives; moq-transcode's feed and fetch pipeline), so the
238	/// future migrates between executor workers and the apartment is opened on
239	/// one and closed on another.
240	#[test]
241	fn the_codec_stays_on_one_thread_however_it_is_driven() {
242		let _probe = probe::exclusive();
243
244		let sink = Arc::new(Mutex::new(Some(
245			pollster::block_on(Sink::open(&probe_catalog(), &probe_config())).unwrap(),
246		)));
247
248		// Drive it the way a migrating task does: a fresh caller thread every
249		// time, none of them the one that opened it.
250		let mut callers = vec![std::thread::current().id()];
251		for index in 0..3u64 {
252			let sink = sink.clone();
253			let caller = std::thread::spawn(move || {
254				let mut guard = sink.lock().unwrap();
255				let sink = guard.as_mut().unwrap();
256				let frames = pollster::block_on(sink.decode(Bytes::from_static(b"au"), at(index), index == 0)).unwrap();
257				// The frame really came back, so the assertions below are about a
258				// decoder that ran rather than one that no-opped.
259				assert_eq!(frames.len(), 1);
260				assert_eq!(frames[0].timestamp, at(index));
261				std::thread::current().id()
262			});
263			callers.push(caller.join().unwrap());
264		}
265
266		// ...and dropped from yet another.
267		let closer = std::thread::spawn(move || {
268			sink.lock().unwrap().take();
269			std::thread::current().id()
270		});
271		callers.push(closer.join().unwrap());
272
273		let log = probe::take();
274		for what in ["open", "decode", "drop"] {
275			assert!(log.iter().any(|(event, _)| *event == what), "no {what} in {log:?}");
276		}
277
278		let threads: HashSet<ThreadId> = log.iter().map(|(_, id)| *id).collect();
279		assert_eq!(threads.len(), 1, "the codec ran on more than one thread: {log:?}");
280
281		let codec = threads.into_iter().next().unwrap();
282		assert!(
283			!callers.contains(&codec),
284			"the codec ran on a caller's thread rather than its own: {log:?}"
285		);
286	}
287}