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