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