ez-ffmpeg 0.11.0

A safe and ergonomic Rust interface for FFmpeg integration, designed for ease of use.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Helper functions that wrap or re-implement FFmpeg's common utility routines. Keeping these
//! thin veneers in one place makes it easy to compare our Rust code with the original C
//! implementations from libavutil/libavformat when debugging.

use ffmpeg_sys_next::{
    av_dict_free, av_dict_get, av_dict_iterate, av_dict_set, av_strerror, AVDictionary,
    AVRational, AV_DICT_MATCH_CASE, AV_ERROR_MAX_STRING_SIZE,
};
use std::collections::HashMap;
use std::ffi::{CStr, CString};

/// Owns an `*mut AVDictionary` and frees it on drop.
///
/// FFmpeg consumer APIs (avformat_open_input, avcodec_open2, av_opt_set_dict2)
/// take `&mut *mut AVDictionary` and leave the UNRECOGNIZED entries behind in
/// a reallocated dictionary — which leaks on every early return unless
/// something owns it. Construct the guard immediately after building the
/// dict, hand `as_double_ptr()` to the consumer, and let drop clean up
/// whatever remains on every path.
pub(crate) struct DictGuard {
    dict: *mut AVDictionary,
}

impl DictGuard {
    pub(crate) fn new(dict: *mut AVDictionary) -> Self {
        Self { dict }
    }

    /// Pointer for FFmpeg consumer APIs; the guard keeps owning the result.
    pub(crate) fn as_double_ptr(&mut self) -> *mut *mut AVDictionary {
        &mut self.dict
    }

    pub(crate) fn as_ptr(&self) -> *const AVDictionary {
        self.dict as *const _
    }

    /// Iterate leftover keys (the entries no consumer recognized).
    pub(crate) fn leftover_keys(&self) -> Vec<String> {
        let mut keys = Vec::new();
        let mut entry = std::ptr::null();
        unsafe {
            loop {
                entry = av_dict_iterate(self.dict, entry);
                if entry.is_null() {
                    break;
                }
                keys.push(CStr::from_ptr((*entry).key).to_string_lossy().into_owned());
            }
        }
        keys
    }

    /// Remove one key (e.g. an internally injected option that must not be
    /// reported as "unrecognized user option").
    pub(crate) fn remove(&mut self, key: &CStr) {
        unsafe {
            if !av_dict_get(self.dict, key.as_ptr(), std::ptr::null(), AV_DICT_MATCH_CASE)
                .is_null()
            {
                // Setting a key to NULL deletes it (libavutil/dict.c).
                av_dict_set(&mut self.dict, key.as_ptr(), std::ptr::null(), 0);
            }
        }
    }
}

impl Drop for DictGuard {
    fn drop(&mut self) {
        unsafe {
            av_dict_free(&mut self.dict);
        }
    }
}

/// Convert an optional `HashMap<CString, CString>` into an `AVDictionary` by invoking
/// `av_dict_set()` for each entry.
///
/// FFmpeg reference: `av_dict_set()` in `libavutil/dict.c` uses the same ownership rules; the
/// caller is still responsible for freeing the resulting dictionary via `av_dict_free()`.
pub(crate) fn hashmap_to_avdictionary(
    opts: &Option<HashMap<CString, CString>>,
) -> *mut AVDictionary {
    let mut av_dict: *mut AVDictionary = std::ptr::null_mut();

    if let Some(map) = opts {
        for (key, value) in map {
            unsafe {
                av_dict_set(&mut av_dict, key.as_ptr(), value.as_ptr(), 0);
            }
        }
    }

    av_dict
}

/// Convert Rust String to C-compatible CString, handling null bytes
///
/// This is a utility function for safe String → CString conversion with proper error handling.
/// FFmpeg's C API requires null-terminated strings, but Rust Strings can contain null bytes.
///
/// # Arguments
/// * `s` - Rust string slice to convert
///
/// # Returns
/// * `Ok(CString)` - Successfully converted string
/// * `Err(String)` - String contains embedded null bytes (invalid for C strings)
///
/// # Note
/// Currently unused but kept as a utility function for future features that need
/// safe String → CString conversion with validation. Used internally by
/// hashmap_to_avdictionary_string().
#[allow(dead_code)]
pub(crate) fn string_to_cstring(s: &str) -> Result<CString, String> {
    CString::new(s).map_err(|e| format!("String contains null byte: {}", e))
}

