use std::ffi::{CStr, CString, c_char, c_double, c_int, c_void};
mod ffi {
use super::{c_char, c_double, c_int, c_void};
pub enum Handle {}
pub enum RenderCtx {}
pub const FORMAT_DOUBLE: c_int = 5;
pub const EVENT_SHUTDOWN: c_int = 1;
pub const EVENT_END_FILE: c_int = 7;
pub const EVENT_FILE_LOADED: c_int = 8;
pub const EVENT_SEEK: c_int = 20;
pub const EVENT_PLAYBACK_RESTART: c_int = 21;
pub const END_FILE_EOF: c_int = 0;
pub const END_FILE_ERROR: c_int = 4;
pub const RENDER_PARAM_API_TYPE: c_int = 1;
pub const RENDER_PARAM_OPENGL_INIT_PARAMS: c_int = 2;
pub const RENDER_PARAM_OPENGL_FBO: c_int = 3;
pub const RENDER_PARAM_FLIP_Y: c_int = 4;
pub const RENDER_PARAM_DRM_DRAW_SURFACE_SIZE: c_int = 15;
pub const RENDER_PARAM_DRM_DISPLAY_V2: c_int = 16;
pub const RENDER_UPDATE_FRAME: u64 = 1;
pub const API_TYPE_OPENGL: &[u8] = b"opengl\0";
#[repr(C)]
pub struct Event {
pub event_id: c_int,
pub error: c_int,
pub reply_userdata: u64,
pub data: *mut c_void,
}
#[repr(C)]
pub struct RenderParam {
pub type_: c_int,
pub data: *mut c_void,
}
pub type GetProcAddress = extern "C" fn(*mut c_void, *const c_char) -> *mut c_void;
#[repr(C)]
pub struct OpenglInitParams {
pub get_proc_address: GetProcAddress,
pub get_proc_address_ctx: *mut c_void,
}
#[repr(C)]
pub struct DrmParams {
pub fd: c_int,
pub crtc_id: c_int,
pub connector_id: c_int,
pub atomic_request_ptr: *mut *mut c_void,
pub render_fd: c_int,
}
#[repr(C)]
pub struct DrmDrawSurfaceSize {
pub width: c_int,
pub height: c_int,
}
#[repr(C)]
pub struct OpenglFbo {
pub fbo: c_int,
pub w: c_int,
pub h: c_int,
pub internal_format: c_int,
}
#[link(name = "mpv")]
unsafe extern "C" {
pub fn mpv_create() -> *mut Handle;
pub fn mpv_initialize(ctx: *mut Handle) -> c_int;
pub fn mpv_terminate_destroy(ctx: *mut Handle);
pub fn mpv_set_option_string(ctx: *mut Handle, name: *const c_char, data: *const c_char) -> c_int;
#[allow(dead_code)] pub fn mpv_set_property_string(ctx: *mut Handle, name: *const c_char, data: *const c_char) -> c_int;
pub fn mpv_get_property(ctx: *mut Handle, name: *const c_char, format: c_int, data: *mut c_void) -> c_int;
pub fn mpv_get_property_string(ctx: *mut Handle, name: *const c_char) -> *mut c_char;
pub fn mpv_free(data: *mut c_void);
pub fn mpv_command(ctx: *mut Handle, args: *const *const c_char) -> c_int;
pub fn mpv_wait_event(ctx: *mut Handle, timeout: c_double) -> *mut Event;
pub fn mpv_error_string(error: c_int) -> *const c_char;
pub fn mpv_event_name(id: c_int) -> *const c_char;
pub fn mpv_render_context_create(res: *mut *mut RenderCtx, mpv: *mut Handle, params: *const RenderParam) -> c_int;
pub fn mpv_render_context_render(ctx: *mut RenderCtx, params: *const RenderParam) -> c_int;
pub fn mpv_render_context_update(ctx: *mut RenderCtx) -> u64;
pub fn mpv_render_context_free(ctx: *mut RenderCtx);
}
}
#[derive(Debug, PartialEq)]
pub enum Event {
None,
FileLoaded,
Seek,
PlaybackRestart,
EndFile(bool),
PlaybackError,
Shutdown,
Other(String),
}
pub struct Mpv {
ctx: *mut ffi::Handle,
}
impl Mpv {
pub fn new() -> Result<Mpv, String> {
let ctx = unsafe { ffi::mpv_create() };
if ctx.is_null() {
return Err("mpv_create failed".into());
}
Ok(Mpv { ctx })
}
pub fn set_option(&self, name: &str, value: &str) -> Result<(), String> {
let (n, v) = (cstr(name), cstr(value));
check(unsafe { ffi::mpv_set_option_string(self.ctx, n.as_ptr(), v.as_ptr()) }, "set_option")
}
#[allow(dead_code)] pub fn set_property(&self, name: &str, value: &str) -> Result<(), String> {
let (n, v) = (cstr(name), cstr(value));
check(unsafe { ffi::mpv_set_property_string(self.ctx, n.as_ptr(), v.as_ptr()) }, "set_property")
}
pub fn init(&self) -> Result<(), String> {
check(unsafe { ffi::mpv_initialize(self.ctx) }, "initialize")
}
pub fn command(&self, args: &[&str]) -> Result<(), String> {
let owned: Vec<CString> = args.iter().map(|a| cstr(a)).collect();
let mut ptrs: Vec<*const c_char> = owned.iter().map(|c| c.as_ptr()).collect();
ptrs.push(std::ptr::null());
check(unsafe { ffi::mpv_command(self.ctx, ptrs.as_ptr()) }, "command")
}
pub fn loadfile(&self, url: &str) -> Result<(), String> {
self.command(&["loadfile", url])
}
pub fn loadfile_with(&self, url: &str, options: &str) -> Result<(), String> {
self.command(&["loadfile", url, "replace", "-1", options])
}
pub fn set_audio_files(&self, urls: &[&str]) -> Result<(), String> {
self.command(&["change-list", "audio-files", "clr", ""])?;
for url in urls {
self.command(&["change-list", "audio-files", "append", url])?;
}
Ok(())
}
pub fn set_sub_files(&self, urls: &[&str]) -> Result<(), String> {
self.command(&["change-list", "sub-files", "clr", ""])?;
for url in urls {
self.command(&["change-list", "sub-files", "append", url])?;
}
Ok(())
}
pub fn get_f64(&self, name: &str) -> Option<f64> {
let n = cstr(name);
let mut v: f64 = 0.0;
let r = unsafe {
ffi::mpv_get_property(self.ctx, n.as_ptr(), ffi::FORMAT_DOUBLE, &mut v as *mut f64 as *mut c_void)
};
(r >= 0).then_some(v)
}
pub fn get_string(&self, name: &str) -> Option<String> {
let n = cstr(name);
let ptr = unsafe { ffi::mpv_get_property_string(self.ctx, n.as_ptr()) };
if ptr.is_null() {
return None;
}
let s = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned();
unsafe { ffi::mpv_free(ptr as *mut c_void) };
Some(s)
}
pub fn wait_event(&self, timeout: f64) -> Event {
let ev = unsafe { &*ffi::mpv_wait_event(self.ctx, timeout) };
match ev.event_id {
0 => Event::None,
ffi::EVENT_FILE_LOADED => Event::FileLoaded,
ffi::EVENT_SEEK => Event::Seek,
ffi::EVENT_PLAYBACK_RESTART => Event::PlaybackRestart,
ffi::EVENT_END_FILE => {
if ev.data.is_null() {
Event::PlaybackError
} else {
let reason = unsafe { *(ev.data as *const c_int) };
if reason == ffi::END_FILE_ERROR { Event::PlaybackError }
else { Event::EndFile(reason == ffi::END_FILE_EOF) }
}
}
ffi::EVENT_SHUTDOWN => Event::Shutdown,
id => Event::Other(event_name(id)),
}
}
}
impl Mpv {
pub fn render_context(&self, get_proc: ffi::GetProcAddress, drm: Option<&DrmDisplay>) -> Result<RenderContext<'_>, String> {
let mut init = ffi::OpenglInitParams {
get_proc_address: get_proc,
get_proc_address_ctx: std::ptr::null_mut(),
};
let mut drm_params = ffi::DrmParams {
fd: drm.map_or(-1, |d| d.fd),
crtc_id: drm.map_or(0, |d| d.crtc_id as c_int),
connector_id: drm.map_or(0, |d| d.connector_id as c_int),
atomic_request_ptr: drm.map_or(std::ptr::null_mut(), |d| d.request),
render_fd: -1,
};
let mut draw_size = ffi::DrmDrawSurfaceSize {
width: drm.map_or(0, |d| d.draw_size.0),
height: drm.map_or(0, |d| d.draw_size.1),
};
let mut params = vec![
ffi::RenderParam {
type_: ffi::RENDER_PARAM_API_TYPE,
data: ffi::API_TYPE_OPENGL.as_ptr() as *mut c_void,
},
ffi::RenderParam {
type_: ffi::RENDER_PARAM_OPENGL_INIT_PARAMS,
data: &mut init as *mut _ as *mut c_void,
},
];
if drm.is_some() {
params.push(ffi::RenderParam {
type_: ffi::RENDER_PARAM_DRM_DISPLAY_V2,
data: &mut drm_params as *mut _ as *mut c_void,
});
params.push(ffi::RenderParam {
type_: ffi::RENDER_PARAM_DRM_DRAW_SURFACE_SIZE,
data: &mut draw_size as *mut _ as *mut c_void,
});
}
params.push(ffi::RenderParam { type_: 0, data: std::ptr::null_mut() });
let mut ctx: *mut ffi::RenderCtx = std::ptr::null_mut();
check(
unsafe { ffi::mpv_render_context_create(&mut ctx, self.ctx, params.as_ptr()) },
"render_context_create",
)?;
Ok(RenderContext { ctx, _mpv: self })
}
}
pub struct DrmDisplay {
pub fd: c_int,
pub crtc_id: u32,
pub connector_id: u32,
pub request: *mut *mut c_void,
pub draw_size: (i32, i32),
}
pub struct RenderContext<'a> {
ctx: *mut ffi::RenderCtx,
_mpv: &'a Mpv,
}
impl RenderContext<'_> {
pub fn frame_ready(&self) -> bool {
unsafe { ffi::mpv_render_context_update(self.ctx) & ffi::RENDER_UPDATE_FRAME != 0 }
}
pub fn render(&self, fbo: i32, w: i32, h: i32) -> Result<(), String> {
let mut f = ffi::OpenglFbo { fbo, w, h, internal_format: 0 };
let mut flip: c_int = 0;
let params = [
ffi::RenderParam {
type_: ffi::RENDER_PARAM_OPENGL_FBO,
data: &mut f as *mut _ as *mut c_void,
},
ffi::RenderParam {
type_: ffi::RENDER_PARAM_FLIP_Y,
data: &mut flip as *mut _ as *mut c_void,
},
ffi::RenderParam { type_: 0, data: std::ptr::null_mut() },
];
check(
unsafe { ffi::mpv_render_context_render(self.ctx, params.as_ptr()) },
"render",
)
}
}
impl Drop for RenderContext<'_> {
fn drop(&mut self) {
unsafe { ffi::mpv_render_context_free(self.ctx) };
}
}
impl Drop for Mpv {
fn drop(&mut self) {
unsafe { ffi::mpv_terminate_destroy(self.ctx) };
}
}
fn cstr(s: &str) -> CString {
CString::new(s).unwrap_or_default()
}
fn check(code: c_int, what: &str) -> Result<(), String> {
if code >= 0 {
Ok(())
} else {
let msg = unsafe { CStr::from_ptr(ffi::mpv_error_string(code)) };
Err(format!("{what}: {}", msg.to_string_lossy()))
}
}
fn event_name(id: c_int) -> String {
unsafe { CStr::from_ptr(ffi::mpv_event_name(id)) }
.to_string_lossy()
.into_owned()
}
pub fn edl_track(url: &str, lang: &str, title: &str) -> String {
let quote = |v: &str| format!("%{}%{v}", v.len());
format!("edl://!track_meta,lang={},title={};{}", quote(lang), quote(title), quote(url))
}
pub fn edl_sub_track(url: &str, lang: &str, title: &str) -> String {
let quote = |v: &str| format!("%{}%{v}", v.len());
format!(
"edl://!no_clip;!delay_open,media_type=sub,codec=webvtt;!track_meta,lang={},title={};{}",
quote(lang), quote(title), quote(url)
)
}
#[cfg(test)]
mod tests {
#[test]
fn edl_track_quotes_values_by_byte_length() {
assert_eq!(super::edl_track("https://h/v?a=1&b=2", "de", "Deutsch (Original)"),
"edl://!track_meta,lang=%2%de,title=%18%Deutsch (Original);%19%https://h/v?a=1&b=2");
assert_eq!(super::edl_track("u", "", "Ü"), "edl://!track_meta,lang=%0%,title=%2%Ü;%1%u");
}
}