// 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.
#[allow(clippy::missing_safety_doc)]
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.
#[allow(clippy::missing_safety_doc)]
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, 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
///
/// ```dont_run
/// # use libmpv2::{Mpv};
/// # use libmpv2::mpv_node::MpvNode;
/// # use libmpv2::mpv::errors::Result;
/// # use std::collections::HashMap;
/// #
/// # fn main() -> Result<()> {
/// # let mpv = Mpv::new()?;
/// 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")))]));
/// # Ok(())
/// # }
/// ```
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()) }
}
}