/// Convert HashMap<String, String> to FFmpeg's AVDictionary
///
/// This function provides a type-safe way to convert Rust HashMap to FFmpeg's C dictionary.
/// Unlike hashmap_to_avdictionary() which takes CString values, this accepts String values
/// and performs validation to ensure they don't contain null bytes.
///
/// # Arguments
/// * `opts` - Optional HashMap with String keys and values
///
/// # Returns
/// * `Ok(*mut AVDictionary)` - Pointer to created dictionary (caller must free)
/// * `Err(String)` - Key or value contains embedded null bytes
///
/// # Memory Management
/// Returned AVDictionary must be freed by calling av_dict_free() or similar FFmpeg function.
///
/// # Note
/// Currently unused but kept as a utility function for future user-facing API that accepts
/// String-based options. The metadata implementation uses direct CString conversion instead.
#[allow(dead_code)]
/// Convert a `HashMap<String, String>` into an `AVDictionary`, validating that every key/value is
/// a valid C string before calling into FFmpeg.
///
/// FFmpeg reference: this mirrors the CLI path in `fftools/cmdutils.c` where user-facing strings
/// are validated and then passed to `av_dict_set()`.
pub(crate) fn hashmap_to_avdictionary_string(
    opts: &Option<HashMap<String, String>>,
) -> Result<*mut AVDictionary, String> {
    let mut av_dict: *mut AVDictionary = std::ptr::null_mut();

    if let Some(map) = opts {
        for (key, value) in map {
            let c_key = string_to_cstring(key)?;
            let c_value = string_to_cstring(value)?;
            unsafe {
                av_dict_set(&mut av_dict, c_key.as_ptr(), c_value.as_ptr(), 0);
            }
        }
    }

    Ok(av_dict)
}

/// Safe wrapper around `av_strerror()` that returns a Rust `String` instead of writing into a
/// caller-provided buffer.
///
/// FFmpeg reference: `av_strerror()` in `libavutil/error.c` uses a fixed-size buffer (defined by
/// `AV_ERROR_MAX_STRING_SIZE`), which we allocate on the stack and convert into UTF-8.
pub fn av_err2str(err: i32) -> String {
    unsafe {
        let mut buffer = [0i8; AV_ERROR_MAX_STRING_SIZE];
        av_strerror(err, buffer.as_mut_ptr(), AV_ERROR_MAX_STRING_SIZE);
        let c_str = CStr::from_ptr(buffer.as_ptr());
        match c_str.to_str() {
            Ok(s) => s.to_string(),
            Err(_) => format!("Unknown error: {}", err),
        }
    }
}

/// Rust implementation of FFmpeg's `av_rescale_q_rnd`.
///
/// This function is reimplemented in Rust rather than using FFmpeg's C version because:
/// - In C, `enum AVRounding` values can be freely combined with bitwise OR (e.g., `AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX`)
/// - In Rust, `enum AVRounding` is a strict type that cannot hold combined bit values without `unsafe transmute`
/// - By accepting `u32` as the rounding parameter, we avoid type safety issues while maintaining full compatibility
///
/// Rescales a 64-bit integer by 2 rational numbers with specified rounding.
///
/// The operation is mathematically equivalent to `a * bq / cq`.
///
/// # Arguments
/// * `a` - The value to rescale
/// * `bq` - Source rational (time base)
/// * `cq` - Destination rational (time base)
/// * `rnd` - Rounding mode as `u32`, supports bitwise OR combinations like `AV_ROUND_NEAR_INF as u32 | AV_ROUND_PASS_MINMAX as u32`
///
/// # Returns
/// Rescaled value
///
/// # Reference
/// FFmpeg `libavutil/mathematics.c:134-140`
pub(crate) fn av_rescale_q_rnd(a: i64, bq: AVRational, cq: AVRational, rnd: u32) -> i64 {
    let b = bq.num as i64 * cq.den as i64;
    let c = cq.num as i64 * bq.den as i64;
    av_rescale_rnd(a, b, c, rnd)
}

