1use std::fmt::Write as _;
19use std::path::{Path, PathBuf};
20use std::sync::{Arc, OnceLock};
21
22use sha2::{Digest, Sha256};
23
24use super::ffi;
25
26pub const OPENH264_VERSION: &str = "2.7.0";
28
29pub const OPENH264_NOTICE: &str = "OpenH264 Video Codec provided by Cisco Systems, Inc.";
31
32pub const OPENH264_INSTALL_COMMAND: &str =
34 "python -m alpha2 install --openh264, or npx @alpha2-sh/node install --openh264";
35
36const COMPATIBLE_API: &[(u32, u32)] = &[(2, 6), (2, 7)];
39
40struct PinnedLibrary {
42 target: &'static str,
43 library: &'static str,
44 sha256: &'static str,
45 system_candidates: &'static [&'static str],
46}
47
48#[cfg_attr(
51 not(all(target_os = "macos", target_arch = "aarch64")),
52 allow(dead_code)
53)]
54pub(crate) const MACOS_SYSTEM_CANDIDATES: &[&str] = &[
55 "/opt/homebrew/lib/libopenh264.8.dylib",
56 "/usr/local/lib/libopenh264.8.dylib",
57 "/opt/local/lib/libopenh264.8.dylib",
58];
59
60#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
61const PINNED: Option<PinnedLibrary> = Some(PinnedLibrary {
62 target: "linux-x64",
63 library: "libopenh264-2.7.0-linux64.8.so",
64 sha256: "1522f873a482923c5ec018a4415e1866898197528828c50f90fd300c94620df3",
65 system_candidates: &["libopenh264.so.8"],
66});
67#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
68const PINNED: Option<PinnedLibrary> = Some(PinnedLibrary {
69 target: "linux-arm64",
70 library: "libopenh264-2.7.0-linux-arm64.8.so",
71 sha256: "9ac664b0154fd0868491c2a904397b8a05779874c0603b8b1f16ad15de1ffd82",
72 system_candidates: &["libopenh264.so.8"],
73});
74#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
75const PINNED: Option<PinnedLibrary> = Some(PinnedLibrary {
76 target: "macos-arm64",
77 library: "libopenh264-2.7.0-mac-arm64.dylib",
78 sha256: "2e1ae3cd10c08328ca03a1f304fd2a1a2a7e5658c6d5a8582101a963f7995ef0",
79 system_candidates: MACOS_SYSTEM_CANDIDATES,
80});
81#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
82const PINNED: Option<PinnedLibrary> = Some(PinnedLibrary {
83 target: "win-x64",
84 library: "openh264-2.7.0-win64.dll",
85 sha256: "0ef5e7f3b38753b15c13afa0418d4d7805fa228e6adfd3e425bbf758cff1c288",
86 system_candidates: &[],
87});
88#[cfg(not(any(
89 all(
90 target_os = "linux",
91 any(target_arch = "x86_64", target_arch = "aarch64")
92 ),
93 all(target_os = "macos", target_arch = "aarch64"),
94 all(target_os = "windows", target_arch = "x86_64"),
95)))]
96const PINNED: Option<PinnedLibrary> = None;
97
98#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
100#[non_exhaustive]
101pub enum OpenH264Provider {
102 Cisco,
104 System,
106}
107
108impl OpenH264Provider {
109 #[must_use]
111 pub const fn as_str(self) -> &'static str {
112 match self {
113 Self::Cisco => "cisco-openh264",
114 Self::System => "system",
115 }
116 }
117}
118
119pub struct OpenH264Library {
121 pub(crate) api: Arc<ffi::Api>,
122 provider: OpenH264Provider,
123 version: String,
124 path: String,
125}
126
127impl std::fmt::Debug for OpenH264Library {
128 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 formatter
130 .debug_struct("OpenH264Library")
131 .field("provider", &self.provider)
132 .field("version", &self.version)
133 .field("path", &self.path)
134 .finish_non_exhaustive()
135 }
136}
137
138impl OpenH264Library {
139 #[must_use]
141 pub const fn provider(&self) -> OpenH264Provider {
142 self.provider
143 }
144
145 #[must_use]
147 pub fn version(&self) -> &str {
148 &self.version
149 }
150
151 #[must_use]
153 pub fn path(&self) -> &str {
154 &self.path
155 }
156}
157
158#[derive(Clone, Debug)]
160#[non_exhaustive]
161pub enum OpenH264Status {
162 Available(Arc<OpenH264Library>),
164 Disabled,
166 NotInstalled(String),
168 Unusable(String),
170}
171
172impl OpenH264Status {
173 #[must_use]
175 pub const fn as_str(&self) -> &'static str {
176 match self {
177 Self::Available(_) => "available",
178 Self::Disabled => "disabled",
179 Self::NotInstalled(_) => "not-installed",
180 Self::Unusable(_) => "unusable",
181 }
182 }
183
184 #[must_use]
186 pub fn library(&self) -> Option<&Arc<OpenH264Library>> {
187 match self {
188 Self::Available(library) => Some(library),
189 _ => None,
190 }
191 }
192
193 #[must_use]
195 pub fn reason(&self) -> Option<String> {
196 match self {
197 Self::Available(_) => None,
198 Self::Disabled => Some("ALPHA2_OPENH264=0 turned OpenH264 off".to_owned()),
199 Self::NotInstalled(reason) | Self::Unusable(reason) => Some(reason.clone()),
200 }
201 }
202
203 #[must_use]
205 pub fn remedy(&self) -> Option<String> {
206 match self {
207 Self::Available(_) | Self::Disabled => None,
208 Self::NotInstalled(_) | Self::Unusable(_) if PINNED.is_none() => None,
209 Self::NotInstalled(_) => Some(format!(
210 "install Cisco's OpenH264 binary: {OPENH264_INSTALL_COMMAND}"
211 )),
212 Self::Unusable(_) => Some(format!(
213 "reinstall Cisco's OpenH264 binary with --force ({OPENH264_INSTALL_COMMAND} \
214 --force), or fix ALPHA2_OPENH264_LIBRARY"
215 )),
216 }
217 }
218}
219
220#[must_use]
222pub fn openh264_status() -> &'static OpenH264Status {
223 static STATUS: OnceLock<OpenH264Status> = OnceLock::new();
224 STATUS.get_or_init(|| {
225 let status = resolve(&Lookup::from_environment());
226 if let OpenH264Status::Unusable(reason) = &status {
227 alpha2_os::diagnostic!("alpha2 native: OpenH264 is not used: {reason}");
228 }
229 status
230 })
231}
232
233pub(crate) struct Lookup {
235 pub(crate) enabled: Option<String>,
237 pub(crate) library: Option<PathBuf>,
239 pub(crate) unverified: bool,
241 pub(crate) cache: Option<PathBuf>,
243 pub(crate) system: &'static [&'static str],
246}
247
248pub(crate) fn system_candidates() -> &'static [&'static str] {
250 PINNED
251 .as_ref()
252 .map_or(&[], |pinned| pinned.system_candidates)
253}
254
255impl Lookup {
256 fn from_environment() -> Self {
257 let set = |name: &str| std::env::var_os(name).filter(|value| !value.is_empty());
258 Self {
259 enabled: set("ALPHA2_OPENH264").map(|value| value.to_string_lossy().into_owned()),
260 library: set("ALPHA2_OPENH264_LIBRARY").map(PathBuf::from),
261 unverified: set("ALPHA2_OPENH264_UNVERIFIED").is_some_and(|value| value == "1"),
262 cache: host_cache_directory(),
263 system: system_candidates(),
264 }
265 }
266}
267
268pub(crate) fn host_cache_directory() -> Option<PathBuf> {
272 let variable = |name: &str| {
273 std::env::var_os(name)
274 .filter(|value| !value.to_string_lossy().trim().is_empty())
275 .map(PathBuf::from)
276 };
277 if let Some(cache) = variable("ALPHA2_HOST_CACHE_DIR") {
278 return Some(if cache.is_absolute() {
279 cache
280 } else {
281 std::env::current_dir().ok()?.join(cache)
282 });
283 }
284 if cfg!(target_os = "windows") {
285 let base = variable("LOCALAPPDATA")
286 .or_else(|| variable("USERPROFILE").map(|home| home.join("AppData").join("Local")))?;
287 return Some(base.join("alpha2"));
288 }
289 let home = variable("HOME");
290 if cfg!(target_os = "macos") {
291 return Some(home?.join("Library").join("Caches").join("alpha2"));
292 }
293 let base = variable("XDG_CACHE_HOME")
294 .filter(|path| path.is_absolute())
295 .or_else(|| home.map(|home| home.join(".cache")))?;
296 Some(base.join("alpha2"))
297}
298
299pub(crate) fn cached_library_path(cache: &Path) -> Option<PathBuf> {
301 let pinned = PINNED.as_ref()?;
302 Some(
303 cache
304 .join(format!("openh264-{OPENH264_VERSION}-{}", pinned.target))
305 .join(pinned.library),
306 )
307}
308
309pub(crate) fn resolve(lookup: &Lookup) -> OpenH264Status {
311 if lookup.enabled.as_deref() == Some("0") {
312 return OpenH264Status::Disabled;
313 }
314 if let Some(path) = &lookup.library {
315 return if lookup.unverified {
316 load(path, OpenH264Provider::System, None)
317 } else {
318 match PINNED.as_ref() {
319 Some(pinned) => load(path, OpenH264Provider::Cisco, Some(pinned.sha256)),
320 None => OpenH264Status::Unusable(format!(
321 "ALPHA2_OPENH264_LIBRARY={}: Cisco publishes no binary alpha2 knows the \
322 SHA-256 of for this platform; set ALPHA2_OPENH264_UNVERIFIED=1 to load it \
323 anyway",
324 path.display()
325 )),
326 }
327 };
328 }
329 let mut looked = Vec::new();
330 if let (Some(cache), Some(pinned)) = (&lookup.cache, PINNED.as_ref())
331 && let Some(path) = cached_library_path(cache)
332 {
333 if path.is_file() {
334 return load(&path, OpenH264Provider::Cisco, Some(pinned.sha256));
335 }
336 looked.push(path.display().to_string());
337 }
338 for name in lookup.system {
339 let path = Path::new(name);
340 let shown = if path.is_absolute() {
341 (*name).to_owned()
342 } else {
343 format!("{name} on the loader path")
344 };
345 if path.is_absolute() && !path.is_file() {
346 looked.push(shown);
347 continue;
348 }
349 match ffi::Api::load(path) {
350 Ok(api) => return checked(api, OpenH264Provider::System, name),
351 Err(ffi::LoadError::Open(_)) => looked.push(shown),
352 Err(error) => return OpenH264Status::Unusable(refusal(name, &error)),
353 }
354 }
355 OpenH264Status::NotInstalled(if PINNED.is_none() {
356 "Cisco publishes no OpenH264 binary for this platform".to_owned()
357 } else if looked.is_empty() {
358 "no OpenH264 library was found".to_owned()
359 } else {
360 format!("no OpenH264 library at {}", looked.join(" or "))
361 })
362}
363
364fn load(path: &Path, provider: OpenH264Provider, sha256: Option<&str>) -> OpenH264Status {
366 let shown = path.display().to_string();
367 if let Some(expected) = sha256 {
368 let actual = match file_sha256(path) {
369 Ok(actual) => actual,
370 Err(error) => {
371 return OpenH264Status::Unusable(format!("{shown} cannot be read: {error}"));
372 }
373 };
374 if actual != expected {
375 return OpenH264Status::Unusable(format!(
376 "{shown} is not Cisco's OpenH264 {OPENH264_VERSION} binary for this platform \
377 (SHA-256 {actual}, expected {expected})"
378 ));
379 }
380 }
381 match ffi::Api::load(path) {
382 Ok(api) => checked(api, provider, &shown),
383 Err(error) => OpenH264Status::Unusable(refusal(&shown, &error) + &quarantine_note(path)),
384 }
385}
386
387fn quarantine_note(path: &Path) -> String {
390 if !alpha2_os::quarantined(path) {
391 return String::new();
392 }
393 format!(
394 "; macOS quarantined it (a browser download), and Gatekeeper refuses an unnotarized \
395 library: remove the attribute (xattr -d com.apple.quarantine '{}') or install it with \
396 python -m alpha2 install --openh264 or npx @alpha2-sh/node install --openh264",
397 path.display()
398 )
399}
400
401fn checked(api: ffi::Api, provider: OpenH264Provider, shown: &str) -> OpenH264Status {
403 let version = api.version();
404 if !COMPATIBLE_API.contains(&(version.major, version.minor)) {
405 return OpenH264Status::Unusable(format!(
406 "{shown} is OpenH264 {}.{}.{}, and alpha2 is built against the 2.6/2.7 API",
407 version.major, version.minor, version.revision
408 ));
409 }
410 OpenH264Status::Available(Arc::new(OpenH264Library {
411 api: Arc::new(api),
412 provider,
413 version: format!("{}.{}.{}", version.major, version.minor, version.revision),
414 path: shown.to_owned(),
415 }))
416}
417
418fn refusal(shown: &str, error: &ffi::LoadError) -> String {
419 match error {
420 ffi::LoadError::Open(message) => {
421 format!("{shown} was found but is not loadable: {message}")
422 }
423 ffi::LoadError::Symbol(symbol, message) => {
424 format!("{shown} is not an OpenH264 encoder library (no {symbol}: {message})")
425 }
426 }
427}
428
429pub(crate) fn file_sha256(path: &Path) -> std::io::Result<String> {
431 use std::io::Read;
432 let mut file = std::fs::File::open(path)?;
433 let mut hasher = Sha256::new();
434 let mut buffer = vec![0_u8; 1 << 16];
435 loop {
436 let read = file.read(&mut buffer)?;
437 if read == 0 {
438 break;
439 }
440 hasher.update(&buffer[..read]);
441 }
442 Ok(hasher
443 .finalize()
444 .iter()
445 .fold(String::with_capacity(64), |mut hex, byte| {
446 let _ = write!(hex, "{byte:02x}");
447 hex
448 }))
449}