Skip to main content

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}