ff-sys 0.7.2

Low-level FFmpeg FFI bindings for Rust
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
//! Build script for ff-sys crate.
//!
//! This script handles:
//! - Platform-specific `FFmpeg` library detection
//! - bindgen code generation for FFI bindings
//!
//! # Windows (VCPKG)
//!
//! Requires FFmpeg installed via VCPKG:
//! ```bash
//! vcpkg install ffmpeg:x64-windows
//! ```
//!
//! Environment variables:
//! - `VCPKG_ROOT`: Path to VCPKG installation (default: `C:\vcpkg`)
//! - `LIBCLANG_PATH`: Path to LLVM/clang bin directory (for bindgen)
//!
//! # macOS (Homebrew)
//!
//! Requires FFmpeg installed via Homebrew:
//! ```bash
//! brew install ffmpeg
//! ```
//!
//! Environment variables:
//! - `HOMEBREW_PREFIX`: Path to Homebrew installation (auto-detected if not set)
//!   - Apple Silicon: `/opt/homebrew`
//!   - Intel: `/usr/local`
//!
//! # Linux (pkg-config)
//!
//! Requires FFmpeg development packages:
//! - Ubuntu/Debian: `apt install libavcodec-dev libavformat-dev libswscale-dev libswresample-dev`
//! - Fedora: `dnf install ffmpeg-devel`
//! - Arch: `pacman -S ffmpeg`

// Build scripts are allowed to use panic/expect for fatal configuration errors
#![allow(clippy::panic)]
#![allow(clippy::expect_used)]

use std::env;
use std::path::{Path, PathBuf};

fn main() {
    // docs.rs does not have FFmpeg installed.  Emit empty bindings so that the
    // crate compiles; docsrs_stubs.rs (included by lib.rs) provides stub types.
    if env::var("DOCS_RS").is_ok() {
        let out_path = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
        std::fs::write(out_path.join("bindings.rs"), b"")
            .expect("Couldn't write stub bindings for docs.rs");
        println!("cargo:rustc-cfg=docsrs");
        return;
    }

    // Detect target platform
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();

    // Configure platform-specific linking and collect include paths
    let include_paths = match target_os.as_str() {
        "windows" => configure_windows(),
        "macos" => configure_macos(),
        "linux" => configure_linux(),
        other => panic!("Unsupported platform: {other}"),
    };

    // Emit cfg flags based on detected FFmpeg/library API variants
    emit_api_cfg_flags(&include_paths);

    // Generate FFI bindings
    generate_bindings(&include_paths);
}

/// FFmpeg libraries required for linking
const FFMPEG_LIBS: &[&str] = &[
    "avformat",
    "avcodec",
    "avutil",
    "swscale",
    "swresample",
    "avfilter",
];

/// Configure `FFmpeg` linking for Windows via VCPKG.
///
/// Returns include paths for bindgen.
fn configure_windows() -> Vec<String> {
    // Rebuild if environment variables change
    println!("cargo:rerun-if-env-changed=VCPKG_ROOT");
    println!("cargo:rerun-if-env-changed=LIBCLANG_PATH");

    let vcpkg_root = env::var("VCPKG_ROOT").unwrap_or_else(|_| "C:\\vcpkg".to_string());
    let installed_path = Path::new(&vcpkg_root).join("installed").join("x64-windows");

    // Verify VCPKG FFmpeg installation exists
    let lib_path = installed_path.join("lib");
    let include_path = installed_path.join("include");
    let bin_path = installed_path.join("bin");

    if !lib_path.exists() {
        panic!(
            "VCPKG FFmpeg not found at: {}\n\
            Please install FFmpeg via VCPKG:\n\
            vcpkg install ffmpeg:x64-windows",
            lib_path.display()
        );
    }

    // Verify required libraries exist
    for lib in FFMPEG_LIBS {
        let lib_file = lib_path.join(format!("{lib}.lib"));
        if !lib_file.exists() {
            panic!(
                "FFmpeg library not found: {}\n\
                Please reinstall FFmpeg via VCPKG:\n\
                vcpkg install ffmpeg:x64-windows",
                lib_file.display()
            );
        }
    }

    // Set library search path
    println!("cargo:rustc-link-search=native={}", lib_path.display());

    // Link FFmpeg libraries (dynamic linking)
    for lib in FFMPEG_LIBS {
        println!("cargo:rustc-link-lib=dylib={lib}");
    }

    // Set DLL search path for runtime
    // This helps locate FFmpeg DLLs when running the application
    if bin_path.exists() {
        println!("cargo:rustc-env=FFMPEG_DLL_PATH={}", bin_path.display());
    }

    // Configure LLVM/clang path for bindgen
    configure_llvm_for_bindgen();

    vec![include_path.to_string_lossy().into_owned()]
}

