use eframe::glow::{self, HasContext as _};
use libmpv2::Mpv;
use libmpv2::render::{OpenGLInitParams, RenderContext, RenderParam, RenderParamApiType, mpv_render_update};
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)]
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),
}
struct MpvContainer {
ctx: RenderContext<'static>,
mpv: Mpv,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FramebufferSize {
pub width: i32,
pub height: i32,
}
impl FramebufferSize {
pub const fn new(width: i32, height: i32) -> Self { Self { width, height } }
}
pub struct PlayerState {
mpv: MpvContainer,
gl: Arc<glow::Context>,
framebuffer: glow::NativeFramebuffer,
texture: glow::NativeTexture,
framebuffer_size: FramebufferSize,
}
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 fb_size = FramebufferSize::new(1280, 720);
let (framebuffer, texture) =
unsafe { Self::allocate_framebuffer(&gl, fb_size.width, fb_size.height)? };
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)?;
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,
framebuffer,
texture,
framebuffer_size: fb_size,
})
}
pub fn load_file<'p>(&self, paths: impl AsRef<[&'p str]>) -> Result<(), BackendError> {
let paths = paths.as_ref();
self.mpv.mpv.command("loadfile", paths)?;
Ok(())
}
pub fn toggle_pause(&self) -> Result<(), BackendError> {
let paused: bool = self.mpv.mpv.get_property("pause")?;
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(crate) fn get_current_framebuffer(
&mut self,
size: FramebufferSize,
) -> Result<(glow::NativeFramebuffer, FramebufferSize), BackendError> {
if self.framebuffer_size != size && size.width > 0 && size.height > 0 {
unsafe {
self.gl.delete_framebuffer(self.framebuffer);
self.gl.delete_texture(self.texture);
let (fbo, tex) = Self::allocate_framebuffer(&self.gl, size.width, size.height)?;
self.framebuffer = fbo;
self.texture = tex;
}
self.framebuffer_size = size;
}
let flags = self.mpv.ctx.update().unwrap_or(0);
if flags & mpv_render_update::Frame != 0 {
self.mpv
.ctx
.render::<()>(self.framebuffer.0.get() as i32, size.width, size.height, true)?;
}
Ok((self.framebuffer, self.framebuffer_size))
}
pub fn destroy_gl_resources(&self) {
unsafe {
self.gl.delete_framebuffer(self.framebuffer);
self.gl.delete_texture(self.texture);
}
}
#[expect(unsafe_op_in_unsafe_fn)]
unsafe fn allocate_framebuffer(
gl: &glow::Context,
width: i32,
height: i32,
) -> Result<(glow::NativeFramebuffer, glow::NativeTexture), BackendError> {
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((framebuffer, texture))
}
}