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