/// Configure LLVM/clang path for bindgen on Windows.
///
/// bindgen requires libclang to parse C headers. On Windows, this is typically
/// provided by an LLVM installation.
fn configure_llvm_for_bindgen() {
    // Common LLVM installation paths on Windows
    let llvm_paths = [
        env::var("LIBCLANG_PATH").ok(),
        Some("C:\\Program Files\\LLVM\\bin".to_string()),
        Some("C:\\Program Files (x86)\\LLVM\\bin".to_string()),
        env::var("LLVM_HOME").ok().map(|p| format!("{p}\\bin")),
    ];

    for path in llvm_paths.into_iter().flatten() {
        let clang_dll = Path::new(&path).join("libclang.dll");
        if clang_dll.exists() {
            // Set LIBCLANG_PATH for bindgen
            // SAFETY: This is a build script running in a single-threaded context.
            // Setting environment variables is safe here as no other threads are
            // accessing environment variables concurrently.
            unsafe {
                env::set_var("LIBCLANG_PATH", &path);
            }
            return;
        }
    }

    // If LIBCLANG_PATH is already set, assume it's valid
    if env::var("LIBCLANG_PATH").is_ok() {
        return;
    }

    // Warn but don't fail - bindgen might find it through other means
    println!(
        "cargo:warning=LLVM/clang not found. Set LIBCLANG_PATH environment variable \
         to the LLVM bin directory containing libclang.dll"
    );
}

/// Configure `FFmpeg` linking for macOS via Homebrew.
///
/// This function tries the following detection methods in order:
/// 1. Homebrew installation (Apple Silicon: `/opt/homebrew`, Intel: `/usr/local`)
/// 2. pkg-config as a fallback
///
/// Returns include paths for bindgen.
fn configure_macos() -> Vec<String> {
    println!("cargo:rerun-if-env-changed=HOMEBREW_PREFIX");
    println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");

    // Try Homebrew first
    if let Some(paths) = try_homebrew() {
        return paths;
    }

    // Fall back to pkg-config
    if let Some(paths) = try_pkgconfig_unix() {
        return paths;
    }

    panic!(
        "FFmpeg not found on macOS.\n\
        Please install FFmpeg via Homebrew:\n\
        brew install ffmpeg\n\n\
        Or ensure pkg-config can find FFmpeg:\n\
        export PKG_CONFIG_PATH=\"/path/to/ffmpeg/lib/pkgconfig\""
    );
}

/// Try to configure FFmpeg via Homebrew.
///
/// Returns include paths if successful, None if FFmpeg is not found.
fn try_homebrew() -> Option<Vec<String>> {
    // Detect Homebrew prefix
    // - Apple Silicon (arm64): /opt/homebrew
    // - Intel (x86_64): /usr/local
    let homebrew_prefix = env::var("HOMEBREW_PREFIX").unwrap_or_else(|_| {
        // Auto-detect based on architecture
        let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
        if arch == "aarch64" {
            "/opt/homebrew".to_string()
        } else {
            "/usr/local".to_string()
        }
    });

    let homebrew_path = Path::new(&homebrew_prefix);
    let lib_path = homebrew_path.join("lib");
    let include_path = homebrew_path.join("include");

    // Verify Homebrew installation exists
    if !lib_path.exists() {
        return None;
    }

    // Verify FFmpeg libraries exist
    let mut all_found = true;
    for lib in FFMPEG_LIBS {
        // Check for .dylib files (macOS dynamic libraries)
        let dylib_file = lib_path.join(format!("lib{lib}.dylib"));
        if !dylib_file.exists() {
            // Also check for .a files (static libraries)
            let static_file = lib_path.join(format!("lib{lib}.a"));
            if !static_file.exists() {
                all_found = false;
                break;
            }
        }
    }

    if !all_found {
        return None;
    }

    // Verify include path contains FFmpeg headers
    let avcodec_header = include_path.join("libavcodec").join("avcodec.h");
    if !avcodec_header.exists() {
        return None;
    }

    // Set library search path
    println!("cargo:rustc-link-search=native={}", lib_path.display());

    // Link FFmpeg libraries (dynamic linking)
    for lib in FFMPEG_LIBS {
        println!("cargo:rustc-link-lib=dylib={lib}");
    }

    Some(vec![include_path.to_string_lossy().into_owned()])
}

