Skip to main content

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}