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}