/// Configure `FFmpeg` linking for Linux via pkg-config.
///
/// Returns the include paths detected by pkg-config.
fn configure_linux() -> Vec<String> {
    println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");

    if let Some(paths) = try_pkgconfig_unix() {
        return paths;
    }

    panic!(
        "FFmpeg not found on Linux.\n\
        Please install FFmpeg development packages:\n\n\
        Ubuntu/Debian:\n\
        sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev\n\n\
        Fedora:\n\
        sudo dnf install ffmpeg-devel\n\n\
        Arch Linux:\n\
        sudo pacman -S ffmpeg\n\n\
        If FFmpeg is installed in a non-standard location, set PKG_CONFIG_PATH:\n\
        export PKG_CONFIG_PATH=\"/path/to/ffmpeg/lib/pkgconfig\""
    );
}

/// Minimum versions per library required for FFmpeg 7.x.
///
/// Each libav* library has its own version number independent of the FFmpeg
/// suite version. These values correspond to the library versions shipped
/// with FFmpeg 7.0.
///
/// | Library        | FFmpeg 6.x | FFmpeg 7.x |
/// |----------------|-----------|-----------|
/// | libavformat    | 60.x      | 61.x      |
/// | libavcodec     | 60.x      | 61.x      |
/// | libavutil      | 58.x      | 59.x      |
/// | libswscale     | 7.x       | 8.x       |
/// | libswresample  | 4.x       | 5.x       |
const PKGCONFIG_LIBS: &[(&str, &str)] = &[
    ("libavformat", "61.0"),
    ("libavcodec", "61.0"),
    ("libavutil", "59.0"),
    ("libswscale", "8.0"),
    ("libswresample", "5.0"),
    ("libavfilter", "10.0"),
];

/// Try to configure FFmpeg via pkg-config (Unix systems).
///
/// Returns include paths if successful, None if FFmpeg is not found.
fn try_pkgconfig_unix() -> Option<Vec<String>> {
    let mut include_paths = Vec::new();
    let mut all_found = true;

    for (lib, min_version) in PKGCONFIG_LIBS {
        match pkg_config::Config::new()
            .atleast_version(min_version)
            .probe(lib)
        {
            Ok(library) => {
                // Collect include paths from pkg-config
                for path in &library.include_paths {
                    let path_str = path.to_string_lossy().to_string();
                    if !include_paths.contains(&path_str) {
                        include_paths.push(path_str);
                    }
                }
            }
            Err(e) => {
                // Log the error but continue checking other libraries
                println!("cargo:warning=pkg-config: {lib} not found: {e}");
                all_found = false;
                break;
            }
        }
    }

    if all_found { Some(include_paths) } else { None }
}

