media_pp/core/control.rs
1use std::time::{Duration, Instant};
2
3use crate::pp_log::pp_trace;
4use crossbeam_channel::{Receiver, Sender, unbounded};
5
6use crate::{
7 bus::{Bus, BusEvent},
8 element::SourceElement,
9 error::Result,
10};
11
12/// A command that can be sent down a running [`crate::pipeline::Pipeline`]
13/// — travels the same pad-to-pad path `MediaBuffer` does (see
14/// [`crate::element::Sink::control`]), but through a dedicated channel
15/// instead of riding along as data: unlike `Eos`, it has to be able to
16/// reach every element even mid-stream, and (for `Queue`) jump ahead of
17/// whatever data is already backed up rather than wait in line behind it.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ControlMsg {
20 /// Freeze in place. Every [`crate::queue::Queue`] downstream stops
21 /// pulling from its data channel until `Resume`/`Stop` — which also
22 /// backpressures anything feeding it, since a full queue blocks the
23 /// sender. Pairs with [`crate::clock::Clock::pause`], which
24 /// [`crate::pipeline::Pipeline::pause`] calls at the same time so
25 /// paced elements don't see a jump once resumed.
26 Pause,
27 /// Undoes `Pause`.
28 Resume,
29 /// Abandon immediately rather than draining to a natural `Eos` —
30 /// whatever's in flight is dropped, not flushed. The pipeline isn't
31 /// reusable afterward; build a new one for the next run.
32 Stop,
33 /// Jump to an absolute position from the start of the media.
34 /// Handled in two parts, both inside [`drain_control`]: the source
35 /// itself repositions via [`crate::element::SourceElement::seek`]
36 /// *before* this is forwarded downstream, then the forward cascades
37 /// as usual — a [`crate::queue::Queue`] drops whatever it has
38 /// buffered (it predates the seek) instead of delivering it, and a
39 /// decoder flushes its internal reference-frame state. Unlike
40 /// `Pause`, this doesn't block waiting for anything further: it's a
41 /// one-shot repositioning, not a state to later undo with `Resume`.
42 Seek(Duration),
43}
44
45/// One in-flight control request: the message plus a rendezvous channel
46/// the receiver acks once it (and everything it cascaded into downstream)
47/// has finished handling it — this is what makes
48/// [`ControlSender::send`] synchronous. Fields are `pub(crate)` so
49/// [`crate::queue::Queue`]'s worker loop can match on one directly out of
50/// a `crossbeam_channel::select!` arm (which needs the raw `Receiver`,
51/// not the [`ControlReceiver::try_recv`]/[`ControlReceiver::recv`]
52/// wrappers used everywhere else).
53pub(crate) struct Request {
54 pub(crate) msg: ControlMsg,
55 pub(crate) ack: Sender<()>,
56}
57
58/// The sending half of a control channel — cloneable, cheap, `Send +
59/// Sync`. [`crate::pipeline::Pipeline`] holds one to reach its source;
60/// [`crate::queue::Queue`] holds one internally to reach its worker
61/// thread across the thread boundary it owns.
62#[derive(Clone)]
63pub struct ControlSender {
64 tx: Sender<Request>,
65}
66
67/// The receiving half — not `Clone` in spirit (only one thing should be
68/// driving a given control channel at a time) but crossbeam's
69/// `Receiver<T>` is a cheap shared handle under the hood, which is
70/// exactly what [`crate::pipeline::Pipeline::run`] needs: it clones this
71/// into a fresh worker thread on every call.
72#[derive(Clone)]
73pub struct ControlReceiver {
74 pub(crate) rx: Receiver<Request>,
75}
76
77pub fn channel() -> (ControlSender, ControlReceiver) {
78 let (tx, rx) = unbounded();
79 (ControlSender { tx }, ControlReceiver { rx })
80}
81
82impl ControlSender {
83 /// Sends `msg` and blocks until the receiver — and, transitively,
84 /// everything downstream of it — has finished handling it. A no-op
85 /// (returns immediately) if nothing is on the other end to receive it
86 /// (e.g. the pipeline already finished).
87 pub fn send(&self, msg: ControlMsg) {
88 let (ack_tx, ack_rx) = crossbeam_channel::bounded(0);
89 if self.tx.send(Request { msg, ack: ack_tx }).is_ok() {
90 let _ = ack_rx.recv();
91 }
92 }
93}
94
95impl ControlReceiver {
96 pub(crate) fn try_recv(&self) -> Option<(ControlMsg, Sender<()>)> {
97 self.rx.try_recv().ok().map(|r| (r.msg, r.ack))
98 }
99
100 pub(crate) fn recv(&self) -> Option<(ControlMsg, Sender<()>)> {
101 self.rx.recv().ok().map(|r| (r.msg, r.ack))
102 }
103}
104
105/// What draining pending control messages actually did — whether `Stop`
106/// ended it, and how long (if any) was spent frozen inside a `Pause`/
107/// `Resume` pair. A source built on wall-clock scheduling (an elapsed-time
108/// budget like [`crate::elements::TestAudioSource`]/
109/// [`crate::elements::AudioMixer`], or an absolute next-tick deadline like
110/// [`crate::elements::TestVideoSource`]/`DxgiCaptureSource`)
111/// has to fold `paused_for` back into its own schedule after every
112/// [`drain_control`] call — real (`Instant`) time keeps moving during a
113/// `Pause`, but the media timeline must not, or `Resume` would look like a
114/// burst of catch-up work owed all at once.
115#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
116pub struct ControlOutcome {
117 /// `true` if `Stop` was seen: the caller should return `Ok(())`
118 /// immediately, without pushing a final `Eos` (`Stop` means abandon,
119 /// not drain to completion).
120 pub stopped: bool,
121 /// Wall-clock time from starting the synchronous downstream `Pause`
122 /// cascade through finishing the matching `Resume` (or terminating
123 /// `Stop`) cascade during this call — `Duration::ZERO` if no `Pause`
124 /// was seen. Still meaningful
125 /// even when `stopped` is `true` (the sender simply going away while
126 /// paused is treated the same as `Stop`, see `wait_out_pause`), so a
127 /// caller that also tracks its own paused-time total can fold this in
128 /// unconditionally rather than only on the non-stopped path.
129 pub paused_for: Duration,
130}
131
132/// Call once per loop iteration in a [`SourceElement::run`] implementation,
133/// right before pulling the next unit of work — mirrors how a natural
134/// `Eos` is pushed into the source's own pads at the end of that same
135/// loop, just for externally-triggered control instead.
136///
137/// Drains every pending message (see `apply_one` for what "handling
138/// one" means, including `Pause`'s blocking wait). Non-blocking if
139/// nothing's pending — a [`SourceElement::run`] whose own "next unit of
140/// work" can't be waited on via `control`'s own channel (e.g.
141/// [`crate::elements::FileDemuxer`]'s blocking file read) calls this once
142/// before that blocking step; one that *can* (e.g.
143/// [`crate::elements::AppSource`]'s channel receive) selects on both
144/// instead, calling `apply_one`/`wait_out_pause` directly so a
145/// pending `Stop` is never left waiting behind a slow/absent producer —
146/// same reason `WasapiCaptureSource` also drives the
147/// raw receiver directly, to bracket the wait with resetting/restarting
148/// its capture device rather than leaving it running unread through the
149/// whole pause.
150///
151/// See [`ControlOutcome`] for what the return value means.
152pub fn drain_control<S: SourceElement>(
153 control: &ControlReceiver,
154 source: &mut S,
155 bus: &Bus,
156) -> Result<ControlOutcome> {
157 let mut paused_for = Duration::ZERO;
158 while let Some((msg, ack)) = control.try_recv() {
159 if msg == ControlMsg::Pause {
160 // Start measuring before forwarding Pause. `apply_one` is a
161 // synchronous cascade and may itself spend substantial time
162 // waiting for a busy Queue/Sink to become paused; the source
163 // produces no media during that time, so it belongs to the
164 // frozen interval just as much as the later wait for Resume.
165 let pause_start = Instant::now();
166 apply_one(source, bus, msg, &ack)?;
167 let stopped = wait_out_pause(control, source, bus)?;
168 paused_for += pause_start.elapsed();
169 if stopped {
170 return Ok(ControlOutcome {
171 stopped: true,
172 paused_for,
173 });
174 }
175 continue;
176 }
177 if apply_one(source, bus, msg, &ack)? {
178 return Ok(ControlOutcome {
179 stopped: true,
180 paused_for,
181 });
182 }
183 }
184 Ok(ControlOutcome {
185 stopped: false,
186 paused_for,
187 })
188}
189
190/// Applies one already-received control message to `source`: repositions
191/// it first on `Seek` (see [`apply_seek`]), then forwards `msg` to every
192/// one of `source`'s pads (so it cascades through the graph exactly like
193/// a data buffer would), then acks. Returns `true` for `Stop` — same
194/// meaning as [`drain_control`]'s own return.
195pub(crate) fn apply_one<S: SourceElement>(
196 source: &mut S,
197 bus: &Bus,
198 msg: ControlMsg,
199 ack: &Sender<()>,
200) -> Result<bool> {
201 let is_stop = apply_one_unacked(source, bus, msg)?;
202 let _ = ack.send(());
203 Ok(is_stop)
204}
205
206/// The forwarding half of [`apply_one`], split out for a source that must
207/// finish source-local state changes before the synchronous request is
208/// acknowledged. [`crate::elements::WasapiCaptureSource`] uses this for
209/// `Resume`: downstream is resumed first, then its capture device is
210/// restarted, and only then may the caller observe the request as done.
211pub(crate) fn apply_one_unacked<S: SourceElement>(
212 source: &mut S,
213 bus: &Bus,
214 msg: ControlMsg,
215) -> Result<bool> {
216 pp_trace!(
217 pp_log: source.pp_log(),
218 "event=control control={msg:?} phase=received"
219 );
220 let result: Result<bool> = (|| {
221 apply_seek(source, bus, msg)?;
222 for pad in source.src_pads() {
223 pad.control(msg)?;
224 }
225 Ok(msg == ControlMsg::Stop)
226 })();
227 match &result {
228 Ok(_) => pp_trace!(
229 pp_log: source.pp_log(),
230 "event=control control={msg:?} phase=completed outcome=ok"
231 ),
232 Err(error) => pp_trace!(
233 pp_log: source.pp_log(),
234 "event=control control={msg:?} phase=completed outcome=error error={error}"
235 ),
236 }
237 result
238}
239
240/// Blocks on `control` alone — not whatever `source.run()` itself is
241/// otherwise waiting on — until `Resume` or `Stop`, applying (and acking)
242/// every message seen in between via [`apply_one`]. Returns `true` if
243/// `Stop` ended it (including the sender simply going away, treated the
244/// same as `Stop`); `false` once `Resume` arrives.
245pub(crate) fn wait_out_pause<S: SourceElement>(
246 control: &ControlReceiver,
247 source: &mut S,
248 bus: &Bus,
249) -> Result<bool> {
250 loop {
251 let Some((msg, ack)) = control.recv() else {
252 return Ok(true); // sender gone — treat like Stop
253 };
254 if apply_one(source, bus, msg, &ack)? {
255 return Ok(true);
256 }
257 if msg == ControlMsg::Resume {
258 return Ok(false);
259 }
260 // Another Pause while already paused: already forwarded above
261 // (harmless no-op downstream), just keep waiting.
262 }
263}
264
265/// `Seek`'s source-specific half of `drain_control` — repositions
266/// `source` (see [`SourceElement::seek`]) and reports where it actually
267/// landed via [`BusEvent::Seeked`], since that can differ from what was
268/// requested. No-op for every other [`ControlMsg`].
269fn apply_seek<S: SourceElement>(source: &mut S, bus: &Bus, msg: ControlMsg) -> Result<()> {
270 if let ControlMsg::Seek(target) = msg {
271 let landed = source.seek(target)?;
272 bus.post(
273 source.pp_log(),
274 BusEvent::Seeked {
275 element_type: source.element_type(),
276 name: source.name(),
277 requested: target,
278 landed,
279 },
280 );
281 }
282 Ok(())
283}
284
285#[cfg(test)]
286mod tests {
287 use std::{sync::Arc, thread};
288
289 use crate::pp_log::PpLog;
290
291 use super::*;
292 use crate::{
293 buffer::MediaBuffer,
294 element::{Element, ElementType, Sink, Source, element_pp_log},
295 pad::SrcPad,
296 };
297
298 /// A `SourceElement` with no real I/O — just enough surface for
299 /// `drain_control`/`wait_out_pause` to drive, since this module's own
300 /// logic doesn't care what the source actually produces.
301 struct DummySource {
302 pp_log: PpLog,
303 pad: SrcPad,
304 }
305
306 impl DummySource {
307 fn new() -> Self {
308 Self {
309 pp_log: element_pp_log(ElementType::Other, "dummy", None),
310 pad: SrcPad::new("dummy_src"),
311 }
312 }
313 }
314
315 impl Element for DummySource {
316 fn name(&self) -> Arc<str> {
317 "dummy".into()
318 }
319
320 fn element_type(&self) -> ElementType {
321 ElementType::Other
322 }
323
324 fn pp_log(&self) -> &PpLog {
325 &self.pp_log
326 }
327
328 fn pp_log_mut(&mut self) -> &mut PpLog {
329 &mut self.pp_log
330 }
331 }
332
333 impl Source for DummySource {
334 fn src_pads(&mut self) -> &mut [SrcPad] {
335 std::slice::from_mut(&mut self.pad)
336 }
337 }
338
339 impl SourceElement for DummySource {
340 fn run(&mut self, _control: &ControlReceiver, _bus: &Bus) -> Result<()> {
341 unreachable!("not exercised by these tests")
342 }
343
344 fn seek(&mut self, target: Duration) -> Result<Duration> {
345 Ok(target)
346 }
347 }
348
349 struct SlowPauseSink {
350 pp_log: PpLog,
351 pause_delay: Duration,
352 }
353
354 impl Element for SlowPauseSink {
355 fn name(&self) -> Arc<str> {
356 "slow-pause".into()
357 }
358
359 fn element_type(&self) -> ElementType {
360 ElementType::Other
361 }
362
363 fn pp_log(&self) -> &PpLog {
364 &self.pp_log
365 }
366
367 fn pp_log_mut(&mut self) -> &mut PpLog {
368 &mut self.pp_log
369 }
370 }
371
372 impl Sink for SlowPauseSink {
373 fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
374 Ok(())
375 }
376
377 fn control(&mut self, msg: ControlMsg) -> Result<()> {
378 if msg == ControlMsg::Pause {
379 thread::sleep(self.pause_delay);
380 }
381 Ok(())
382 }
383 }
384
385 /// The edge case called out in `wait_out_pause`'s own docs: the
386 /// `ControlSender` going away entirely (e.g. the owning `Pipeline`
387 /// dropped) while paused has to be treated the same as an explicit
388 /// `Stop`, not left blocking forever on a channel nothing will ever
389 /// send on again.
390 #[test]
391 fn wait_out_pause_treats_a_dropped_sender_as_stop() {
392 let (tx, rx) = channel();
393 drop(tx);
394
395 let (bus, _bus_rx) = Bus::new();
396 let mut source = DummySource::new();
397
398 let stopped = wait_out_pause(&rx, &mut source, &bus)
399 .expect("no real seek/push happens on this path, so this can't fail");
400 assert!(
401 stopped,
402 "a dropped ControlSender must be treated the same as an explicit Stop"
403 );
404 }
405
406 /// `wait_out_pause` blocks past any number of redundant `Pause`s and
407 /// only returns (`Ok(false)`, meaning "keep running") once `Resume`
408 /// actually arrives.
409 #[test]
410 fn wait_out_pause_blocks_until_resume_then_returns_false() {
411 let (tx, rx) = channel();
412 let (bus, _bus_rx) = Bus::new();
413 let mut source = DummySource::new();
414
415 let worker = thread::spawn(move || wait_out_pause(&rx, &mut source, &bus));
416
417 // A redundant Pause while already paused: per `wait_out_pause`'s
418 // own docs, forwarded (harmless no-op downstream) and then it
419 // keeps waiting rather than returning.
420 tx.send(ControlMsg::Pause);
421 tx.send(ControlMsg::Resume);
422
423 let stopped = worker
424 .join()
425 .expect("worker must not panic")
426 .expect("no real seek/push happens on this path, so this can't fail");
427 assert!(
428 !stopped,
429 "Resume must unblock wait_out_pause with Ok(false)"
430 );
431 }
432
433 /// `paused_for` starts when the source begins forwarding Pause, not
434 /// only after every downstream element has finally acknowledged it.
435 /// Otherwise a slow control cascade is miscounted as playable media
436 /// time and an elapsed-time source catches that interval up as a burst.
437 #[test]
438 fn drain_control_counts_the_pause_cascade_as_paused_time() {
439 let pause_delay = Duration::from_millis(80);
440 let (tx, rx) = channel();
441 let controller = thread::spawn(move || {
442 tx.send(ControlMsg::Pause);
443 tx.send(ControlMsg::Resume);
444 });
445
446 let (bus, _bus_rx) = Bus::new();
447 let mut source = DummySource::new();
448 source.pad.link(Box::new(SlowPauseSink {
449 pause_delay,
450 pp_log: element_pp_log(ElementType::Other, "slow-pause", None),
451 }));
452
453 let outcome = loop {
454 let outcome = drain_control(&rx, &mut source, &bus)
455 .expect("the synthetic control cascade cannot fail");
456 if outcome.paused_for > Duration::ZERO {
457 break outcome;
458 }
459 thread::yield_now();
460 };
461 controller.join().expect("controller must not panic");
462
463 assert!(!outcome.stopped);
464 assert!(
465 outcome.paused_for >= Duration::from_millis(60),
466 "the {:?} Pause cascade was omitted from paused_for: {:?}",
467 pause_delay,
468 outcome.paused_for
469 );
470 }
471}