/// Rust implementation of FFmpeg's `av_rescale_rnd`.
///
/// This is a direct port of FFmpeg's rescaling algorithm to avoid `unsafe transmute` when
/// combining `AVRounding` enum flags. C allows `enum` values to be freely OR-ed together,
/// but Rust's type system requires enum values to be valid variants. By using `u32` for
/// the rounding parameter, we maintain type safety while preserving FFmpeg's behavior.
///
/// Rescales a 64-bit integer with rounding to nearest.
///
/// The operation is mathematically equivalent to `a * b / c`, but writing that
/// directly can overflow.
///
/// # Arguments
/// * `a` - The value to rescale
/// * `b` - Numerator
/// * `c` - Denominator
/// * `rnd` - Rounding mode as `u32`. The lower bits (0-5) specify the base rounding mode,
///          and bit 8192 (`AV_ROUND_PASS_MINMAX`) passes `INT64_MIN/MAX` through unchanged.
///
/// # Returns
/// Rescaled value, or `i64::MIN` on error
///
/// # Reference
/// FFmpeg `libavutil/mathematics.c:58-127`
fn av_rescale_rnd(a: i64, b: i64, c: i64, mut rnd: u32) -> i64 {
    const AV_ROUND_PASS_MINMAX: u32 = ffmpeg_sys_next::AVRounding::AV_ROUND_PASS_MINMAX as u32;
    const INT_MAX: i64 = i32::MAX as i64;

    // av_assert2(c > 0);
    // av_assert2(b >= 0);
    // av_assert2((unsigned)(rnd&~AV_ROUND_PASS_MINMAX)<=5 && (rnd&~AV_ROUND_PASS_MINMAX)!=4);
    if c <= 0
        || b < 0
        || !((rnd & !AV_ROUND_PASS_MINMAX) <= 5 && (rnd & !AV_ROUND_PASS_MINMAX) != 4)
    {
        return i64::MIN;
    }

    // if (rnd & AV_ROUND_PASS_MINMAX) {
    //     if (a == INT64_MIN || a == INT64_MAX)
    //         return a;
    //     rnd -= AV_ROUND_PASS_MINMAX;
    // }
    if (rnd & AV_ROUND_PASS_MINMAX) != 0 {
        if a == i64::MIN || a == i64::MAX {
            return a;
        }
        rnd -= AV_ROUND_PASS_MINMAX;
    }

    // if (a < 0)
    //     return -(uint64_t)av_rescale_rnd(-FFMAX(a, -INT64_MAX), b, c, rnd ^ ((rnd >> 1) & 1));
    if a < 0 {
        // -FFMAX(a, -INT64_MAX) equivalent to -a.max(-INT64_MAX)
        let neg_a = -a.max(-i64::MAX);
        let neg_result = av_rescale_rnd(neg_a, b, c, rnd ^ ((rnd >> 1) & 1));
        return -((neg_result as u64) as i64);
    }

    // if (rnd == AV_ROUND_NEAR_INF)
    //     r = c / 2;
    // else if (rnd & 1)
    //     r = c - 1;
    let r = if rnd == ffmpeg_sys_next::AVRounding::AV_ROUND_NEAR_INF as u32 {
        c / 2
    } else if (rnd & 1) != 0 {
        c - 1
    } else {
        0
    };

    // Fast path: if (b <= INT_MAX && c <= INT_MAX)
    if b <= INT_MAX && c <= INT_MAX {
        // if (a <= INT_MAX)
        //     return (a * b + r) / c;
        if a <= INT_MAX {
            return (a * b + r) / c;
        } else {
            // int64_t ad = a / c;
            // int64_t a2 = (a % c * b + r) / c;
            // if (ad >= INT32_MAX && b && ad > (INT64_MAX - a2) / b)
            //     return INT64_MIN;
            // return ad * b + a2;
            let ad = a / c;
            let a2 = (a % c * b + r) / c;
            if ad >= INT_MAX && b != 0 && ad > (i64::MAX - a2) / b {
                return i64::MIN;
            }
            return ad * b + a2;
        }
    }

    // Large value path: 128-bit precision
    rescale_large(a, b, c, r)
}

