tokio_ffmpeg_cli/
progress_event.rs

1use std::collections::HashMap;
2
3const FRAME_KEY: &str = "frame";
4const FPS_KEY: &str = "fps";
5const BITRATE_KEY: &str = "bitrate";
6const TOTAL_SIZE_KEY: &str = "total_size";
7const OUT_TIME_US_KEY: &str = "out_time_us";
8const OUT_TIME_MS_KEY: &str = "out_time_ms";
9const OUT_TIME_KEY: &str = "out_time";
10const DUP_FRAMES_KEY: &str = "dup_frames";
11const DROP_FRAMES_KEY: &str = "drop_frames";
12const SPEED_KEY: &str = "speed";
13const PROGRESS_KEY: &str = "progress";
14
15const N_A: &str = "N/A";
16
17/// An error that occurs while building a [`ProgressEvent`]
18#[derive(Debug, thiserror::Error)]
19pub enum LineBuilderError {
20    /// The `key=value` pair is invalid
21    #[error("invalid key value pair")]
22    InvalidKeyValuePair,
23
24    /// Invalid integer value for a key
25    #[error("invalid integer value for key \"{key}\" with value \"{value}\"")]
26    InvalidIntegerValue {
27        key: &'static str,
28        value: Box<str>,
29        #[source]
30        error: std::num::ParseIntError,
31    },
32
33    /// Invalid float value for a key
34    #[error("invalid float value for key \"{0}\"")]
35    InvalidFloatValue(&'static str, #[source] std::num::ParseFloatError),
36
37    /// Got a duplicate key
38    #[error("duplicate key \"{key}\"")]
39    DuplicateKey {
40        /// The duplicate key
41        key: Box<str>,
42        /// The new duplicate value
43        new_value: Box<str>,
44        /// The old value
45        old_value: Box<str>,
46    },
47
48    /// Missing a key=value pair
49    #[error("missing key value pair for key \"{0}\"")]
50    MissingKeyValuePair(&'static str),
51}
52
53/// An event about the encoding progress sent by ffmpeg
54#[derive(Debug)]
55pub struct ProgressEvent {
56    /// The frame number
57    pub frame: u64,
58
59    /// The fps
60    pub fps: f64,
61
62    /// The bitrate
63    pub bitrate: Box<str>,
64
65    /// The progress
66    pub progress: Box<str>,
67
68    /// The total size.
69    ///
70    /// None means either it was not present, or it was N/A
71    pub total_size: Option<u64>,
72
73    /// The out time in us
74    pub out_time_us: i64,
75
76    /// The out time in ms
77    pub out_time_ms: i64,
78
79    /// The out time
80    pub out_time: Box<str>,
81
82    /// The # of dup frames
83    pub dup_frames: u64,
84
85    /// The # of dropped frames
86    pub drop_frames: u64,
87
88    /// The speed.
89    ///
90    /// None means it was N/A.
91    pub speed: Option<f64>,
92
93    /// Extra K/Vs
94    pub extra: HashMap<Box<str>, Box<str>>,
95}
96
97impl ProgressEvent {
98    /// Try to make a [`ProgressEvent`] from optional parts.
99    #[expect(clippy::too_many_arguments)]
100    pub(crate) fn try_from_optional_parts(
101        frame: Option<u64>,
102        fps: Option<f64>,
103        bitrate: Option<Box<str>>,
104        total_size: Option<Option<u64>>,
105        out_time_us: Option<i64>,
106        out_time_ms: Option<i64>,
107        out_time: Option<Box<str>>,
108        dup_frames: Option<u64>,
109        drop_frames: Option<u64>,
110        speed: Option<Option<f64>>,
111        progress: Option<Box<str>>,
112        extra: HashMap<Box<str>, Box<str>>,
113    ) -> Result<Self, LineBuilderError> {
114        Ok(ProgressEvent {
115            frame: frame.ok_or(LineBuilderError::MissingKeyValuePair(FRAME_KEY))?,
116            fps: fps.ok_or(LineBuilderError::MissingKeyValuePair(FPS_KEY))?,
117            bitrate: bitrate.ok_or(LineBuilderError::MissingKeyValuePair(BITRATE_KEY))?,
118            total_size: total_size.ok_or(LineBuilderError::MissingKeyValuePair(TOTAL_SIZE_KEY))?,
119            out_time_us: out_time_us
120                .ok_or(LineBuilderError::MissingKeyValuePair(OUT_TIME_US_KEY))?,
121            out_time_ms: out_time_ms
122                .ok_or(LineBuilderError::MissingKeyValuePair(OUT_TIME_MS_KEY))?,
123            out_time: out_time.ok_or(LineBuilderError::MissingKeyValuePair(OUT_TIME_KEY))?,
124            dup_frames: dup_frames.ok_or(LineBuilderError::MissingKeyValuePair(DUP_FRAMES_KEY))?,
125            drop_frames: drop_frames
126                .ok_or(LineBuilderError::MissingKeyValuePair(DROP_FRAMES_KEY))?,
127            speed: speed.ok_or(LineBuilderError::MissingKeyValuePair(SPEED_KEY))?,
128            progress: progress.ok_or(LineBuilderError::MissingKeyValuePair(PROGRESS_KEY))?,
129            extra,
130        })
131    }
132}
133
134/// A builder to assemble lines into a [`ProgressEvent`].
135#[derive(Debug)]
136pub(crate) struct ProgressEventLineBuilder {
137    maybe_frame: Option<u64>,
138    maybe_fps: Option<f64>,
139    maybe_bitrate: Option<Box<str>>,
140    maybe_total_size: Option<Option<u64>>,
141    maybe_out_time_us: Option<i64>,
142    maybe_out_time_ms: Option<i64>,
143    maybe_out_time: Option<Box<str>>,
144    maybe_dup_frames: Option<u64>,
145    maybe_drop_frames: Option<u64>,
146    maybe_speed: Option<Option<f64>>,
147
148    extra: HashMap<Box<str>, Box<str>>,
149}
150
151impl ProgressEventLineBuilder {
152    /// Make a new [`ProgressEventLineBuilder`].
153    pub(crate) fn new() -> Self {
154        Self {
155            maybe_frame: None,
156            maybe_fps: None,
157            maybe_bitrate: None,
158            maybe_total_size: None,
159            maybe_out_time_us: None,
160            maybe_out_time_ms: None,
161            maybe_out_time: None,
162            maybe_dup_frames: None,
163            maybe_drop_frames: None,
164            maybe_speed: None,
165
166            extra: HashMap::new(),
167        }
168    }
169
170    /// Push a line to this builder.
171    ///
172    /// Returns `Some` if the new line would cause the builder to complete a [`ProgressEvent`].
173    pub(crate) fn push(&mut self, line: &str) -> Result<Option<ProgressEvent>, LineBuilderError> {
174        let line = line.trim();
175
176        let (key, value) = line
177            .split_once('=')
178            .ok_or(LineBuilderError::InvalidKeyValuePair)?;
179
180        fn parse_kv_u64(
181            key: &'static str,
182            value: &str,
183            store: &mut Option<u64>,
184        ) -> Result<(), LineBuilderError> {
185            if let Some(store) = store {
186                return Err(LineBuilderError::DuplicateKey {
187                    key: key.into(),
188                    new_value: value.into(),
189                    old_value: store.to_string().into(),
190                });
191            }
192            *store =
193                Some(
194                    value
195                        .parse()
196                        .map_err(|error| LineBuilderError::InvalidIntegerValue {
197                            key,
198                            value: value.into(),
199                            error,
200                        })?,
201                );
202            Ok(())
203        }
204
205        fn parse_kv_i64(
206            key: &'static str,
207            value: &str,
208            store: &mut Option<i64>,
209        ) -> Result<(), LineBuilderError> {
210            if let Some(store) = store {
211                return Err(LineBuilderError::DuplicateKey {
212                    key: key.into(),
213                    new_value: value.into(),
214                    old_value: store.to_string().into(),
215                });
216            }
217            *store =
218                Some(
219                    value
220                        .parse()
221                        .map_err(|error| LineBuilderError::InvalidIntegerValue {
222                            key,
223                            value: value.into(),
224                            error,
225                        })?,
226                );
227            Ok(())
228        }
229
230        fn parse_kv_f64(
231            key: &'static str,
232            value: &str,
233            store: &mut Option<f64>,
234        ) -> Result<(), LineBuilderError> {
235            if let Some(store) = store {
236                return Err(LineBuilderError::DuplicateKey {
237                    key: key.into(),
238                    old_value: store.to_string().into(),
239                    new_value: value.into(),
240                });
241            }
242            *store = Some(
243                value
244                    .parse()
245                    .map_err(|e| LineBuilderError::InvalidFloatValue(key, e))?,
246            );
247            Ok(())
248        }
249
250        match key {
251            FRAME_KEY => {
252                parse_kv_u64(FRAME_KEY, value, &mut self.maybe_frame)?;
253                Ok(None)
254            }
255            FPS_KEY => {
256                parse_kv_f64(FPS_KEY, value, &mut self.maybe_fps)?;
257                Ok(None)
258            }
259            BITRATE_KEY => {
260                let value = value.trim();
261
262                if let Some(old_value) = self.maybe_bitrate.take() {
263                    Err(LineBuilderError::DuplicateKey {
264                        key: BITRATE_KEY.into(),
265                        old_value,
266                        new_value: value.into(),
267                    })
268                } else {
269                    self.maybe_bitrate = Some(value.into());
270                    Ok(None)
271                }
272            }
273            TOTAL_SIZE_KEY => {
274                if value == N_A {
275                    self.maybe_total_size = Some(None);
276                } else {
277                    if let Some(maybe_total_size) = self.maybe_total_size {
278                        return Err(LineBuilderError::DuplicateKey {
279                            key: TOTAL_SIZE_KEY.into(),
280                            old_value: maybe_total_size
281                                .map(|v| Box::<str>::from(v.to_string()))
282                                .unwrap_or_else(|| N_A.into()),
283                            new_value: value.into(),
284                        });
285                    }
286                    self.maybe_total_size = Some(Some(value.parse().map_err(|error| {
287                        LineBuilderError::InvalidIntegerValue {
288                            key: TOTAL_SIZE_KEY,
289                            value: value.into(),
290                            error,
291                        }
292                    })?));
293                }
294
295                Ok(None)
296            }
297            OUT_TIME_US_KEY => {
298                parse_kv_i64(OUT_TIME_US_KEY, value, &mut self.maybe_out_time_us)?;
299                Ok(None)
300            }
301            OUT_TIME_MS_KEY => {
302                parse_kv_i64(OUT_TIME_MS_KEY, value, &mut self.maybe_out_time_ms)?;
303                Ok(None)
304            }
305            OUT_TIME_KEY => {
306                if let Some(old_value) = self.maybe_out_time.take() {
307                    Err(LineBuilderError::DuplicateKey {
308                        key: OUT_TIME_KEY.into(),
309                        old_value,
310                        new_value: value.into(),
311                    })
312                } else {
313                    self.maybe_out_time = Some(value.into());
314                    Ok(None)
315                }
316            }
317            DUP_FRAMES_KEY => {
318                parse_kv_u64(DUP_FRAMES_KEY, value, &mut self.maybe_dup_frames)?;
319                Ok(None)
320            }
321            DROP_FRAMES_KEY => {
322                parse_kv_u64(DROP_FRAMES_KEY, value, &mut self.maybe_drop_frames)?;
323                Ok(None)
324            }
325            SPEED_KEY => {
326                let value = value.trim_end_matches('x').trim_end_matches('X').trim();
327
328                if value == N_A {
329                    self.maybe_speed = Some(None);
330                } else {
331                    if let Some(maybe_speed) = self.maybe_speed {
332                        return Err(LineBuilderError::DuplicateKey {
333                            key: SPEED_KEY.into(),
334                            old_value: maybe_speed
335                                .map(|v| Box::<str>::from(v.to_string()))
336                                .unwrap_or_else(|| N_A.into()),
337                            new_value: value.into(),
338                        });
339                    }
340                    self.maybe_speed =
341                        Some(Some(value.parse().map_err(|e| {
342                            LineBuilderError::InvalidFloatValue(SPEED_KEY, e)
343                        })?));
344                }
345
346                Ok(None)
347            }
348            PROGRESS_KEY => {
349                let event = ProgressEvent::try_from_optional_parts(
350                    self.maybe_frame.take(),
351                    self.maybe_fps.take(),
352                    self.maybe_bitrate.take(),
353                    self.maybe_total_size.take(),
354                    self.maybe_out_time_us.take(),
355                    self.maybe_out_time_ms.take(),
356                    self.maybe_out_time.take(),
357                    self.maybe_dup_frames.take(),
358                    self.maybe_drop_frames.take(),
359                    self.maybe_speed.take(),
360                    Some(value.into()),
361                    std::mem::take(&mut self.extra),
362                )?;
363
364                Ok(Some(event))
365            }
366            key => {
367                if let Some(old_value) = self.extra.insert(key.into(), value.into()) {
368                    Err(LineBuilderError::DuplicateKey {
369                        key: key.into(),
370                        old_value,
371                        new_value: value.into(),
372                    })
373                } else {
374                    Ok(None)
375                }
376            }
377        }
378    }
379}