use bindgen::RustTarget;
use bindgen::{callbacks, Bindings};
use camino::Utf8Path as Path;
use camino::Utf8PathBuf as PathBuf;
use once_cell::sync::Lazy;
use std::{collections::HashSet, env, fs};
static LIBS: Lazy<[&str; 7]> = Lazy::new(|| {
[
"avcodec",
"avdevice",
"avfilter",
"avformat",
"avutil",
"swresample",
"swscale",
]
});
static HEADERS: Lazy<Vec<PathBuf>> = Lazy::new(|| {
[
"libavcodec/ac3_parser.h",
"libavcodec/adts_parser.h",
"libavcodec/avcodec.h",
"libavcodec/avdct.h",
"libavcodec/avfft.h",
"libavcodec/bsf.h",
"libavcodec/codec.h",
"libavcodec/codec_desc.h",
"libavcodec/codec_id.h",
"libavcodec/codec_par.h",
"libavcodec/defs.h",
"libavcodec/dirac.h",
"libavcodec/dv_profile.h",
"libavcodec/jni.h",
"libavcodec/mediacodec.h",
"libavcodec/packet.h",
"libavcodec/version.h",
"libavcodec/version_major.h",
"libavcodec/vorbis_parser.h",
"libavdevice/avdevice.h",
"libavdevice/version.h",
"libavdevice/version_major.h",
"libavfilter/avfilter.h",
"libavfilter/buffersink.h",
"libavfilter/buffersrc.h",
"libavfilter/version.h",
"libavfilter/version_major.h",
"libavformat/avformat.h",
"libavformat/avio.h",
"libavformat/version.h",
"libavformat/version_major.h",
"libavutil/adler32.h",
"libavutil/aes.h",
"libavutil/aes_ctr.h",
"libavutil/ambient_viewing_environment.h",
"libavutil/attributes.h",
"libavutil/audio_fifo.h",
"libavutil/avassert.h",
"libavutil/avconfig.h",
"libavutil/avstring.h",
"libavutil/avutil.h",
"libavutil/base64.h",
"libavutil/blowfish.h",
"libavutil/bprint.h",
"libavutil/bswap.h",
"libavutil/buffer.h",
"libavutil/camellia.h",
"libavutil/cast5.h",
"libavutil/channel_layout.h",
"libavutil/common.h",
"libavutil/cpu.h",
"libavutil/crc.h",
"libavutil/csp.h",
"libavutil/des.h",
"libavutil/detection_bbox.h",
"libavutil/dict.h",
"libavutil/display.h",
"libavutil/dovi_meta.h",
"libavutil/downmix_info.h",
"libavutil/encryption_info.h",
"libavutil/error.h",
"libavutil/eval.h",
"libavutil/executor.h",
"libavutil/ffversion.h",
"libavutil/fifo.h",
"libavutil/file.h",
"libavutil/film_grain_params.h",
"libavutil/frame.h",
"libavutil/hash.h",
"libavutil/hdr_dynamic_metadata.h",
"libavutil/hdr_dynamic_vivid_metadata.h",
"libavutil/hmac.h",
"libavutil/hwcontext.h",
"libavutil/imgutils.h",
"libavutil/intfloat.h",
"libavutil/intreadwrite.h",
"libavutil/lfg.h",
"libavutil/log.h",
"libavutil/lzo.h",
"libavutil/macros.h",
"libavutil/mastering_display_metadata.h",
"libavutil/mathematics.h",
"libavutil/md5.h",
"libavutil/mem.h",
"libavutil/motion_vector.h",
"libavutil/murmur3.h",
"libavutil/opt.h",
"libavutil/parseutils.h",
"libavutil/pixdesc.h",
"libavutil/pixelutils.h",
"libavutil/pixfmt.h",
"libavutil/random_seed.h",
"libavutil/rational.h",
"libavutil/rc4.h",
"libavutil/replaygain.h",
"libavutil/ripemd.h",
"libavutil/samplefmt.h",
"libavutil/sha.h",
"libavutil/sha512.h",
"libavutil/spherical.h",
"libavutil/stereo3d.h",
"libavutil/tea.h",
"libavutil/threadmessage.h",
"libavutil/time.h",
"libavutil/timecode.h",
"libavutil/timestamp.h",
"libavutil/tree.h",
"libavutil/twofish.h",
"libavutil/tx.h",
"libavutil/uuid.h",
"libavutil/version.h",
"libavutil/video_enc_params.h",
"libavutil/video_hint.h",
"libavutil/xtea.h",
"libswresample/swresample.h",
"libswresample/version.h",
"libswresample/version_major.h",
"libswscale/swscale.h",
"libswscale/version.h",
"libswscale/version_major.h",
]
.into_iter()
.map(|x| Path::new(x).into_iter().collect())
.collect()
});
#[derive(Debug)]
struct FilterCargoCallbacks {
emitted_macro: HashSet<&'static str>,
}
impl FilterCargoCallbacks {
fn new(set: HashSet<&'static str>) -> Self {
Self { emitted_macro: set }
}
}
impl callbacks::ParseCallbacks for FilterCargoCallbacks {
fn will_parse_macro(&self, name: &str) -> callbacks::MacroParsingBehavior {
if self.emitted_macro.contains(name) {
callbacks::MacroParsingBehavior::Ignore
} else {
callbacks::MacroParsingBehavior::Default
}
}
}
fn use_prebuilt_binding(from: &Path, to: &Path) {
fs::copy(from, to).expect("Prebuilt binding file failed to be copied.");
}
fn generate_bindings(ffmpeg_include_dir: &Path, headers: &[PathBuf]) -> Bindings {
if !Path::new(ffmpeg_include_dir).exists() {
panic!(
"FFmpeg include dir: `{:?}` doesn't exits",
ffmpeg_include_dir
);
}
let filter_callback = FilterCargoCallbacks::new(
[
"FP_NAN",
"FP_INFINITE",
"FP_ZERO",
"FP_SUBNORMAL",
"FP_NORMAL",
]
.into_iter()
.collect(),
);
headers
.iter()
.map(|header| ffmpeg_include_dir.join(header))
.filter(|path| {
let exists = Path::new(&path).exists();
if !exists {
eprintln!("Header path `{:?}` not found.", path);
}
exists
})
.fold(
{
bindgen::builder()
.impl_debug(true)
.rust_target(RustTarget::stable(68, 0).ok().unwrap())
.parse_callbacks(Box::new(filter_callback))
.clang_arg(format!("-I{}", ffmpeg_include_dir))
.blocklist_type("__mingw_ldbl_type_t")
.prepend_enum_name(false)
},
|builder, header| builder.header(header),
)
.generate()
.expect("Binding generation failed.")
}
fn static_linking_with_libs_dir(library_names: &[&str], ffmpeg_libs_dir: &Path) {
println!("cargo:rustc-link-search=native={}", ffmpeg_libs_dir);
for library_name in library_names {
println!("cargo:rustc-link-lib=static={}", library_name);
}
}
#[allow(dead_code)]
pub struct EnvVars {
docs_rs: Option<String>,
out_dir: Option<PathBuf>,
ffmpeg_include_dir: Option<PathBuf>,
ffmpeg_dll_path: Option<PathBuf>,
ffmpeg_pkg_config_path: Option<PathBuf>,
ffmpeg_libs_dir: Option<PathBuf>,
ffmpeg_binding_path: Option<PathBuf>,
}
impl EnvVars {
fn init() -> Self {
println!("cargo:rerun-if-env-changed=DOCS_RS");
println!("cargo:rerun-if-env-changed=OUT_DIR");
println!("cargo:rerun-if-env-changed=FFMPEG_INCLUDE_DIR");
println!("cargo:rerun-if-env-changed=FFMPEG_DLL_PATH");
println!("cargo:rerun-if-env-changed=FFMPEG_PKG_CONFIG_PATH");
println!("cargo:rerun-if-env-changed=FFMPEG_LIBS_DIR");
println!("cargo:rerun-if-env-changed=FFMPEG_BINDING_PATH");
Self {
docs_rs: env::var("DOCS_RS").ok(),
out_dir: env::var("OUT_DIR").ok().map(remove_verbatim),
ffmpeg_include_dir: env::var("FFMPEG_INCLUDE_DIR").ok().map(remove_verbatim),
ffmpeg_dll_path: env::var("FFMPEG_DLL_PATH").ok().map(remove_verbatim),
ffmpeg_pkg_config_path: env::var("FFMPEG_PKG_CONFIG_PATH").ok().map(remove_verbatim),
ffmpeg_libs_dir: env::var("FFMPEG_LIBS_DIR").ok().map(remove_verbatim),
ffmpeg_binding_path: env::var("FFMPEG_BINDING_PATH").ok().map(remove_verbatim),
}
}
}
fn remove_verbatim(path: String) -> PathBuf {
let path = if let Some(path) = path.strip_prefix(r#"\\?\"#) {
path.to_string()
} else {
path
};
PathBuf::from(path)
}
#[cfg(not(target_os = "windows"))]
mod pkg_config_linking {
use super::*;
pub fn linking_with_pkg_config(
library_names: &[&str],
) -> Result<Vec<PathBuf>, pkg_config::Error> {
for libname in library_names {
pkg_config::Config::new()
.cargo_metadata(false)
.env_metadata(false)
.print_system_libs(false)
.print_system_cflags(false)
.probe(&format!("lib{}", libname))?;
}
let mut paths = HashSet::new();
for libname in library_names {
let new_paths = pkg_config::Config::new()
.probe(&format!("lib{}", libname))
.unwrap_or_else(|_| panic!("{} not found!", libname))
.include_paths;
for new_path in new_paths {
let new_path = new_path.to_str().unwrap().to_string();
paths.insert(new_path);
}
}
Ok(paths.into_iter().map(PathBuf::from).collect())
}
}
#[cfg(feature = "link_vcpkg_ffmpeg")]
mod vcpkg_linking {
use super::*;
fn linking_with_vcpkg(
_env_vars: &EnvVars,
_library_names: &[&str],
) -> Result<Vec<PathBuf>, vcpkg::Error> {
Ok(vcpkg::Config::new()
.find_package("ffmpeg")?
.include_paths
.into_iter()
.map(|x| PathBuf::from_path_buf(x).unwrap())
.collect())
}
pub fn linking_with_vcpkg_and_bindgen(
env_vars: &EnvVars,
output_binding_path: &Path,
) -> Result<(), vcpkg::Error> {
let include_paths = linking_with_vcpkg(env_vars, &*LIBS)?;
if let Some(ffmpeg_binding_path) = env_vars.ffmpeg_binding_path.as_ref() {
use_prebuilt_binding(ffmpeg_binding_path, output_binding_path);
} else {
generate_bindings(&include_paths[0], &HEADERS)
.write_to_file(output_binding_path)
.expect("Cannot write binding to file.");
}
Ok(())
}
}
fn dynamic_linking(env_vars: &EnvVars) {
let ffmpeg_dll_path = env_vars.ffmpeg_dll_path.as_ref().unwrap();
let output_binding_path = &env_vars.out_dir.as_ref().unwrap().join("binding.rs");
let (ffmpeg_dll_name, ffmpeg_dll_dir) = {
let mut ffmpeg_dll_path = PathBuf::from(ffmpeg_dll_path);
let ffmpeg_dll_filename = ffmpeg_dll_path.file_stem().unwrap();
let ffmpeg_dll_name = if cfg!(target_os = "windows") {
ffmpeg_dll_filename
} else {
ffmpeg_dll_filename.trim_start_matches("lib")
}
.to_string();
ffmpeg_dll_path.pop();
let ffmpeg_dll_path = ffmpeg_dll_path.to_string();
(ffmpeg_dll_name, ffmpeg_dll_path)
};
println!("cargo:rustc-link-lib=dylib={}", ffmpeg_dll_name);
println!("cargo:rustc-link-search=native={}", ffmpeg_dll_dir);
if let Some(ffmpeg_binding_path) = env_vars.ffmpeg_binding_path.as_ref() {
use_prebuilt_binding(ffmpeg_binding_path, output_binding_path);
} else if let Some(ffmpeg_include_dir) = env_vars.ffmpeg_include_dir.as_ref() {
generate_bindings(ffmpeg_include_dir, &HEADERS)
.write_to_file(output_binding_path)
.expect("Cannot write binding to file.");
} else {
panic!("No binding generation method is set!");
}
}
fn static_linking(env_vars: &EnvVars) {
let output_binding_path = &env_vars.out_dir.as_ref().unwrap().join("binding.rs");
#[cfg(not(target_os = "windows"))]
{
fn static_linking_with_pkg_config_and_bindgen(
env_vars: &EnvVars,
output_binding_path: &Path,
) -> Result<(), pkg_config::Error> {
let include_paths = pkg_config_linking::linking_with_pkg_config(&*LIBS)?;
if let Some(ffmpeg_binding_path) = env_vars.ffmpeg_binding_path.as_ref() {
use_prebuilt_binding(ffmpeg_binding_path, output_binding_path);
} else if let Some(ffmpeg_include_dir) = env_vars.ffmpeg_include_dir.as_ref() {
generate_bindings(ffmpeg_include_dir, &HEADERS)
.write_to_file(output_binding_path)
.expect("Cannot write binding to file.");
} else {
generate_bindings(&include_paths[0], &HEADERS)
.write_to_file(output_binding_path)
.expect("Cannot write binding to file.");
}
Ok(())
}
if let Some(ffmpeg_pkg_config_path) = env_vars.ffmpeg_pkg_config_path.as_ref() {
if !Path::new(ffmpeg_pkg_config_path).exists() {
panic!(
"error: FFMPEG_PKG_CONFIG_PATH is set to `{}`, which does not exist.",
ffmpeg_pkg_config_path
);
}
env::set_var("PKG_CONFIG_PATH", ffmpeg_pkg_config_path);
static_linking_with_pkg_config_and_bindgen(env_vars, output_binding_path)
.expect("Static linking with pkg-config failed.");
} else if let Some(ffmpeg_libs_dir) = env_vars.ffmpeg_libs_dir.as_ref() {
static_linking_with_libs_dir(&*LIBS, ffmpeg_libs_dir);
if let Some(ffmpeg_binding_path) = env_vars.ffmpeg_binding_path.as_ref() {
use_prebuilt_binding(ffmpeg_binding_path, output_binding_path);
} else if let Some(ffmpeg_include_dir) = env_vars.ffmpeg_include_dir.as_ref() {
generate_bindings(ffmpeg_include_dir, &HEADERS)
.write_to_file(output_binding_path)
.expect("Cannot write binding to file.");
} else {
panic!("No binding generation method is set!");
}
} else {
#[cfg(not(any(feature = "link_system_ffmpeg", feature = "link_vcpkg_ffmpeg")))]
panic!(
"
!!!!!!! rusty_ffmpeg: No linking method set!
Use `FFMPEG_PKG_CONFIG_PATH` or `FFMPEG_LIBS_DIR` if you have prebuilt FFmpeg libraries.
Enable `link_system_ffmpeg` feature if you want to link ffmpeg libraries installed in system path(which can be probed by pkg-config).
Enable `link_vcpkg_ffmpeg` feature if you want to link ffmpeg libraries installed by vcpkg.
"
);
#[cfg(any(feature = "link_system_ffmpeg", feature = "link_vcpkg_ffmpeg"))]
{
let mut success = false;
let mut error = String::new();
#[cfg(feature = "link_system_ffmpeg")]
if !success {
if let Err(e) =
static_linking_with_pkg_config_and_bindgen(env_vars, output_binding_path)
{
error.push('\n');
error.push_str(&format!("Link system FFmpeg failed: {:?}", e));
} else {
println!("Link system FFmpeg succeeded.");
success = true;
}
}
#[cfg(feature = "link_vcpkg_ffmpeg")]
if !success {
if let Err(e) =
vcpkg_linking::linking_with_vcpkg_and_bindgen(env_vars, output_binding_path)
{
error.push('\n');
error.push_str(&format!("Link vcpkg FFmpeg failed: {:?}", e));
} else {
println!("Link vcpkg FFmpeg succeeded.");
success = true;
}
}
if !success {
panic!("FFmpeg linking trial failed: {}", error);
}
}
}
}
#[cfg(target_os = "windows")]
{
if let Some(ffmpeg_libs_dir) = env_vars.ffmpeg_libs_dir.as_ref() {
static_linking_with_libs_dir(&*LIBS, ffmpeg_libs_dir);
if let Some(ffmpeg_binding_path) = env_vars.ffmpeg_binding_path.as_ref() {
use_prebuilt_binding(ffmpeg_binding_path, output_binding_path);
} else if let Some(ffmpeg_include_dir) = env_vars.ffmpeg_include_dir.as_ref() {
generate_bindings(ffmpeg_include_dir, &HEADERS)
.write_to_file(output_binding_path)
.expect("Cannot write binding to file.");
} else {
panic!("No binding generation method is set!");
}
} else {
#[cfg(feature = "link_vcpkg_ffmpeg")]
vcpkg_linking::linking_with_vcpkg_and_bindgen(env_vars, output_binding_path)
.expect("Linking FFmpeg with vcpkg failed.");
#[cfg(not(feature = "link_vcpkg_ffmpeg"))]
panic!(
"
!!!!!!! rusty_ffmpeg: No linking method set!
Use FFMPEG_LIBS_DIR if you have prebuilt FFmpeg libraries.
Enable `link_vcpkg_ffmpeg` feature if you want to link ffmpeg provided by vcpkg.
"
);
}
}
}
fn docs_rs_linking(env_vars: &EnvVars) {
let binding_file_path = &env_vars.out_dir.as_ref().unwrap().join("binding.rs");
use_prebuilt_binding(Path::new("src/binding.rs"), binding_file_path);
}
fn main() {
let env_vars = EnvVars::init();
if env_vars.docs_rs.is_some() {
docs_rs_linking(&env_vars);
} else if env_vars.ffmpeg_dll_path.is_some() {
dynamic_linking(&env_vars);
} else {
static_linking(&env_vars);
}
}