about summary refs log tree commit diff stats
path: root/crates/libmpv2/src/mpv
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-23 12:57:19 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-23 12:58:02 +0200
commit0ae5018c33cc4bfe27583c9902472b499f4bd269 (patch)
treeafc2fbfcb126215f47afbc32e555d203d4d6d88c /crates/libmpv2/src/mpv
parentchore(yt_dlp/progress_hook): Also consider the `total_bytes_estimate` field (diff)
downloadyt-0ae5018c33cc4bfe27583c9902472b499f4bd269.tar.gz
yt-0ae5018c33cc4bfe27583c9902472b499f4bd269.zip
refactor(libmpv2): Move to the `crates` directory
Diffstat (limited to 'crates/libmpv2/src/mpv')
-rw-r--r--crates/libmpv2/src/mpv/errors.rs110
-rw-r--r--crates/libmpv2/src/mpv/events.rs383
-rw-r--r--crates/libmpv2/src/mpv/protocol.rs261
-rw-r--r--crates/libmpv2/src/mpv/render.rs406
4 files changed, 1160 insertions, 0 deletions
diff --git a/crates/libmpv2/src/mpv/errors.rs b/crates/libmpv2/src/mpv/errors.rs
new file mode 100644
index 0000000..a2baee5
--- /dev/null
+++ b/crates/libmpv2/src/mpv/errors.rs
@@ -0,0 +1,110 @@
+// 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>.
+
+use std::{ffi::NulError, os::raw as ctype, str::Utf8Error};
+
+use thiserror::Error;
+
+use super::mpv_error;
+
+#[allow(missing_docs)]
+pub type Result<T> = ::std::result::Result<T, Error>;
+
+#[derive(Error, Debug)]
+pub enum Error {
+    #[error("loading file failed: {error}")]
+    Loadfile { error: String },
+
+    #[error("version mismatch detected! Linked version ({linked}) is unequal to the loaded version ({loaded})")]
+    VersionMismatch {
+        linked: ctype::c_ulong,
+        loaded: ctype::c_ulong,
+    },
+
+    #[error("invalid utf8 returned")]
+    InvalidUtf8,
+
+    #[error("null pointer returned")]
+    Null,
+
+    #[error("raw error returned: {}", to_string_mpv_error(*(.0)))]
+    Raw(crate::MpvError),
+}
+
+impl From<NulError> for Error {
+    fn from(_other: NulError) -> Error {
+        Error::Null
+    }
+}
+
+impl From<Utf8Error> for Error {
+    fn from(_other: Utf8Error) -> Error {
+        Error::InvalidUtf8
+    }
+}
+impl From<crate::MpvError> for Error {
+    fn from(other: crate::MpvError) -> Error {
+        Error::Raw(other)
+    }
+}
+
+pub(crate) fn to_string_mpv_error(num: crate::MpvError) -> String {
+    let (error, help) = to_string_mpv_error_raw(num);
+
+    if help.is_empty() {
+        error.to_owned()
+    } else {
+        format!("{} ({})", error, help)
+    }
+}
+
+fn to_string_mpv_error_raw(num: crate::MpvError) -> (&'static str, &'static str) {
+    // debug!("Turning error num '{}' to a string.", num);
+
+    match num {
+        mpv_error::EventQueueFull => (
+            "The event ringbuffer is full.",
+            "This means the client is choked, and can't receive any events. This can happen when too many asynchronous requests have been made, but not answered. Probably never happens in practice, unless the mpv core is frozen for some reason, and the client keeps making asynchronous requests. (Bugs in the client API implementation could also trigger this, e.g. if events become \"lost\".)",
+        ),
+
+        mpv_error::NoMem => ("Memory allocation failed.", ""),
+
+        mpv_error::Uninitialized => ("The mpv core wasn't configured and initialized yet", " See the notes in mpv_create()."),
+
+        mpv_error::InvalidParameter => ("Generic catch-all error if a parameter is set to an invalid or unsupported value.", "This is used if there is no better error code."),
+
+        mpv_error::OptionNotFound => ("Trying to set an option that doesn't exist.", ""),
+        mpv_error::OptionFormat => ("Trying to set an option using an unsupported MPV_FORMAT.", ""),
+        mpv_error::OptionError => ("Setting the option failed", " Typically this happens if the provided option value could not be parsed."),
+
+        mpv_error::PropertyNotFound => ("The accessed property doesn't exist.", ""),
+        mpv_error::PropertyFormat => ("Trying to set or get a property using an unsupported MPV_FORMAT.", ""),
+        mpv_error::PropertyUnavailable => ("The property exists, but is not available", "This usually happens when the associated subsystem is not active, e.g. querying audio parameters while audio is disabled."),
+        mpv_error::PropertyError => ("Error setting or getting a property.", ""),
+
+        mpv_error::Command => ("General error when running a command with mpv_command and similar.", ""),
+
+        mpv_error::LoadingFailed => ("Generic error on loading (usually used with mpv_event_end_file.error).", ""),
+
+        mpv_error::AoInitFailed => ("Initializing the audio output failed.", ""),
+        mpv_error::VoInitFailed => ("Initializing the video output failed.", ""),
+
+        mpv_error::NothingToPlay => ("There was no audio or video data to play", "This also happens if the file was recognized, but did not contain any audio or video streams, or no streams were selected."),
+
+        mpv_error::UnknownFormat => ("     * When trying to load the file, the file format could not be determined, or the file was too broken to open it.", ""),
+
+        mpv_error::Generic => ("Generic error for signaling that certain system requirements are not fulfilled.", ""),
+        mpv_error::NotImplemented => ("The API function which was called is a stub only", ""),
+        mpv_error::Unsupported => ("Unspecified error.", ""),
+
+        mpv_error::Success => unreachable!("This is not an error. It's just here, to ensure that the 0 case marks an success'"),
+        _ => unreachable!("Mpv seems to have changed it's constants."),
+    }
+}
diff --git a/crates/libmpv2/src/mpv/events.rs b/crates/libmpv2/src/mpv/events.rs
new file mode 100644
index 0000000..cbe1ef3
--- /dev/null
+++ b/crates/libmpv2/src/mpv/events.rs
@@ -0,0 +1,383 @@
+// 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>.
+
+use crate::mpv_node::sys_node::SysMpvNode;
+use crate::{mpv::mpv_err, *};
+
+use std::ffi::{c_void, CString};
+use std::os::raw as ctype;
+use std::ptr::NonNull;
+use std::slice;
+
+/// An `Event`'s ID.
+pub use libmpv2_sys::mpv_event_id as EventId;
+
+use self::mpv_node::MpvNode;
+pub mod mpv_event_id {
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_AUDIO_RECONFIG as AudioReconfig;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_CLIENT_MESSAGE as ClientMessage;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_COMMAND_REPLY as CommandReply;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_END_FILE as EndFile;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_FILE_LOADED as FileLoaded;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_GET_PROPERTY_REPLY as GetPropertyReply;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_HOOK as Hook;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_LOG_MESSAGE as LogMessage;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_NONE as None;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_PLAYBACK_RESTART as PlaybackRestart;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_PROPERTY_CHANGE as PropertyChange;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_QUEUE_OVERFLOW as QueueOverflow;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_SEEK as Seek;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_SET_PROPERTY_REPLY as SetPropertyReply;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_SHUTDOWN as Shutdown;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_START_FILE as StartFile;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_TICK as Tick;
+    pub use libmpv2_sys::mpv_event_id_MPV_EVENT_VIDEO_RECONFIG as VideoReconfig;
+}
+
+#[derive(Debug)]
+/// Data that is returned by both `GetPropertyReply` and `PropertyChange` events.
+pub enum PropertyData<'a> {
+    Str(&'a str),
+    OsdStr(&'a str),
+    Flag(bool),
+    Int64(i64),
+    Double(ctype::c_double),
+    Node(MpvNode),
+}
+
+impl<'a> PropertyData<'a> {
+    // SAFETY: meant to extract the data from an event property. See `mpv_event_property` in
+    // `client.h`
+    unsafe fn from_raw(format: MpvFormat, ptr: *mut ctype::c_void) -> Result<PropertyData<'a>> {
+        assert!(!ptr.is_null());
+        match format {
+            mpv_format::Flag => Ok(PropertyData::Flag(*(ptr as *mut bool))),
+            mpv_format::String => {
+                let char_ptr = *(ptr as *mut *mut ctype::c_char);
+                Ok(PropertyData::Str(mpv_cstr_to_str!(char_ptr)?))
+            }
+            mpv_format::OsdString => {
+                let char_ptr = *(ptr as *mut *mut ctype::c_char);
+                Ok(PropertyData::OsdStr(mpv_cstr_to_str!(char_ptr)?))
+            }
+            mpv_format::Double => Ok(PropertyData::Double(*(ptr as *mut f64))),
+            mpv_format::Int64 => Ok(PropertyData::Int64(*(ptr as *mut i64))),
+            mpv_format::Node => {
+                let sys_node = *(ptr as *mut libmpv2_sys::mpv_node);
+                let node = SysMpvNode::new(sys_node, false);
+                return Ok(PropertyData::Node(node.value().unwrap()));
+            }
+            mpv_format::None => unreachable!(),
+            _ => unimplemented!(),
+        }
+    }
+}
+
+pub type PlaylistEntryId = i64;
+
+#[derive(Debug)]
+pub enum Event<'a> {
+    /// Received when the player is shutting down
+    Shutdown,
+    /// *Has not been tested*, received when explicitly asked to MPV
+    LogMessage {
+        prefix: &'a str,
+        level: &'a str,
+        text: &'a str,
+        log_level: LogLevel,
+    },
+    /// Received when using get_property_async
+    GetPropertyReply {
+        name: &'a str,
+        result: PropertyData<'a>,
+        reply_userdata: u64,
+    },
+    /// Received when using set_property_async
+    SetPropertyReply(u64),
+    /// Received when using command_async
+    CommandReply(u64),
+    /// Event received when a new file is playing
+    StartFile(PlaylistEntryId),
+    /// Event received when the file being played currently has stopped, for an error or not
+    EndFile(EndFileReason),
+    /// Event received when a file has been *loaded*, but has not been started
+    FileLoaded,
+    ClientMessage(Vec<&'a str>),
+    VideoReconfig,
+    AudioReconfig,
+    /// The player changed current position
+    Seek,
+    PlaybackRestart,
+    /// Received when used with observe_property
+    PropertyChange {
+        name: &'a str,
+        change: PropertyData<'a>,
+        reply_userdata: u64,
+    },
+    /// Received when the Event Queue is full
+    QueueOverflow,
+    /// A deprecated event
+    Deprecated(libmpv2_sys::mpv_event),
+}
+
+unsafe extern "C" fn wu_wrapper<F: Fn() + Send + 'static>(ctx: *mut c_void) {
+    if ctx.is_null() {
+        panic!("ctx for wakeup wrapper is NULL");
+    }
+
+    (*(ctx as *mut F))();
+}
+
+/// Context to listen to events.
+pub struct EventContext {
+    ctx: NonNull<libmpv2_sys::mpv_handle>,
+    wakeup_callback_cleanup: Option<Box<dyn FnOnce()>>,
+}
+
+unsafe impl Send for EventContext {}
+
+impl EventContext {
+    pub fn new(ctx: NonNull<libmpv2_sys::mpv_handle>) -> Self {
+        EventContext {
+            ctx,
+            wakeup_callback_cleanup: None,
+        }
+    }
+
+    /// Enable an event.
+    pub fn enable_event(&self, ev: events::EventId) -> Result<()> {
+        mpv_err((), unsafe {
+            libmpv2_sys::mpv_request_event(self.ctx.as_ptr(), ev, 1)
+        })
+    }
+
+    /// Enable all, except deprecated, events.
+    pub fn enable_all_events(&self) -> Result<()> {
+        for i in (2..9).chain(16..19).chain(20..23).chain(24..26) {
+            self.enable_event(i)?;
+        }
+        Ok(())
+    }
+
+    /// Disable an event.
+    pub fn disable_event(&self, ev: events::EventId) -> Result<()> {
+        mpv_err((), unsafe {
+            libmpv2_sys::mpv_request_event(self.ctx.as_ptr(), ev, 0)
+        })
+    }
+
+    /// Diable all deprecated events.
+    pub fn disable_deprecated_events(&self) -> Result<()> {
+        self.disable_event(libmpv2_sys::mpv_event_id_MPV_EVENT_IDLE)?;
+        Ok(())
+    }
+
+    /// Diable all events.
+    pub fn disable_all_events(&self) -> Result<()> {
+        for i in 2..26 {
+            self.disable_event(i as _)?;
+        }
+        Ok(())
+    }
+
+    /// Observe `name` property for changes. `id` can be used to unobserve this (or many) properties
+    /// again.
+    pub fn observe_property(&self, name: &str, format: Format, id: u64) -> Result<()> {
+        let name = CString::new(name)?;
+        mpv_err((), unsafe {
+            libmpv2_sys::mpv_observe_property(
+                self.ctx.as_ptr(),
+                id,
+                name.as_ptr(),
+                format.as_mpv_format() as _,
+            )
+        })
+    }
+
+    /// Unobserve any property associated with `id`.
+    pub fn unobserve_property(&self, id: u64) -> Result<()> {
+        mpv_err((), unsafe {
+            libmpv2_sys::mpv_unobserve_property(self.ctx.as_ptr(), id)
+        })
+    }
+
+    /// Wait for `timeout` seconds for an `Event`. Passing `0` as `timeout` will poll.
+    /// For more information, as always, see the mpv-sys docs of `mpv_wait_event`.
+    ///
+    /// This function is intended to be called repeatedly in a wait-event loop.
+    ///
+    /// Returns `Some(Err(...))` if there was invalid utf-8, or if either an
+    /// `MPV_EVENT_GET_PROPERTY_REPLY`, `MPV_EVENT_SET_PROPERTY_REPLY`, `MPV_EVENT_COMMAND_REPLY`,
+    /// or `MPV_EVENT_PROPERTY_CHANGE` event failed, or if `MPV_EVENT_END_FILE` reported an error.
+    pub fn wait_event(&mut self, timeout: f64) -> Option<Result<Event>> {
+        let event = unsafe { *libmpv2_sys::mpv_wait_event(self.ctx.as_ptr(), timeout) };
+
+        // debug!("Got an event from mpv: {:#?}", event);
+
+        if event.event_id != mpv_event_id::None {
+            if let Err(e) = mpv_err((), event.error) {
+                return Some(Err(e));
+            }
+        }
+
+        match event.event_id {
+            mpv_event_id::None => None,
+            mpv_event_id::Shutdown => Some(Ok(Event::Shutdown)),
+            mpv_event_id::LogMessage => {
+                let log_message =
+                    unsafe { *(event.data as *mut libmpv2_sys::mpv_event_log_message) };
+
+                let prefix = unsafe { mpv_cstr_to_str!(log_message.prefix) };
+                Some(prefix.and_then(|prefix| {
+                    Ok(Event::LogMessage {
+                        prefix,
+                        level: unsafe { mpv_cstr_to_str!(log_message.level)? },
+                        text: unsafe { mpv_cstr_to_str!(log_message.text)? },
+                        log_level: log_message.log_level,
+                    })
+                }))
+            }
+            mpv_event_id::GetPropertyReply => {
+                let property = unsafe { *(event.data as *mut libmpv2_sys::mpv_event_property) };
+
+                let name = unsafe { mpv_cstr_to_str!(property.name) };
+                Some(name.and_then(|name| {
+                    // SAFETY: safe because we are passing format + data from an mpv_event_property
+                    let result = unsafe { PropertyData::from_raw(property.format, property.data) }?;
+
+                    Ok(Event::GetPropertyReply {
+                        name,
+                        result,
+                        reply_userdata: event.reply_userdata,
+                    })
+                }))
+            }
+            mpv_event_id::SetPropertyReply => Some(mpv_err(
+                Event::SetPropertyReply(event.reply_userdata),
+                event.error,
+            )),
+            mpv_event_id::CommandReply => Some(mpv_err(
+                Event::CommandReply(event.reply_userdata),
+                event.error,
+            )),
+            mpv_event_id::StartFile => {
+                let playlist_id = unsafe { *(event.data as *mut i64) };
+
+                Some(Ok(Event::StartFile(playlist_id)))
+            }
+            mpv_event_id::EndFile => {
+                let end_file = unsafe { *(event.data as *mut libmpv2_sys::mpv_event_end_file) };
+
+                // debug!("Got an end file event, with error code '{:#?}'", end_file);
+
+                if let Err(e) = mpv_err((), end_file.error) {
+                    Some(Err(e))
+                } else {
+                    Some(Ok(Event::EndFile(end_file.reason.into())))
+                }
+            }
+            mpv_event_id::FileLoaded => Some(Ok(Event::FileLoaded)),
+            mpv_event_id::ClientMessage => {
+                let client_message =
+                    unsafe { *(event.data as *mut libmpv2_sys::mpv_event_client_message) };
+                let messages = unsafe {
+                    slice::from_raw_parts_mut(client_message.args, client_message.num_args as _)
+                };
+                Some(Ok(Event::ClientMessage(
+                    messages
+                        .iter()
+                        .map(|msg| unsafe { mpv_cstr_to_str!(*msg) })
+                        .collect::<Result<Vec<_>>>()
+                        .unwrap(),
+                )))
+            }
+            mpv_event_id::VideoReconfig => Some(Ok(Event::VideoReconfig)),
+            mpv_event_id::AudioReconfig => Some(Ok(Event::AudioReconfig)),
+            mpv_event_id::Seek => Some(Ok(Event::Seek)),
+            mpv_event_id::PlaybackRestart => Some(Ok(Event::PlaybackRestart)),
+            mpv_event_id::PropertyChange => {
+                let property = unsafe { *(event.data as *mut libmpv2_sys::mpv_event_property) };
+
+                // This happens if the property is not available. For example,
+                // if you reached EndFile while observing a property.
+                if property.format == mpv_format::None {
+                    None
+                } else {
+                    let name = unsafe { mpv_cstr_to_str!(property.name) };
+                    Some(name.and_then(|name| {
+                        // SAFETY: safe because we are passing format + data from an mpv_event_property
+                        let change =
+                            unsafe { PropertyData::from_raw(property.format, property.data) }?;
+
+                        Ok(Event::PropertyChange {
+                            name,
+                            change,
+                            reply_userdata: event.reply_userdata,
+                        })
+                    }))
+                }
+            }
+            mpv_event_id::QueueOverflow => Some(Ok(Event::QueueOverflow)),
+            _ => Some(Ok(Event::Deprecated(event))),
+        }
+    }
+
+    /// Set a custom function that should be called when there are new events. Use this if
+    /// blocking in [wait_event](#method.wait_event) to wait for new events is not feasible.
+    ///
+    /// Keep in mind that the callback will be called from foreign threads. You must not make
+    /// any assumptions of the environment, and you must return as soon as possible (i.e. no
+    /// long blocking waits). Exiting the callback through any other means than a normal return
+    /// is forbidden (no throwing exceptions, no `longjmp()` calls). You must not change any
+    /// local thread state (such as the C floating point environment).
+    ///
+    /// You are not allowed to call any client API functions inside of the callback. In
+    /// particular, you should not do any processing in the callback, but wake up another
+    /// thread that does all the work. The callback is meant strictly for notification only,
+    /// and is called from arbitrary core parts of the player, that make no considerations for
+    /// reentrant API use or allowing the callee to spend a lot of time doing other things.
+    /// Keep in mind that it’s also possible that the callback is called from a thread while a
+    /// mpv API function is called (i.e. it can be reentrant).
+    ///
+    /// In general, the client API expects you to call [wait_event](#method.wait_event) to receive
+    /// notifications, and the wakeup callback is merely a helper utility to make this easier in
+    /// certain situations. Note that it’s possible that there’s only one wakeup callback
+    /// invocation for multiple events. You should call [wait_event](#method.wait_event) with no timeout until
+    /// `None` is returned, at which point the event queue is empty.
+    ///
+    /// If you actually want to do processing in a callback, spawn a thread that does nothing but
+    /// call [wait_event](#method.wait_event) in a loop and dispatches the result to a callback.
+    ///
+    /// Only one wakeup callback can be set.
+    pub fn set_wakeup_callback<F: Fn() + Send + 'static>(&mut self, callback: F) {
+        if let Some(wakeup_callback_cleanup) = self.wakeup_callback_cleanup.take() {
+            wakeup_callback_cleanup();
+        }
+        let raw_callback = Box::into_raw(Box::new(callback));
+        self.wakeup_callback_cleanup = Some(Box::new(move || unsafe {
+            drop(Box::from_raw(raw_callback));
+        }) as Box<dyn FnOnce()>);
+        unsafe {
+            libmpv2_sys::mpv_set_wakeup_callback(
+                self.ctx.as_ptr(),
+                Some(wu_wrapper::<F>),
+                raw_callback as *mut c_void,
+            );
+        }
+    }
+}
+
+impl Drop for EventContext {
+    fn drop(&mut self) {
+        if let Some(wakeup_callback_cleanup) = self.wakeup_callback_cleanup.take() {
+            wakeup_callback_cleanup();
+        }
+    }
+}
diff --git a/crates/libmpv2/src/mpv/protocol.rs b/crates/libmpv2/src/mpv/protocol.rs
new file mode 100644
index 0000000..4ae4f16
--- /dev/null
+++ b/crates/libmpv2/src/mpv/protocol.rs
@@ -0,0 +1,261 @@
+// 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>.
+
+use super::*;
+
+use std::alloc::{self, Layout};
+use std::marker::PhantomData;
+use std::mem;
+use std::os::raw as ctype;
+use std::panic;
+use std::panic::RefUnwindSafe;
+use std::slice;
+use std::sync::{atomic::Ordering, Mutex};
+
+impl Mpv {
+    /// Create a context with which custom protocols can be registered.
+    ///
+    /// # Panics
+    /// Panics if a context already exists
+    pub fn create_protocol_context<T, U>(&self) -> ProtocolContext<T, U>
+    where
+        T: RefUnwindSafe,
+        U: RefUnwindSafe,
+    {
+        match self.protocols_guard.compare_exchange(
+            false,
+            true,
+            Ordering::AcqRel,
+            Ordering::Acquire,
+        ) {
+            Ok(_) => ProtocolContext::new(self.ctx, PhantomData::<&Self>),
+            Err(_) => panic!("A protocol context already exists"),
+        }
+    }
+}
+
+/// Return a persistent `T` that is passed to all other `Stream*` functions, panic on errors.
+pub type StreamOpen<T, U> = fn(&mut U, &str) -> T;
+/// Do any necessary cleanup.
+pub type StreamClose<T> = fn(Box<T>);
+/// Seek to the given offset. Return the new offset, or either `MpvError::Generic` if seeking
+/// failed or panic.
+pub type StreamSeek<T> = fn(&mut T, i64) -> i64;
+/// Target buffer with fixed capacity.
+/// Return either the number of read bytes, `0` on EOF, or either `-1` or panic on error.
+pub type StreamRead<T> = fn(&mut T, &mut [ctype::c_char]) -> i64;
+/// Return the total size of the stream in bytes. Panic on error.
+pub type StreamSize<T> = fn(&mut T) -> i64;
+
+unsafe extern "C" fn open_wrapper<T, U>(
+    user_data: *mut ctype::c_void,
+    uri: *mut ctype::c_char,
+    info: *mut libmpv2_sys::mpv_stream_cb_info,
+) -> ctype::c_int
+where
+    T: RefUnwindSafe,
+    U: RefUnwindSafe,
+{
+    let data = user_data as *mut ProtocolData<T, U>;
+
+    (*info).cookie = user_data;
+    (*info).read_fn = Some(read_wrapper::<T, U>);
+    (*info).seek_fn = Some(seek_wrapper::<T, U>);
+    (*info).size_fn = Some(size_wrapper::<T, U>);
+    (*info).close_fn = Some(close_wrapper::<T, U>);
+
+    let ret = panic::catch_unwind(|| {
+        let uri = mpv_cstr_to_str!(uri as *const _).unwrap();
+        ptr::write(
+            (*data).cookie,
+            ((*data).open_fn)(&mut (*data).user_data, uri),
+        );
+    });
+
+    if ret.is_ok() {
+        0
+    } else {
+        mpv_error::Generic as _
+    }
+}
+
+unsafe extern "C" fn read_wrapper<T, U>(
+    cookie: *mut ctype::c_void,
+    buf: *mut ctype::c_char,
+    nbytes: u64,
+) -> i64
+where
+    T: RefUnwindSafe,
+    U: RefUnwindSafe,
+{
+    let data = cookie as *mut ProtocolData<T, U>;
+
+    let ret = panic::catch_unwind(|| {
+        let slice = slice::from_raw_parts_mut(buf, nbytes as _);
+        ((*data).read_fn)(&mut *(*data).cookie, slice)
+    });
+    if let Ok(ret) = ret {
+        ret
+    } else {
+        -1
+    }
+}
+
+unsafe extern "C" fn seek_wrapper<T, U>(cookie: *mut ctype::c_void, offset: i64) -> i64
+where
+    T: RefUnwindSafe,
+    U: RefUnwindSafe,
+{
+    let data = cookie as *mut ProtocolData<T, U>;
+
+    if (*data).seek_fn.is_none() {
+        return mpv_error::Unsupported as _;
+    }
+
+    let ret =
+        panic::catch_unwind(|| (*(*data).seek_fn.as_ref().unwrap())(&mut *(*data).cookie, offset));
+    if let Ok(ret) = ret {
+        ret
+    } else {
+        mpv_error::Generic as _
+    }
+}
+
+unsafe extern "C" fn size_wrapper<T, U>(cookie: *mut ctype::c_void) -> i64
+where
+    T: RefUnwindSafe,
+    U: RefUnwindSafe,
+{
+    let data = cookie as *mut ProtocolData<T, U>;
+
+    if (*data).size_fn.is_none() {
+        return mpv_error::Unsupported as _;
+    }
+
+    let ret = panic::catch_unwind(|| (*(*data).size_fn.as_ref().unwrap())(&mut *(*data).cookie));
+    if let Ok(ret) = ret {
+        ret
+    } else {
+        mpv_error::Unsupported as _
+    }
+}
+
+#[allow(unused_must_use)]
+unsafe extern "C" fn close_wrapper<T, U>(cookie: *mut ctype::c_void)
+where
+    T: RefUnwindSafe,
+    U: RefUnwindSafe,
+{
+    let data = Box::from_raw(cookie as *mut ProtocolData<T, U>);
+
+    panic::catch_unwind(|| ((*data).close_fn)(Box::from_raw((*data).cookie)));
+}
+
+struct ProtocolData<T, U> {
+    cookie: *mut T,
+    user_data: U,
+
+    open_fn: StreamOpen<T, U>,
+    close_fn: StreamClose<T>,
+    read_fn: StreamRead<T>,
+    seek_fn: Option<StreamSeek<T>>,
+    size_fn: Option<StreamSize<T>>,
+}
+
+/// This context holds state relevant to custom protocols.
+/// It is created by calling `Mpv::create_protocol_context`.
+pub struct ProtocolContext<'parent, T: RefUnwindSafe, U: RefUnwindSafe> {
+    ctx: NonNull<libmpv2_sys::mpv_handle>,
+    protocols: Mutex<Vec<Protocol<T, U>>>,
+    _does_not_outlive: PhantomData<&'parent Mpv>,
+}
+
+unsafe impl<'parent, T: RefUnwindSafe, U: RefUnwindSafe> Send for ProtocolContext<'parent, T, U> {}
+unsafe impl<'parent, T: RefUnwindSafe, U: RefUnwindSafe> Sync for ProtocolContext<'parent, T, U> {}
+
+impl<'parent, T: RefUnwindSafe, U: RefUnwindSafe> ProtocolContext<'parent, T, U> {
+    fn new(
+        ctx: NonNull<libmpv2_sys::mpv_handle>,
+        marker: PhantomData<&'parent Mpv>,
+    ) -> ProtocolContext<'parent, T, U> {
+        ProtocolContext {
+            ctx,
+            protocols: Mutex::new(Vec::new()),
+            _does_not_outlive: marker,
+        }
+    }
+
+    /// Register a custom `Protocol`. Once a protocol has been registered, it lives as long as
+    /// `Mpv`.
+    ///
+    /// Returns `Error::Mpv(MpvError::InvalidParameter)` if a protocol with the same name has
+    /// already been registered.
+    pub fn register(&self, protocol: Protocol<T, U>) -> Result<()> {
+        let mut protocols = self.protocols.lock().unwrap();
+        protocol.register(self.ctx.as_ptr())?;
+        protocols.push(protocol);
+        Ok(())
+    }
+}
+
+/// `Protocol` holds all state used by a custom protocol.
+pub struct Protocol<T: Sized + RefUnwindSafe, U: RefUnwindSafe> {
+    name: String,
+    data: *mut ProtocolData<T, U>,
+}
+
+impl<T: RefUnwindSafe, U: RefUnwindSafe> Protocol<T, U> {
+    /// `name` is the prefix of the protocol, e.g. `name://path`.
+    ///
+    /// `user_data` is data that will be passed to `open_fn`.
+    ///
+    /// # Safety
+    /// Do not call libmpv functions in any supplied function.
+    /// All panics of the provided functions are catched and can be used as generic error returns.
+    pub unsafe fn new(
+        name: String,
+        user_data: U,
+        open_fn: StreamOpen<T, U>,
+        close_fn: StreamClose<T>,
+        read_fn: StreamRead<T>,
+        seek_fn: Option<StreamSeek<T>>,
+        size_fn: Option<StreamSize<T>>,
+    ) -> Protocol<T, U> {
+        let c_layout = Layout::from_size_align(mem::size_of::<T>(), mem::align_of::<T>()).unwrap();
+        let cookie = alloc::alloc(c_layout) as *mut T;
+        let data = Box::into_raw(Box::new(ProtocolData {
+            cookie,
+            user_data,
+
+            open_fn,
+            close_fn,
+            read_fn,
+            seek_fn,
+            size_fn,
+        }));
+
+        Protocol { name, data }
+    }
+
+    fn register(&self, ctx: *mut libmpv2_sys::mpv_handle) -> Result<()> {
+        let name = CString::new(&self.name[..])?;
+        unsafe {
+            mpv_err(
+                (),
+                libmpv2_sys::mpv_stream_cb_add_ro(
+                    ctx,
+                    name.as_ptr(),
+                    self.data as *mut _,
+                    Some(open_wrapper::<T, U>),
+                ),
+            )
+        }
+    }
+}
diff --git a/crates/libmpv2/src/mpv/render.rs b/crates/libmpv2/src/mpv/render.rs
new file mode 100644
index 0000000..91db34e
--- /dev/null
+++ b/crates/libmpv2/src/mpv/render.rs
@@ -0,0 +1,406 @@
+// 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>.
+
+use crate::{mpv::mpv_err, Error, Result};
+use std::collections::HashMap;
+use std::ffi::{c_void, CStr};
+use std::os::raw::c_int;
+use std::ptr;
+
+type DeleterFn = unsafe fn(*mut c_void);
+
+pub struct RenderContext {
+    ctx: *mut libmpv2_sys::mpv_render_context,
+    update_callback_cleanup: Option<Box<dyn FnOnce()>>,
+}
+
+/// For initializing the mpv OpenGL state via RenderParam::OpenGLInitParams
+pub struct OpenGLInitParams<GLContext> {
+    /// This retrieves OpenGL function pointers, and will use them in subsequent
+    /// operation.
+    /// Usually, you can simply call the GL context APIs from this callback (e.g.
+    /// glXGetProcAddressARB or wglGetProcAddress), but some APIs do not always
+    /// return pointers for all standard functions (even if present); in this
+    /// case you have to compensate by looking up these functions yourself when
+    /// libmpv wants to resolve them through this callback.
+    /// libmpv will not normally attempt to resolve GL functions on its own, nor
+    /// does it link to GL libraries directly.
+    pub get_proc_address: fn(ctx: &GLContext, name: &str) -> *mut c_void,
+
+    /// Value passed as ctx parameter to get_proc_address().
+    pub ctx: GLContext,
+}
+
+/// For RenderParam::FBO
+pub struct FBO {
+    pub fbo: i32,
+    pub width: i32,
+    pub height: i32,
+}
+
+#[repr(u32)]
+#[derive(Clone)]
+pub enum RenderFrameInfoFlag {
+    Present = libmpv2_sys::mpv_render_frame_info_flag_MPV_RENDER_FRAME_INFO_PRESENT,
+    Redraw = libmpv2_sys::mpv_render_frame_info_flag_MPV_RENDER_FRAME_INFO_REDRAW,
+    Repeat = libmpv2_sys::mpv_render_frame_info_flag_MPV_RENDER_FRAME_INFO_REPEAT,
+    BlockVSync = libmpv2_sys::mpv_render_frame_info_flag_MPV_RENDER_FRAME_INFO_BLOCK_VSYNC,
+}
+
+impl From<u64> for RenderFrameInfoFlag {
+    // mpv_render_frame_info_flag is u32, but mpv_render_frame_info.flags is u64 o\
+    fn from(val: u64) -> Self {
+        let val = val as u32;
+        match val {
+            libmpv2_sys::mpv_render_frame_info_flag_MPV_RENDER_FRAME_INFO_PRESENT => {
+                RenderFrameInfoFlag::Present
+            }
+            libmpv2_sys::mpv_render_frame_info_flag_MPV_RENDER_FRAME_INFO_REDRAW => {
+                RenderFrameInfoFlag::Redraw
+            }
+            libmpv2_sys::mpv_render_frame_info_flag_MPV_RENDER_FRAME_INFO_REPEAT => {
+                RenderFrameInfoFlag::Repeat
+            }
+            libmpv2_sys::mpv_render_frame_info_flag_MPV_RENDER_FRAME_INFO_BLOCK_VSYNC => {
+                RenderFrameInfoFlag::BlockVSync
+            }
+            _ => panic!("Tried converting invalid value to RenderFrameInfoFlag"),
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct RenderFrameInfo {
+    pub flags: RenderFrameInfoFlag,
+    pub target_time: i64,
+}
+
+pub enum RenderParamApiType {
+    OpenGl,
+}
+
+pub enum RenderParam<GLContext> {
+    Invalid,
+    ApiType(RenderParamApiType),
+    InitParams(OpenGLInitParams<GLContext>),
+    FBO(FBO),
+    FlipY(bool),
+    Depth(i32),
+    ICCProfile(Vec<u8>),
+    AmbientLight(i32),
+    X11Display(*const c_void),
+    WaylandDisplay(*const c_void),
+    AdvancedControl(bool),
+    NextFrameInfo(RenderFrameInfo),
+    BlockForTargetTime(bool),
+    SkipRendering(bool),
+}
+
+impl<C> From<&RenderParam<C>> for u32 {
+    fn from(val: &RenderParam<C>) -> Self {
+        match val {
+            RenderParam::Invalid => 0,
+            RenderParam::ApiType(_) => 1,
+            RenderParam::InitParams(_) => 2,
+            RenderParam::FBO(_) => 3,
+            RenderParam::FlipY(_) => 4,
+            RenderParam::Depth(_) => 5,
+            RenderParam::ICCProfile(_) => 6,
+            RenderParam::AmbientLight(_) => 7,
+            RenderParam::X11Display(_) => 8,
+            RenderParam::WaylandDisplay(_) => 9,
+            RenderParam::AdvancedControl(_) => 10,
+            RenderParam::NextFrameInfo(_) => 11,
+            RenderParam::BlockForTargetTime(_) => 12,
+            RenderParam::SkipRendering(_) => 13,
+        }
+    }
+}
+
+unsafe extern "C" fn gpa_wrapper<GLContext>(ctx: *mut c_void, name: *const i8) -> *mut c_void {
+    if ctx.is_null() {
+        panic!("ctx for get_proc_address wrapper is NULL");
+    }
+
+    let params: *mut OpenGLInitParams<GLContext> = ctx as _;
+    let params = &*params;
+    (params.get_proc_address)(
+        &params.ctx,
+        CStr::from_ptr(name)
+            .to_str()
+            .expect("Could not convert function name to str"),
+    )
+}
+
+unsafe extern "C" fn ru_wrapper<F: Fn() + Send + 'static>(ctx: *mut c_void) {
+    if ctx.is_null() {
+        panic!("ctx for render_update wrapper is NULL");
+    }
+
+    (*(ctx as *mut F))();
+}
+
+impl<C> From<OpenGLInitParams<C>> for libmpv2_sys::mpv_opengl_init_params {
+    fn from(val: OpenGLInitParams<C>) -> Self {
+        Self {
+            get_proc_address: Some(gpa_wrapper::<OpenGLInitParams<C>>),
+            get_proc_address_ctx: Box::into_raw(Box::new(val)) as *mut c_void,
+        }
+    }
+}
+
+impl<C> From<RenderParam<C>> for libmpv2_sys::mpv_render_param {
+    fn from(val: RenderParam<C>) -> Self {
+        let type_ = u32::from(&val);
+        let data = match val {
+            RenderParam::Invalid => ptr::null_mut(),
+            RenderParam::ApiType(api_type) => match api_type {
+                RenderParamApiType::OpenGl => {
+                    libmpv2_sys::MPV_RENDER_API_TYPE_OPENGL.as_ptr() as *mut c_void
+                }
+            },
+            RenderParam::InitParams(params) => {
+                Box::into_raw(Box::new(libmpv2_sys::mpv_opengl_init_params::from(params)))
+                    as *mut c_void
+            }
+            RenderParam::FBO(fbo) => Box::into_raw(Box::new(fbo)) as *mut c_void,
+            RenderParam::FlipY(flip) => Box::into_raw(Box::new(flip as c_int)) as *mut c_void,
+            RenderParam::Depth(depth) => Box::into_raw(Box::new(depth)) as *mut c_void,
+            RenderParam::ICCProfile(bytes) => {
+                Box::into_raw(bytes.into_boxed_slice()) as *mut c_void
+            }
+            RenderParam::AmbientLight(lux) => Box::into_raw(Box::new(lux)) as *mut c_void,
+            RenderParam::X11Display(ptr) => ptr as *mut _,
+            RenderParam::WaylandDisplay(ptr) => ptr as *mut _,
+            RenderParam::AdvancedControl(adv_ctrl) => {
+                Box::into_raw(Box::new(adv_ctrl as c_int)) as *mut c_void
+            }
+            RenderParam::NextFrameInfo(frame_info) => {
+                Box::into_raw(Box::new(frame_info)) as *mut c_void
+            }
+            RenderParam::BlockForTargetTime(block) => {
+                Box::into_raw(Box::new(block as c_int)) as *mut c_void
+            }
+            RenderParam::SkipRendering(skip_rendering) => {
+                Box::into_raw(Box::new(skip_rendering as c_int)) as *mut c_void
+            }
+        };
+        Self { type_, data }
+    }
+}
+
+unsafe fn free_void_data<T>(ptr: *mut c_void) {
+    drop(Box::<T>::from_raw(ptr as *mut T));
+}
+
+unsafe fn free_init_params<C>(ptr: *mut c_void) {
+    let params = Box::from_raw(ptr as *mut libmpv2_sys::mpv_opengl_init_params);
+    drop(Box::from_raw(
+        params.get_proc_address_ctx as *mut OpenGLInitParams<C>,
+    ));
+}
+
+impl RenderContext {
+    pub fn new<C>(
+        mpv: &mut libmpv2_sys::mpv_handle,
+        params: impl IntoIterator<Item = RenderParam<C>>,
+    ) -> Result<Self> {
+        let params: Vec<_> = params.into_iter().collect();
+        let mut raw_params: Vec<libmpv2_sys::mpv_render_param> = Vec::new();
+        raw_params.reserve(params.len() + 1);
+        let mut raw_ptrs: HashMap<*const c_void, DeleterFn> = HashMap::new();
+
+        for p in params {
+            // The render params are type-erased after they are passed to mpv. This is where we last
+            // know their real types, so we keep a deleter here.
+            let deleter: Option<DeleterFn> = match p {
+                RenderParam::InitParams(_) => Some(free_init_params::<C>),
+                RenderParam::FBO(_) => Some(free_void_data::<FBO>),
+                RenderParam::FlipY(_) => Some(free_void_data::<i32>),
+                RenderParam::Depth(_) => Some(free_void_data::<i32>),
+                RenderParam::ICCProfile(_) => Some(free_void_data::<Box<[u8]>>),
+                RenderParam::AmbientLight(_) => Some(free_void_data::<i32>),
+                RenderParam::NextFrameInfo(_) => Some(free_void_data::<RenderFrameInfo>),
+                _ => None,
+            };
+            let raw_param: libmpv2_sys::mpv_render_param = p.into();
+            if let Some(deleter) = deleter {
+                raw_ptrs.insert(raw_param.data, deleter);
+            }
+
+            raw_params.push(raw_param);
+        }
+        // the raw array must end with type = 0
+        raw_params.push(libmpv2_sys::mpv_render_param {
+            type_: 0,
+            data: ptr::null_mut(),
+        });
+
+        unsafe {
+            let raw_array =
+                Box::into_raw(raw_params.into_boxed_slice()) as *mut libmpv2_sys::mpv_render_param;
+            let ctx = Box::into_raw(Box::new(std::ptr::null_mut() as _));
+            let err = libmpv2_sys::mpv_render_context_create(ctx, &mut *mpv, raw_array);
+            drop(Box::from_raw(raw_array));
+            for (ptr, deleter) in raw_ptrs.iter() {
+                (deleter)(*ptr as _);
+            }
+
+            mpv_err(
+                Self {
+                    ctx: *Box::from_raw(ctx),
+                    update_callback_cleanup: None,
+                },
+                err,
+            )
+        }
+    }
+
+    pub fn set_parameter<C>(&self, param: RenderParam<C>) -> Result<()> {
+        unsafe {
+            mpv_err(
+                (),
+                libmpv2_sys::mpv_render_context_set_parameter(
+                    self.ctx,
+                    libmpv2_sys::mpv_render_param::from(param),
+                ),
+            )
+        }
+    }
+
+    pub fn get_info<C>(&self, param: RenderParam<C>) -> Result<RenderParam<C>> {
+        let is_next_frame_info = matches!(param, RenderParam::NextFrameInfo(_));
+        let raw_param = libmpv2_sys::mpv_render_param::from(param);
+        let res = unsafe { libmpv2_sys::mpv_render_context_get_info(self.ctx, raw_param) };
+        if res == 0 {
+            if !is_next_frame_info {
+                panic!("I don't know how to handle this info type.");
+            }
+            let raw_frame_info = raw_param.data as *mut libmpv2_sys::mpv_render_frame_info;
+            unsafe {
+                let raw_frame_info = *raw_frame_info;
+                return Ok(RenderParam::NextFrameInfo(RenderFrameInfo {
+                    flags: raw_frame_info.flags.into(),
+                    target_time: raw_frame_info.target_time,
+                }));
+            }
+        }
+        Err(Error::Raw(res))
+    }
+
+    /// Render video.
+    ///
+    /// Typically renders the video to a target surface provided via `fbo`
+    /// (the details depend on the backend in use). Options like "panscan" are
+    /// applied to determine which part of the video should be visible and how the
+    /// video should be scaled. You can change these options at runtime by using the
+    /// mpv property API.
+    ///
+    /// The renderer will reconfigure itself every time the target surface
+    /// configuration (such as size) is changed.
+    ///
+    /// This function implicitly pulls a video frame from the internal queue and
+    /// renders it. If no new frame is available, the previous frame is redrawn.
+    /// The update callback set with [set_update_callback](Self::set_update_callback)
+    /// notifies you when a new frame was added. The details potentially depend on
+    /// the backends and the provided parameters.
+    ///
+    /// Generally, libmpv will invoke your update callback some time before the video
+    /// frame should be shown, and then lets this function block until the supposed
+    /// display time. This will limit your rendering to video FPS. You can prevent
+    /// this by setting the "video-timing-offset" global option to 0. (This applies
+    /// only to "audio" video sync mode.)
+    ///
+    /// # Arguments
+    ///
+    /// * `fbo` - A framebuffer object to render to. In OpenGL, 0 is the current backbuffer
+    /// * `width` - The width of the framebuffer in pixels. This is used for scaling the
+    ///             video properly.
+    /// * `height` - The height of the framebuffer in pixels. This is used for scaling the
+    ///              video properly.
+    /// * `flip` - Whether to draw the image upside down. This is needed for OpenGL because
+    ///            it uses a coordinate system with positive Y up, but videos use positive
+    ///            Y down.
+    pub fn render<GLContext>(&self, fbo: i32, width: i32, height: i32, flip: bool) -> Result<()> {
+        let mut raw_params: Vec<libmpv2_sys::mpv_render_param> = Vec::with_capacity(3);
+        let mut raw_ptrs: HashMap<*const c_void, DeleterFn> = HashMap::new();
+
+        let raw_param: libmpv2_sys::mpv_render_param =
+            RenderParam::<GLContext>::FBO(FBO { fbo, width, height }).into();
+        raw_ptrs.insert(raw_param.data, free_void_data::<FBO>);
+        raw_params.push(raw_param);
+        let raw_param: libmpv2_sys::mpv_render_param = RenderParam::<GLContext>::FlipY(flip).into();
+        raw_ptrs.insert(raw_param.data, free_void_data::<i32>);
+        raw_params.push(raw_param);
+        // the raw array must end with type = 0
+        raw_params.push(libmpv2_sys::mpv_render_param {
+            type_: 0,
+            data: ptr::null_mut(),
+        });
+
+        let raw_array =
+            Box::into_raw(raw_params.into_boxed_slice()) as *mut libmpv2_sys::mpv_render_param;
+
+        let ret = unsafe {
+            mpv_err(
+                (),
+                libmpv2_sys::mpv_render_context_render(self.ctx, raw_array),
+            )
+        };
+        unsafe {
+            drop(Box::from_raw(raw_array));
+        }
+
+        unsafe {
+            for (ptr, deleter) in raw_ptrs.iter() {
+                (deleter)(*ptr as _);
+            }
+        }
+
+        ret
+    }
+
+    /// Set the callback that notifies you when a new video frame is available, or if the video display
+    /// configuration somehow changed and requires a redraw. Similar to [EventContext::set_wakeup_callback](crate::events::EventContext::set_wakeup_callback), you
+    /// must not call any mpv API from the callback, and all the other listed restrictions apply (such
+    /// as not exiting the callback by throwing exceptions).
+    ///
+    /// This can be called from any thread, except from an update callback. In case of the OpenGL backend,
+    /// no OpenGL state or API is accessed.
+    ///
+    /// Calling this will raise an update callback immediately.
+    pub fn set_update_callback<F: Fn() + Send + 'static>(&mut self, callback: F) {
+        if let Some(update_callback_cleanup) = self.update_callback_cleanup.take() {
+            update_callback_cleanup();
+        }
+        let raw_callback = Box::into_raw(Box::new(callback));
+        self.update_callback_cleanup = Some(Box::new(move || unsafe {
+            drop(Box::from_raw(raw_callback));
+        }) as Box<dyn FnOnce()>);
+        unsafe {
+            libmpv2_sys::mpv_render_context_set_update_callback(
+                self.ctx,
+                Some(ru_wrapper::<F>),
+                raw_callback as *mut c_void,
+            );
+        }
+    }
+}
+
+impl Drop for RenderContext {
+    fn drop(&mut self) {
+        if let Some(update_callback_cleanup) = self.update_callback_cleanup.take() {
+            update_callback_cleanup();
+        }
+        unsafe {
+            libmpv2_sys::mpv_render_context_free(self.ctx);
+        }
+    }
+}