clipbin-obs starts the pinned libobs on a dedicated thread, opens exactly the bundled plugin allowlist, and records official capture sources through one hardware encoder into keyframe-aligned Matroska segments. Status reaches Recording only once the source has a picture and the segment grows on disk. clipbin-probe lists and verifies hardware encoders and records from a real capture source, printing status, health and segment events for validation.
201 lines
6.3 KiB
Rust
201 lines
6.3 KiB
Rust
use std::{
|
|
env, fmt, io,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
/// Where the bundled OBS runtime's data and plugins live.
|
|
///
|
|
/// Packaged builds keep OBS's own install layout and place the Clipbin
|
|
/// executable where OBS expects its front end, because OBS finds helper
|
|
/// executables such as `obs-ffmpeg-mux` next to the running executable:
|
|
///
|
|
/// * Linux: `<root>/bin/clipbin`, `<root>/lib/obs-plugins`, `<root>/share/obs`.
|
|
/// * Windows: `<root>/bin/64bit/clipbin.exe`, `<root>/obs-plugins/64bit`, `<root>/data`.
|
|
///
|
|
/// Development builds use `CLIPBIN_OBS_RUNTIME_DIR` (see `.cargo/config.toml`),
|
|
/// the directory holding `<os>-<arch>` runtimes assembled by `scripts/obs/`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct RuntimeLayout {
|
|
root: PathBuf,
|
|
}
|
|
|
|
impl RuntimeLayout {
|
|
pub fn new(root: impl Into<PathBuf>) -> Self {
|
|
Self { root: root.into() }
|
|
}
|
|
|
|
/// Finds the runtime for this executable, preferring the development override.
|
|
pub fn discover() -> Result<Self, RuntimeError> {
|
|
// Debug builds also remember the development runtime from build time,
|
|
// so binaries in target/ work when started outside Cargo.
|
|
let dev_dir = env::var_os("CLIPBIN_OBS_RUNTIME_DIR")
|
|
.map(PathBuf::from)
|
|
.or_else(|| {
|
|
cfg!(debug_assertions)
|
|
.then(|| option_env!("CLIPBIN_OBS_RUNTIME_DIR").map(PathBuf::from))?
|
|
});
|
|
let layout = match dev_dir {
|
|
Some(dir) => Self::new(dir.join(platform_dir())),
|
|
None => {
|
|
let exe = env::current_exe().map_err(RuntimeError::Executable)?;
|
|
Self::from_executable(&exe).ok_or_else(|| RuntimeError::Layout(exe.clone()))?
|
|
}
|
|
};
|
|
layout.check()?;
|
|
Ok(layout)
|
|
}
|
|
|
|
fn from_executable(exe: &Path) -> Option<Self> {
|
|
let bin_dir = exe.parent()?;
|
|
let root = if cfg!(windows) {
|
|
bin_dir.parent()?.parent()?
|
|
} else {
|
|
bin_dir.parent()?
|
|
};
|
|
Some(Self::new(root))
|
|
}
|
|
|
|
pub fn root(&self) -> &Path {
|
|
&self.root
|
|
}
|
|
|
|
pub fn libobs_data_dir(&self) -> PathBuf {
|
|
if cfg!(windows) {
|
|
self.root.join("data").join("libobs")
|
|
} else {
|
|
self.root.join("share").join("obs").join("libobs")
|
|
}
|
|
}
|
|
|
|
pub fn plugin_bin_dir(&self) -> PathBuf {
|
|
if cfg!(windows) {
|
|
self.root.join("obs-plugins").join("64bit")
|
|
} else {
|
|
self.root.join("lib").join("obs-plugins")
|
|
}
|
|
}
|
|
|
|
pub fn plugin_data_dir(&self, plugin: &str) -> PathBuf {
|
|
let base = if cfg!(windows) {
|
|
self.root.join("data").join("obs-plugins")
|
|
} else {
|
|
self.root.join("share").join("obs").join("obs-plugins")
|
|
};
|
|
base.join(plugin)
|
|
}
|
|
|
|
pub fn plugin_library(&self, plugin: &str) -> PathBuf {
|
|
let extension = if cfg!(windows) { "dll" } else { "so" };
|
|
self.plugin_bin_dir().join(format!("{plugin}.{extension}"))
|
|
}
|
|
|
|
/// The OBS version recorded when the runtime was assembled.
|
|
pub fn version(&self) -> Option<String> {
|
|
let manifest = std::fs::read_to_string(self.root.join("VERSION")).ok()?;
|
|
manifest
|
|
.lines()
|
|
.find_map(|line| line.strip_prefix("obs="))
|
|
.map(str::to_owned)
|
|
}
|
|
|
|
fn check(&self) -> Result<(), RuntimeError> {
|
|
if !self.libobs_data_dir().is_dir() {
|
|
return Err(RuntimeError::Missing(self.libobs_data_dir()));
|
|
}
|
|
for plugin in PLUGINS {
|
|
if !self.plugin_library(plugin).is_file() {
|
|
return Err(RuntimeError::Missing(self.plugin_library(plugin)));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// The official OBS plugins Clipbin loads, and the only ones it ships.
|
|
/// Keep in sync with the allowlists in scripts/obs/.
|
|
#[cfg(windows)]
|
|
pub const PLUGINS: &[&str] = &[
|
|
"win-capture",
|
|
"win-wasapi",
|
|
"obs-ffmpeg",
|
|
"obs-nvenc",
|
|
"obs-qsv11",
|
|
];
|
|
#[cfg(not(windows))]
|
|
pub const PLUGINS: &[&str] = &[
|
|
"linux-capture",
|
|
"linux-pipewire",
|
|
"linux-pulseaudio",
|
|
"obs-ffmpeg",
|
|
"obs-nvenc",
|
|
];
|
|
|
|
/// The runtime directory name scripts/obs/ assembles for this platform, such as
|
|
/// `linux-x86_64` or `windows-x86_64`.
|
|
pub fn platform_dir() -> String {
|
|
format!("{}-{}", env::consts::OS, env::consts::ARCH)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum RuntimeError {
|
|
Executable(io::Error),
|
|
Layout(PathBuf),
|
|
Missing(PathBuf),
|
|
}
|
|
|
|
impl fmt::Display for RuntimeError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Executable(error) => write!(formatter, "cannot locate the executable: {error}"),
|
|
Self::Layout(exe) => write!(
|
|
formatter,
|
|
"{} is not inside an OBS runtime layout",
|
|
exe.display()
|
|
),
|
|
Self::Missing(dir) => write!(
|
|
formatter,
|
|
"the OBS runtime is incomplete: {} is missing (run scripts/obs/ to assemble it)",
|
|
dir.display()
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for RuntimeError {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn packaged_layout_is_relative_to_the_executable() {
|
|
let (exe, root) = if cfg!(windows) {
|
|
(r"C:\Clipbin\bin\64bit\clipbin.exe", r"C:\Clipbin")
|
|
} else {
|
|
("/opt/clipbin/bin/clipbin", "/opt/clipbin")
|
|
};
|
|
let layout = RuntimeLayout::from_executable(Path::new(exe)).unwrap();
|
|
assert_eq!(layout.root(), Path::new(root));
|
|
}
|
|
|
|
#[test]
|
|
fn plugin_paths_follow_the_obs_install_layout() {
|
|
let layout = RuntimeLayout::new("rt");
|
|
if cfg!(windows) {
|
|
assert_eq!(layout.plugin_bin_dir(), Path::new(r"rt\obs-plugins\64bit"));
|
|
assert_eq!(layout.libobs_data_dir(), Path::new(r"rt\data\libobs"));
|
|
} else {
|
|
assert_eq!(layout.plugin_bin_dir(), Path::new("rt/lib/obs-plugins"));
|
|
assert_eq!(layout.libobs_data_dir(), Path::new("rt/share/obs/libobs"));
|
|
assert_eq!(
|
|
layout.plugin_library("obs-ffmpeg"),
|
|
Path::new("rt/lib/obs-plugins/obs-ffmpeg.so")
|
|
);
|
|
assert_eq!(
|
|
layout.plugin_data_dir("obs-ffmpeg"),
|
|
Path::new("rt/share/obs/obs-plugins/obs-ffmpeg")
|
|
);
|
|
}
|
|
}
|
|
}
|