webrtc_sys_build/
lib.rs

1// Copyright 2025 LiveKit, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::path::PathBuf;
16use std::{
17    collections::HashSet,
18    env,
19    fs::{self, File},
20    io::{self, BufRead, Write},
21    path,
22    process::Command,
23};
24
25use anyhow::{anyhow, Context, Result};
26use fs2::FileExt;
27use regex::Regex;
28use reqwest::StatusCode;
29
30pub const SCRATH_PATH: &str = "livekit_webrtc";
31pub const WEBRTC_TAG: &str = "webrtc-0001d84-2";
32pub const IGNORE_DEFINES: [&str; 2] = ["CR_CLANG_REVISION", "CR_XCODE_VERSION"];
33
34pub fn target_os() -> String {
35    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
36    let target = env::var("TARGET").unwrap();
37    let is_simulator = target.ends_with("-sim");
38
39    match target_os.as_str() {
40        "windows" => "win",
41        "macos" => "mac",
42        "ios" => {
43            if is_simulator {
44                "ios-simulator"
45            } else {
46                "ios-device"
47            }
48        }
49        _ => &target_os,
50    }
51    .to_string()
52}
53
54pub fn target_arch() -> String {
55    let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
56    match target_arch.as_str() {
57        "aarch64" => "arm64",
58        "x86_64" => "x64",
59        _ => &target_arch,
60    }
61    .to_owned()
62}
63
64/// The full name of the webrtc library
65/// e.g. mac-x64-release (Same name on GH releases)
66pub fn webrtc_triple() -> String {
67    let profile = if use_debug() { "debug" } else { "release" };
68    format!("{}-{}-{}", target_os(), target_arch(), profile)
69}
70
71/// Using debug builds of webrtc is still experimental for now
72/// On Windows, Rust doesn't link against libcmtd on debug, which is an issue
73/// Default to false (even on cargo debug)
74pub fn use_debug() -> bool {
75    let var = env::var("LK_DEBUG_WEBRTC");
76    var.is_ok() && var.unwrap() == "true"
77}
78
79/// The location of the custom build is defined by the user
80pub fn custom_dir() -> Option<path::PathBuf> {
81    if let Ok(path) = env::var("LK_CUSTOM_WEBRTC") {
82        return Some(path::PathBuf::from(path));
83    }
84    None
85}
86
87/// Location of the downloaded webrtc binaries
88/// The reason why we don't use OUT_DIR is because we sometimes need to share the same binaries
89/// across multiple crates without dependencies constraints
90/// This also has the benefit of not re-downloading the binaries for each crate
91pub fn prebuilt_dir() -> path::PathBuf {
92    let target_dir = scratch::path(SCRATH_PATH);
93    path::Path::new(&target_dir).join(format!(
94        "livekit/{}-{}/{}",
95        webrtc_triple(),
96        WEBRTC_TAG,
97        webrtc_triple()
98    ))
99}
100
101pub fn download_url() -> String {
102    format!(
103        "https://github.com/livekit/client-sdk-rust/releases/download/{}/{}.zip",
104        WEBRTC_TAG,
105        format!("webrtc-{}", webrtc_triple())
106    )
107}
108
109/// Used location of libwebrtc depending on whether it's a custom build or not
110pub fn webrtc_dir() -> path::PathBuf {
111    if let Some(path) = custom_dir() {
112        return path;
113    }
114
115    prebuilt_dir()
116}
117
118pub fn webrtc_defines() -> Vec<(String, Option<String>)> {
119    // read preprocessor definitions from webrtc.ninja
120    let defines_re = Regex::new(r"-D(\w+)(?:=([^\s]+))?").unwrap();
121    let mut files = vec![webrtc_dir().join("webrtc.ninja")];
122    // include desktop_capture.ninja to avoid ABI mismatch for DesktopCaptureOptions due to WEBRTC_USE_X11 missing
123    // libwebrtc does not implement desktop capture on Android
124    if env::var("CARGO_CFG_TARGET_OS").unwrap() != "android" {
125        files.push(webrtc_dir().join("desktop_capture.ninja"));
126    }
127
128    let mut seen = HashSet::new();
129    let mut vec = Vec::new();
130
131    for path in files {
132        let gni = fs::File::open(&path)
133            .unwrap_or_else(|e| panic!("Could not open ninja file: {path:?}\n{e:?}"));
134
135        let mut defines_line = String::default();
136        io::BufReader::new(gni).read_line(&mut defines_line).unwrap();
137        for cap in defines_re.captures_iter(&defines_line) {
138            let define_name = &cap[1];
139            let define_value = cap.get(2).map(|m| m.as_str());
140            if IGNORE_DEFINES.contains(&define_name) {
141                continue;
142            }
143            let value = define_value.map(str::to_string);
144            let name = define_name.to_owned();
145            if seen.insert((name.clone(), value.clone())) {
146                vec.push((name, value));
147            }
148        }
149    }
150
151    vec
152}
153
154pub fn configure_jni_symbols() -> Result<()> {
155    download_webrtc().context("Failed to download WebRTC binaries for JNI configuration")?;
156
157    let toolchain = android_ndk_toolchain().context("Failed to locate Android NDK toolchain")?;
158    let toolchain_bin = toolchain.join("bin");
159
160    let webrtc_dir = webrtc_dir();
161    let webrtc_lib = webrtc_dir.join("lib");
162
163    let out_dir = path::PathBuf::from(env::var("OUT_DIR").unwrap());
164
165    // Find JNI symbols
166    let readelf_output = Command::new(toolchain_bin.join("llvm-readelf"))
167        .arg("-Ws")
168        .arg(webrtc_lib.join("libwebrtc.a"))
169        .output()
170        .expect("failed to run llvm-readelf");
171
172    let jni_regex = Regex::new(r"(Java_org_webrtc.*)").unwrap();
173    let content = String::from_utf8_lossy(&readelf_output.stdout);
174    let jni_symbols: Vec<&str> =
175        jni_regex.captures_iter(&content).map(|cap| cap.get(1).unwrap().as_str()).collect();
176
177    if jni_symbols.is_empty() {
178        return Err(anyhow!("No JNI symbols found")); // Shouldn't happen
179    }
180
181    // Keep JNI symbols
182    for symbol in &jni_symbols {
183        println!("cargo:rustc-link-arg=-Wl,--undefined={}", symbol);
184    }
185
186    // Version script
187    let vs_path = out_dir.join("webrtc_jni.map");
188    let mut vs_file = fs::File::create(&vs_path).context("Failed to create version script file")?;
189
190    let jni_symbols = jni_symbols.join("; ");
191    write!(vs_file, "JNI_WEBRTC {{\n\tglobal: {}; \n}};", jni_symbols)
192        .context("Failed to write version script")?;
193
194    println!("cargo:rustc-link-arg=-Wl,--version-script={}", vs_path.display());
195
196    Ok(())
197}
198
199pub fn download_webrtc() -> Result<()> {
200    let dir = scratch::path(SCRATH_PATH);
201    // temporary fix to avoid github workflow issue
202    fs::create_dir_all(&dir).context("Failed to create scratch_path")?;
203    let flock = File::create(dir.join(".lock"))
204        .context("Failed to create lock file for WebRTC download")?;
205    flock.lock_exclusive().context("Failed to acquire exclusive lock for WebRTC download")?;
206
207    let webrtc_dir = webrtc_dir();
208    if webrtc_dir.exists() {
209        return Ok(());
210    }
211
212    let mut resp = reqwest::blocking::get(download_url())
213        .context("Failed to send HTTP request to download WebRTC")?;
214    if resp.status() != StatusCode::OK {
215        return Err(anyhow!("failed to download webrtc: {}", resp.status()));
216    }
217
218    let out_dir = env::var("OUT_DIR").unwrap();
219    let tmp_path = PathBuf::from(out_dir).join("webrtc.zip");
220    let mut file = fs::File::options()
221        .write(true)
222        .read(true)
223        .create(true)
224        .open(&tmp_path)
225        .context("Failed to create temporary file for WebRTC download")?;
226    resp.copy_to(&mut file).context("Failed to write WebRTC download to temporary file")?;
227
228    let mut archive = zip::ZipArchive::new(file).context("Failed to open WebRTC zip archive")?;
229    archive.extract(webrtc_dir.parent().unwrap()).context("Failed to extract WebRTC archive")?;
230    drop(archive);
231
232    fs::remove_file(&tmp_path).context("Failed to remove temporary WebRTC zip file")?;
233    Ok(())
234}
235
236pub fn android_ndk_toolchain() -> Result<path::PathBuf> {
237    let host_os = host_os();
238
239    let home = env::var("HOME");
240    let local = env::var("LOCALAPPDATA");
241
242    let home = if host_os == Some("linux") {
243        path::PathBuf::from(home.unwrap())
244    } else if host_os == Some("darwin") {
245        path::PathBuf::from(home.unwrap()).join("Library")
246    } else if host_os == Some("windows") {
247        path::PathBuf::from(local.unwrap())
248    } else {
249        return Err(anyhow!("Unsupported host OS"));
250    };
251
252    let ndk_dir = || -> Option<path::PathBuf> {
253        let ndk_env = env::var("ANDROID_NDK_HOME");
254        if let Ok(ndk_env) = ndk_env {
255            return Some(path::PathBuf::from(ndk_env));
256        }
257
258        let ndk_dir = home.join("Android/sdk/ndk");
259        if !ndk_dir.exists() {
260            return None;
261        }
262
263        // Find the highest version
264        let versions = fs::read_dir(ndk_dir.clone());
265        if versions.is_err() {
266            return None;
267        }
268
269        let version = versions
270            .unwrap()
271            .filter_map(Result::ok)
272            .filter_map(|dir| dir.file_name().to_str().map(ToOwned::to_owned))
273            .filter_map(|dir| semver::Version::parse(&dir).ok())
274            .max_by(semver::Version::cmp);
275
276        version.as_ref()?;
277
278        let version = version.unwrap();
279        Some(ndk_dir.join(version.to_string()))
280    }();
281
282    if let Some(ndk_dir) = ndk_dir {
283        let llvm_dir = if host_os == Some("linux") {
284            "linux-x86_64"
285        } else if host_os == Some("darwin") {
286            "darwin-x86_64"
287        } else if host_os == Some("windows") {
288            "windows-x86_64"
289        } else {
290            return Err(anyhow!("Unsupported host OS"));
291        };
292
293        Ok(ndk_dir.join(format!("toolchains/llvm/prebuilt/{}", llvm_dir)))
294    } else {
295        Err(anyhow!("Android NDK not found, please set ANDROID_NDK_HOME to your NDK path"))
296    }
297}
298
299fn host_os() -> Option<&'static str> {
300    let host = env::var("HOST").unwrap();
301    if host.contains("darwin") {
302        Some("darwin")
303    } else if host.contains("linux") {
304        Some("linux")
305    } else if host.contains("windows") {
306        Some("windows")
307    } else {
308        None
309    }
310}