use std::path::PathBuf;
use std::{
collections::HashSet,
env,
fs::{self, File},
io::{self, BufRead, Write},
path,
process::Command,
};
use anyhow::{anyhow, Context, Result};
use fs2::FileExt;
use regex::Regex;
use reqwest::StatusCode;
pub const SCRATH_PATH: &str = "livekit_webrtc";
pub const WEBRTC_TAG: &str = "webrtc-24f6822-2";
pub const IGNORE_DEFINES: [&str; 2] = ["CR_CLANG_REVISION", "CR_XCODE_VERSION"];
pub fn target_os() -> String {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let target = env::var("TARGET").unwrap();
let is_simulator = target.ends_with("-sim");
match target_os.as_str() {
"windows" => "win",
"macos" => "mac",
"ios" => {
if is_simulator {
"ios-simulator"
} else {
"ios-device"
}
}
_ => &target_os,
}
.to_string()
}
pub fn target_arch() -> String {
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
match target_arch.as_str() {
"aarch64" => "arm64",
"x86_64" => "x64",
_ => &target_arch,
}
.to_owned()
}
pub fn webrtc_triple() -> String {
let profile = if use_debug() { "debug" } else { "release" };
format!("{}-{}-{}", target_os(), target_arch(), profile)
}
pub fn use_debug() -> bool {
let var = env::var("LK_DEBUG_WEBRTC");
var.is_ok() && var.unwrap() == "true"
}
pub fn custom_dir() -> Option<path::PathBuf> {
if let Ok(path) = env::var("LK_CUSTOM_WEBRTC") {
return Some(path::PathBuf::from(path));
}
None
}
pub fn prebuilt_dir() -> path::PathBuf {
let target_dir = scratch::path(SCRATH_PATH);
path::Path::new(&target_dir).join(format!(
"livekit/{}-{}/{}",
webrtc_triple(),
WEBRTC_TAG,
webrtc_triple()
))
}
pub fn download_url() -> String {
format!(
"https://github.com/livekit/rust-sdks/releases/download/{}/{}.zip",
WEBRTC_TAG,
format!("webrtc-{}", webrtc_triple())
)
}
pub fn webrtc_dir() -> path::PathBuf {
if let Some(path) = custom_dir() {
return path;
}
prebuilt_dir()
}
pub fn webrtc_defines() -> Vec<(String, Option<String>)> {
let defines_re = Regex::new(r"-D(\w+)(?:=([^\s]+))?").unwrap();
let mut files = vec![webrtc_dir().join("webrtc.ninja")];
if env::var("CARGO_CFG_TARGET_OS").unwrap() != "android" {
files.push(webrtc_dir().join("desktop_capture.ninja"));
}
let mut seen = HashSet::new();
let mut vec = Vec::new();
for path in files {
let gni = fs::File::open(&path)
.unwrap_or_else(|e| panic!("Could not open ninja file: {path:?}\n{e:?}"));
let mut defines_line = String::default();
io::BufReader::new(gni).read_line(&mut defines_line).unwrap();
for cap in defines_re.captures_iter(&defines_line) {
let define_name = &cap[1];
let define_value = cap.get(2).map(|m| m.as_str());
if IGNORE_DEFINES.contains(&define_name) {
continue;
}
let value = define_value.map(str::to_string);
let name = define_name.to_owned();
if seen.insert((name.clone(), value.clone())) {
vec.push((name, value));
}
}
}
vec
}
pub fn configure_jni_symbols() -> Result<()> {
download_webrtc().context("Failed to download WebRTC binaries for JNI configuration")?;
let toolchain = android_ndk_toolchain().context("Failed to locate Android NDK toolchain")?;
let toolchain_bin = toolchain.join("bin");
let webrtc_dir = webrtc_dir();
let webrtc_lib = webrtc_dir.join("lib");
let out_dir = path::PathBuf::from(env::var("OUT_DIR").unwrap());
let readelf_output = Command::new(toolchain_bin.join("llvm-readelf"))
.arg("-Ws")
.arg(webrtc_lib.join("libwebrtc.a"))
.output()
.expect("failed to run llvm-readelf");
let jni_regex = Regex::new(r"(Java_livekit_org_webrtc.*)").unwrap();
let content = String::from_utf8_lossy(&readelf_output.stdout);
let jni_symbols: Vec<&str> =
jni_regex.captures_iter(&content).map(|cap| cap.get(1).unwrap().as_str()).collect();
if jni_symbols.is_empty() {
return Err(anyhow!("No JNI symbols found")); }
for symbol in &jni_symbols {
println!("cargo:rustc-link-arg=-Wl,--undefined={}", symbol);
}
let vs_path = out_dir.join("webrtc_jni.map");
let mut vs_file = fs::File::create(&vs_path).context("Failed to create version script file")?;
let jni_symbols = jni_symbols.join("; ");
write!(vs_file, "JNI_WEBRTC {{\n\tglobal: {}; \n}};", jni_symbols)
.context("Failed to write version script")?;
println!("cargo:rustc-link-arg=-Wl,--version-script={}", vs_path.display());
Ok(())
}
pub fn download_webrtc() -> Result<()> {
let dir = scratch::path(SCRATH_PATH);
fs::create_dir_all(&dir).context("Failed to create scratch_path")?;
let flock = File::create(dir.join(".lock"))
.context("Failed to create lock file for WebRTC download")?;
flock.lock_exclusive().context("Failed to acquire exclusive lock for WebRTC download")?;
let webrtc_dir = webrtc_dir();
if webrtc_dir.exists() {
return Ok(());
}
let mut resp = reqwest::blocking::get(download_url())
.context("Failed to send HTTP request to download WebRTC")?;
if resp.status() != StatusCode::OK {
return Err(anyhow!("failed to download webrtc: {}", resp.status()));
}
let out_dir = env::var("OUT_DIR").unwrap();
let tmp_path = PathBuf::from(out_dir).join("webrtc.zip");
let mut file = fs::File::options()
.write(true)
.read(true)
.create(true)
.open(&tmp_path)
.context("Failed to create temporary file for WebRTC download")?;
resp.copy_to(&mut file).context("Failed to write WebRTC download to temporary file")?;
let mut archive = zip::ZipArchive::new(file).context("Failed to open WebRTC zip archive")?;
archive.extract(webrtc_dir.parent().unwrap()).context("Failed to extract WebRTC archive")?;
drop(archive);
fs::remove_file(&tmp_path).context("Failed to remove temporary WebRTC zip file")?;
Ok(())
}
pub fn android_ndk_toolchain() -> Result<path::PathBuf> {
let host_os = host_os();
let home = env::var("HOME");
let local = env::var("LOCALAPPDATA");
let home = if host_os == Some("linux") {
path::PathBuf::from(home.unwrap())
} else if host_os == Some("darwin") {
path::PathBuf::from(home.unwrap()).join("Library")
} else if host_os == Some("windows") {
path::PathBuf::from(local.unwrap())
} else {
return Err(anyhow!("Unsupported host OS"));
};
let ndk_dir = || -> Option<path::PathBuf> {
let ndk_env = env::var("ANDROID_NDK_HOME");
if let Ok(ndk_env) = ndk_env {
return Some(path::PathBuf::from(ndk_env));
}
let ndk_dir = home.join("Android/sdk/ndk");
if !ndk_dir.exists() {
return None;
}
let versions = fs::read_dir(ndk_dir.clone());
if versions.is_err() {
return None;
}
let version = versions
.unwrap()
.filter_map(Result::ok)
.filter_map(|dir| dir.file_name().to_str().map(ToOwned::to_owned))
.filter_map(|dir| semver::Version::parse(&dir).ok())
.max_by(semver::Version::cmp);
version.as_ref()?;
let version = version.unwrap();
Some(ndk_dir.join(version.to_string()))
}();
if let Some(ndk_dir) = ndk_dir {
let llvm_dir = if host_os == Some("linux") {
"linux-x86_64"
} else if host_os == Some("darwin") {
"darwin-x86_64"
} else if host_os == Some("windows") {
"windows-x86_64"
} else {
return Err(anyhow!("Unsupported host OS"));
};
Ok(ndk_dir.join(format!("toolchains/llvm/prebuilt/{}", llvm_dir)))
} else {
Err(anyhow!("Android NDK not found, please set ANDROID_NDK_HOME to your NDK path"))
}
}
fn host_os() -> Option<&'static str> {
let host = env::var("HOST").unwrap();
if host.contains("darwin") {
Some("darwin")
} else if host.contains("linux") {
Some("linux")
} else if host.contains("windows") {
Some("windows")
} else {
None
}
}