Files
One-KVM/libs/hwcodec/build.rs
2026-05-24 09:44:41 +00:00

697 lines
25 KiB
Rust

use cc::Build;
use std::{
env,
path::{Path, PathBuf},
};
fn main() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let cpp_dir = manifest_dir.join("cpp");
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed={}", cpp_dir.display());
let mut builder = Build::new();
build_common(&mut builder);
ffmpeg::build_ffmpeg(&mut builder);
builder.static_crt(true).compile("hwcodec");
}
fn build_common(builder: &mut Build) {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let common_dir = manifest_dir.join("cpp").join("common");
let mut bindings = bindgen::builder()
.header(common_dir.join("common.h").to_string_lossy().to_string())
.header(common_dir.join("callback.h").to_string_lossy().to_string())
.rustified_enum(".*")
.parse_callbacks(Box::new(CommonCallbacks));
if target_os == "android" {
print_android_bindgen_env();
bindings = bindings.clang_args(android_clang_args());
}
bindings
.generate()
.unwrap()
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("common_ffi.rs"))
.unwrap();
// system
#[cfg(windows)]
{
for lib in ["d3d11", "dxgi"] {
println!("cargo:rustc-link-lib={}", lib);
}
}
builder.include(&common_dir);
// platform
let platform_path = common_dir.join("platform");
#[cfg(windows)]
{
let win_path = platform_path.join("win");
builder.include(&win_path);
builder.file(win_path.join("win.cpp"));
}
#[cfg(target_os = "linux")]
{
let linux_path = platform_path.join("linux");
builder.include(&linux_path);
builder.file(linux_path.join("linux.cpp"));
}
// Unsupported platforms
if target_os != "windows" && target_os != "linux" && target_os != "android" {
panic!(
"Unsupported OS: {}. Only Windows, Linux, and Android are supported.",
target_os
);
}
// tool
builder.files(["log.cpp", "util.cpp"].map(|f| common_dir.join(f)));
}
#[derive(Debug)]
struct CommonCallbacks;
impl bindgen::callbacks::ParseCallbacks for CommonCallbacks {
fn add_derives(&self, info: &bindgen::callbacks::DeriveInfo<'_>) -> Vec<String> {
let names = vec!["DataFormat", "SurfaceFormat", "API"];
if names.contains(&info.name) {
vec!["Serialize", "Deserialize"]
.drain(..)
.map(|s| s.to_string())
.collect()
} else {
vec![]
}
}
}
fn print_android_bindgen_env() {
println!("cargo:rerun-if-env-changed=ANDROID_NDK_HOME");
println!("cargo:rerun-if-env-changed=ANDROID_NDK_ROOT");
println!("cargo:rerun-if-env-changed=NDK_HOME");
println!("cargo:rerun-if-env-changed=ANDROID_HOME");
println!("cargo:rerun-if-env-changed=ANDROID_SDK_ROOT");
println!("cargo:rerun-if-env-changed=CARGO_NDK_PLATFORM");
}
fn android_clang_args() -> Vec<String> {
let ndk = android_ndk_home();
let target = env::var("TARGET").unwrap_or_default();
let toolchain = ndk.join("toolchains/llvm/prebuilt").join(host_tag());
let sysroot = toolchain.join("sysroot");
let clang_include = toolchain
.join("lib/clang")
.join(clang_version(&toolchain))
.join("include");
let api = env::var("CARGO_NDK_PLATFORM")
.ok()
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or(21);
let clang_target = android_clang_target(&target);
vec![
format!("--target={clang_target}"),
format!("--sysroot={}", sysroot.display()),
format!("-D__ANDROID_API__={api}"),
format!("-isystem{}", clang_include.display()),
format!("-isystem{}", sysroot.join("usr/include").display()),
format!(
"-isystem{}",
sysroot.join("usr/include").join(clang_target).display()
),
]
}
fn android_clang_target(target: &str) -> &'static str {
match target {
"aarch64-linux-android" => "aarch64-linux-android",
"armv7-linux-androideabi" => "armv7a-linux-androideabi",
"i686-linux-android" => "i686-linux-android",
"x86_64-linux-android" => "x86_64-linux-android",
other => panic!("unsupported Android target for hwcodec bindgen: {other}"),
}
}
fn android_ndk_home() -> PathBuf {
for key in ["ANDROID_NDK_HOME", "ANDROID_NDK_ROOT", "NDK_HOME"] {
if let Ok(value) = env::var(key) {
return PathBuf::from(value);
}
}
for key in ["ANDROID_HOME", "ANDROID_SDK_ROOT"] {
if let Ok(value) = env::var(key) {
let ndk_dir = PathBuf::from(value).join("ndk");
if let Some(newest) = newest_child_dir(&ndk_dir) {
return newest;
}
}
}
panic!(
"hwcodec Android bindgen requires ANDROID_NDK_HOME, ANDROID_NDK_ROOT, NDK_HOME, \
or ANDROID_HOME/ANDROID_SDK_ROOT with an ndk directory"
);
}
fn newest_child_dir(path: &Path) -> Option<PathBuf> {
let mut entries = std::fs::read_dir(path)
.ok()?
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.is_dir())
.collect::<Vec<_>>();
entries.sort();
entries.pop()
}
fn host_tag() -> &'static str {
if cfg!(target_os = "linux") {
"linux-x86_64"
} else if cfg!(target_os = "macos") {
"darwin-x86_64"
} else if cfg!(target_os = "windows") {
"windows-x86_64"
} else {
panic!("unsupported host OS for Android NDK");
}
}
fn clang_version(toolchain: &Path) -> String {
let clang_dir = toolchain.join("lib/clang");
let mut entries = std::fs::read_dir(&clang_dir)
.unwrap_or_else(|_| panic!("missing NDK clang directory: {}", clang_dir.display()))
.filter_map(|entry| entry.ok())
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect::<Vec<_>>();
entries.sort();
entries
.pop()
.unwrap_or_else(|| panic!("no clang versions found under: {}", clang_dir.display()))
}
mod ffmpeg {
use super::*;
pub fn build_ffmpeg(builder: &mut Build) {
ffmpeg_ffi();
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android") {
link_android_ffmpeg(builder);
build_ffmpeg_ram(builder);
return;
}
// Try VCPKG first, fallback to system FFmpeg via pkg-config
if let Some(vcpkg_installed) = vcpkg_installed_root() {
link_vcpkg(builder, vcpkg_installed);
} else {
// Use system FFmpeg via pkg-config
link_system_ffmpeg(builder);
}
link_os();
build_ffmpeg_ram(builder);
build_ffmpeg_hw(builder);
build_ffmpeg_capture(builder);
}
fn link_android_ffmpeg(builder: &mut Build) {
let root = std::env::var("ONE_KVM_ANDROID_FFMPEG_ROOT").unwrap_or_else(|_| {
panic!(
"ONE_KVM_ANDROID_FFMPEG_ROOT is required when building hwcodec for Android. \
It must point to an FFmpeg Android build with MediaCodec enabled."
)
});
let root = PathBuf::from(root);
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let abi = match target_arch.as_str() {
"aarch64" => "arm64-v8a",
"arm" => "armeabi-v7a",
"x86" => "x86",
"x86_64" => "x86_64",
_ => target_arch.as_str(),
};
let abi_root = root.join(abi);
let lib_dir = if abi_root.join("lib").exists() {
abi_root.join("lib")
} else {
root.join("lib")
};
let include_dir = if abi_root.join("include").exists() {
abi_root.join("include")
} else {
root.join("include")
};
if !include_dir.exists() || !lib_dir.exists() {
panic!(
"Invalid ONE_KVM_ANDROID_FFMPEG_ROOT: include/lib not found for ABI {} under {}",
abi,
root.display()
);
}
println!("cargo:rustc-link-search=native={}", lib_dir.display());
builder.include(&include_dir);
let use_static = std::env::var("ONE_KVM_ANDROID_FFMPEG_STATIC")
.map(|value| value != "0")
.unwrap_or(true);
for lib in ["avcodec", "avutil"] {
if use_static {
println!("cargo:rustc-link-lib=static={}", lib);
} else {
println!("cargo:rustc-link-lib={}", lib);
}
}
println!("cargo:rustc-link-lib=log");
println!("cargo:rustc-link-lib=mediandk");
println!("cargo:rustc-link-lib=android");
println!("cargo:rustc-link-lib=dl");
println!("cargo:rustc-link-lib=m");
println!("cargo:rustc-link-lib=z");
println!("cargo:rustc-link-lib=c++_shared");
println!("cargo:info=Using Android FFmpeg from {}", root.display());
}
fn vcpkg_installed_root() -> Option<PathBuf> {
println!("cargo:rerun-if-env-changed=VCPKG_INSTALLED_DIR");
println!("cargo:rerun-if-env-changed=VCPKG_ROOT");
if let Ok(path) = std::env::var("VCPKG_INSTALLED_DIR") {
if !path.trim().is_empty() {
return Some(PathBuf::from(path));
}
}
std::env::var("VCPKG_ROOT")
.ok()
.filter(|path| !path.trim().is_empty())
.map(|path| PathBuf::from(path).join("installed"))
}
/// Link system FFmpeg using pkg-config or custom path
/// Supports both static and dynamic linking based on FFMPEG_STATIC env var
fn link_system_ffmpeg(builder: &mut Build) {
use std::process::Command;
// Check if static linking is requested
let use_static = std::env::var("FFMPEG_STATIC")
.map(|v| v == "1")
.unwrap_or(false);
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
// Try custom library path first:
// 1. Check ONE_KVM_LIBS_PATH environment variable (explicit override)
// 2. Fall back to architecture-based detection
let custom_lib_path = if let Ok(path) = std::env::var("ONE_KVM_LIBS_PATH") {
path
} else {
match target_arch.as_str() {
"x86_64" => "/usr/local",
"aarch64" => "/usr/aarch64-linux-gnu",
"arm" => "/usr/arm-linux-gnueabihf",
_ => "",
}
.to_string()
};
// Check if our custom FFmpeg exists
if !custom_lib_path.is_empty() {
let lib_dir = Path::new(&custom_lib_path).join("lib");
let include_dir = Path::new(&custom_lib_path).join("include");
let avcodec_lib = lib_dir.join("libavcodec.a");
if avcodec_lib.exists() {
println!("cargo:info=Using custom FFmpeg from {}", custom_lib_path);
println!("cargo:rustc-link-search=native={}", lib_dir.display());
builder.include(&include_dir);
// Static link FFmpeg core libraries
println!("cargo:rustc-link-lib=static=avcodec");
println!("cargo:rustc-link-lib=static=avutil");
// Link hardware acceleration dependencies (dynamic)
// These vary by architecture
if target_arch == "x86_64" {
// VAAPI for x86_64
println!("cargo:rustc-link-lib=va");
println!("cargo:rustc-link-lib=va-drm");
println!("cargo:rustc-link-lib=va-x11"); // Required for vaGetDisplay
println!("cargo:rustc-link-lib=mfx");
} else {
// RKMPP for ARM
println!("cargo:rustc-link-lib=rockchip_mpp");
let rga_static = lib_dir.join("librga.a");
if rga_static.exists() {
println!("cargo:rustc-link-lib=static=rga");
} else {
println!("cargo:rustc-link-lib=rga");
}
}
// Software codec dependencies (dynamic - GPL)
println!("cargo:rustc-link-lib=x264");
println!("cargo:rustc-link-lib=x265");
// VPX - check if static version exists in our custom path
let vpx_static = lib_dir.join("libvpx.a");
if vpx_static.exists() {
println!("cargo:rustc-link-lib=static=vpx");
} else {
println!("cargo:rustc-link-lib=vpx");
}
return;
}
}
// Fallback to pkg-config
// Only need libavcodec and libavutil for encoding
let libs = ["libavcodec", "libavutil"];
for lib in &libs {
// Get cflags
if let Ok(output) = Command::new("pkg-config").args(["--cflags", lib]).output() {
if output.status.success() {
let cflags = String::from_utf8_lossy(&output.stdout);
for flag in cflags.split_whitespace() {
if flag.starts_with("-I") {
builder.include(&flag[2..]);
}
}
}
}
// Get libs - use --static flag for static linking
let pkg_config_args = if use_static {
vec!["--static", "--libs", lib]
} else {
vec!["--libs", lib]
};
if let Ok(output) = Command::new("pkg-config").args(&pkg_config_args).output() {
if output.status.success() {
let libs_str = String::from_utf8_lossy(&output.stdout);
let mut link_paths: Vec<String> = Vec::new();
for flag in libs_str.split_whitespace() {
if flag.starts_with("-L") {
let path = flag[2..].to_string();
println!("cargo:rustc-link-search=native={}", path);
link_paths.push(path);
} else if flag.starts_with("-l") {
let lib_name = &flag[2..];
if use_static {
// For static linking, link FFmpeg libs statically, others dynamically
if lib_name.starts_with("av") || lib_name == "swresample" {
println!("cargo:rustc-link-lib=static={}", lib_name);
} else if lib_name == "rga"
&& link_paths
.iter()
.any(|path| Path::new(path).join("librga.a").exists())
{
println!("cargo:rustc-link-lib=static=rga");
} else {
// Runtime libraries (va, drm, etc.) must be dynamic
println!("cargo:rustc-link-lib={}", lib_name);
}
} else {
println!("cargo:rustc-link-lib={}", lib_name);
}
}
}
} else {
panic!("pkg-config failed for {}. Install FFmpeg development libraries: sudo apt install libavcodec-dev libavutil-dev", lib);
}
} else {
panic!(
"pkg-config not found. Install pkg-config and FFmpeg development libraries."
);
}
}
if use_static {
println!("cargo:info=Using system FFmpeg via pkg-config (static linking)");
} else {
println!("cargo:info=Using system FFmpeg via pkg-config (dynamic linking)");
}
}
fn link_vcpkg(builder: &mut Build, mut path: PathBuf) -> PathBuf {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
if target_arch == "x86_64" {
target_arch = "x64".to_owned();
} else if target_arch == "x86" {
target_arch = "x86".to_owned();
} else if target_arch == "loongarch64" {
target_arch = "loongarch64".to_owned();
} else if target_arch == "aarch64" {
target_arch = "arm64".to_owned();
} else {
target_arch = "arm".to_owned();
}
let mut target = if target_os == "windows" {
"x64-windows-static".to_owned()
} else {
format!("{}-{}", target_arch, target_os)
};
if target_arch == "x86" {
target = target.replace("x64", "x86");
}
println!("cargo:info={}", target);
path.push(target);
println!(
"{}",
format!(
"cargo:rustc-link-search=native={}",
path.join("lib").to_str().unwrap()
)
);
{
// avdevice/avformat are needed by the Windows DirectShow capture bridge.
let mut static_libs = vec!["avcodec", "avutil"];
if target_os == "windows" {
static_libs.extend([
"avformat",
"avdevice",
"avfilter",
"swresample",
"swscale",
"vpx",
"libx264",
"x265-static",
]);
}
for lib in static_libs {
println!("cargo:rustc-link-lib=static={}", lib);
}
if target_os == "windows" {
link_windows_qsv_lib(&path.join("lib"));
}
}
let include = path.join("include");
println!("{}", format!("cargo:include={}", include.to_str().unwrap()));
builder.include(&include);
include
}
fn link_windows_qsv_lib(lib_dir: &Path) {
if lib_dir.join("libmfx.lib").exists() {
println!("cargo:rustc-link-lib=static=libmfx");
println!("cargo:info=Using Windows QSV support library libmfx.lib");
return;
}
println!(
"cargo:warning=Windows QSV support library not found in {}",
lib_dir.display()
);
}
fn link_os() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let dyn_libs: Vec<&str> = if target_os == "windows" {
[
"User32", "bcrypt", "ole32", "advapi32", "mfuuid", "strmiids",
]
.to_vec()
} else if target_os == "linux" {
// Base libraries for all Linux platforms
let mut v = vec!["drm", "stdc++"];
// x86_64: needs X11 for VAAPI and zlib
if target_arch == "x86_64" {
v.push("X11");
v.push("z");
}
// ARM (aarch64, arm): no X11 needed, uses RKMPP/V4L2
v
} else if target_os == "android" {
Vec::new()
} else {
panic!(
"Unsupported OS: {}. Only Windows, Linux, and Android are supported.",
target_os
);
};
for lib in dyn_libs.iter() {
println!("cargo:rustc-link-lib={}", lib);
}
}
fn ffmpeg_ffi() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("common");
let ffi_header_path = ffmpeg_ram_dir.join("ffmpeg_ffi.h");
println!("cargo:rerun-if-changed={}", ffi_header_path.display());
let ffi_header = ffi_header_path.to_string_lossy().to_string();
let mut bindings = bindgen::builder()
.header(ffi_header)
.rustified_enum(".*");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android") {
print_android_bindgen_env();
bindings = bindings.clang_args(android_clang_args());
}
bindings
.generate()
.unwrap()
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_ffi.rs"))
.unwrap();
}
fn build_ffmpeg_ram(builder: &mut Build) {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("ffmpeg_ram");
let ffi_header = ffmpeg_ram_dir
.join("ffmpeg_ram_ffi.h")
.to_string_lossy()
.to_string();
let mut bindings = bindgen::builder()
.header(ffi_header)
.rustified_enum(".*");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android") {
print_android_bindgen_env();
bindings = bindings.clang_args(android_clang_args());
}
bindings
.generate()
.unwrap()
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_ram_ffi.rs"))
.unwrap();
builder.file(ffmpeg_ram_dir.join("ffmpeg_ram_encode.cpp"));
// RKMPP decode only exists on ARM builds where FFmpeg is compiled with RKMPP support.
// Avoid compiling this file on x86/x64 where `AV_HWDEVICE_TYPE_RKMPP` doesn't exist.
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let enable_rkmpp = target_os != "android"
&& matches!(target_arch.as_str(), "aarch64" | "arm")
|| std::env::var_os("CARGO_FEATURE_RKMPP").is_some();
if enable_rkmpp {
builder.file(ffmpeg_ram_dir.join("ffmpeg_ram_decode.cpp"));
} else {
println!(
"cargo:info=Skipping ffmpeg_ram_decode.cpp (RKMPP) for arch {}",
target_arch
);
}
}
fn build_ffmpeg_capture(builder: &mut Build) {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "windows" {
return;
}
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let capture_header = manifest_dir
.join("cpp")
.join("ffmpeg_capture_ffi.h")
.to_string_lossy()
.to_string();
bindgen::builder()
.header(capture_header)
.rustified_enum(".*")
.generate()
.unwrap()
.write_to_file(
Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_capture_ffi.rs"),
)
.unwrap();
builder.file(manifest_dir.join("cpp").join("ffmpeg_capture.cpp"));
println!("cargo:rustc-link-lib=strmiids");
println!("cargo:rustc-link-lib=oleaut32");
println!("cargo:rustc-link-lib=quartz");
}
fn build_ffmpeg_hw(builder: &mut Build) {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw");
let ffi_header = ffmpeg_hw_dir
.join("ffmpeg_hw_ffi.h")
.to_string_lossy()
.to_string();
bindgen::builder()
.header(ffi_header)
.rustified_enum(".*")
.generate()
.unwrap()
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_hw_ffi.rs"))
.unwrap();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let enable_rkmpp = target_os != "android"
&& matches!(target_arch.as_str(), "aarch64" | "arm")
|| std::env::var_os("CARGO_FEATURE_RKMPP").is_some();
if enable_rkmpp {
// Include RGA headers for NV16->NV12 conversion (RGA im2d API)
let rga_sys_dirs = [
Path::new("/usr/aarch64-linux-gnu/include/rga"),
Path::new("/usr/include/rga"),
];
let mut added = false;
for dir in rga_sys_dirs.iter() {
if dir.exists() {
builder.include(dir);
added = true;
}
}
if !added {
// Fallback to repo-local rkrga headers if present
let repo_root = manifest_dir
.parent()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.unwrap_or_else(|| manifest_dir.clone());
let rkrga_dir = repo_root.join("ffmpeg").join("rkrga");
if rkrga_dir.exists() {
builder.include(rkrga_dir.join("include"));
builder.include(rkrga_dir.join("im2d_api"));
}
}
builder.file(ffmpeg_hw_dir.join("ffmpeg_hw_mjpeg_h26x.cpp"));
} else {
println!(
"cargo:info=Skipping ffmpeg_hw_mjpeg_h26x.cpp (RKMPP) for arch {}",
target_arch
);
}
}
}