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}