use std::{
collections::HashSet,
sync::{Arc, Condvar, Mutex},
time::{Duration, Instant},
};
use thiserror::Error;
use crate::pp_log::pp_trace;
use crossbeam_channel::{Receiver, Sender, unbounded};
use crate::{
bus::{Bus, BusEvent},
element::{ElementType, SourceElement},
error::Result,
graph::ElementId,
};
#[derive(Debug, Clone)]
pub enum ControlMsg {
Pause,
Resume,
Stop,
Flush,
CheckSeek(Arc<SeekCheckContext>),
Preroll(Arc<PrerollContext>),
Seek(Duration),
}
impl PartialEq for ControlMsg {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Pause, Self::Pause)
| (Self::Resume, Self::Resume)
| (Self::Stop, Self::Stop)
| (Self::Flush, Self::Flush) => true,
(Self::Seek(left), Self::Seek(right)) => left == right,
(Self::CheckSeek(left), Self::CheckSeek(right)) => Arc::ptr_eq(left, right),
(Self::Preroll(left), Self::Preroll(right)) => Arc::ptr_eq(left, right),
_ => false,
}
}
}
impl Eq for ControlMsg {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeekRejectReason {
LiveSource,
SourceNotSeekable,
ElementNotSeekable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SeekRejection {
pub element_type: ElementType,
pub name: Arc<str>,
pub reason: SeekRejectReason,
}
#[derive(Debug, Default)]
pub struct SeekCheckContext {
rejections: Mutex<Vec<SeekRejection>>,
}
impl SeekCheckContext {
pub fn new() -> Self {
Self::default()
}
pub fn reject(&self, element_type: ElementType, name: Arc<str>, reason: SeekRejectReason) {
let rejection = SeekRejection {
element_type,
name,
reason,
};
let mut rejections = self
.rejections
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if !rejections.contains(&rejection) {
rejections.push(rejection);
}
}
pub fn rejections(&self) -> Vec<SeekRejection> {
self.rejections
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
pub fn result(&self) -> std::result::Result<(), SeekError> {
let rejections = self.rejections();
if rejections.is_empty() {
Ok(())
} else {
Err(SeekError { rejections })
}
}
}
#[derive(Debug, Error)]
#[error("pipeline seek rejected by {rejections:?}")]
pub struct SeekError {
rejections: Vec<SeekRejection>,
}
impl SeekError {
pub fn rejections(&self) -> &[SeekRejection] {
&self.rejections
}
}
#[derive(Debug, Default)]
struct PrerollState {
ready: HashSet<ElementId>,
cancelled: bool,
}
#[derive(Debug)]
pub struct PrerollContext {
expected: HashSet<ElementId>,
target: Option<Duration>,
state: Mutex<PrerollState>,
changed: Condvar,
}
impl PrerollContext {
pub fn new(terminals: impl IntoIterator<Item = ElementId>) -> Self {
Self {
expected: terminals.into_iter().collect(),
target: None,
state: Mutex::new(PrerollState::default()),
changed: Condvar::new(),
}
}
pub fn for_seek(terminals: impl IntoIterator<Item = ElementId>, target: Duration) -> Self {
Self {
expected: terminals.into_iter().collect(),
target: Some(target),
state: Mutex::new(PrerollState::default()),
changed: Condvar::new(),
}
}
pub fn target(&self) -> Option<Duration> {
self.target
}
pub fn mark_ready(&self, terminal: ElementId) {
if !self.expected.contains(&terminal) {
return;
}
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if state.ready.insert(terminal) {
self.changed.notify_all();
}
}
pub fn mark_eos(&self, terminal: ElementId) {
self.mark_ready(terminal);
}
pub fn mark_departed(&self, terminal: ElementId) {
self.mark_ready(terminal);
}
pub fn is_ready(&self, terminal: ElementId) -> bool {
let state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
!state.cancelled && state.ready.contains(&terminal)
}
pub(crate) fn are_ready(&self, terminals: &[ElementId]) -> bool {
let state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
!state.cancelled && terminals.iter().all(|id| state.ready.contains(id))
}
pub fn is_complete(&self) -> bool {
let state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
!state.cancelled && self.expected.is_subset(&state.ready)
}
pub fn cancel(&self) {
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
state.cancelled = true;
self.changed.notify_all();
}
pub fn wait(&self, timeout: Duration) -> std::result::Result<(), PrerollError> {
let deadline = Instant::now() + timeout;
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
loop {
if state.cancelled {
return Err(PrerollError::Cancelled);
}
let mut pending: Vec<_> = self.expected.difference(&state.ready).copied().collect();
pending.sort_unstable();
if pending.is_empty() {
return Ok(());
}
let now = Instant::now();
if now >= deadline {
return Err(PrerollError::TimedOut { pending });
}
let remaining = deadline.saturating_duration_since(now);
let (next, _) = self
.changed
.wait_timeout(state, remaining)
.unwrap_or_else(|poisoned| poisoned.into_inner());
state = next;
}
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PrerollError {
#[error("preroll was cancelled")]
Cancelled,
#[error("preroll timed out with pending terminals {pending:?}")]
TimedOut { pending: Vec<ElementId> },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RequestKind {
Control(ControlMsg),
Finish,
}
pub(crate) struct Request {
pub(crate) kind: RequestKind,
pub(crate) ack: Sender<()>,
}
#[derive(Clone)]
pub struct ControlSender {
tx: Sender<Request>,
}
#[derive(Clone)]
pub struct ControlReceiver {
pub(crate) rx: Receiver<Request>,
}
pub fn channel() -> (ControlSender, ControlReceiver) {
let (tx, rx) = unbounded();
(ControlSender { tx }, ControlReceiver { rx })
}
impl ControlSender {
pub fn send(&self, msg: ControlMsg) {
self.send_request(RequestKind::Control(msg));
}
pub(crate) fn finish(&self) {
self.send_request(RequestKind::Finish);
}
fn send_request(&self, kind: RequestKind) {
let (ack_tx, ack_rx) = crossbeam_channel::bounded(0);
if self.tx.send(Request { kind, ack: ack_tx }).is_ok() {
let _ = ack_rx.recv();
}
}
}
impl ControlReceiver {
pub(crate) fn try_recv(&self) -> Option<(RequestKind, Sender<()>)> {
self.rx.try_recv().ok().map(|r| (r.kind, r.ack))
}
pub(crate) fn recv(&self) -> Option<(RequestKind, Sender<()>)> {
self.rx.recv().ok().map(|r| (r.kind, r.ack))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ControlOutcome {
pub stopped: bool,
pub paused_for: Duration,
}
pub fn drain_control<S: SourceElement>(
control: &ControlReceiver,
source: &mut S,
bus: &Bus,
) -> Result<ControlOutcome> {
let mut paused_for = Duration::ZERO;
while let Some((request, ack)) = control.try_recv() {
let RequestKind::Control(msg) = request else {
apply_finish(source, bus, &ack);
return Ok(ControlOutcome {
stopped: true,
paused_for,
});
};
if msg == ControlMsg::Pause {
let pause_start = Instant::now();
apply_one(source, bus, &msg, &ack)?;
let stopped = wait_out_pause(control, source, bus)?;
paused_for += pause_start.elapsed();
if stopped {
return Ok(ControlOutcome {
stopped: true,
paused_for,
});
}
continue;
}
if apply_one(source, bus, &msg, &ack)? {
return Ok(ControlOutcome {
stopped: true,
paused_for,
});
}
}
Ok(ControlOutcome {
stopped: false,
paused_for,
})
}
pub(crate) fn apply_finish<S: SourceElement>(source: &mut S, bus: &Bus, ack: &Sender<()>) {
pp_trace!(
pp_log: source.pp_log(),
"event=finish phase=received"
);
let pp_log = source.pp_log().clone();
let element_type = source.element_type();
let name = source.name();
for pad in source.src_pads() {
if let Err(error) = pad.push_eos(&pp_log) {
bus.post(
&pp_log,
BusEvent::Error {
element_type,
name: name.clone(),
error,
},
);
}
}
let _ = ack.send(());
pp_trace!(
pp_log: source.pp_log(),
"event=finish phase=completed outcome=ok"
);
}
pub(crate) fn apply_one<S: SourceElement>(
source: &mut S,
bus: &Bus,
msg: &ControlMsg,
ack: &Sender<()>,
) -> Result<bool> {
let is_stop = apply_one_unacked(source, bus, msg)?;
let _ = ack.send(());
Ok(is_stop)
}
pub(crate) fn apply_one_unacked<S: SourceElement>(
source: &mut S,
bus: &Bus,
msg: &ControlMsg,
) -> Result<bool> {
pp_trace!(
pp_log: source.pp_log(),
"event=control control={msg:?} phase=received"
);
let result: Result<bool> = (|| {
apply_seek_check(source, msg);
source.on_control(msg);
apply_seek(source, bus, msg)?;
for pad in source.src_pads() {
pad.control(msg.clone())?;
}
Ok(*msg == ControlMsg::Stop)
})();
match &result {
Ok(_) => pp_trace!(
pp_log: source.pp_log(),
"event=control control={msg:?} phase=completed outcome=ok"
),
Err(error) => pp_trace!(
pp_log: source.pp_log(),
"event=control control={msg:?} phase=completed outcome=error error={error}"
),
}
result
}
pub(crate) fn wait_out_pause<S: SourceElement>(
control: &ControlReceiver,
source: &mut S,
bus: &Bus,
) -> Result<bool> {
loop {
let Some((request, ack)) = control.recv() else {
return Ok(true); };
let RequestKind::Control(msg) = request else {
apply_finish(source, bus, &ack);
return Ok(true);
};
if apply_one(source, bus, &msg, &ack)? {
return Ok(true);
}
if matches!(msg, ControlMsg::Resume | ControlMsg::Preroll(_)) {
return Ok(false);
}
}
}
fn apply_seek_check<S: SourceElement>(source: &S, msg: &ControlMsg) {
let ControlMsg::CheckSeek(context) = msg else {
return;
};
let reason = if source.is_live() {
Some(SeekRejectReason::LiveSource)
} else if !source.is_seekable() {
Some(SeekRejectReason::SourceNotSeekable)
} else {
None
};
if let Some(reason) = reason {
context.reject(source.element_type(), source.name(), reason);
}
}
fn apply_seek<S: SourceElement>(source: &mut S, bus: &Bus, msg: &ControlMsg) -> Result<()> {
if let ControlMsg::Seek(target) = msg {
let landed = source.seek(*target)?;
bus.post(
source.pp_log(),
BusEvent::Seeked {
element_type: source.element_type(),
name: source.name(),
requested: *target,
landed,
},
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::{sync::Arc, thread};
use crate::pp_log::PpLog;
use super::*;
use crate::{
buffer::MediaBuffer,
element::{Element, ElementType, Sink, Source, element_pp_log},
pad::SrcPad,
};
struct DummySource {
pp_log: PpLog,
pad: SrcPad,
flushes: usize,
}
impl DummySource {
fn new() -> Self {
Self {
flushes: 0,
pp_log: element_pp_log(ElementType::Other, "dummy", None),
pad: SrcPad::new("dummy_src"),
}
}
}
impl Element for DummySource {
fn name(&self) -> Arc<str> {
"dummy".into()
}
fn element_type(&self) -> ElementType {
ElementType::Other
}
fn pp_log(&self) -> &PpLog {
&self.pp_log
}
fn pp_log_mut(&mut self) -> &mut PpLog {
&mut self.pp_log
}
}
impl Source for DummySource {
fn src_pads(&mut self) -> &mut [SrcPad] {
std::slice::from_mut(&mut self.pad)
}
}
impl SourceElement for DummySource {
fn is_live(&self) -> bool {
false
}
fn is_seekable(&self) -> bool {
false
}
fn run(&mut self, _control: &ControlReceiver, _bus: &Bus) -> Result<()> {
unreachable!("not exercised by these tests")
}
fn on_control(&mut self, msg: &ControlMsg) {
if *msg == ControlMsg::Flush {
self.flushes += 1;
}
}
fn seek(&mut self, target: Duration) -> Result<Duration> {
Ok(target)
}
}
#[test]
fn flush_reaches_the_source_itself_and_nothing_else_does() {
let (bus, _bus_rx) = Bus::new();
let mut source = DummySource::new();
for msg in [
ControlMsg::Pause,
ControlMsg::Resume,
ControlMsg::Seek(Duration::from_secs(1)),
ControlMsg::CheckSeek(Arc::new(SeekCheckContext::new())),
] {
apply_one_unacked(&mut source, &bus, &msg).expect("control applies");
}
assert_eq!(source.flushes, 0, "only Flush may discard source-held data");
apply_one_unacked(&mut source, &bus, &ControlMsg::Flush).expect("flush applies");
assert_eq!(source.flushes, 1);
}
struct SlowPauseSink {
pp_log: PpLog,
pause_delay: Duration,
}
impl Element for SlowPauseSink {
fn name(&self) -> Arc<str> {
"slow-pause".into()
}
fn element_type(&self) -> ElementType {
ElementType::Other
}
fn pp_log(&self) -> &PpLog {
&self.pp_log
}
fn pp_log_mut(&mut self) -> &mut PpLog {
&mut self.pp_log
}
}
impl Sink for SlowPauseSink {
fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
Ok(())
}
fn control(&mut self, msg: ControlMsg) -> Result<()> {
if msg == ControlMsg::Pause {
thread::sleep(self.pause_delay);
}
Ok(())
}
}
#[test]
fn wait_out_pause_treats_a_dropped_sender_as_stop() {
let (tx, rx) = channel();
drop(tx);
let (bus, _bus_rx) = Bus::new();
let mut source = DummySource::new();
let stopped = wait_out_pause(&rx, &mut source, &bus)
.expect("no real seek/push happens on this path, so this can't fail");
assert!(
stopped,
"a dropped ControlSender must be treated the same as an explicit Stop"
);
}
#[test]
fn seek_check_collects_a_non_seekable_source_without_mutating_it() {
let context = Arc::new(SeekCheckContext::new());
let (ack, _ack_rx) = crossbeam_channel::bounded(1);
let (bus, _bus_rx) = Bus::new();
let mut source = DummySource::new();
apply_one(
&mut source,
&bus,
&ControlMsg::CheckSeek(Arc::clone(&context)),
&ack,
)
.expect("capability checks must not fail the control cascade");
let error = context.result().expect_err("dummy source is not seekable");
assert_eq!(
error.rejections(),
[SeekRejection {
element_type: ElementType::Other,
name: "dummy".into(),
reason: SeekRejectReason::SourceNotSeekable,
}]
);
}
#[test]
fn preroll_waits_for_every_terminal_and_reports_pending_ids() {
let first = ElementId::for_test(1);
let second = ElementId::for_test(2);
let context = PrerollContext::new([first, second]);
context.mark_ready(first);
assert_eq!(
context.wait(Duration::ZERO),
Err(PrerollError::TimedOut {
pending: vec![second]
})
);
context.mark_eos(second);
assert_eq!(context.wait(Duration::ZERO), Ok(()));
}
#[test]
fn preroll_wait_can_be_cancelled() {
let context = PrerollContext::new([ElementId::for_test(1)]);
context.cancel();
assert_eq!(
context.wait(Duration::from_secs(1)),
Err(PrerollError::Cancelled)
);
}
#[test]
fn wait_out_pause_returns_when_preroll_arrives() {
let (tx, rx) = channel();
let (bus, _bus_rx) = Bus::new();
let mut source = DummySource::new();
let context = Arc::new(PrerollContext::new([]));
let worker = thread::spawn(move || wait_out_pause(&rx, &mut source, &bus));
tx.send(ControlMsg::Preroll(context));
assert!(!worker.join().unwrap().unwrap());
}
#[test]
fn wait_out_pause_blocks_until_resume_then_returns_false() {
let (tx, rx) = channel();
let (bus, _bus_rx) = Bus::new();
let mut source = DummySource::new();
let worker = thread::spawn(move || wait_out_pause(&rx, &mut source, &bus));
tx.send(ControlMsg::Pause);
tx.send(ControlMsg::Resume);
let stopped = worker
.join()
.expect("worker must not panic")
.expect("no real seek/push happens on this path, so this can't fail");
assert!(
!stopped,
"Resume must unblock wait_out_pause with Ok(false)"
);
}
#[test]
fn drain_control_counts_the_pause_cascade_as_paused_time() {
let pause_delay = Duration::from_millis(80);
let (tx, rx) = channel();
let controller = thread::spawn(move || {
tx.send(ControlMsg::Pause);
tx.send(ControlMsg::Resume);
});
let (bus, _bus_rx) = Bus::new();
let mut source = DummySource::new();
source.pad.link(Box::new(SlowPauseSink {
pause_delay,
pp_log: element_pp_log(ElementType::Other, "slow-pause", None),
}));
let outcome = loop {
let outcome = drain_control(&rx, &mut source, &bus)
.expect("the synthetic control cascade cannot fail");
if outcome.paused_for > Duration::ZERO {
break outcome;
}
thread::yield_now();
};
controller.join().expect("controller must not panic");
assert!(!outcome.stopped);
assert!(
outcome.paused_for >= Duration::from_millis(60),
"the {:?} Pause cascade was omitted from paused_for: {:?}",
pause_delay,
outcome.paused_for
);
}
}