diff --git a/Cargo.lock b/Cargo.lock index 68d0b55..19781a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -881,6 +881,14 @@ dependencies = [ "gpui", ] +[[package]] +name = "clipbin-media" +version = "0.1.0" +dependencies = [ + "clipbin-core", + "ffmpeg-next", +] + [[package]] name = "clipbin-obs" version = "0.1.0" @@ -1680,6 +1688,31 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ffmpeg-next" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6380599799e175191eb7ffe82c97f36a2a90a36cbc54c738a903e5287d7f516a" +dependencies = [ + "bitflags 2.13.2", + "ffmpeg-sys-next", + "libc", +] + +[[package]] +name = "ffmpeg-sys-next" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b939bf79dd5949412a4b81cfe21a07f48ea21b47fcbb5f57816c8c2de5ae30b" +dependencies = [ + "bindgen 0.72.1", + "cc", + "libc", + "num_cpus", + "pkg-config", + "vcpkg", +] + [[package]] name = "filedescriptor" version = "0.8.3" @@ -5965,6 +5998,12 @@ dependencies = [ "sval_serde", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 4b247a3..63378d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/clipbin-core", "crates/clipbin-desktop", + "crates/clipbin-media", "crates/clipbin-obs", "crates/clipbin-probe", "crates/rig-pointer", @@ -19,6 +20,7 @@ publish = false [workspace.dependencies] clipbin-core = { path = "crates/clipbin-core" } +clipbin-media = { path = "crates/clipbin-media" } clipbin-obs = { path = "crates/clipbin-obs" } gpui = "=0.2.2" # The tested OBS set: the libobs crate's bundled headers are OBS 32.0.4, matching diff --git a/crates/clipbin-media/Cargo.toml b/crates/clipbin-media/Cargo.toml new file mode 100644 index 0000000..12934c9 --- /dev/null +++ b/crates/clipbin-media/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "clipbin-media" +description = "Reading recorded segments: clip export (remux) and playback decoding, via FFmpeg." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[dependencies] +clipbin-core.workspace = true +ffmpeg-next = { version = "9", default-features = false, features = ["codec", "format", "software-scaling", "software-resampling"] } + +[lints] +workspace = true diff --git a/crates/clipbin-media/src/lib.rs b/crates/clipbin-media/src/lib.rs new file mode 100644 index 0000000..f55d659 --- /dev/null +++ b/crates/clipbin-media/src/lib.rs @@ -0,0 +1,69 @@ +//! Reading recorded segments with FFmpeg: clip export (remuxing without +//! re-encoding) and decoding for playback. +//! +//! FFmpeg is linked dynamically: the system's libraries on Linux (the OBS +//! runtime already depends on them) and, on Windows, the FFmpeg DLLs shipped +//! in the official OBS release that scripts/obs/assemble-windows.sh bundles. + +use std::{path::Path, sync::Once, time::Duration}; + +pub use ffmpeg_next as ffmpeg; + +/// Initialises FFmpeg once per process. +pub fn init() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + let _ = ffmpeg::init(); + ffmpeg::log::set_level(ffmpeg::log::Level::Error); + }); +} + +/// Basic facts about a segment file. +#[derive(Debug, Clone, PartialEq)] +pub struct Probe { + pub duration: Duration, + pub video_codec: String, + pub width: u32, + pub height: u32, + pub audio_tracks: usize, +} + +pub fn probe(path: &Path) -> Result { + init(); + let input = ffmpeg::format::input(path)?; + let video = input + .streams() + .best(ffmpeg::media::Type::Video) + .ok_or(ffmpeg::Error::StreamNotFound)?; + let decoder = ffmpeg::codec::context::Context::from_parameters(video.parameters())? + .decoder() + .video()?; + let micros = input.duration().max(0) as u64; + Ok(Probe { + duration: Duration::from_micros(micros), + video_codec: decoder.id().name().to_owned(), + width: decoder.width(), + height: decoder.height(), + audio_tracks: input + .streams() + .filter(|stream| stream.parameters().medium() == ffmpeg::media::Type::Audio) + .count(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Set CLIPBIN_TEST_SEGMENT to a recorded segment to run this. + #[test] + fn probes_a_recorded_segment() { + let Some(path) = std::env::var_os("CLIPBIN_TEST_SEGMENT") else { + return; + }; + let probe = probe(Path::new(&path)).unwrap(); + assert!(probe.duration > Duration::ZERO); + assert!(probe.width > 0 && probe.height > 0); + println!("{probe:?}"); + } +}