use eframe::glow::{self, HasContext as _};
use libmpv2::render::{OpenGLInitParams, RenderContext, RenderParam, RenderParamApiType, mpv_render_update};
use libmpv2::{Mpv, mpv_error};
use std::ffi::{CStr, CString, c_void};
use std::sync::{Arc, OnceLock};
#[expect(clippy::type_complexity)]
static GPA: OnceLock<Arc<dyn Fn(&CStr) -> *const c_void + Send + Sync>> = OnceLock::new();
fn gl_get_proc_address(_ctx: &(), name: &str) -> *mut c_void {
let gpa = GPA.get().expect("GPA not initialized");
let cname = CString::new(name).expect("null byte in GL function name");
gpa(&cname) as *mut c_void
}
#[derive(Debug, thiserror::Error, PartialEq, Eq, Hash)]
pub enum BackendError {
#[error("This library only functions when egui is using glow, but the GL context was unavailable.")]
ExpectedGlow,
#[error("libmpv2: {0}")]
Mpv(#[from] libmpv2::Error),
#[error("glow: {0}")]
Glow(String),
#[error("Incomplete framebuffer (0x{0:x}); Ensure RGBA8 support.")]
IncompleteFramebuffer(u32),
#[error("Tried to use GL resoures after they were destroyed by PlayerState::destroy_gl_resources.")]
UseAfterDestroy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FramebufferSize {
pub width: i32,
pub height: i32,
}
struct MpvContainer {
ctx: RenderContext<'static>,
mpv: Mpv,
}
#[derive(Clone, Copy)]
pub(crate) struct GlResources {
texture: glow::NativeTexture,
framebuffer: glow::NativeFramebuffer,
framebuffer_size: FramebufferSize,
}
impl GlResources {
pub(crate) fn framebuffer(&self) -> glow::NativeFramebuffer { self.framebuffer }
pub(crate) fn framebuffer_size(&self) -> FramebufferSize { self.framebuffer_size }
}
pub struct PlayerState {
mpv: MpvContainer,
gl: Arc<glow::Context>,
gl_resources: Option<GlResources>,
}
impl PlayerState {
pub fn new(cc: &eframe::CreationContext<'_>) -> Result<Self, BackendError> {
let gl = cc.gl.clone().ok_or(BackendError::ExpectedGlow)?;
let gpa = cc
.get_proc_address
.clone()
.expect("egui's get_proc_address wrapper is unavailable despite using glow");
let _ = GPA.get_or_init(|| gpa.clone());
let mpv = Mpv::new()?;
if cfg!(debug_assertions) {
mpv.set_property("terminal", "yes")?;
}
mpv.set_property("vo", "libmpv")?;
mpv.set_property("hwdec", "auto-safe")?;
mpv.set_property("video-timing-offset", 0.0_f64)?;
mpv.set_property("keep-open", "yes")?;
let render_ctx = mpv.create_render_context([
RenderParam::ApiType(RenderParamApiType::OpenGl),
RenderParam::InitParams(OpenGLInitParams {
get_proc_address: gl_get_proc_address,
ctx: (),
}),
])?;
let mut render_ctx: RenderContext<'static> = unsafe { std::mem::transmute(render_ctx) };
let egui_ctx = cc.egui_ctx.clone();
render_ctx.set_update_callback(move || {
egui_ctx.request_repaint();
});
Ok(Self {
mpv: MpvContainer { ctx: render_ctx, mpv },
gl,
gl_resources: None,
})
}
pub fn load_file(&self, path: impl AsRef<str>) -> Result<(), BackendError> {
let path = path.as_ref();
self.mpv.mpv.command("loadfile", &[path])?;
Ok(())
}
pub fn dimensions(&self) -> Result<Option<(i64, i64)>, BackendError> {
let w = self.get_optional_property::<i64>("video-out-params/dw")?;
let h = self.get_optional_property::<i64>("video-out-params/dh")?;
Ok(w.zip(h))
}
pub fn aspect_ratio(&self) -> Result<Option<f32>, BackendError> {
Ok(self.dimensions()?.map(|(w, h)| w as f32 / h as f32))
}
pub fn duration(&self) -> Result<Option<f64>, BackendError> { self.get_optional_property("duration") }
pub fn time_pos(&self) -> Result<Option<f64>, BackendError> { self.get_optional_property("time-pos") }
pub fn seek_to(&self, seconds: f64) -> Result<(), BackendError> {
self.mpv.mpv.set_property("time-pos", seconds)?;
Ok(())
}
pub fn is_paused(&self) -> Result<bool, BackendError> { Ok(self.mpv.mpv.get_property::<bool>("pause")?) }
pub fn toggle_pause(&self) -> Result<(), BackendError> {
let paused = self.is_paused().unwrap_or(true);
self.mpv.mpv.set_property("pause", !paused)?;
Ok(())
}
pub fn play(&self) -> Result<(), BackendError> {
self.mpv.mpv.set_property("pause", false)?;
Ok(())
}
pub fn pause(&self) -> Result<(), BackendError> {
self.mpv.mpv.set_property("pause", true)?;
Ok(())
}
pub fn destroy_gl_resources(&mut self) {
if let Some(GlResources {
framebuffer,
texture,
framebuffer_size: _,
}) = self.gl_resources.take()
{
unsafe {
self.gl.delete_framebuffer(framebuffer);
self.gl.delete_texture(texture);
}
}
}
pub(crate) fn get_current_framebuffer(
&mut self,
size: FramebufferSize,
) -> Result<GlResources, BackendError> {
let res = if let Some(res) = self.gl_resources {
if res.framebuffer_size != size && size.width > 0 && size.height > 0 {
unsafe {
self.gl.delete_framebuffer(res.framebuffer);
self.gl.delete_texture(res.texture);
self.gl_resources = Some(Self::allocate_framebuffer(&self.gl, size)?);
}
}
self.gl_resources.unwrap()
} else {
let res = unsafe { Self::allocate_framebuffer(&self.gl, size)? };
self.gl_resources = Some(res);
res
};
let flags = self.mpv.ctx.update().unwrap_or(0);
if flags & mpv_render_update::Frame != 0 {
self.mpv
.ctx
.render::<()>(res.framebuffer.0.get() as i32, size.width, size.height, true)?;
}
Ok(res)
}
fn get_optional_property<T: libmpv2::GetData>(&self, name: &str) -> Result<Option<T>, BackendError> {
match self.mpv.mpv.get_property(name) {
Ok(value) => Ok(Some(value)),
Err(libmpv2::Error::Raw(e)) if e == mpv_error::PropertyUnavailable => Ok(None),
Err(e) => Err(BackendError::Mpv(e)),
}
}
#[expect(unsafe_op_in_unsafe_fn)]
unsafe fn allocate_framebuffer(
gl: &glow::Context,
size: FramebufferSize,
) -> Result<GlResources, BackendError> {
let FramebufferSize { width, height } = size;
let texture = gl.create_texture().map_err(BackendError::Glow)?;
gl.bind_texture(glow::TEXTURE_2D, Some(texture));
gl.tex_image_2d(
glow::TEXTURE_2D,
0,
glow::RGBA8 as i32,
width,
height,
0,
glow::RGBA,
glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(None),
);
for (name, val) in [
(glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32),
(glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32),
(glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32),
(glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32),
] {
gl.tex_parameter_i32(glow::TEXTURE_2D, name, val);
}
gl.bind_texture(glow::TEXTURE_2D, None);
let framebuffer = gl.create_framebuffer().map_err(BackendError::Glow)?;
gl.bind_framebuffer(glow::FRAMEBUFFER, Some(framebuffer));
gl.framebuffer_texture_2d(
glow::FRAMEBUFFER,
glow::COLOR_ATTACHMENT0,
glow::TEXTURE_2D,
Some(texture),
0,
);
let status = gl.check_framebuffer_status(glow::FRAMEBUFFER);
if status != glow::FRAMEBUFFER_COMPLETE {
gl.bind_framebuffer(glow::FRAMEBUFFER, None);
return Err(BackendError::IncompleteFramebuffer(status));
}
gl.bind_framebuffer(glow::FRAMEBUFFER, None);
Ok(GlResources {
texture,
framebuffer,
framebuffer_size: size,
})
}
}