/// Emit Cargo cfg flags for FFmpeg API variants based on library versions.
///
/// Different FFmpeg major versions ship different API shapes for the same
/// functionality. We detect the installed version from the headers and emit
/// cfg flags so that Rust source code can conditionally compile the correct
/// constant/type names without relying on platform assumptions.
///
/// # libswscale SWS flags
///
/// | FFmpeg suite | libswscale | SWS_* constants |
/// |-------------|------------|-----------------|
/// | 7.x         | 8.x        | `#define` macros → `SWS_FAST_BILINEAR` etc. |
/// | 8.x         | 9.x        | C enum `SwsFlags` → `SwsFlags_SWS_FAST_BILINEAR` etc. |
///
/// Emits `ffmpeg_sws_flags_enum` when libswscale major version ≥ 9.
///
/// # `AV_BUFFERSRC_FLAG_KEEP_REF` type
///
/// bindgen generates `AV_BUFFERSRC_FLAG_KEEP_REF` as `u32` on Linux/macOS
/// (pkg-config / Homebrew FFmpeg) but as `i32` on Windows (VCPKG FFmpeg).
///
/// Emits `ffmpeg_buffersrc_flag_u32` on platforms where the type is `u32`,
/// so callers can use the normalized `BUFFERSRC_FLAG_KEEP_REF_I32` constant
/// instead of casting at every call site.
fn emit_api_cfg_flags(include_paths: &[String]) {
    let swscale_major = read_version_major(include_paths, "libswscale");

    if let Some(major) = swscale_major {
        if major >= 9 {
            // FFmpeg 8.x: SWS_* flags are a C enum, bindgen generates SwsFlags_SWS_*
            println!("cargo:rustc-cfg=ffmpeg8");
        }
    } else {
        println!(
            "cargo:warning=Could not detect libswscale version; \
             assuming FFmpeg 7.x (#define SWS_* constants)"
        );
    }

    // On Linux and macOS, bindgen generates AV_BUFFERSRC_FLAG_KEEP_REF as u32.
    // On Windows (VCPKG), it is generated as i32.
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
    if matches!(target_os.as_str(), "linux" | "macos") {
        println!("cargo:rustc-cfg=ffmpeg_buffersrc_flag_u32");
    }
}

/// Read the major version number from a libav*/libsw* version header.
///
/// Searches `include_paths` for `<lib>/version_major.h` (preferred) or
/// `<lib>/version.h` and returns the value of `LIB*_VERSION_MAJOR`.
fn read_version_major(include_paths: &[String], lib: &str) -> Option<u32> {
    for base in include_paths {
        let base = Path::new(base).join(lib);
        let candidates = [base.join("version_major.h"), base.join("version.h")];

        for path in &candidates {
            let Ok(content) = std::fs::read_to_string(path) else {
                continue;
            };

            // Look for a line like:  #define LIBSWSCALE_VERSION_MAJOR  9
            let needle = format!(
                "LIB{}_VERSION_MAJOR",
                lib.trim_start_matches("lib").to_ascii_uppercase()
            );
            for line in content.lines() {
                if line.contains(&needle) {
                    if let Some(val) = line.split_whitespace().last() {
                        if let Ok(n) = val.parse::<u32>() {
                            return Some(n);
                        }
                    }
                }
            }
        }
    }
    None
}

/// Generate FFI bindings using bindgen.
///
/// # Arguments
/// * `include_paths` - Include paths collected from platform-specific configuration
fn generate_bindings(include_paths: &[String]) {
    // Build bindgen with include paths
    let mut builder = bindgen::Builder::default().header("wrapper.h");

    // Add all include paths
    for path in include_paths {
        builder = builder.clang_arg(format!("-I{path}"));
    }

    let bindings = builder
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        // Allowlist FFmpeg functions
        .allowlist_function("av_.*")
        .allowlist_function("avformat_.*")
        .allowlist_function("avcodec_.*")
        .allowlist_function("sws_.*")
        .allowlist_function("swr_.*")
        // Allowlist FFmpeg types
        .allowlist_type("AV.*")
        .allowlist_type("Sws.*")
        .allowlist_type("Swr.*")
        // Allowlist FFmpeg constants
        .allowlist_var("AV_.*")
        .allowlist_var("AVERROR.*")
        .allowlist_var("AVSEEK_.*")
        .allowlist_var("AVIO_.*")
        .allowlist_var("SWS_.*")
        .allowlist_var("SWR_.*")
        // Allowlist libavfilter functions and types
        .allowlist_function("avfilter_.*")
        .allowlist_function("av_buffersrc_.*")
        .allowlist_function("av_buffersink_.*")
        .allowlist_type("AVFilter.*")
        .allowlist_var("AV_BUFFERSRC_.*")
        .allowlist_var("AV_BUFFERSINK_.*")
        // Derive traits for safety and convenience
        .derive_debug(true)
        .derive_default(true)
        // Disable doc comments - FFmpeg C comments contain invalid Rust code
        .generate_comments(false)
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");

    // Rerun build script if wrapper.h changes
    println!("cargo:rerun-if-changed=wrapper.h");
}