ff-decode 0.13.0

Video and audio decoding - the Rust way
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
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
//! Memory usage tests for ff-decode.
//!
//! These tests validate that the ff-* implementation has efficient memory usage:
//! - Frames should be pooled and reused
//! - Memory should not grow unbounded during long decoding sessions
//! - Seeking should not cause memory leaks
//!
//! Note: Exact memory measurements are system-dependent, so we use relative measurements.

// Tests are allowed to use unwrap() for simplicity
#![allow(clippy::unwrap_used)]

use std::path::PathBuf;
use std::time::Duration;

use std::sync::Arc;

use ff_common::VecPool;
use ff_decode::{FramePool, HardwareAccel, SeekMode, VideoDecoder};

// ============================================================================
// Test Helpers
// ============================================================================

/// Returns the path to the test assets directory.
fn assets_dir() -> PathBuf {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    PathBuf::from(format!("{}/../../assets", manifest_dir))
}

/// Returns the path to the test video file.
fn test_video_path() -> PathBuf {
    assets_dir().join("video/gameplay.mp4")
}

/// Creates a test decoder with hardware acceleration disabled for consistency.
fn create_decoder() -> VideoDecoder {
    VideoDecoder::open(&test_video_path())
        .hardware_accel(HardwareAccel::None)
        .build()
        .expect("Failed to create decoder")
}

/// Gets an estimate of current memory usage (platform-specific, best effort).
#[cfg(target_os = "windows")]
fn get_memory_usage_bytes() -> Option<usize> {
    use std::mem;
    use winapi::um::processthreadsapi::GetCurrentProcess;
    use winapi::um::psapi::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};

    unsafe {
        let process = GetCurrentProcess();
        let mut pmc: PROCESS_MEMORY_COUNTERS = mem::zeroed();
        pmc.cb = mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;

        if GetProcessMemoryInfo(process, &mut pmc, pmc.cb) != 0 {
            Some(pmc.WorkingSetSize)
        } else {
            None
        }
    }
}

#[cfg(not(target_os = "windows"))]
fn get_memory_usage_bytes() -> Option<usize> {
    // On Unix systems, we can read from /proc/self/statm
    // This is a simplified version; production code might use a crate like `sysinfo`
    None
}

/// Helper to format bytes in a human-readable format.
fn format_bytes(bytes: usize) -> String {
    const KB: usize = 1024;
    const MB: usize = 1024 * KB;
    const GB: usize = 1024 * MB;

    if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} bytes", bytes)
    }
}

// ============================================================================
// Memory Usage Tests
// ============================================================================

#[test]
fn test_memory_stability_during_sequential_decode() {
    let mut decoder = create_decoder();

    // Get initial memory usage
    let initial_memory = get_memory_usage_bytes();

    // Decode 300 frames (10 seconds at 30fps)
    const FRAME_COUNT: usize = 300;
    let mut decoded_count = 0;

    for _ in 0..FRAME_COUNT {
        match decoder.decode_one() {
            Ok(Some(_frame)) => {
                // Frame is used here, then dropped
                decoded_count += 1;
            }
            Ok(None) => break,
            Err(e) => panic!("Decode failed: {}", e),
        }
    }

    // Get final memory usage
    let final_memory = get_memory_usage_bytes();

    println!("Decoded {} frames", decoded_count);

    if let (Some(initial), Some(final_mem)) = (initial_memory, final_memory) {
        let growth = if final_mem > initial {
            final_mem - initial
        } else {
            0
        };

        println!("Initial memory: {}", format_bytes(initial));
        println!("Final memory: {}", format_bytes(final_mem));
        println!("Memory growth: {}", format_bytes(growth));

        // Memory growth should be minimal (under 100MB for 300 frames)
        // This allows for some caching and normal allocations
        const MAX_GROWTH_MB: usize = 100;
        let max_growth_bytes = MAX_GROWTH_MB * 1024 * 1024;

        assert!(
            growth < max_growth_bytes,
            "Memory growth too high: {} (threshold: {})",
            format_bytes(growth),
            format_bytes(max_growth_bytes)
        );
    } else {
        println!("Memory measurement not available on this platform");
    }
}

