Skip to main content

gosuto_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 = "libwebrtc-m137";
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 = base_dir();
93    target_dir.join(format!(
94        "livekit/{}-{}/{}",
95        webrtc_triple(),
96        WEBRTC_TAG,
97        webrtc_triple()
98    ))
99}
100
101/// Returns the base directory for storing WebRTC binaries.
102/// On Windows, uses env::temp_dir() instead of scratch to keep paths shorter.
103/// The zip 2.x crate's extract() calls canonicalize() which prepends \\?\ on Windows,
104/// bypassing the 260-character MAX_PATH limit entirely.
105fn base_dir() -> path::PathBuf {
106    if cfg!(target_os = "windows") {
107        env::temp_dir().join(SCRATH_PATH)
108    } else {
109        scratch::path(SCRATH_PATH)
110    }
111}
112
113pub fn download_url() -> String {
114    format!(
115        "https://github.com/MaikBuse/gosuto-livekit-sdks/releases/download/{}/{}.zip",
116        WEBRTC_TAG,
117        format!("webrtc-{}", webrtc_triple())
118    )
119}
120
121/// Used location of libwebrtc depending on whether it's a custom build or not
122pub fn webrtc_dir() -> path::PathBuf {
123    if let Some(path) = custom_dir() {
124        return path;
125    }
126
127    prebuilt_dir()
128}
129
130pub fn webrtc_defines() -> Vec<(String, Option<String>)> {
131    // read preprocessor definitions from webrtc.ninja
132    let defines_re = Regex::new(r"-D(\w+)(?:=([^\s]+))?").unwrap();
133    let mut files = vec![webrtc_dir().join("webrtc.ninja")];
134    // include desktop_capture.ninja to avoid ABI mismatch for DesktopCaptureOptions due to WEBRTC_USE_X11 missing
135    // libwebrtc does not implement desktop capture on Android
136    if env::var("CARGO_CFG_TARGET_OS").unwrap() != "android" {
137        files.push(webrtc_dir().join("desktop_capture.ninja"));
138    }
139
140    let mut seen = HashSet::new();
141    let mut vec = Vec::new();
142
143    for path in files {
144        let gni = fs::File::open(&path)
145            .unwrap_or_else(|e| panic!("Could not open ninja file: {path:?}\n{e:?}"));
146
147        let mut defines_line = String::default();
148        io::BufReader::new(gni).read_line(&mut defines_line).unwrap();
149        for cap in defines_re.captures_iter(&defines_line) {
150            let define_name = &cap[1];
151            let define_value = cap.get(2).map(|m| m.as_str());
152            if IGNORE_DEFINES.contains(&define_name) {
153                continue;
154            }
155            let value = define_value.map(str::to_string);
156            let name = define_name.to_owned();
157            if seen.insert((name.clone(), value.clone())) {
158                vec.push((name, value));
159            }
160        }
161    }
162
163    vec
164}
165
166pub fn configure_jni_symbols() -> Result<()> {
167    download_webrtc().context("Failed to download WebRTC binaries for JNI configuration")?;
168
169    let toolchain = android_ndk_toolchain().context("Failed to locate Android NDK toolchain")?;
170    let toolchain_bin = toolchain.join("bin");
171
172    let webrtc_dir = webrtc_dir();
173    let webrtc_lib = webrtc_dir.join("lib");
174
175    let out_dir = path::PathBuf::from(env::var("OUT_DIR").unwrap());
176
177    // Find JNI symbols
178    let readelf_output = Command::new(toolchain_bin.join("llvm-readelf"))
179        .arg("-Ws")
180        .arg(webrtc_lib.join("libwebrtc.a"))
181        .output()
182        .expect("failed to run llvm-readelf");
183
184    let jni_regex = Regex::new(r"(Java_org_webrtc.*)").unwrap();
185    let content = String::from_utf8_lossy(&readelf_output.stdout);
186    let jni_symbols: Vec<&str> =
187        jni_regex.captures_iter(&content).map(|cap| cap.get(1).unwrap().as_str()).collect();
188
189    if jni_symbols.is_empty() {
190        return Err(anyhow!("No JNI symbols found")); // Shouldn't happen
191    }
192
193    // Keep JNI symbols
194    for symbol in &jni_symbols {
195        println!("cargo:rustc-link-arg=-Wl,--undefined={}", symbol);
196    }
197
198    // Version script
199    let vs_path = out_dir.join("webrtc_jni.map");
200    let mut vs_file = fs::File::create(&vs_path).context("Failed to create version script file")?;
201
202    let jni_symbols = jni_symbols.join("; ");
203    write!(vs_file, "JNI_WEBRTC {{\n\tglobal: {}; \n}};", jni_symbols)
204        .context("Failed to write version script")?;
205
206    println!("cargo:rustc-link-arg=-Wl,--version-script={}", vs_path.display());
207
208    Ok(())
209}
210
211pub fn download_webrtc() -> Result<()> {
212    let dir = base_dir();
213    fs::create_dir_all(&dir).context("Failed to create base_dir")?;
214    let flock = File::create(dir.join(".lock"))
215        .context("Failed to create lock file for WebRTC download")?;
216    flock.lock_exclusive().context("Failed to acquire exclusive lock for WebRTC download")?;
217
218    let webrtc_dir = webrtc_dir();
219    if webrtc_dir.exists() {
220        return Ok(());
221    }
222
223    let mut resp = reqwest::blocking::get(download_url())
224        .context("Failed to send HTTP request to download WebRTC")?;
225    if resp.status() != StatusCode::OK {
226        return Err(anyhow!("failed to download webrtc: {}", resp.status()));
227    }
228
229    let out_dir = env::var("OUT_DIR").unwrap();
230    let tmp_path = PathBuf::from(out_dir).join("webrtc.zip");
231    let mut file = fs::File::options()
232        .write(true)
233        .read(true)
234        .create(true)
235        .open(&tmp_path)
236        .context("Failed to create temporary file for WebRTC download")?;
237    resp.copy_to(&mut file).context("Failed to write WebRTC download to temporary file")?;
238
239    let mut archive = zip::ZipArchive::new(file).context("Failed to open WebRTC zip archive")?;
240    let extract_dir = webrtc_dir.parent().unwrap();
241    fs::create_dir_all(extract_dir).context("Failed to create extraction directory")?;
242    archive.extract(extract_dir).context("Failed to extract WebRTC archive")?;
243    drop(archive);
244
245    fs::remove_file(&tmp_path).context("Failed to remove temporary WebRTC zip file")?;
246    Ok(())
247}
248
249pub fn android_ndk_toolchain() -> Result<path::PathBuf> {
250    let host_os = host_os();
251
252    let home = env::var("HOME");
253    let local = env::var("LOCALAPPDATA");
254
255    let home = if host_os == Some("linux") {
256        path::PathBuf::from(home.unwrap())
257    } else if host_os == Some("darwin") {
258        path::PathBuf::from(home.unwrap()).join("Library")
259    } else if host_os == Some("windows") {
260        path::PathBuf::from(local.unwrap())
261    } else {
262        return Err(anyhow!("Unsupported host OS"));
263    };
264
265    let ndk_dir = || -> Option<path::PathBuf> {
266        let ndk_env = env::var("ANDROID_NDK_HOME");
267        if let Ok(ndk_env) = ndk_env {
268            return Some(path::PathBuf::from(ndk_env));
269        }
270
271        let ndk_dir = home.join("Android/sdk/ndk");
272        if !ndk_dir.exists() {
273            return None;
274        }
275
276        // Find the highest version
277        let versions = fs::read_dir(ndk_dir.clone());
278        if versions.is_err() {
279            return None;
280        }
281
282        let version = versions
283            .unwrap()
284            .filter_map(Result::ok)
285            .filter_map(|dir| dir.file_name().to_str().map(ToOwned::to_owned))
286            .filter_map(|dir| semver::Version::parse(&dir).ok())
287            .max_by(semver::Version::cmp);
288
289        version.as_ref()?;
290
291        let version = version.unwrap();
292        Some(ndk_dir.join(version.to_string()))
293    }();
294
295    if let Some(ndk_dir) = ndk_dir {
296        let llvm_dir = if host_os == Some("linux") {
297            "linux-x86_64"
298        } else if host_os == Some("darwin") {
299            "darwin-x86_64"
300        } else if host_os == Some("windows") {
301            "windows-x86_64"
302        } else {
303            return Err(anyhow!("Unsupported host OS"));
304        };
305
306        Ok(ndk_dir.join(format!("toolchains/llvm/prebuilt/{}", llvm_dir)))
307    } else {
308        Err(anyhow!("Android NDK not found, please set ANDROID_NDK_HOME to your NDK path"))
309    }
310}
311
312fn host_os() -> Option<&'static str> {
313    let host = env::var("HOST").unwrap();
314    if host.contains("darwin") {
315        Some("darwin")
316    } else if host.contains("linux") {
317        Some("linux")
318    } else if host.contains("windows") {
319        Some("windows")
320    } else {
321        None
322    }
323}