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}