Skip to main content

ez_ffmpeg/core/scheduler/progress/
snapshot.rs

1//! Owned progress snapshots: [`Progress`] and its per-output entries.
2
3use super::state::ProgressState;
4use super::tracker::OutputTelemetry;
5use std::time::Duration;
6
7/// A point-in-time, owned snapshot of a scheduled FFmpeg job's progress,
8/// obtained from [`ProgressHandle::snapshot`](super::ProgressHandle::snapshot).
9///
10/// A snapshot is plain data: it stays valid (and unchanged) no matter what
11/// the job does afterwards. Take a new snapshot to observe newer values.
12#[non_exhaustive]
13#[derive(Debug, Clone)]
14pub struct Progress {
15    state: ProgressState,
16    elapsed: Duration,
17    outputs: Vec<OutputProgress>,
18}
19
20impl Progress {
21    pub(crate) fn new(state: ProgressState, elapsed: Duration, outputs: Vec<OutputProgress>) -> Self {
22        Self {
23            state,
24            elapsed,
25            outputs,
26        }
27    }
28
29    /// The job's lifecycle phase at the moment of the snapshot.
30    pub fn state(&self) -> ProgressState {
31        self.state
32    }
33
34    /// Wall-clock time since `start()`, frozen once the job reaches
35    /// [`ProgressState::Ended`]. This is real elapsed time and **includes
36    /// time spent paused** — divide-by-elapsed rates in a long-paused job
37    /// sag accordingly.
38    pub fn elapsed(&self) -> Duration {
39        self.elapsed
40    }
41
42    /// Per-output progress, indexed by output declaration order (the order
43    /// outputs were added to the builder). Always one entry per output —
44    /// multi-output jobs such as an HLS ladder report each rung separately.
45    pub fn outputs(&self) -> &[OutputProgress] {
46        &self.outputs
47    }
48}
49
50/// Progress of one output of the job.
51///
52/// Apart from [`output_index`](Self::output_index), which is always present,
53/// every telemetry metric is an `Option`: `None` means "not knowable right
54/// now" (or not applicable to this output), never a fabricated zero. In
55/// particular:
56///
57/// - a packet-sink output delivers packets through callbacks instead of a
58///   muxer, so all of its optional metrics stay `None`;
59/// - an `AVFMT_NOFILE` muxer (HLS, `null`, ...) manages its own I/O, so
60///   [`total_size`](Self::total_size) and
61///   [`bitrate_kbps`](Self::bitrate_kbps) stay `None`.
62#[non_exhaustive]
63#[derive(Debug, Clone)]
64pub struct OutputProgress {
65    output_index: usize,
66    video_packets: Option<u64>,
67    fps: Option<f64>,
68    out_time_us: Option<i64>,
69    total_size: Option<u64>,
70    bitrate_kbps: Option<f64>,
71    speed: Option<f64>,
72}
73
74impl OutputProgress {
75    /// Reads one output's shared telemetry into an owned snapshot entry,
76    /// deriving the rate fields from `elapsed`.
77    pub(crate) fn collect(
78        output_index: usize,
79        telemetry: &OutputTelemetry,
80        elapsed: Duration,
81    ) -> Self {
82        let video_packets = telemetry.video_packets();
83        let out_time_us = telemetry.out_time_us();
84        let total_size = telemetry.total_size();
85        let elapsed_secs = elapsed.as_secs_f64();
86        let fps = match video_packets {
87            Some(packets) if elapsed_secs > 0.0 => Some(packets as f64 / elapsed_secs),
88            _ => None,
89        };
90        // Bytes * 8 / milliseconds is numerically kbit/s — the same formula
91        // the CLI progress line uses.
92        let bitrate_kbps = match (total_size, out_time_us) {
93            (Some(bytes), Some(t_us)) if t_us > 0 => {
94                Some(bytes as f64 * 8.0 * 1000.0 / t_us as f64)
95            }
96            _ => None,
97        };
98        let speed = match out_time_us {
99            Some(t_us) if t_us >= 0 && elapsed_secs > 0.0 => {
100                Some(t_us as f64 / 1_000_000.0 / elapsed_secs)
101            }
102            _ => None,
103        };
104        Self {
105            output_index,
106            video_packets,
107            fps,
108            out_time_us,
109            total_size,
110            bitrate_kbps,
111            speed,
112        }
113    }
114
115    /// Position of this output in the job's output declaration order.
116    pub fn output_index(&self) -> usize {
117        self.output_index
118    }
119
120    /// Packets of the selected video stream (the lowest-index video stream
121    /// of this output) successfully committed to the muxer.
122    ///
123    /// This is the typed counterpart of the CLI's `frame=` counter, but it
124    /// deliberately is not called "frames": bitstream filters between the
125    /// encoder and the muxer can change the packet count, so what is
126    /// counted here is committed *packets*. `None` when the output has no
127    /// video stream, before the muxer has resolved its streams, and for
128    /// packet-sink outputs.
129    pub fn video_packets(&self) -> Option<u64> {
130        self.video_packets
131    }
132
133    /// Video packets per second of wall-clock time
134    /// ([`video_packets`](Self::video_packets) / elapsed). `None` whenever
135    /// `video_packets` is `None` or no time has elapsed.
136    pub fn fps(&self) -> Option<f64> {
137        self.fps
138    }
139
140    /// The output's progress position in microseconds: the minimum
141    /// post-fixup timestamp watermark across this output's *active* streams,
142    /// where a watermark advances only when a packet was **successfully
143    /// written** to the muxer.
144    ///
145    /// Strict like the fftools progress line: while any active stream has
146    /// not committed its first packet this is `None` rather than a guess.
147    /// Streams that finished early leave the active set (a sparse or short
148    /// stream cannot pin the value forever), and once every stream finished
149    /// the value freezes at the output's high-water mark. Across snapshots
150    /// the sequence of `Some` values is **monotonic non-decreasing**.
151    pub fn out_time_us(&self) -> Option<i64> {
152        self.out_time_us
153    }
154
155    /// Bytes written to the output so far (header included), as reported by
156    /// the muxer's I/O context after the most recent successful write.
157    /// `None` for `AVFMT_NOFILE` muxers (e.g. HLS — its segment sizes are
158    /// not observable here), for packet-sink outputs, and before the first
159    /// write.
160    pub fn total_size(&self) -> Option<u64> {
161        self.total_size
162    }
163
164    /// Average bitrate so far in kbit/s
165    /// ([`total_size`](Self::total_size) * 8 / [`out_time_us`](Self::out_time_us)).
166    /// `None` whenever either input is unavailable or the position is not
167    /// yet positive.
168    pub fn bitrate_kbps(&self) -> Option<f64> {
169        self.bitrate_kbps
170    }
171
172    /// Processing speed relative to media time: media seconds produced per
173    /// wall-clock second (the CLI's `speed=`). `None` whenever
174    /// [`out_time_us`](Self::out_time_us) is unavailable or negative, or no
175    /// time has elapsed.
176    pub fn speed(&self) -> Option<f64> {
177        self.speed
178    }
179
180    /// Convenience: this output's position as a percentage of a
181    /// caller-supplied total duration in microseconds (for example from
182    /// `container_info`), clamped to `[0, 100]`.
183    ///
184    /// The library does not derive the percentage itself: only the caller
185    /// knows the intended total (trims, `-t` limits and live inputs make
186    /// any guess wrong). Returns `None` when the position is unknown or
187    /// `total_us` is not positive.
188    pub fn percent_of(&self, total_us: i64) -> Option<f64> {
189        if total_us <= 0 {
190            return None;
191        }
192        let out_time_us = self.out_time_us?;
193        Some((out_time_us as f64 / total_us as f64 * 100.0).clamp(0.0, 100.0))
194    }
195}