#[test]
fn test_memory_stability_during_repeated_seeking() {
    let mut decoder = create_decoder();

    // Get initial memory usage
    let initial_memory = get_memory_usage_bytes();

    // Perform 100 seeks to different positions
    const SEEK_COUNT: usize = 100;
    let positions = [
        Duration::from_secs(1),
        Duration::from_secs(3),
        Duration::from_secs(5),
        Duration::from_secs(7),
        Duration::from_secs(2),
    ];

    for i in 0..SEEK_COUNT {
        let pos = positions[i % positions.len()];
        decoder.seek(pos, SeekMode::Keyframe).expect("Seek failed");
        let _ = decoder.decode_one().expect("Decode failed");
    }

    // Get final memory usage
    let final_memory = get_memory_usage_bytes();

    println!("Performed {} seeks", SEEK_COUNT);

    if let (Some(initial), Some(final_mem)) = (initial_memory, final_memory) {
        let growth = if final_mem > initial {
            final_mem - initial
        } else {
            0
        };

        println!("Initial memory: {}", format_bytes(initial));
        println!("Final memory: {}", format_bytes(final_mem));
        println!("Memory growth: {}", format_bytes(growth));

        // Memory growth should be minimal during seeking
        const MAX_GROWTH_MB: usize = 50;
        let max_growth_bytes = MAX_GROWTH_MB * 1024 * 1024;

        assert!(
            growth < max_growth_bytes,
            "Memory growth during seeking too high: {} (threshold: {})",
            format_bytes(growth),
            format_bytes(max_growth_bytes)
        );
    } else {
        println!("Memory measurement not available on this platform");
    }
}

#[test]
fn test_no_memory_leak_after_decoder_drop() {
    // Get initial memory usage
    let initial_memory = get_memory_usage_bytes();

    // Create and drop multiple decoders
    const DECODER_COUNT: usize = 10;
    for _ in 0..DECODER_COUNT {
        let mut decoder = create_decoder();

        // Decode a few frames
        for _ in 0..10 {
            if decoder.decode_one().is_err() {
                break;
            }
        }

        // Decoder is dropped here
    }

    // Force garbage collection (not guaranteed in Rust, but we can try)
    // In a real memory profiler, we would use tools like valgrind or heaptrack

    // Get final memory usage
    let final_memory = get_memory_usage_bytes();

    if let (Some(initial), Some(final_mem)) = (initial_memory, final_memory) {
        let growth = if final_mem > initial {
            final_mem - initial
        } else {
            0
        };

        println!("Created and dropped {} decoders", DECODER_COUNT);
        println!("Initial memory: {}", format_bytes(initial));
        println!("Final memory: {}", format_bytes(final_mem));
        println!("Memory growth: {}", format_bytes(growth));

        // Memory growth should be minimal after decoder cleanup
        // Allow 50MB for normal allocations and OS behavior
        const MAX_GROWTH_MB: usize = 50;
        let max_growth_bytes = MAX_GROWTH_MB * 1024 * 1024;

        assert!(
            growth < max_growth_bytes,
            "Possible memory leak detected: {} (threshold: {})",
            format_bytes(growth),
            format_bytes(max_growth_bytes)
        );
    } else {
        println!("Memory measurement not available on this platform");
    }
}

#[test]
fn test_frame_memory_is_released() {
    let mut decoder = create_decoder();

    // Get baseline memory
    let baseline_memory = get_memory_usage_bytes();

    // Decode frames in a scope, so they're dropped
    {
        let mut frames = Vec::new();
        for _ in 0..10 {
            if let Ok(Some(frame)) = decoder.decode_one() {
                frames.push(frame);
            }
        }

        // Get memory with frames in scope
        let with_frames_memory = get_memory_usage_bytes();

        if let (Some(baseline), Some(with_frames)) = (baseline_memory, with_frames_memory) {
            println!(
                "Memory with {} frames in scope: {}",
                frames.len(),
                format_bytes(with_frames)
            );
            println!(
                "Frame memory usage: {}",
                format_bytes(if with_frames > baseline {
                    with_frames - baseline
                } else {
                    0
                })
            );
        }

        // Frames are dropped here
    }

    // Decode a few more frames to ensure decoder still works
    for _ in 0..5 {
        let _ = decoder.decode_one();
    }

    // Get memory after frames dropped
    let after_drop_memory = get_memory_usage_bytes();

    if let (Some(baseline), Some(after_drop)) = (baseline_memory, after_drop_memory) {
        let growth = if after_drop > baseline {
            after_drop - baseline
        } else {
            0
        };

        println!("Baseline memory: {}", format_bytes(baseline));
        println!("Memory after frame drop: {}", format_bytes(after_drop));
        println!("Net growth: {}", format_bytes(growth));

        // Memory should return close to baseline (allow 60MB variance for FFmpeg internal caching)
        const MAX_GROWTH_MB: usize = 60;
        let max_growth_bytes = MAX_GROWTH_MB * 1024 * 1024;

        assert!(
            growth < max_growth_bytes,
            "Frame memory not properly released: {} (threshold: {})",
            format_bytes(growth),
            format_bytes(max_growth_bytes)
        );
    } else {
        println!("Memory measurement not available on this platform");
    }
}

