1use 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
64pub fn webrtc_triple() -> String {
67 let profile = if use_debug() { "debug" } else { "release" };
68 format!("{}-{}-{}", target_os(), target_arch(), profile)
69}
70
71pub fn use_debug() -> bool {
75 let var = env::var("LK_DEBUG_WEBRTC");
76 var.is_ok() && var.unwrap() == "true"
77}
78
79pub 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
87pub 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
109pub 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 let defines_re = Regex::new(r"-D(\w+)(?:=([^\s]+))?").unwrap();
121 let mut files = vec![webrtc_dir().join("webrtc.ninja")];
122 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 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")); }
180
181 for symbol in &jni_symbols {
183 println!("cargo:rustc-link-arg=-Wl,--undefined={}", symbol);
184 }
185
186 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 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 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}