Skip to main content

ez_ffmpeg/core/scheduler/progress/
handle.rs

1//! The cloneable, read-only progress handle.
2
3use super::snapshot::{OutputProgress, Progress};
4use super::state::ProgressState;
5use super::tracker::ProgressTracker;
6use crate::core::scheduler::ffmpeg_scheduler::{STATUS_ABORT, STATUS_END, STATUS_PAUSE};
7use std::sync::atomic::{AtomicUsize, Ordering};
8use std::sync::Arc;
9
10/// A cheap, cloneable, thread-safe, **read-only** view of a scheduled FFmpeg
11/// job's progress, obtained from `FfmpegScheduler::progress_handle` in the
12/// `Running` or `Paused` state.
13///
14/// The handle holds only reference-counted `std` synchronization state — no
15/// FFmpeg pointers, no callbacks — so it is `Send + Sync`, can be cloned
16/// freely across threads, and **remains safe to use after the scheduler has
17/// been consumed** (by `wait()`/`stop()`/`abort()`) **or dropped**: from
18/// then on [`snapshot`](Self::snapshot) keeps returning the job's final
19/// frozen values with [`ProgressState::Ended`].
20///
21/// Observing progress is pull-based: call `snapshot()` whenever a fresh
22/// value is wanted. Each call is a handful of atomic loads; no locks are
23/// taken and no FFmpeg call is made.
24#[derive(Clone)]
25pub struct ProgressHandle {
26    /// The scheduler's status word (`STATUS_*` in `ffmpeg_scheduler`).
27    status: Arc<AtomicUsize>,
28    /// The scheduler's seqlock-style pause parity: odd means a
29    /// pause()/resume() transition is in flight or the job is paused.
30    pause_epoch: Arc<AtomicUsize>,
31    tracker: Arc<ProgressTracker>,
32}
33
34impl ProgressHandle {
35    pub(crate) fn new(
36        status: Arc<AtomicUsize>,
37        pause_epoch: Arc<AtomicUsize>,
38        tracker: Arc<ProgressTracker>,
39    ) -> Self {
40        Self {
41            status,
42            pause_epoch,
43            tracker,
44        }
45    }
46
47    /// Takes a point-in-time snapshot of the job's progress.
48    pub fn snapshot(&self) -> Progress {
49        // Sample the state FIRST: reading it after the values would let a
50        // completion sealed in between produce a snapshot claiming `Ended`
51        // while its values predate the freeze. Sampled this way, a snapshot
52        // that says `Ended` was taken entirely after the latch sealed, so
53        // its values are the frozen finals.
54        let state = self.state();
55        let elapsed = self.tracker.elapsed();
56        let outputs = self
57            .tracker
58            .outputs()
59            .iter()
60            .enumerate()
61            .map(|(index, telemetry)| OutputProgress::collect(index, telemetry, elapsed))
62            .collect();
63        Progress::new(state, elapsed, outputs)
64    }
65
66    /// Whether every tracked worker of the job has torn down — the same
67    /// edge `wait()`/`stop()` unblock on, and the point at which
68    /// [`snapshot`](Self::snapshot) values (elapsed included) freeze.
69    ///
70    /// Note this is stricter than `FfmpegScheduler::is_ended`, which reports
71    /// the terminal status *signal* (a stopping job answers `true` there
72    /// while workers may still be flushing).
73    pub fn is_ended(&self) -> bool {
74        self.tracker.is_completed()
75    }
76
77    /// Derives the job-level state. Precedence: the sealed completion latch
78    /// wins (workers all gone — `Ended`), then a published terminal signal
79    /// (`Finishing`: teardown in flight), then pause, then the
80    /// producers-drained edge separating `Running` from `Finishing`.
81    fn state(&self) -> ProgressState {
82        if self.tracker.is_completed() {
83            return ProgressState::Ended;
84        }
85        match self.status.load(Ordering::Acquire) {
86            STATUS_PAUSE => ProgressState::Paused,
87            STATUS_END | STATUS_ABORT => ProgressState::Finishing,
88            _ => {
89                // STATUS_RUN (STATUS_INIT is unreachable through a handle:
90                // they are only obtainable after start() published RUN).
91                // An odd pause epoch marks the pause()/resume() transition
92                // windows, where "paused" is the conservative answer (see
93                // the epoch invariants on FfmpegScheduler::pause).
94                if self.pause_epoch.load(Ordering::Acquire) % 2 == 1 {
95                    ProgressState::Paused
96                } else if self.tracker.inputs_drained() {
97                    ProgressState::Finishing
98                } else {
99                    ProgressState::Running
100                }
101            }
102        }
103    }
104}
105
106impl std::fmt::Debug for ProgressHandle {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        f.debug_struct("ProgressHandle")
109            .field("state", &self.state())
110            .field("is_ended", &self.is_ended())
111            .finish_non_exhaustive()
112    }
113}