use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Once};
use parking_lot::Mutex;
use rsmpv::{EndFileReason, Event, Format, Mpv, PropertyData, sys};
use crate::error::{Error, Result, describe_code};
use crate::render::{GlRender, GlRenderOptions, ProcAddressFn, RenderBackend, SwRender};
fn ensure_c_numeric_locale() {
static ONCE: Once = Once::new();
ONCE.call_once(|| unsafe {
libc::setlocale(libc::LC_NUMERIC, c"C".as_ptr());
});
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum PlaybackEvent {
Loaded,
Ended {
reason: EndReason,
},
PlaybackRestart,
PropertyChanged {
id: ObserveId,
name: String,
value: PropertyValue,
},
Shutdown,
Failed {
code: i32,
message: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EndReason {
Eof,
Stop,
Quit,
Redirect,
Other(i32),
}
fn end_reason(reason: EndFileReason) -> EndReason {
match reason {
EndFileReason::Eof => EndReason::Eof,
EndFileReason::Stop => EndReason::Stop,
EndFileReason::Quit => EndReason::Quit,
EndFileReason::Redirect => EndReason::Redirect,
EndFileReason::Unknown(code) => EndReason::Other(code),
_ => EndReason::Other(-1),
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum PropertyValue {
Flag(bool),
Int(i64),
Double(f64),
Str(String),
}
fn property_value(data: PropertyData) -> Option<PropertyValue> {
match data {
PropertyData::Flag(v) => Some(PropertyValue::Flag(v)),
PropertyData::Int64(v) => Some(PropertyValue::Int(v)),
PropertyData::Double(v) => Some(PropertyValue::Double(v)),
PropertyData::String(s) | PropertyData::OsdString(s) => Some(PropertyValue::Str(s)),
_ => None,
}
}
impl From<bool> for PropertyValue {
fn from(v: bool) -> Self {
Self::Flag(v)
}
}
impl From<i64> for PropertyValue {
fn from(v: i64) -> Self {
Self::Int(v)
}
}
impl From<i32> for PropertyValue {
fn from(v: i32) -> Self {
Self::Int(v.into())
}
}
impl From<u32> for PropertyValue {
fn from(v: u32) -> Self {
Self::Int(v.into())
}
}
impl From<f64> for PropertyValue {
fn from(v: f64) -> Self {
Self::Double(v)
}
}
impl From<&str> for PropertyValue {
fn from(v: &str) -> Self {
Self::Str(v.to_owned())
}
}
impl From<String> for PropertyValue {
fn from(v: String) -> Self {
Self::Str(v)
}
}
mod sealed {
use crate::error::Result;
pub trait PropertyGetImpl: Sized {
fn get_property(mpv: &rsmpv::Mpv, name: &str) -> Result<Self>;
}
macro_rules! impl_property_get {
($($t:ty),*) => {$(
impl PropertyGetImpl for $t {
fn get_property(mpv: &rsmpv::Mpv, name: &str) -> Result<Self> {
Ok(mpv.get_property(name)?)
}
}
)*};
}
impl_property_get!(bool, i64, f64, String);
}
pub trait PropertyGet: sealed::PropertyGetImpl {}
impl PropertyGet for bool {}
impl PropertyGet for i64 {}
impl PropertyGet for f64 {}
impl PropertyGet for String {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropertyFormat {
Flag,
Int,
Double,
Str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObserveId(u64);
pub struct EngineBuilder {
props: Vec<(String, String)>,
}
impl EngineBuilder {
pub fn property(mut self, name: &str, value: &str) -> Self {
self.props.push((name.into(), value.into()));
self
}
pub fn build(self) -> Result<Engine> {
ensure_c_numeric_locale();
let mut builder = Mpv::builder()?;
for (name, value) in &self.props {
builder = builder.set_property(name, value.as_str())?;
}
let mpv = Arc::new(builder.build()?);
Ok(Engine {
render: Mutex::new(None),
attach: Mutex::new(()),
pump: Mutex::new(()),
next_observe_id: AtomicU64::new(1),
mpv,
})
}
}
pub struct Engine {
render: Mutex<Option<RenderBackend>>,
attach: Mutex<()>,
pump: Mutex<()>,
next_observe_id: AtomicU64,
mpv: Arc<Mpv>,
}
impl Engine {
pub fn builder() -> EngineBuilder {
EngineBuilder { props: vec![] }
}
pub fn video() -> EngineBuilder {
Self::builder()
.property("vo", "libmpv")
.property("hwdec", "auto-safe")
.property("keep-open", "always")
}
pub fn headless() -> EngineBuilder {
Self::builder()
.property("vo", "null")
.property("keep-open", "always")
}
pub fn load(&self, source: &str) -> Result<()> {
self.mpv.command(&["loadfile", source])?;
Ok(())
}
pub fn load_paused(&self, source: &str) -> Result<()> {
self.set_paused(true)?;
self.load(source)
}
pub fn command(&self, name: &str, args: &[&str]) -> Result<()> {
let mut argv = Vec::with_capacity(args.len() + 1);
argv.push(name);
argv.extend_from_slice(args);
self.mpv.command(&argv)?;
Ok(())
}
pub fn set_property(&self, name: &str, value: impl Into<PropertyValue>) -> Result<()> {
match value.into() {
PropertyValue::Flag(v) => self.mpv.set_property(name, v)?,
PropertyValue::Int(v) => self.mpv.set_property(name, v)?,
PropertyValue::Double(v) => self.mpv.set_property(name, v)?,
PropertyValue::Str(v) => self.mpv.set_property(name, v)?,
}
Ok(())
}
pub fn get_property<T: PropertyGet>(&self, name: &str) -> Result<T> {
T::get_property(&self.mpv, name)
}
pub fn set_paused(&self, paused: bool) -> Result<()> {
self.set_property("pause", paused)
}
pub fn is_paused(&self) -> bool {
self.get_property("pause").unwrap_or(false)
}
pub fn is_idle(&self) -> bool {
self.get_property("idle-active").unwrap_or(true)
}
pub fn stop(&self) -> Result<()> {
self.command("stop", &[])
}
pub fn set_volume(&self, percent: f64) -> Result<()> {
self.set_property("volume", percent)
}
pub fn volume(&self) -> Option<f64> {
self.get_property("volume").ok()
}
pub fn set_muted(&self, muted: bool) -> Result<()> {
self.set_property("mute", muted)
}
pub fn is_muted(&self) -> bool {
self.get_property("mute").unwrap_or(false)
}
pub fn set_speed(&self, speed: f64) -> Result<()> {
self.set_property("speed", speed)
}
pub fn speed(&self) -> Option<f64> {
self.get_property("speed").ok()
}
pub fn position(&self) -> Option<f64> {
self.get_property("time-pos").ok()
}
pub fn duration(&self) -> Option<f64> {
self.get_property("duration").ok()
}
pub fn seek_absolute(&self, secs: f64) -> Result<()> {
self.command("seek", &[&format!("{secs:.3}"), "absolute"])
}
pub fn seek_relative(&self, secs: f64) -> Result<()> {
self.command("seek", &[&format!("{secs:.3}"), "relative"])
}
pub fn observe(&self, name: &str, format: PropertyFormat) -> Result<ObserveId> {
let fmt = match format {
PropertyFormat::Flag => Format::Flag,
PropertyFormat::Int => Format::Int64,
PropertyFormat::Double => Format::Double,
PropertyFormat::Str => Format::String,
};
let id = self.next_observe_id.fetch_add(1, Ordering::Relaxed);
self.mpv.observe_property(id, name, fmt)?;
Ok(ObserveId(id))
}
pub fn unobserve(&self, id: ObserveId) -> Result<()> {
self.mpv.unobserve_property(id.0)?;
Ok(())
}
pub fn set_wakeup_callback(&self, on_wakeup: impl Fn() + Send + Sync + 'static) {
self.mpv.set_wakeup_callback(on_wakeup);
}
pub fn pump_events(&self) -> Vec<PlaybackEvent> {
let _guard = self.pump.lock();
let mut out = Vec::new();
while let Some(ev) = self.mpv.poll_event() {
match ev {
Event::Shutdown => out.push(PlaybackEvent::Shutdown),
Event::FileLoaded => out.push(PlaybackEvent::Loaded),
Event::EndFile {
reason: EndFileReason::Error,
error,
..
} => {
let code = error
.and_then(|e| e.raw_code())
.unwrap_or(sys::MPV_ERROR_GENERIC);
out.push(PlaybackEvent::Failed {
code,
message: describe_code(code),
});
}
Event::EndFile { reason, .. } => out.push(PlaybackEvent::Ended {
reason: end_reason(reason),
}),
Event::PlaybackRestart => out.push(PlaybackEvent::PlaybackRestart),
Event::PropertyChange {
userdata,
name,
data,
} => {
if let Some(value) = property_value(data) {
out.push(PlaybackEvent::PropertyChanged {
id: ObserveId(userdata),
name,
value,
});
}
}
_ => {}
}
}
out
}
pub unsafe fn attach_gl_render(
&self,
get_proc_address: ProcAddressFn,
options: GlRenderOptions,
on_update: impl Fn() + Send + Sync + 'static,
) -> Result<()> {
let _attaching = self.attach.lock();
if self.render.lock().is_some() {
return Err(Error::AlreadyAttached);
}
let render =
unsafe { GlRender::create(self.mpv.clone(), get_proc_address, options, on_update)? };
*self.render.lock() = Some(RenderBackend::Gl(render));
tracing::debug!("mpv GL render context attached");
Ok(())
}
pub fn attach_sw_render(&self, on_update: impl Fn() + Send + Sync + 'static) -> Result<()> {
let _attaching = self.attach.lock();
if self.render.lock().is_some() {
return Err(Error::AlreadyAttached);
}
let render = SwRender::create(self.mpv.clone(), on_update)?;
*self.render.lock() = Some(RenderBackend::Sw(render));
tracing::debug!("mpv software render context attached");
Ok(())
}
pub fn detach_render(&self) {
if self.render.lock().take().is_some() {
tracing::debug!("mpv render context detached");
}
}
pub fn has_render(&self) -> bool {
self.render.lock().is_some()
}
pub fn render_update(&self) -> bool {
self.render
.lock()
.as_mut()
.is_some_and(RenderBackend::update)
}
pub fn render_gl(&self, fbo: i32, w: i32, h: i32, flip_y: bool) -> Result<()> {
match self.render.lock().as_mut() {
Some(RenderBackend::Gl(r)) => r.render(fbo, w, h, flip_y),
Some(_) => Err(Error::RenderBackendMismatch),
None => Ok(()),
}
}
pub fn render_sw(&self, w: i32, h: i32, buf: &mut Vec<u8>) -> Result<()> {
match self.render.lock().as_mut() {
Some(RenderBackend::Sw(r)) => r.render(w, h, buf),
Some(_) => Err(Error::RenderBackendMismatch),
None => Ok(()),
}
}
}