about summary refs log tree commit diff stats
path: root/libmpv2/src/mpv.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-21 10:49:23 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-21 11:28:43 +0200
commit1debeb77f7986de1b659dcfdc442de6415e1d9f5 (patch)
tree4df3e7c3f6a2d1ec116e4088c5ace7f143a8b05f /libmpv2/src/mpv.rs
downloadyt-1debeb77f7986de1b659dcfdc442de6415e1d9f5.tar.gz
yt-1debeb77f7986de1b659dcfdc442de6415e1d9f5.zip
chore: Initial Commit
This repository was migrated out of my nixos-config.
Diffstat (limited to 'libmpv2/src/mpv.rs')
-rw-r--r--libmpv2/src/mpv.rs620
1 files changed, 620 insertions, 0 deletions
diff --git a/libmpv2/src/mpv.rs b/libmpv2/src/mpv.rs
new file mode 100644
index 0000000..9d554a6
--- /dev/null
+++ b/libmpv2/src/mpv.rs
@@ -0,0 +1,620 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+macro_rules! mpv_cstr_to_str {
+    ($cstr: expr) => {
+        std::ffi::CStr::from_ptr($cstr)
+            .to_str()
+            .map_err(Error::from)
+    };
+}
+
+mod errors;
+
+/// Event handling
+pub mod events;
+/// Custom protocols (`protocol://$url`) for playback
+#[cfg(feature = "protocols")]
+pub mod protocol;
+/// Custom rendering
+#[cfg(feature = "render")]
+pub mod render;
+
+use log::debug;
+
+pub use self::errors::*;
+use self::events::EventContext;
+use super::*;
+
+use std::{
+    ffi::CString,
+    mem::MaybeUninit,
+    ops::Deref,
+    ptr::{self, NonNull},
+    sync::atomic::AtomicBool,
+};
+
+fn mpv_err<T>(ret: T, err: ctype::c_int) -> Result<T> {
+    if err == 0 {
+        Ok(ret)
+    } else {
+        // debug!("Creating a raw error: {}", to_string_mpv_error(err));
+        Err(Error::Raw(err))
+    }
+}
+
+/// This trait describes which types are allowed to be passed to getter mpv APIs.
+pub unsafe trait GetData: Sized {
+    #[doc(hidden)]
+    fn get_from_c_void<T, F: FnMut(*mut ctype::c_void) -> Result<T>>(mut fun: F) -> Result<Self> {
+        let mut val = MaybeUninit::uninit();
+        let _ = fun(val.as_mut_ptr() as *mut _)?;
+        Ok(unsafe { val.assume_init() })
+    }
+    fn get_format() -> Format;
+}
+
+/// This trait describes which types are allowed to be passed to setter mpv APIs.
+pub unsafe trait SetData: Sized {
+    #[doc(hidden)]
+    fn call_as_c_void<T, F: FnMut(*mut ctype::c_void) -> Result<T>>(
+        mut self,
+        mut fun: F,
+    ) -> Result<T> {
+        fun(&mut self as *mut Self as _)
+    }
+    fn get_format() -> Format;
+}
+
+unsafe impl GetData for f64 {
+    fn get_format() -> Format {
+        Format::Double
+    }
+}
+
+unsafe impl SetData for f64 {
+    fn get_format() -> Format {
+        Format::Double
+    }
+}
+
+unsafe impl GetData for i64 {
+    fn get_format() -> Format {
+        Format::Int64
+    }
+}
+
+pub mod mpv_node {
+    use self::sys_node::SysMpvNode;
+    use crate::{Error, Format, GetData, Result};
+    use std::{mem::MaybeUninit, os::raw::c_void, ptr};
+
+    #[derive(Debug, Clone)]
+    pub enum MpvNode {
+        String(String),
+        Flag(bool),
+        Int64(i64),
+        Double(f64),
+        ArrayIter(MpvNodeArrayIter),
+        MapIter(MpvNodeMapIter),
+        None,
+    }
+
+    impl MpvNode {
+        pub fn bool(&self) -> Option<bool> {
+            if let MpvNode::Flag(value) = *self {
+                Some(value)
+            } else {
+                None
+            }
+        }
+        pub fn i64(&self) -> Option<i64> {
+            if let MpvNode::Int64(value) = *self {
+                Some(value)
+            } else {
+                None
+            }
+        }
+        pub fn f64(&self) -> Option<f64> {
+            if let MpvNode::Double(value) = *self {
+                Some(value)
+            } else {
+                None
+            }
+        }
+
+        pub fn str(&self) -> Option<&str> {
+            if let MpvNode::String(value) = self {
+                Some(value)
+            } else {
+                None
+            }
+        }
+
+        pub fn array(self) -> Option<MpvNodeArrayIter> {
+            if let MpvNode::ArrayIter(value) = self {
+                Some(value)
+            } else {
+                None
+            }
+        }
+
+        pub fn map(self) -> Option<MpvNodeMapIter> {
+            if let MpvNode::MapIter(value) = self {
+                Some(value)
+            } else {
+                None
+            }
+        }
+    }
+
+    impl PartialEq for MpvNode {
+        fn eq(&self, other: &Self) -> bool {
+            match (self, other) {
+                (Self::String(l0), Self::String(r0)) => l0 == r0,
+                (Self::Flag(l0), Self::Flag(r0)) => l0 == r0,
+                (Self::Int64(l0), Self::Int64(r0)) => l0 == r0,
+                (Self::Double(l0), Self::Double(r0)) => l0 == r0,
+                (Self::ArrayIter(l0), Self::ArrayIter(r0)) => l0.clone().eq(r0.clone()),
+                (Self::MapIter(l0), Self::MapIter(r0)) => l0.clone().eq(r0.clone()),
+                _ => core::mem::discriminant(self) == core::mem::discriminant(other),
+            }
+        }
+    }
+
+    #[derive(Debug)]
+    struct DropWrapper(libmpv2_sys::mpv_node);
+
+    impl Drop for DropWrapper {
+        fn drop(&mut self) {
+            unsafe {
+                libmpv2_sys::mpv_free_node_contents(&mut self.0 as *mut libmpv2_sys::mpv_node)
+            };
+        }
+    }
+
+    pub mod sys_node {
+        use super::{DropWrapper, MpvNode, MpvNodeArrayIter, MpvNodeMapIter};
+        use crate::{mpv_error, mpv_format, Error, Result};
+        use std::rc::Rc;
+
+        #[derive(Debug, Clone)]
+        pub struct SysMpvNode {
+            // Reference counted pointer to a parent node so it stays alive long enough.
+            //
+            // MPV has one big cleanup function that takes a node so store the parent node
+            // and force it to stay alive until the reference count hits 0.
+            parent: Option<Rc<DropWrapper>>,
+            node: libmpv2_sys::mpv_node,
+        }
+
+        impl SysMpvNode {
+            pub fn new(node: libmpv2_sys::mpv_node, drop: bool) -> Self {
+                Self {
+                    parent: if drop {
+                        Some(Rc::new(DropWrapper(node)))
+                    } else {
+                        None
+                    },
+                    node,
+                }
+            }
+
+            pub fn child(self: Self, node: libmpv2_sys::mpv_node) -> Self {
+                Self {
+                    parent: self.parent,
+                    node,
+                }
+            }
+
+            pub fn value(&self) -> Result<MpvNode> {
+                let node = self.node;
+                Ok(match node.format {
+                    mpv_format::Flag => MpvNode::Flag(unsafe { node.u.flag } == 1),
+                    mpv_format::Int64 => MpvNode::Int64(unsafe { node.u.int64 }),
+                    mpv_format::Double => MpvNode::Double(unsafe { node.u.double_ }),
+                    mpv_format::String => {
+                        let text = unsafe { mpv_cstr_to_str!(node.u.string) }?.to_owned();
+                        MpvNode::String(text)
+                    }
+                    mpv_format::Array => {
+                        let list = unsafe { *node.u.list };
+                        let iter = MpvNodeArrayIter {
+                            node: self.clone(),
+                            start: unsafe { *node.u.list }.values,
+                            end: unsafe { list.values.offset(list.num.try_into().unwrap()) },
+                        };
+                        return Ok(MpvNode::ArrayIter(iter));
+                    }
+
+                    mpv_format::Map => MpvNode::MapIter(MpvNodeMapIter {
+                        list: unsafe { *node.u.list },
+                        curr: 0,
+                        node: self.clone(),
+                    }),
+                    mpv_format::None => MpvNode::None,
+                    _ => return Err(Error::Raw(mpv_error::PropertyError)),
+                })
+            }
+        }
+    }
+
+    #[derive(Debug, Clone)]
+    pub struct MpvNodeArrayIter {
+        // Reference counted pointer to a parent node so it stays alive long enough.
+        //
+        // MPV has one big cleanup function that takes a node so store the parent node
+        // and force it to stay alive until the reference count hits 0.
+        node: SysMpvNode,
+        start: *const libmpv2_sys::mpv_node,
+        end: *const libmpv2_sys::mpv_node,
+    }
+
+    impl Iterator for MpvNodeArrayIter {
+        type Item = MpvNode;
+
+        fn next(&mut self) -> Option<Self::Item> {
+            if self.start == self.end {
+                None
+            } else {
+                unsafe {
+                    let result = ptr::read(self.start);
+                    let node = SysMpvNode::child(self.node.clone(), result);
+                    self.start = self.start.offset(1);
+                    node.value().ok()
+                }
+            }
+        }
+    }
+
+    #[derive(Debug, Clone)]
+    pub struct MpvNodeMapIter {
+        // Reference counted pointer to a parent node so it stays alive long enough.
+        //
+        // MPV has one big cleanup function that takes a node so store the parent node
+        // and force it to stay alive until the reference count hits 0.
+        node: SysMpvNode,
+        list: libmpv2_sys::mpv_node_list,
+        curr: usize,
+    }
+
+    impl Iterator for MpvNodeMapIter {
+        type Item = (String, MpvNode);
+
+        fn next(&mut self) -> Option<Self::Item> {
+            if self.curr >= self.list.num.try_into().unwrap() {
+                None
+            } else {
+                let offset = self.curr.try_into().unwrap();
+                let (key, value) = unsafe {
+                    (
+                        mpv_cstr_to_str!(*self.list.keys.offset(offset)),
+                        *self.list.values.offset(offset),
+                    )
+                };
+                self.curr += 1;
+                let node = SysMpvNode::child(self.node.clone(), value);
+                Some((key.unwrap().to_string(), node.value().unwrap()))
+            }
+        }
+    }
+
+    unsafe impl GetData for MpvNode {
+        fn get_from_c_void<T, F: FnMut(*mut c_void) -> Result<T>>(mut fun: F) -> Result<Self> {
+            let mut val = MaybeUninit::uninit();
+            fun(val.as_mut_ptr() as *mut _)?;
+            let sys_node = unsafe { val.assume_init() };
+            let node = SysMpvNode::new(sys_node, true);
+            node.value()
+        }
+
+        fn get_format() -> Format {
+            Format::Node
+        }
+    }
+}
+
+unsafe impl SetData for i64 {
+    fn get_format() -> Format {
+        Format::Int64
+    }
+}
+
+unsafe impl GetData for bool {
+    fn get_format() -> Format {
+        Format::Flag
+    }
+}
+
+unsafe impl SetData for bool {
+    fn call_as_c_void<T, F: FnMut(*mut ctype::c_void) -> Result<T>>(self, mut fun: F) -> Result<T> {
+        let mut cpy: i64 = if self { 1 } else { 0 };
+        fun(&mut cpy as *mut i64 as *mut _)
+    }
+
+    fn get_format() -> Format {
+        Format::Flag
+    }
+}
+
+unsafe impl GetData for String {
+    fn get_from_c_void<T, F: FnMut(*mut ctype::c_void) -> Result<T>>(mut fun: F) -> Result<String> {
+        let ptr = &mut ptr::null();
+        fun(ptr as *mut *const ctype::c_char as _)?;
+
+        let ret = unsafe { mpv_cstr_to_str!(*ptr) }?.to_owned();
+        unsafe { libmpv2_sys::mpv_free(*ptr as *mut _) };
+        Ok(ret)
+    }
+
+    fn get_format() -> Format {
+        Format::String
+    }
+}
+
+unsafe impl SetData for String {
+    fn call_as_c_void<T, F: FnMut(*mut ctype::c_void) -> Result<T>>(self, mut fun: F) -> Result<T> {
+        let string = CString::new(self)?;
+        fun((&mut string.as_ptr()) as *mut *const ctype::c_char as *mut _)
+    }
+
+    fn get_format() -> Format {
+        Format::String
+    }
+}
+
+/// Wrapper around an `&str` returned by mpv, that properly deallocates it with mpv's allocator.
+#[derive(Debug, Hash, Eq, PartialEq)]
+pub struct MpvStr<'a>(&'a str);
+impl<'a> Deref for MpvStr<'a> {
+    type Target = str;
+
+    fn deref(&self) -> &str {
+        self.0
+    }
+}
+impl<'a> Drop for MpvStr<'a> {
+    fn drop(&mut self) {
+        unsafe { libmpv2_sys::mpv_free(self.0.as_ptr() as *mut u8 as _) };
+    }
+}
+
+unsafe impl<'a> GetData for MpvStr<'a> {
+    fn get_from_c_void<T, F: FnMut(*mut ctype::c_void) -> Result<T>>(
+        mut fun: F,
+    ) -> Result<MpvStr<'a>> {
+        let ptr = &mut ptr::null();
+        let _ = fun(ptr as *mut *const ctype::c_char as _)?;
+
+        Ok(MpvStr(unsafe { mpv_cstr_to_str!(*ptr) }?))
+    }
+
+    fn get_format() -> Format {
+        Format::String
+    }
+}
+
+unsafe impl<'a> SetData for &'a str {
+    fn call_as_c_void<T, F: FnMut(*mut ctype::c_void) -> Result<T>>(self, mut fun: F) -> Result<T> {
+        let string = CString::new(self)?;
+        fun((&mut string.as_ptr()) as *mut *const ctype::c_char as *mut _)
+    }
+
+    fn get_format() -> Format {
+        Format::String
+    }
+}
+
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+/// Subset of `mpv_format` used by the public API.
+pub enum Format {
+    String,
+    Flag,
+    Int64,
+    Double,
+    Node,
+}
+
+impl Format {
+    fn as_mpv_format(&self) -> MpvFormat {
+        match *self {
+            Format::String => mpv_format::String,
+            Format::Flag => mpv_format::Flag,
+            Format::Int64 => mpv_format::Int64,
+            Format::Double => mpv_format::Double,
+            Format::Node => mpv_format::Node,
+        }
+    }
+}
+
+/// Context passed to the `initializer` of `Mpv::with_initialzer`.
+pub struct MpvInitializer {
+    ctx: *mut libmpv2_sys::mpv_handle,
+}
+
+impl MpvInitializer {
+    /// Set the value of a property.
+    pub fn set_property<T: SetData>(&self, name: &str, data: T) -> Result<()> {
+        let name = CString::new(name)?;
+        let format = T::get_format().as_mpv_format() as _;
+        data.call_as_c_void(|ptr| {
+            mpv_err((), unsafe {
+                libmpv2_sys::mpv_set_property(self.ctx, name.as_ptr(), format, ptr)
+            })
+        })
+    }
+
+    /// Set the value of an option
+    pub fn set_option<T: SetData>(&self, name: &str, data: T) -> Result<()> {
+        let name = CString::new(name)?;
+        let format = T::get_format().as_mpv_format() as _;
+        data.call_as_c_void(|ptr| {
+            mpv_err((), unsafe {
+                libmpv2_sys::mpv_set_option(self.ctx, name.as_ptr(), format, ptr)
+            })
+        })
+    }
+}
+
+/// The central mpv context.
+pub struct Mpv {
+    /// The handle to the mpv core
+    pub ctx: NonNull<libmpv2_sys::mpv_handle>,
+    event_context: EventContext,
+    #[cfg(feature = "protocols")]
+    protocols_guard: AtomicBool,
+}
+
+unsafe impl Send for Mpv {}
+unsafe impl Sync for Mpv {}
+
+impl Drop for Mpv {
+    fn drop(&mut self) {
+        unsafe {
+            libmpv2_sys::mpv_terminate_destroy(self.ctx.as_ptr());
+        }
+    }
+}
+
+impl Mpv {
+    /// Create a new `Mpv`.
+    /// The default settings can be probed by running: `$ mpv --show-profile=libmpv`.
+    pub fn new() -> Result<Mpv> {
+        Mpv::with_initializer(|_| Ok(()))
+    }
+
+    /// Create a new `Mpv`.
+    /// The same as `Mpv::new`, but you can set properties before `Mpv` is initialized.
+    pub fn with_initializer<F: FnOnce(MpvInitializer) -> Result<()>>(
+        initializer: F,
+    ) -> Result<Mpv> {
+        let api_version = unsafe { libmpv2_sys::mpv_client_api_version() };
+        if crate::MPV_CLIENT_API_MAJOR != api_version >> 16 {
+            return Err(Error::VersionMismatch {
+                linked: crate::MPV_CLIENT_API_VERSION,
+                loaded: api_version,
+            });
+        }
+
+        let ctx = unsafe { libmpv2_sys::mpv_create() };
+        if ctx.is_null() {
+            return Err(Error::Null);
+        }
+
+        initializer(MpvInitializer { ctx })?;
+        mpv_err((), unsafe { libmpv2_sys::mpv_initialize(ctx) }).map_err(|err| {
+            unsafe { libmpv2_sys::mpv_terminate_destroy(ctx) };
+            err
+        })?;
+
+        let ctx = unsafe { NonNull::new_unchecked(ctx) };
+
+        Ok(Mpv {
+            ctx,
+            event_context: EventContext::new(ctx),
+            #[cfg(feature = "protocols")]
+            protocols_guard: AtomicBool::new(false),
+        })
+    }
+
+    /// Execute a command
+    pub fn execute(&self, name: &str, args: &[&str]) -> Result<()> {
+        if args.is_empty() {
+            debug!("Running mpv command: '{}'", name);
+        } else {
+            debug!("Running mpv command: '{} {}'", name, args.join(" "));
+        }
+
+        self.command(name, args)?;
+
+        Ok(())
+    }
+
+    /// Load a configuration file. The path has to be absolute, and a file.
+    pub fn load_config(&self, path: &str) -> Result<()> {
+        let file = CString::new(path)?.into_raw();
+        let ret = mpv_err((), unsafe {
+            libmpv2_sys::mpv_load_config_file(self.ctx.as_ptr(), file)
+        });
+        unsafe {
+            drop(CString::from_raw(file));
+        };
+        ret
+    }
+
+    pub fn event_context(&self) -> &EventContext {
+        &self.event_context
+    }
+
+    pub fn event_context_mut(&mut self) -> &mut EventContext {
+        &mut self.event_context
+    }
+
+    /// Send a command to the `Mpv` instance. This uses `mpv_command_string` internally,
+    /// so that the syntax is the same as described in the [manual for the input.conf](https://mpv.io/manual/master/#list-of-input-commands).
+    ///
+    /// Note that you may have to escape strings with `""` when they contain spaces.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use libmpv2::{Mpv};
+    /// # use libmpv2::mpv_node::MpvNode;
+    /// # use std::collections::HashMap;
+    /// mpv.command("loadfile", &["test-data/jellyfish.mp4", "append-play"]).unwrap();
+    /// # let node = mpv.get_property::<MpvNode>("playlist").unwrap();
+    /// # let mut list = node.array().unwrap().collect::<Vec<_>>();
+    /// # let map = list.pop().unwrap().map().unwrap().collect::<HashMap<_, _>>();
+    /// # assert_eq!(map, HashMap::from([(String::from("id"), MpvNode::Int64(1)), (String::from("current"), MpvNode::Flag(true)), (String::from("filename"), MpvNode::String(String::from("test-data/jellyfish.mp4")))]));
+    /// ```
+    pub fn command(&self, name: &str, args: &[&str]) -> Result<()> {
+        let mut cmd = name.to_owned();
+
+        for elem in args {
+            cmd.push(' ');
+            cmd.push_str(elem);
+        }
+
+        let raw = CString::new(cmd)?;
+        mpv_err((), unsafe {
+            libmpv2_sys::mpv_command_string(self.ctx.as_ptr(), raw.as_ptr())
+        })
+    }
+
+    /// Set the value of a property.
+    pub fn set_property<T: SetData>(&self, name: &str, data: T) -> Result<()> {
+        let name = CString::new(name)?;
+        let format = T::get_format().as_mpv_format() as _;
+        data.call_as_c_void(|ptr| {
+            mpv_err((), unsafe {
+                libmpv2_sys::mpv_set_property(self.ctx.as_ptr(), name.as_ptr(), format, ptr)
+            })
+        })
+    }
+
+    /// Get the value of a property.
+    pub fn get_property<T: GetData>(&self, name: &str) -> Result<T> {
+        let name = CString::new(name)?;
+
+        let format = T::get_format().as_mpv_format() as _;
+        T::get_from_c_void(|ptr| {
+            mpv_err((), unsafe {
+                libmpv2_sys::mpv_get_property(self.ctx.as_ptr(), name.as_ptr(), format, ptr)
+            })
+        })
+    }
+
+    /// Internal time in microseconds, this has an arbitrary offset, and will never go backwards.
+    ///
+    /// This can be called at any time, even if it was stated that no API function should be called.
+    pub fn get_internal_time(&self) -> i64 {
+        unsafe { libmpv2_sys::mpv_get_time_us(self.ctx.as_ptr()) }
+    }
+}