use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::path::Path;
use std::sync::OnceLock;
use libloading::{Library, Symbol};
use tracing::{debug, warn};
use crate::error::ProbeError;
const MPV_ERROR_SUCCESS: c_int = 0;
#[repr(C)]
#[allow(dead_code)]
pub(crate) enum MpvFormat {
None = 0,
String = 1,
OsdString = 2,
Flag = 3,
Int64 = 4,
Double = 5,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub(crate) enum MpvEventId {
None = 0,
Shutdown = 1,
LogMessage = 6,
GetPropertyReply = 8,
SetPropertyReply = 9,
CommandReply = 10,
StartFile = 16,
EndFile = 17,
FileLoaded = 18,
Idle = 19, PropertyChange = 22,
}
#[repr(C)]
pub(crate) struct MpvEvent {
pub event_id: c_int,
pub error: c_int,
pub reply_userdata: u64,
pub data: *mut c_void,
}
pub(crate) struct MpvFunctions {
_lib: Library,
pub create: unsafe extern "C" fn() -> *mut c_void,
pub initialize: unsafe extern "C" fn(ctx: *mut c_void) -> c_int,
pub destroy: unsafe extern "C" fn(ctx: *mut c_void),
pub set_option_string:
unsafe extern "C" fn(ctx: *mut c_void, name: *const c_char, data: *const c_char) -> c_int,
pub command: unsafe extern "C" fn(ctx: *mut c_void, args: *mut *const c_char) -> c_int,
pub get_property_string:
unsafe extern "C" fn(ctx: *mut c_void, name: *const c_char) -> *mut c_char,
pub get_property: unsafe extern "C" fn(
ctx: *mut c_void,
name: *const c_char,
format: c_int,
data: *mut c_void,
) -> c_int,
pub free: unsafe extern "C" fn(data: *mut c_void),
pub wait_event: unsafe extern "C" fn(ctx: *mut c_void, timeout: f64) -> *mut MpvEvent,
pub error_string: unsafe extern "C" fn(error: c_int) -> *const c_char,
}
unsafe impl Send for MpvFunctions {}
unsafe impl Sync for MpvFunctions {}
static MPV_LIB: OnceLock<Result<MpvFunctions, String>> = OnceLock::new();
fn get_libmpv_path() -> Option<String> {
std::env::var("CRISPY_LIBMPV_PATH").ok()
}
fn default_lib_names() -> &'static [&'static str] {
#[cfg(target_os = "windows")]
{
&["libmpv-2.dll", "mpv-2.dll", "mpv.dll"]
}
#[cfg(target_os = "macos")]
{
&["libmpv.dylib", "libmpv.2.dylib"]
}
#[cfg(target_os = "linux")]
{
&["libmpv.so", "libmpv.so.2", "libmpv.so.1"]
}
#[cfg(target_os = "android")]
{
&["libmpv.so"]
}
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "android"
)))]
{
&["libmpv.so", "libmpv.dylib"]
}
}
unsafe fn load_lib_from_path(path: &str) -> Result<MpvFunctions, String> {
let lib = unsafe { Library::new(path) }.map_err(|e| format!("failed to load {path}: {e}"))?;
load_symbols(lib)
}
unsafe fn load_lib_default() -> Result<MpvFunctions, String> {
let names = default_lib_names();
let mut last_err = String::from("no library names to try");
for name in names {
match unsafe { Library::new(*name) } {
Ok(lib) => {
debug!(lib_name = *name, "loaded libmpv");
return load_symbols(lib);
}
Err(e) => {
last_err = format!("{name}: {e}");
debug!(lib_name = *name, error = %e, "libmpv candidate not found");
}
}
}
Err(format!(
"libmpv not found on system — last error: {last_err}"
))
}
fn load_symbols(lib: Library) -> Result<MpvFunctions, String> {
unsafe {
macro_rules! sym {
($lib:expr, $name:expr) => {
**$lib
.get::<Symbol<_>>(concat!("mpv_", $name, "\0").as_bytes())
.map_err(|e| format!("symbol mpv_{} not found: {}", $name, e))?
};
}
Ok(MpvFunctions {
create: sym!(lib, "create"),
initialize: sym!(lib, "initialize"),
destroy: sym!(lib, "destroy"),
set_option_string: sym!(lib, "set_option_string"),
command: sym!(lib, "command"),
get_property_string: sym!(lib, "get_property_string"),
get_property: sym!(lib, "get_property"),
free: sym!(lib, "free"),
wait_event: sym!(lib, "wait_event"),
error_string: sym!(lib, "error_string"),
_lib: lib,
})
}
}
pub(crate) fn get_mpv_functions() -> Result<&'static MpvFunctions, ProbeError> {
let result = MPV_LIB.get_or_init(|| {
if let Some(path) = get_libmpv_path() {
debug!(path = %path, "loading libmpv from CRISPY_LIBMPV_PATH");
if !Path::new(&path).exists() {
warn!(path = %path, "CRISPY_LIBMPV_PATH does not exist, trying system default");
return unsafe { load_lib_default() };
}
return unsafe { load_lib_from_path(&path) };
}
debug!("loading libmpv from system default");
unsafe { load_lib_default() }
});
result
.as_ref()
.map_err(|e| ProbeError::MpvUnavailable(e.clone()))
}
pub(crate) struct MpvHandle {
ctx: *mut c_void,
funcs: &'static MpvFunctions,
}
unsafe impl Send for MpvHandle {}
impl MpvHandle {
pub fn new_for_probing() -> Result<Self, ProbeError> {
let funcs = get_mpv_functions()?;
let ctx = unsafe { (funcs.create)() };
if ctx.is_null() {
return Err(ProbeError::MpvInitFailed(
"mpv_create returned null".to_string(),
));
}
let handle = Self { ctx, funcs };
handle.set_option("vid", "no")?;
handle.set_option("aid", "no")?;
handle.set_option("terminal", "no")?;
handle.set_option("msg-level", "all=error")?;
handle.set_option("demuxer-max-bytes", "1MiB")?;
handle.set_option("demuxer-readahead-secs", "2")?;
let err = unsafe { (funcs.initialize)(ctx) };
if err != MPV_ERROR_SUCCESS {
let msg = handle.error_to_string(err);
return Err(ProbeError::MpvInitFailed(msg));
}
Ok(handle)
}
pub fn new_for_screenshot() -> Result<Self, ProbeError> {
let funcs = get_mpv_functions()?;
let ctx = unsafe { (funcs.create)() };
if ctx.is_null() {
return Err(ProbeError::MpvInitFailed(
"mpv_create returned null".to_string(),
));
}
let handle = Self { ctx, funcs };
handle.set_option("vo", "null")?;
handle.set_option("ao", "null")?;
handle.set_option("terminal", "no")?;
handle.set_option("msg-level", "all=error")?;
handle.set_option("pause", "yes")?;
let err = unsafe { (funcs.initialize)(ctx) };
if err != MPV_ERROR_SUCCESS {
let msg = handle.error_to_string(err);
return Err(ProbeError::MpvInitFailed(msg));
}
Ok(handle)
}
fn set_option(&self, name: &str, value: &str) -> Result<(), ProbeError> {
let c_name = CString::new(name)
.map_err(|_| ProbeError::MpvInitFailed("invalid option name".into()))?;
let c_value = CString::new(value)
.map_err(|_| ProbeError::MpvInitFailed("invalid option value".into()))?;
let err =
unsafe { (self.funcs.set_option_string)(self.ctx, c_name.as_ptr(), c_value.as_ptr()) };
if err != MPV_ERROR_SUCCESS {
let msg = self.error_to_string(err);
warn!(option = name, value, error = %msg, "mpv set_option failed");
return Err(ProbeError::MpvCommandFailed {
command: format!("set_option({name}, {value})"),
detail: msg,
});
}
Ok(())
}
pub fn command(&self, args: &[&str]) -> Result<(), ProbeError> {
let c_args: Vec<CString> = args
.iter()
.map(|a| CString::new(*a).expect("mpv command arg contains null byte"))
.collect();
let mut ptrs: Vec<*const c_char> = c_args.iter().map(|s| s.as_ptr()).collect();
ptrs.push(std::ptr::null());
let err = unsafe { (self.funcs.command)(self.ctx, ptrs.as_mut_ptr()) };
if err != MPV_ERROR_SUCCESS {
let msg = self.error_to_string(err);
let cmd_str = args.join(" ");
return Err(ProbeError::MpvCommandFailed {
command: cmd_str,
detail: msg,
});
}
Ok(())
}
pub fn get_property_string(&self, name: &str) -> Option<String> {
let c_name = CString::new(name).ok()?;
let ptr = unsafe { (self.funcs.get_property_string)(self.ctx, c_name.as_ptr()) };
if ptr.is_null() {
return None;
}
let value = unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned();
unsafe { (self.funcs.free)(ptr.cast()) };
Some(value)
}
pub fn get_property_double(&self, name: &str) -> Option<f64> {
let c_name = CString::new(name).ok()?;
let mut value: f64 = 0.0;
let err = unsafe {
(self.funcs.get_property)(
self.ctx,
c_name.as_ptr(),
MpvFormat::Double as c_int,
(&raw mut value).cast(),
)
};
if err == MPV_ERROR_SUCCESS {
Some(value)
} else {
None
}
}
pub fn get_property_i64(&self, name: &str) -> Option<i64> {
let c_name = CString::new(name).ok()?;
let mut value: i64 = 0;
let err = unsafe {
(self.funcs.get_property)(
self.ctx,
c_name.as_ptr(),
MpvFormat::Int64 as c_int,
(&raw mut value).cast(),
)
};
if err == MPV_ERROR_SUCCESS {
Some(value)
} else {
None
}
}
pub fn wait_event(&self, timeout: f64) -> (MpvEventId, c_int) {
let ev = unsafe { (self.funcs.wait_event)(self.ctx, timeout) };
if ev.is_null() {
return (MpvEventId::None, 0);
}
let event_id = unsafe { (*ev).event_id };
let error = unsafe { (*ev).error };
let id = match event_id {
0 => MpvEventId::None,
1 => MpvEventId::Shutdown,
6 => MpvEventId::LogMessage,
8 => MpvEventId::GetPropertyReply,
9 => MpvEventId::SetPropertyReply,
10 => MpvEventId::CommandReply,
16 => MpvEventId::StartFile,
17 => MpvEventId::EndFile,
18 => MpvEventId::FileLoaded,
19 => MpvEventId::Idle,
22 => MpvEventId::PropertyChange,
_ => MpvEventId::None,
};
(id, error)
}
fn error_to_string(&self, code: c_int) -> String {
let ptr = unsafe { (self.funcs.error_string)(code) };
if ptr.is_null() {
return format!("unknown mpv error ({code})");
}
unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned()
}
}
impl Drop for MpvHandle {
fn drop(&mut self) {
if !self.ctx.is_null() {
unsafe { (self.funcs.destroy)(self.ctx) };
self.ctx = std::ptr::null_mut();
}
}
}
pub fn is_mpv_available() -> bool {
get_mpv_functions().is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_lib_names_not_empty() {
assert!(!default_lib_names().is_empty());
}
#[test]
fn get_libmpv_path_reads_env() {
let _path = get_libmpv_path();
}
#[test]
fn is_mpv_available_does_not_panic() {
let _available = is_mpv_available();
}
}