#[test]
fn test_thumbnail_memory_efficiency() {
    let mut decoder = create_decoder();

    // Get baseline memory
    let baseline_memory = get_memory_usage_bytes();

    // Generate 20 thumbnails
    const THUMBNAIL_COUNT: usize = 20;
    let thumbnails = decoder
        .thumbnails(THUMBNAIL_COUNT, 160, 90)
        .expect("Failed to generate thumbnails");

    assert_eq!(thumbnails.len(), THUMBNAIL_COUNT);

    // Get memory with thumbnails
    let with_thumbnails_memory = get_memory_usage_bytes();

    if let (Some(baseline), Some(with_thumbs)) = (baseline_memory, with_thumbnails_memory) {
        let thumbnail_memory = if with_thumbs > baseline {
            with_thumbs - baseline
        } else {
            0
        };

        // Each 160x90 YUV420p thumbnail is approximately:
        // Y: 160*90 = 14,400 bytes
        // U: 80*45 = 3,600 bytes
        // V: 80*45 = 3,600 bytes
        // Total per frame: ~22KB
        // 20 frames: ~440KB

        println!("Thumbnail memory usage: {}", format_bytes(thumbnail_memory));

        // Allow up to 30MB for 20 thumbnails (includes FFmpeg codec/format context overhead)
        const MAX_THUMBNAIL_MEMORY_MB: usize = 30;
        let max_memory_bytes = MAX_THUMBNAIL_MEMORY_MB * 1024 * 1024;

        assert!(
            thumbnail_memory < max_memory_bytes,
            "Thumbnail memory usage too high: {} (threshold: {})",
            format_bytes(thumbnail_memory),
            format_bytes(max_memory_bytes)
        );
    } else {
        println!("Memory measurement not available on this platform");
    }

    // Drop thumbnails
    drop(thumbnails);
}

// ============================================================================
// Memory Efficiency Comparison Tests
// ============================================================================

#[test]
#[ignore = "performance thresholds are environment-dependent; run explicitly with -- --include-ignored"]
fn test_decoder_memory_overhead() {
    // Measure the base memory overhead of creating a decoder
    let baseline_memory = get_memory_usage_bytes();

    let decoder = create_decoder();

    let with_decoder_memory = get_memory_usage_bytes();

    if let (Some(baseline), Some(with_decoder)) = (baseline_memory, with_decoder_memory) {
        let overhead = if with_decoder > baseline {
            with_decoder - baseline
        } else {
            0
        };

        println!("Decoder memory overhead: {}", format_bytes(overhead));

        // Decoder should have minimal overhead (under 50MB)
        const MAX_OVERHEAD_MB: usize = 50;
        let max_overhead_bytes = MAX_OVERHEAD_MB * 1024 * 1024;

        assert!(
            overhead < max_overhead_bytes,
            "Decoder overhead too high: {} (threshold: {})",
            format_bytes(overhead),
            format_bytes(max_overhead_bytes)
        );
    } else {
        println!("Memory measurement not available on this platform");
    }

    drop(decoder);
}

// ============================================================================
// Frame Pool Tests
// ============================================================================

#[test]
fn frame_pool_should_accumulate_buffers_after_decode() {
    let pool = VecPool::new(8);
    let pool_dyn: Arc<dyn FramePool> = Arc::clone(&pool) as Arc<dyn FramePool>;

    let mut decoder = VideoDecoder::open(&test_video_path())
        .hardware_accel(HardwareAccel::None)
        .frame_pool(pool_dyn)
        .build()
        .expect("Failed to create decoder");

    // Decode and immediately drop 5 frames. Each drop should return the
    // buffer to the pool, growing pool.available().
    for _ in 0..5 {
        match decoder.decode_one() {
            Ok(Some(frame)) => drop(frame),
            Ok(None) => break,
            Err(_) => break,
        }
    }

    assert!(
        pool.available() > 0,
        "pool.available() should be > 0 after dropping decoded frames, got {}",
        pool.available()
    );
}

#[test]
fn frame_pool_available_should_be_zero_while_frames_are_held() {
    // Case C: decode multiple frames simultaneously, verify pool is empty
    // while they're held, then verify it fills after they're dropped.
    let pool = VecPool::new(8);
    let pool_dyn: Arc<dyn FramePool> = Arc::clone(&pool) as Arc<dyn FramePool>;

    let mut decoder = VideoDecoder::open(&test_video_path())
        .hardware_accel(HardwareAccel::None)
        .frame_pool(pool_dyn)
        .build()
        .expect("Failed to create decoder");

    // Decode 4 frames and hold them all.
    let mut held_frames = Vec::new();
    for _ in 0..4 {
        match decoder.decode_one() {
            Ok(Some(frame)) => held_frames.push(frame),
            Ok(None) => break,
            Err(_) => break,
        }
    }

    assert_eq!(held_frames.len(), 4, "Should have decoded 4 frames");

    // All buffers are in use — pool must be empty.
    assert_eq!(
        pool.available(),
        0,
        "pool.available() should be 0 while frames are held, got {}",
        pool.available()
    );

    // Drop all frames — buffers should return to the pool.
    drop(held_frames);

    assert!(
        pool.available() > 0,
        "pool.available() should be > 0 after dropping all held frames, got {}",
        pool.available()
    );
}