use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use sha2::{Digest, Sha256};
use super::ffi;
pub const OPENH264_VERSION: &str = "2.7.0";
pub const OPENH264_NOTICE: &str = "OpenH264 Video Codec provided by Cisco Systems, Inc.";
pub const OPENH264_INSTALL_COMMAND: &str =
"python -m alpha2 install --openh264, or npx @alpha2-sh/node install --openh264";
const COMPATIBLE_API: &[(u32, u32)] = &[(2, 6), (2, 7)];
struct PinnedLibrary {
target: &'static str,
library: &'static str,
sha256: &'static str,
system_library: Option<&'static str>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const PINNED: Option<PinnedLibrary> = Some(PinnedLibrary {
target: "linux-x64",
library: "libopenh264-2.7.0-linux64.8.so",
sha256: "1522f873a482923c5ec018a4415e1866898197528828c50f90fd300c94620df3",
system_library: Some("libopenh264.so.8"),
});
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const PINNED: Option<PinnedLibrary> = Some(PinnedLibrary {
target: "linux-arm64",
library: "libopenh264-2.7.0-linux-arm64.8.so",
sha256: "9ac664b0154fd0868491c2a904397b8a05779874c0603b8b1f16ad15de1ffd82",
system_library: Some("libopenh264.so.8"),
});
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
const PINNED: Option<PinnedLibrary> = Some(PinnedLibrary {
target: "macos-arm64",
library: "libopenh264-2.7.0-mac-arm64.dylib",
sha256: "2e1ae3cd10c08328ca03a1f304fd2a1a2a7e5658c6d5a8582101a963f7995ef0",
system_library: Some("libopenh264.8.dylib"),
});
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
const PINNED: Option<PinnedLibrary> = Some(PinnedLibrary {
target: "win-x64",
library: "openh264-2.7.0-win64.dll",
sha256: "0ef5e7f3b38753b15c13afa0418d4d7805fa228e6adfd3e425bbf758cff1c288",
system_library: None,
});
#[cfg(not(any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "windows", target_arch = "x86_64"),
)))]
const PINNED: Option<PinnedLibrary> = None;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum OpenH264Provider {
Cisco,
System,
}
impl OpenH264Provider {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Cisco => "cisco-openh264",
Self::System => "system",
}
}
}
pub struct OpenH264Library {
pub(crate) api: Arc<ffi::Api>,
provider: OpenH264Provider,
version: String,
path: String,
}
impl std::fmt::Debug for OpenH264Library {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("OpenH264Library")
.field("provider", &self.provider)
.field("version", &self.version)
.field("path", &self.path)
.finish_non_exhaustive()
}
}
impl OpenH264Library {
#[must_use]
pub const fn provider(&self) -> OpenH264Provider {
self.provider
}
#[must_use]
pub fn version(&self) -> &str {
&self.version
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum OpenH264Status {
Available(Arc<OpenH264Library>),
Disabled,
NotInstalled(String),
Unusable(String),
}
impl OpenH264Status {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Available(_) => "available",
Self::Disabled => "disabled",
Self::NotInstalled(_) => "not-installed",
Self::Unusable(_) => "unusable",
}
}
#[must_use]
pub fn library(&self) -> Option<&Arc<OpenH264Library>> {
match self {
Self::Available(library) => Some(library),
_ => None,
}
}
#[must_use]
pub fn reason(&self) -> Option<String> {
match self {
Self::Available(_) => None,
Self::Disabled => Some("ALPHA2_OPENH264=0 turned OpenH264 off".to_owned()),
Self::NotInstalled(reason) | Self::Unusable(reason) => Some(reason.clone()),
}
}
#[must_use]
pub fn remedy(&self) -> Option<String> {
match self {
Self::Available(_) | Self::Disabled => None,
Self::NotInstalled(_) | Self::Unusable(_) if PINNED.is_none() => None,
Self::NotInstalled(_) => Some(format!(
"install Cisco's OpenH264 binary: {OPENH264_INSTALL_COMMAND}"
)),
Self::Unusable(_) => Some(format!(
"reinstall Cisco's OpenH264 binary with --force ({OPENH264_INSTALL_COMMAND} \
--force), or fix ALPHA2_OPENH264_LIBRARY"
)),
}
}
}
#[must_use]
pub fn openh264_status() -> &'static OpenH264Status {
static STATUS: OnceLock<OpenH264Status> = OnceLock::new();
STATUS.get_or_init(|| {
let status = resolve(&Lookup::from_environment());
if let OpenH264Status::Unusable(reason) = &status {
alpha2_os::diagnostic!("alpha2 native: OpenH264 is not used: {reason}");
}
status
})
}
pub(crate) struct Lookup {
pub(crate) enabled: Option<String>,
pub(crate) library: Option<PathBuf>,
pub(crate) unverified: bool,
pub(crate) cache: Option<PathBuf>,
pub(crate) system: bool,
}
impl Lookup {
fn from_environment() -> Self {
let set = |name: &str| std::env::var_os(name).filter(|value| !value.is_empty());
Self {
enabled: set("ALPHA2_OPENH264").map(|value| value.to_string_lossy().into_owned()),
library: set("ALPHA2_OPENH264_LIBRARY").map(PathBuf::from),
unverified: set("ALPHA2_OPENH264_UNVERIFIED").is_some_and(|value| value == "1"),
cache: host_cache_directory(),
system: true,
}
}
}
pub(crate) fn host_cache_directory() -> Option<PathBuf> {
let variable = |name: &str| {
std::env::var_os(name)
.filter(|value| !value.to_string_lossy().trim().is_empty())
.map(PathBuf::from)
};
if let Some(cache) = variable("ALPHA2_HOST_CACHE_DIR") {
return Some(if cache.is_absolute() {
cache
} else {
std::env::current_dir().ok()?.join(cache)
});
}
if cfg!(target_os = "windows") {
let base = variable("LOCALAPPDATA")
.or_else(|| variable("USERPROFILE").map(|home| home.join("AppData").join("Local")))?;
return Some(base.join("alpha2"));
}
let home = variable("HOME");
if cfg!(target_os = "macos") {
return Some(home?.join("Library").join("Caches").join("alpha2"));
}
let base = variable("XDG_CACHE_HOME")
.filter(|path| path.is_absolute())
.or_else(|| home.map(|home| home.join(".cache")))?;
Some(base.join("alpha2"))
}
pub(crate) fn cached_library_path(cache: &Path) -> Option<PathBuf> {
let pinned = PINNED.as_ref()?;
Some(
cache
.join(format!("openh264-{OPENH264_VERSION}-{}", pinned.target))
.join(pinned.library),
)
}
pub(crate) fn resolve(lookup: &Lookup) -> OpenH264Status {
if lookup.enabled.as_deref() == Some("0") {
return OpenH264Status::Disabled;
}
if let Some(path) = &lookup.library {
return if lookup.unverified {
load(path, OpenH264Provider::System, None)
} else {
match PINNED.as_ref() {
Some(pinned) => load(path, OpenH264Provider::Cisco, Some(pinned.sha256)),
None => OpenH264Status::Unusable(format!(
"ALPHA2_OPENH264_LIBRARY={}: Cisco publishes no binary alpha2 knows the \
SHA-256 of for this platform; set ALPHA2_OPENH264_UNVERIFIED=1 to load it \
anyway",
path.display()
)),
}
};
}
let mut looked = Vec::new();
if let (Some(cache), Some(pinned)) = (&lookup.cache, PINNED.as_ref())
&& let Some(path) = cached_library_path(cache)
{
if path.is_file() {
return load(&path, OpenH264Provider::Cisco, Some(pinned.sha256));
}
looked.push(path.display().to_string());
}
if lookup.system
&& let Some(name) = PINNED.as_ref().and_then(|pinned| pinned.system_library)
{
match ffi::Api::load(Path::new(name)) {
Ok(api) => return checked(api, OpenH264Provider::System, name),
Err(ffi::LoadError::Open(_)) => looked.push(format!("{name} on the loader path")),
Err(error) => return OpenH264Status::Unusable(refusal(name, &error)),
}
}
OpenH264Status::NotInstalled(if PINNED.is_none() {
"Cisco publishes no OpenH264 binary for this platform".to_owned()
} else if looked.is_empty() {
"no OpenH264 library was found".to_owned()
} else {
format!("no OpenH264 library at {}", looked.join(" or "))
})
}
fn load(path: &Path, provider: OpenH264Provider, sha256: Option<&str>) -> OpenH264Status {
let shown = path.display().to_string();
if let Some(expected) = sha256 {
let actual = match file_sha256(path) {
Ok(actual) => actual,
Err(error) => {
return OpenH264Status::Unusable(format!("{shown} cannot be read: {error}"));
}
};
if actual != expected {
return OpenH264Status::Unusable(format!(
"{shown} is not Cisco's OpenH264 {OPENH264_VERSION} binary for this platform \
(SHA-256 {actual}, expected {expected})"
));
}
}
match ffi::Api::load(path) {
Ok(api) => checked(api, provider, &shown),
Err(error) => OpenH264Status::Unusable(refusal(&shown, &error)),
}
}
fn checked(api: ffi::Api, provider: OpenH264Provider, shown: &str) -> OpenH264Status {
let version = api.version();
if !COMPATIBLE_API.contains(&(version.major, version.minor)) {
return OpenH264Status::Unusable(format!(
"{shown} is OpenH264 {}.{}.{}, and alpha2 is built against the 2.6/2.7 API",
version.major, version.minor, version.revision
));
}
OpenH264Status::Available(Arc::new(OpenH264Library {
api: Arc::new(api),
provider,
version: format!("{}.{}.{}", version.major, version.minor, version.revision),
path: shown.to_owned(),
}))
}
fn refusal(shown: &str, error: &ffi::LoadError) -> String {
match error {
ffi::LoadError::Open(message) => {
format!("{shown} was found but is not loadable: {message}")
}
ffi::LoadError::Symbol(symbol, message) => {
format!("{shown} is not an OpenH264 encoder library (no {symbol}: {message})")
}
}
}
pub(crate) fn file_sha256(path: &Path) -> std::io::Result<String> {
use std::io::Read;
let mut file = std::fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = vec![0_u8; 1 << 16];
loop {
let read = file.read(&mut buffer)?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(hasher
.finalize()
.iter()
.fold(String::with_capacity(64), |mut hex, byte| {
let _ = write!(hex, "{byte:02x}");
hex
}))
}