Skip to main content

media_pp/core/
control.rs

1//! Pause, Resume, Stop, Flush, 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::{
16    collections::HashSet,
17    sync::{Arc, Condvar, Mutex},
18    time::{Duration, Instant},
19};
20
21use thiserror::Error;
22
23use crate::pp_log::pp_trace;
24use crossbeam_channel::{Receiver, Sender, unbounded};
25
26use crate::{
27    bus::{Bus, BusEvent},
28    element::{ElementType, SourceElement},
29    error::Result,
30    graph::ElementId,
31};
32
33/// A command that can be sent down a running [`crate::pipeline::Pipeline`]
34/// — travels the same pad-to-pad path `MediaBuffer` does (see
35/// [`crate::element::Sink::control`]), but through a dedicated channel
36/// instead of riding along as data: unlike `Eos`, it has to be able to
37/// reach every element even mid-stream, and (for `Queue`) jump ahead of
38/// whatever data is already backed up rather than wait in line behind it.
39#[derive(Debug, Clone)]
40pub enum ControlMsg {
41    /// Freeze in place. Every [`crate::queue::Queue`] downstream stops
42    /// pulling from its data channel until `Resume`/`Stop` — which also
43    /// backpressures anything feeding it, since a full queue blocks the
44    /// sender. Pairs with [`crate::clock::Clock::pause`], which
45    /// [`crate::pipeline::Pipeline::pause`] calls at the same time so
46    /// paced elements don't see a jump once resumed.
47    Pause,
48    /// Undoes `Pause`.
49    Resume,
50    /// Abandon immediately rather than draining to a natural `Eos` —
51    /// whatever's in flight is dropped, not flushed. The pipeline isn't
52    /// reusable afterward; build a new one for the next run.
53    Stop,
54    /// Discard buffered data and reset state that belongs to the current
55    /// timeline without changing the source position. Pipelines issue this
56    /// before `Seek`; keeping the two controls separate lets paused preroll
57    /// and future timeline operations compose the same flush boundary.
58    Flush,
59    /// Ask every reachable element whether it can participate in a seek.
60    /// Rejections are collected without mutating playback state; the caller
61    /// inspects the shared context after the synchronous cascade returns.
62    CheckSeek(Arc<SeekCheckContext>),
63    /// Temporarily lets paused source and queue workers process data until
64    /// every expected terminal reports its first new-timeline sample.
65    Preroll(Arc<PrerollContext>),
66    /// Jump to an absolute position from the start of the media.
67    /// The source repositions via [`crate::element::SourceElement::seek`]
68    /// before this is forwarded downstream. Timeline state is discarded by
69    /// the preceding `Flush`, not implicitly by this message.
70    Seek(Duration),
71}
72
73impl PartialEq for ControlMsg {
74    fn eq(&self, other: &Self) -> bool {
75        match (self, other) {
76            (Self::Pause, Self::Pause)
77            | (Self::Resume, Self::Resume)
78            | (Self::Stop, Self::Stop)
79            | (Self::Flush, Self::Flush) => true,
80            (Self::Seek(left), Self::Seek(right)) => left == right,
81            (Self::CheckSeek(left), Self::CheckSeek(right)) => Arc::ptr_eq(left, right),
82            (Self::Preroll(left), Self::Preroll(right)) => Arc::ptr_eq(left, right),
83            _ => false,
84        }
85    }
86}
87
88impl Eq for ControlMsg {}
89
90/// Why one element refused a pipeline-wide seek check.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum SeekRejectReason {
93    /// The source follows an external timeline that cannot be repositioned.
94    LiveSource,
95    /// The source cannot reposition its input timeline.
96    SourceNotSeekable,
97    /// A downstream element cannot preserve its contract across a seek.
98    ElementNotSeekable,
99}
100
101/// One element that prevents a pipeline-wide seek.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct SeekRejection {
104    /// Kind of the element that rejected the check.
105    pub element_type: ElementType,
106    /// Caller-selected instance name of the rejecting element.
107    pub name: Arc<str>,
108    /// Capability that made this element incompatible with seeking.
109    pub reason: SeekRejectReason,
110}
111
112/// Shared result accumulator carried by [`ControlMsg::CheckSeek`].
113#[derive(Debug, Default)]
114pub struct SeekCheckContext {
115    rejections: Mutex<Vec<SeekRejection>>,
116}
117
118impl SeekCheckContext {
119    /// Creates an empty accumulator for one synchronous seek check cascade.
120    pub fn new() -> Self {
121        Self::default()
122    }
123
124    /// Records an element refusal. Repeated visits to the same graph path do
125    /// not make the public result noisy with identical entries.
126    pub fn reject(&self, element_type: ElementType, name: Arc<str>, reason: SeekRejectReason) {
127        let rejection = SeekRejection {
128            element_type,
129            name,
130            reason,
131        };
132        let mut rejections = self
133            .rejections
134            .lock()
135            .unwrap_or_else(|poisoned| poisoned.into_inner());
136        if !rejections.contains(&rejection) {
137            rejections.push(rejection);
138        }
139    }
140
141    /// Returns a snapshot of every distinct rejection collected so far.
142    pub fn rejections(&self) -> Vec<SeekRejection> {
143        self.rejections
144            .lock()
145            .unwrap_or_else(|poisoned| poisoned.into_inner())
146            .clone()
147    }
148
149    /// Succeeds when every visited element accepted the seek check.
150    pub fn result(&self) -> std::result::Result<(), SeekError> {
151        let rejections = self.rejections();
152        if rejections.is_empty() {
153            Ok(())
154        } else {
155            Err(SeekError { rejections })
156        }
157    }
158}
159
160/// A pipeline-wide seek check found at least one incompatible element.
161#[derive(Debug, Error)]
162#[error("pipeline seek rejected by {rejections:?}")]
163pub struct SeekError {
164    rejections: Vec<SeekRejection>,
165}
166
167impl SeekError {
168    /// Elements that rejected the attempted pipeline seek.
169    pub fn rejections(&self) -> &[SeekRejection] {
170        &self.rejections
171    }
172}
173
174#[derive(Debug, Default)]
175struct PrerollState {
176    ready: HashSet<ElementId>,
177    cancelled: bool,
178}
179
180/// Shared completion state for one preroll pass.
181#[derive(Debug)]
182pub struct PrerollContext {
183    expected: HashSet<ElementId>,
184    target: Option<Duration>,
185    state: Mutex<PrerollState>,
186    changed: Condvar,
187}
188
189impl PrerollContext {
190    /// Creates a preroll that completes once every supplied terminal ID is
191    /// marked ready (or EOS-equivalent).
192    pub fn new(terminals: impl IntoIterator<Item = ElementId>) -> Self {
193        Self {
194            expected: terminals.into_iter().collect(),
195            target: None,
196            state: Mutex::new(PrerollState::default()),
197            changed: Condvar::new(),
198        }
199    }
200
201    /// Creates a seek preroll whose decoded timing gates should discard
202    /// samples before `target` while decoding forward from the landed
203    /// keyframe.
204    pub fn for_seek(terminals: impl IntoIterator<Item = ElementId>, target: Duration) -> Self {
205        Self {
206            expected: terminals.into_iter().collect(),
207            target: Some(target),
208            state: Mutex::new(PrerollState::default()),
209            changed: Condvar::new(),
210        }
211    }
212
213    /// Exact requested position for seek preroll, or `None` for ordinary
214    /// first-sample preroll.
215    pub fn target(&self) -> Option<Duration> {
216        self.target
217    }
218
219    /// Marks one expected terminal's first valid sample as ready.
220    pub fn mark_ready(&self, terminal: ElementId) {
221        if !self.expected.contains(&terminal) {
222            return;
223        }
224        let mut state = self
225            .state
226            .lock()
227            .unwrap_or_else(|poisoned| poisoned.into_inner());
228        if state.ready.insert(terminal) {
229            self.changed.notify_all();
230        }
231    }
232
233    /// EOS means this terminal cannot produce a sample and therefore must not
234    /// leave the whole preroll waiting forever.
235    pub fn mark_eos(&self, terminal: ElementId) {
236        self.mark_ready(terminal);
237    }
238
239    /// Stops expecting a terminal that has left the graph.
240    ///
241    /// The expected set is fixed when the seek starts, but the topology is
242    /// not: detaching a `Tee` branch mid-seek removes its terminal without
243    /// removing the obligation to hear from it, and the wait would run to its
244    /// timeout for a sample nobody is left to produce. Like EOS, this is
245    /// "cannot produce one", not "produced one".
246    pub fn mark_departed(&self, terminal: ElementId) {
247        self.mark_ready(terminal);
248    }
249
250    /// Whether this one terminal has already taken its preroll sample.
251    ///
252    /// A terminal stops accepting as soon as *it* is ready, not when the whole
253    /// preroll is. Waiting for the others would let a branch that reached the
254    /// target first keep consuming for as long as the slowest branch takes —
255    /// which is how the two streams end up at different positions when preroll
256    /// finally completes.
257    pub fn is_ready(&self, terminal: ElementId) -> bool {
258        let state = self
259            .state
260            .lock()
261            .unwrap_or_else(|poisoned| poisoned.into_inner());
262        !state.cancelled && state.ready.contains(&terminal)
263    }
264
265    /// Whether every terminal in one downstream branch has completed.
266    pub(crate) fn are_ready(&self, terminals: &[ElementId]) -> bool {
267        let state = self
268            .state
269            .lock()
270            .unwrap_or_else(|poisoned| poisoned.into_inner());
271        !state.cancelled && terminals.iter().all(|id| state.ready.contains(id))
272    }
273
274    /// Returns whether every expected terminal has completed this preroll.
275    pub fn is_complete(&self) -> bool {
276        let state = self
277            .state
278            .lock()
279            .unwrap_or_else(|poisoned| poisoned.into_inner());
280        !state.cancelled && self.expected.is_subset(&state.ready)
281    }
282
283    /// Cancels a pending wait, used by stop and failed seek recovery.
284    pub fn cancel(&self) {
285        let mut state = self
286            .state
287            .lock()
288            .unwrap_or_else(|poisoned| poisoned.into_inner());
289        state.cancelled = true;
290        self.changed.notify_all();
291    }
292
293    /// Waits until every expected terminal is ready, cancellation is
294    /// requested, or `timeout` expires.
295    pub fn wait(&self, timeout: Duration) -> std::result::Result<(), PrerollError> {
296        let deadline = Instant::now() + timeout;
297        let mut state = self
298            .state
299            .lock()
300            .unwrap_or_else(|poisoned| poisoned.into_inner());
301        loop {
302            if state.cancelled {
303                return Err(PrerollError::Cancelled);
304            }
305            let mut pending: Vec<_> = self.expected.difference(&state.ready).copied().collect();
306            pending.sort_unstable();
307            if pending.is_empty() {
308                return Ok(());
309            }
310            let now = Instant::now();
311            if now >= deadline {
312                return Err(PrerollError::TimedOut { pending });
313            }
314            let remaining = deadline.saturating_duration_since(now);
315            let (next, _) = self
316                .changed
317                .wait_timeout(state, remaining)
318                .unwrap_or_else(|poisoned| poisoned.into_inner());
319            state = next;
320        }
321    }
322}
323
324/// Failure while waiting for terminal preroll completion.
325#[derive(Debug, Error, PartialEq, Eq)]
326pub enum PrerollError {
327    /// Stop or recovery cancelled the in-flight preroll.
328    #[error("preroll was cancelled")]
329    Cancelled,
330    /// At least one terminal did not receive a first sample in time.
331    #[error("preroll timed out with pending terminals {pending:?}")]
332    TimedOut { pending: Vec<ElementId> },
333}
334
335/// A request carried by a control channel. Ordinary controls cascade through
336/// the graph immediately; `Finish` is source-only because graceful completion
337/// must enter the graph as an ordered [`crate::buffer::MediaBuffer::Eos`].
338#[derive(Debug, Clone, PartialEq, Eq)]
339pub(crate) enum RequestKind {
340    Control(ControlMsg),
341    Finish,
342}
343
344/// One in-flight control request: the message plus a rendezvous channel
345/// the receiver acks once it (and everything it cascaded into downstream)
346/// has finished handling it — this is what makes
347/// [`ControlSender::send`] synchronous. Fields are `pub(crate)` so
348/// [`crate::queue::Queue`]'s worker loop can match on one directly out of
349/// a `crossbeam_channel::select!` arm (which needs the raw `Receiver`,
350/// not the [`ControlReceiver::try_recv`]/[`ControlReceiver::recv`]
351/// wrappers used everywhere else).
352pub(crate) struct Request {
353    pub(crate) kind: RequestKind,
354    pub(crate) ack: Sender<()>,
355}
356
357/// The sending half of a control channel — cloneable, cheap, `Send +
358/// Sync`. [`crate::pipeline::Pipeline`] holds one to reach its source;
359/// [`crate::queue::Queue`] holds one internally to reach its worker
360/// thread across the thread boundary it owns.
361#[derive(Clone)]
362pub struct ControlSender {
363    tx: Sender<Request>,
364}
365
366/// The receiving half — not `Clone` in spirit (only one thing should be
367/// driving a given control channel at a time) but crossbeam's
368/// `Receiver<T>` is a cheap shared handle under the hood, which is
369/// exactly what [`crate::pipeline::Pipeline::run`] needs: it clones this
370/// into a fresh worker thread on every call.
371#[derive(Clone)]
372pub struct ControlReceiver {
373    pub(crate) rx: Receiver<Request>,
374}
375
376/// Creates a control channel.
377///
378/// The channel is unbounded, because a control request must never be blocked by
379/// backpressure on the data path — that is the whole reason control does not
380/// travel as data. [`Pipeline`](crate::pipeline::Pipeline) creates one per
381/// source; [`Queue`](crate::queue::Queue) creates one to reach its own worker.
382pub fn channel() -> (ControlSender, ControlReceiver) {
383    let (tx, rx) = unbounded();
384    (ControlSender { tx }, ControlReceiver { rx })
385}
386
387impl ControlSender {
388    /// Sends `msg` and blocks until the receiver — and, transitively,
389    /// everything downstream of it — has finished handling it. A no-op
390    /// (returns immediately) if nothing is on the other end to receive it
391    /// (e.g. the pipeline already finished).
392    pub fn send(&self, msg: ControlMsg) {
393        self.send_request(RequestKind::Control(msg));
394    }
395
396    /// Requests source-originated EOS without exposing `Finish` as a
397    /// downstream [`ControlMsg`]. Used only by [`crate::pipeline::Pipeline`].
398    pub(crate) fn finish(&self) {
399        self.send_request(RequestKind::Finish);
400    }
401
402    fn send_request(&self, kind: RequestKind) {
403        let (ack_tx, ack_rx) = crossbeam_channel::bounded(0);
404        if self.tx.send(Request { kind, ack: ack_tx }).is_ok() {
405            let _ = ack_rx.recv();
406        }
407    }
408}
409
410impl ControlReceiver {
411    pub(crate) fn try_recv(&self) -> Option<(RequestKind, Sender<()>)> {
412        self.rx.try_recv().ok().map(|r| (r.kind, r.ack))
413    }
414
415    pub(crate) fn recv(&self) -> Option<(RequestKind, Sender<()>)> {
416        self.rx.recv().ok().map(|r| (r.kind, r.ack))
417    }
418}
419
420/// What draining pending source requests actually did — whether `Stop` or
421/// source-only `Finish` ended it, and how long (if any) was spent frozen
422/// inside a `Pause`/`Resume` pair. A source built on wall-clock scheduling (an elapsed-time
423/// budget like [`crate::elements::TestAudioSource`]/
424/// [`crate::elements::AudioMixer`], or an absolute next-tick deadline like
425/// [`crate::elements::TestVideoSource`]/`DxgiCaptureSource`)
426/// has to fold `paused_for` back into its own schedule after every
427/// [`drain_control`] call — real (`Instant`) time keeps moving during a
428/// `Pause`, but the media timeline must not, or `Resume` would look like a
429/// burst of catch-up work owed all at once.
430#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
431pub struct ControlOutcome {
432    /// `true` if either `Stop` or source-only `Finish` was seen: the caller
433    /// should return `Ok(())` immediately. `Stop` abandons without EOS;
434    /// `Finish` has already pushed ordered EOS from the source boundary.
435    /// Keeping this terminal flag true for both also makes existing custom
436    /// source loops honor the new graceful request without continuing to emit
437    /// after EOS.
438    pub stopped: bool,
439    /// Wall-clock time from starting the synchronous downstream `Pause`
440    /// cascade through finishing the matching `Resume` (or terminating
441    /// `Stop`) cascade during this call — `Duration::ZERO` if no `Pause`
442    /// was seen. Still meaningful
443    /// even when `stopped` is `true` (the sender simply going away while
444    /// paused is treated the same as `Stop`, see `wait_out_pause`), so a
445    /// caller that also tracks its own paused-time total can fold this in
446    /// unconditionally rather than only on the non-stopped path.
447    pub paused_for: Duration,
448}
449
450/// Call once per loop iteration in a [`SourceElement::run`] implementation,
451/// right before pulling the next unit of work — mirrors how a natural
452/// `Eos` is pushed into the source's own pads at the end of that same
453/// loop, just for externally-triggered control instead.
454///
455/// Drains every pending message (see `apply_one` for what "handling
456/// one" means, including `Pause`'s blocking wait). Non-blocking if
457/// nothing's pending — a [`SourceElement::run`] whose own "next unit of
458/// work" can't be waited on via `control`'s own channel (e.g.
459/// [`crate::elements::FileDemuxer`]'s blocking file read) calls this once
460/// before that blocking step; one that *can* (e.g.
461/// [`crate::elements::AppSource`]'s channel receive) selects on both
462/// instead, calling `apply_one`/`wait_out_pause` directly so a
463/// pending `Stop`/`Finish` is never left waiting behind a slow/absent producer —
464/// same reason `WasapiCaptureSource` also drives the
465/// raw receiver directly, to bracket the wait with resetting/restarting
466/// its capture device rather than leaving it running unread through the
467/// whole pause.
468///
469/// See [`ControlOutcome`] for what the return value means.
470pub fn drain_control<S: SourceElement>(
471    control: &ControlReceiver,
472    source: &mut S,
473    bus: &Bus,
474) -> Result<ControlOutcome> {
475    let mut paused_for = Duration::ZERO;
476    while let Some((request, ack)) = control.try_recv() {
477        let RequestKind::Control(msg) = request else {
478            apply_finish(source, bus, &ack);
479            return Ok(ControlOutcome {
480                stopped: true,
481                paused_for,
482            });
483        };
484        if msg == ControlMsg::Pause {
485            // Start measuring before forwarding Pause. `apply_one` is a
486            // synchronous cascade and may itself spend substantial time
487            // waiting for a busy Queue/Sink to become paused; the source
488            // produces no media during that time, so it belongs to the
489            // frozen interval just as much as the later wait for Resume.
490            let pause_start = Instant::now();
491            apply_one(source, bus, &msg, &ack)?;
492            let stopped = wait_out_pause(control, source, bus)?;
493            paused_for += pause_start.elapsed();
494            if stopped {
495                return Ok(ControlOutcome {
496                    stopped: true,
497                    paused_for,
498                });
499            }
500            continue;
501        }
502        if apply_one(source, bus, &msg, &ack)? {
503            return Ok(ControlOutcome {
504                stopped: true,
505                paused_for,
506            });
507        }
508    }
509    Ok(ControlOutcome {
510        stopped: false,
511        paused_for,
512    })
513}
514
515/// Applies one source-only graceful completion request. Unlike
516/// [`apply_one`], this never calls `Sink::control`: EOS has to sit behind every
517/// already-produced buffer in each data path so queues and stateful elements
518/// drain in order.
519pub(crate) fn apply_finish<S: SourceElement>(source: &mut S, bus: &Bus, ack: &Sender<()>) {
520    pp_trace!(
521        pp_log: source.pp_log(),
522        "event=finish phase=received"
523    );
524    let pp_log = source.pp_log().clone();
525    let element_type = source.element_type();
526    let name = source.name();
527    for pad in source.src_pads() {
528        if let Err(error) = pad.push_eos(&pp_log) {
529            bus.post(
530                &pp_log,
531                BusEvent::Error {
532                    element_type,
533                    name: name.clone(),
534                    error,
535                },
536            );
537        }
538    }
539    let _ = ack.send(());
540    pp_trace!(
541        pp_log: source.pp_log(),
542        "event=finish phase=completed outcome=ok"
543    );
544}
545
546/// Applies one already-received control message to `source`: repositions
547/// it first on `Seek` (see [`apply_seek`]), then forwards `msg` to every
548/// one of `source`'s pads (so it cascades through the graph exactly like
549/// a data buffer would), then acks. Returns `true` for `Stop` — same
550/// meaning as [`drain_control`]'s own return.
551pub(crate) fn apply_one<S: SourceElement>(
552    source: &mut S,
553    bus: &Bus,
554    msg: &ControlMsg,
555    ack: &Sender<()>,
556) -> Result<bool> {
557    let is_stop = apply_one_unacked(source, bus, msg)?;
558    let _ = ack.send(());
559    Ok(is_stop)
560}
561
562/// The forwarding half of [`apply_one`], split out for a source that must
563/// finish source-local state changes before the synchronous request is
564/// acknowledged. [`crate::elements::WasapiCaptureSource`] uses this for
565/// `Resume`: downstream is resumed first, then its capture device is
566/// restarted, and only then may the caller observe the request as done.
567pub(crate) fn apply_one_unacked<S: SourceElement>(
568    source: &mut S,
569    bus: &Bus,
570    msg: &ControlMsg,
571) -> Result<bool> {
572    pp_trace!(
573        pp_log: source.pp_log(),
574        "event=control control={msg:?} phase=received"
575    );
576    let result: Result<bool> = (|| {
577        apply_seek_check(source, msg);
578        source.on_control(msg);
579        apply_seek(source, bus, msg)?;
580        for pad in source.src_pads() {
581            pad.control(msg.clone())?;
582        }
583        Ok(*msg == ControlMsg::Stop)
584    })();
585    match &result {
586        Ok(_) => pp_trace!(
587            pp_log: source.pp_log(),
588            "event=control control={msg:?} phase=completed outcome=ok"
589        ),
590        Err(error) => pp_trace!(
591            pp_log: source.pp_log(),
592            "event=control control={msg:?} phase=completed outcome=error error={error}"
593        ),
594    }
595    result
596}
597
598/// Blocks on `control` alone — not whatever `source.run()` itself is
599/// otherwise waiting on — until `Resume`, `Stop`, or `Finish`, applying (and
600/// acking) every request seen in between. Returns `true` if `Stop`/`Finish`
601/// ended it (including the sender simply going away, treated the same as
602/// `Stop`); `false` once `Resume` or `Preroll` arrives.
603pub(crate) fn wait_out_pause<S: SourceElement>(
604    control: &ControlReceiver,
605    source: &mut S,
606    bus: &Bus,
607) -> Result<bool> {
608    loop {
609        let Some((request, ack)) = control.recv() else {
610            return Ok(true); // sender gone — treat like Stop
611        };
612        let RequestKind::Control(msg) = request else {
613            apply_finish(source, bus, &ack);
614            return Ok(true);
615        };
616        if apply_one(source, bus, &msg, &ack)? {
617            return Ok(true);
618        }
619        if matches!(msg, ControlMsg::Resume | ControlMsg::Preroll(_)) {
620            return Ok(false);
621        }
622        // Another Pause while already paused: already forwarded above
623        // (harmless no-op downstream), just keep waiting.
624    }
625}
626
627/// `Seek`'s source-specific half of `drain_control` — repositions
628/// `source` (see [`SourceElement::seek`]) and reports where it actually
629/// landed via [`BusEvent::Seeked`], since that can differ from what was
630/// requested. No-op for every other [`ControlMsg`].
631fn apply_seek_check<S: SourceElement>(source: &S, msg: &ControlMsg) {
632    let ControlMsg::CheckSeek(context) = msg else {
633        return;
634    };
635    let reason = if source.is_live() {
636        Some(SeekRejectReason::LiveSource)
637    } else if !source.is_seekable() {
638        Some(SeekRejectReason::SourceNotSeekable)
639    } else {
640        None
641    };
642    if let Some(reason) = reason {
643        context.reject(source.element_type(), source.name(), reason);
644    }
645}
646
647fn apply_seek<S: SourceElement>(source: &mut S, bus: &Bus, msg: &ControlMsg) -> Result<()> {
648    if let ControlMsg::Seek(target) = msg {
649        let landed = source.seek(*target)?;
650        bus.post(
651            source.pp_log(),
652            BusEvent::Seeked {
653                element_type: source.element_type(),
654                name: source.name(),
655                requested: *target,
656                landed,
657            },
658        );
659    }
660    Ok(())
661}
662
663#[cfg(test)]
664mod tests {
665    use std::{sync::Arc, thread};
666
667    use crate::pp_log::PpLog;
668
669    use super::*;
670    use crate::{
671        buffer::MediaBuffer,
672        element::{Element, ElementType, Sink, Source, element_pp_log},
673        pad::SrcPad,
674    };
675
676    /// A `SourceElement` with no real I/O — just enough surface for
677    /// `drain_control`/`wait_out_pause` to drive, since this module's own
678    /// logic doesn't care what the source actually produces.
679    struct DummySource {
680        pp_log: PpLog,
681        pad: SrcPad,
682        flushes: usize,
683    }
684
685    impl DummySource {
686        fn new() -> Self {
687            Self {
688                flushes: 0,
689                pp_log: element_pp_log(ElementType::Other, "dummy", None),
690                pad: SrcPad::new("dummy_src"),
691            }
692        }
693    }
694
695    impl Element for DummySource {
696        fn name(&self) -> Arc<str> {
697            "dummy".into()
698        }
699
700        fn element_type(&self) -> ElementType {
701            ElementType::Other
702        }
703
704        fn pp_log(&self) -> &PpLog {
705            &self.pp_log
706        }
707
708        fn pp_log_mut(&mut self) -> &mut PpLog {
709            &mut self.pp_log
710        }
711    }
712
713    impl Source for DummySource {
714        fn src_pads(&mut self) -> &mut [SrcPad] {
715            std::slice::from_mut(&mut self.pad)
716        }
717    }
718
719    impl SourceElement for DummySource {
720        fn is_live(&self) -> bool {
721            false
722        }
723
724        fn is_seekable(&self) -> bool {
725            false
726        }
727
728        fn run(&mut self, _control: &ControlReceiver, _bus: &Bus) -> Result<()> {
729            unreachable!("not exercised by these tests")
730        }
731
732        fn on_control(&mut self, msg: &ControlMsg) {
733            if *msg == ControlMsg::Flush {
734                self.flushes += 1;
735            }
736        }
737
738        fn seek(&mut self, target: Duration) -> Result<Duration> {
739            Ok(target)
740        }
741    }
742
743    /// A source that holds data of its own — `FileDemuxer` parks packets for a
744    /// pad that cannot accept one yet — has to discard it on the same boundary
745    /// every downstream element does. Nothing else can: the packets exist only
746    /// there, so releasing them after the reposition is the one way old media
747    /// reaches a decoder that has already reset for the new timeline.
748    #[test]
749    fn flush_reaches_the_source_itself_and_nothing_else_does() {
750        let (bus, _bus_rx) = Bus::new();
751        let mut source = DummySource::new();
752
753        for msg in [
754            ControlMsg::Pause,
755            ControlMsg::Resume,
756            ControlMsg::Seek(Duration::from_secs(1)),
757            ControlMsg::CheckSeek(Arc::new(SeekCheckContext::new())),
758        ] {
759            apply_one_unacked(&mut source, &bus, &msg).expect("control applies");
760        }
761        assert_eq!(source.flushes, 0, "only Flush may discard source-held data");
762
763        apply_one_unacked(&mut source, &bus, &ControlMsg::Flush).expect("flush applies");
764        assert_eq!(source.flushes, 1);
765    }
766
767    struct SlowPauseSink {
768        pp_log: PpLog,
769        pause_delay: Duration,
770    }
771
772    impl Element for SlowPauseSink {
773        fn name(&self) -> Arc<str> {
774            "slow-pause".into()
775        }
776
777        fn element_type(&self) -> ElementType {
778            ElementType::Other
779        }
780
781        fn pp_log(&self) -> &PpLog {
782            &self.pp_log
783        }
784
785        fn pp_log_mut(&mut self) -> &mut PpLog {
786            &mut self.pp_log
787        }
788    }
789
790    impl Sink for SlowPauseSink {
791        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
792            Ok(())
793        }
794
795        fn control(&mut self, msg: ControlMsg) -> Result<()> {
796            if msg == ControlMsg::Pause {
797                thread::sleep(self.pause_delay);
798            }
799            Ok(())
800        }
801    }
802
803    /// The edge case called out in `wait_out_pause`'s own docs: the
804    /// `ControlSender` going away entirely (e.g. the owning `Pipeline`
805    /// dropped) while paused has to be treated the same as an explicit
806    /// `Stop`, not left blocking forever on a channel nothing will ever
807    /// send on again.
808    #[test]
809    fn wait_out_pause_treats_a_dropped_sender_as_stop() {
810        let (tx, rx) = channel();
811        drop(tx);
812
813        let (bus, _bus_rx) = Bus::new();
814        let mut source = DummySource::new();
815
816        let stopped = wait_out_pause(&rx, &mut source, &bus)
817            .expect("no real seek/push happens on this path, so this can't fail");
818        assert!(
819            stopped,
820            "a dropped ControlSender must be treated the same as an explicit Stop"
821        );
822    }
823
824    #[test]
825    fn seek_check_collects_a_non_seekable_source_without_mutating_it() {
826        let context = Arc::new(SeekCheckContext::new());
827        let (ack, _ack_rx) = crossbeam_channel::bounded(1);
828        let (bus, _bus_rx) = Bus::new();
829        let mut source = DummySource::new();
830
831        apply_one(
832            &mut source,
833            &bus,
834            &ControlMsg::CheckSeek(Arc::clone(&context)),
835            &ack,
836        )
837        .expect("capability checks must not fail the control cascade");
838
839        let error = context.result().expect_err("dummy source is not seekable");
840        assert_eq!(
841            error.rejections(),
842            [SeekRejection {
843                element_type: ElementType::Other,
844                name: "dummy".into(),
845                reason: SeekRejectReason::SourceNotSeekable,
846            }]
847        );
848    }
849
850    #[test]
851    fn preroll_waits_for_every_terminal_and_reports_pending_ids() {
852        let first = ElementId::for_test(1);
853        let second = ElementId::for_test(2);
854        let context = PrerollContext::new([first, second]);
855
856        context.mark_ready(first);
857        assert_eq!(
858            context.wait(Duration::ZERO),
859            Err(PrerollError::TimedOut {
860                pending: vec![second]
861            })
862        );
863
864        context.mark_eos(second);
865        assert_eq!(context.wait(Duration::ZERO), Ok(()));
866    }
867
868    #[test]
869    fn preroll_wait_can_be_cancelled() {
870        let context = PrerollContext::new([ElementId::for_test(1)]);
871        context.cancel();
872        assert_eq!(
873            context.wait(Duration::from_secs(1)),
874            Err(PrerollError::Cancelled)
875        );
876    }
877
878    #[test]
879    fn wait_out_pause_returns_when_preroll_arrives() {
880        let (tx, rx) = channel();
881        let (bus, _bus_rx) = Bus::new();
882        let mut source = DummySource::new();
883        let context = Arc::new(PrerollContext::new([]));
884
885        let worker = thread::spawn(move || wait_out_pause(&rx, &mut source, &bus));
886        tx.send(ControlMsg::Preroll(context));
887
888        assert!(!worker.join().unwrap().unwrap());
889    }
890
891    /// `wait_out_pause` blocks past any number of redundant `Pause`s and
892    /// only returns (`Ok(false)`, meaning "keep running") once `Resume`
893    /// actually arrives.
894    #[test]
895    fn wait_out_pause_blocks_until_resume_then_returns_false() {
896        let (tx, rx) = channel();
897        let (bus, _bus_rx) = Bus::new();
898        let mut source = DummySource::new();
899
900        let worker = thread::spawn(move || wait_out_pause(&rx, &mut source, &bus));
901
902        // A redundant Pause while already paused: per `wait_out_pause`'s
903        // own docs, forwarded (harmless no-op downstream) and then it
904        // keeps waiting rather than returning.
905        tx.send(ControlMsg::Pause);
906        tx.send(ControlMsg::Resume);
907
908        let stopped = worker
909            .join()
910            .expect("worker must not panic")
911            .expect("no real seek/push happens on this path, so this can't fail");
912        assert!(
913            !stopped,
914            "Resume must unblock wait_out_pause with Ok(false)"
915        );
916    }
917
918    /// `paused_for` starts when the source begins forwarding Pause, not
919    /// only after every downstream element has finally acknowledged it.
920    /// Otherwise a slow control cascade is miscounted as playable media
921    /// time and an elapsed-time source catches that interval up as a burst.
922    #[test]
923    fn drain_control_counts_the_pause_cascade_as_paused_time() {
924        let pause_delay = Duration::from_millis(80);
925        let (tx, rx) = channel();
926        let controller = thread::spawn(move || {
927            tx.send(ControlMsg::Pause);
928            tx.send(ControlMsg::Resume);
929        });
930
931        let (bus, _bus_rx) = Bus::new();
932        let mut source = DummySource::new();
933        source.pad.link(Box::new(SlowPauseSink {
934            pause_delay,
935            pp_log: element_pp_log(ElementType::Other, "slow-pause", None),
936        }));
937
938        let outcome = loop {
939            let outcome = drain_control(&rx, &mut source, &bus)
940                .expect("the synthetic control cascade cannot fail");
941            if outcome.paused_for > Duration::ZERO {
942                break outcome;
943            }
944            thread::yield_now();
945        };
946        controller.join().expect("controller must not panic");
947
948        assert!(!outcome.stopped);
949        assert!(
950            outcome.paused_for >= Duration::from_millis(60),
951            "the {:?} Pause cascade was omitted from paused_for: {:?}",
952            pause_delay,
953            outcome.paused_for
954        );
955    }
956}