/// 128-bit precision rescaling for large values.
///
/// Rust uses native u128 instead of manual 128-bit simulation because:
/// - Rust has built-in u128 support (unlike C99/C11)
/// - LLVM optimizes u128 operations efficiently on modern hardware
/// - Simpler, more maintainable code with equivalent performance
///
/// # Reference
/// FFmpeg `libavutil/mathematics.c:93-117` (manual 128-bit simulation)
fn rescale_large(a: i64, b: i64, c: i64, r: i64) -> i64 {
    let a = a as u128;
    let b = b as u128;
    let c = c as u128;
    let r = r as u128;

    let result = (a * b + r) / c;

    if result > i64::MAX as u128 {
        i64::MIN
    } else {
        result as i64
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_av_rescale_q_rnd_basic() {
        // Test basic rescaling: 1000 from 1/1000 to 1/90000
        // Expected: 1000 * (1 * 90000) / (1000 * 1) = 90000
        let bq = AVRational { num: 1, den: 1000 };
        let cq = AVRational { num: 1, den: 90000 };

        let result = av_rescale_q_rnd(1000, bq, cq, 5); // AV_ROUND_NEAR_INF = 5
        assert_eq!(result, 90000);
    }

    #[test]
    fn test_av_rescale_q_rnd_with_pass_minmax_flag() {
        // Test AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX = 5 | 8192 = 8197
        let bq = AVRational { num: 1, den: 1000 };
        let cq = AVRational { num: 1, den: 90000 };

        // INT64_MIN should pass through unchanged
        let result = av_rescale_q_rnd(i64::MIN, bq, cq, 8197);
        assert_eq!(result, i64::MIN);

        // INT64_MAX should pass through unchanged
        let result = av_rescale_q_rnd(i64::MAX, bq, cq, 8197);
        assert_eq!(result, i64::MAX);
    }

    #[test]
    fn test_av_rescale_q_rnd_normal_value_with_pass_minmax() {
        // Test normal value with AV_ROUND_PASS_MINMAX flag
        // Flag should be ignored for normal values
        let bq = AVRational { num: 1, den: 1000 };
        let cq = AVRational { num: 1, den: 90000 };

        let result = av_rescale_q_rnd(1000, bq, cq, 8197); // 5 | 8192
        assert_eq!(result, 90000);
    }

    #[test]
    fn test_av_rescale_rnd_negative_value() {
        // Test negative value rescaling
        let result = av_rescale_rnd(-1000, 90000, 1000, 5);
        assert_eq!(result, -90000);
    }

    #[test]
    fn test_av_rescale_rnd_zero() {
        // Test zero rescaling
        let result = av_rescale_rnd(0, 90000, 1000, 5);
        assert_eq!(result, 0);
    }

    #[test]
    fn test_av_rescale_rnd_rounding_modes() {
        // Test different rounding modes
        // (7 * 3 + r) / 5 where r depends on rounding mode

        // AV_ROUND_ZERO = 0: r = 0, result = 21 / 5 = 4
        assert_eq!(av_rescale_rnd(7, 3, 5, 0), 4);

        // AV_ROUND_INF = 1: r = c - 1 = 4, result = 25 / 5 = 5
        assert_eq!(av_rescale_rnd(7, 3, 5, 1), 5);

        // AV_ROUND_NEAR_INF = 5: r = c / 2 = 2, result = 23 / 5 = 4
        assert_eq!(av_rescale_rnd(7, 3, 5, 5), 4);
    }

    #[test]
    fn test_av_rescale_rnd_large_values() {
        // Test with large values that trigger the 128-bit path
        let large_a = i32::MAX as i64 + 1000;
        let result = av_rescale_rnd(large_a, 1000, 1, 5);
        assert_eq!(result, large_a * 1000);
    }

    #[test]
    fn test_av_rescale_rnd_invalid_params() {
        // Test invalid c <= 0
        assert_eq!(av_rescale_rnd(100, 100, 0, 5), i64::MIN);

        // Test invalid b < 0
        assert_eq!(av_rescale_rnd(100, -1, 100, 5), i64::MIN);

        // Test invalid rounding mode (4 is invalid)
        assert_eq!(av_rescale_rnd(100, 100, 100, 4), i64::MIN);
    }
}