""" MPV wrapper using python-mpv (libmpv bindings) with OpenGL render API. Renders video frames to an OpenGL context provided by GTK4's GLArea. No subprocess calls, no X11 wid hacks. """ import ctypes import logging import mpv as libmpv log = logging.getLogger(__name__) def _make_get_proc_address(): """Create a ctypes callback for OpenGL function loading.""" libgl = ctypes.cdll.LoadLibrary("libGL.so.1") libgl.glXGetProcAddressARB.restype = ctypes.c_void_p libgl.glXGetProcAddressARB.argtypes = [ctypes.c_char_p] @ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p) def get_proc_address(_, name): return libgl.glXGetProcAddressARB(name) return get_proc_address _get_proc_address = _make_get_proc_address() class Player: """Wraps a libmpv player with OpenGL render context for GTK4 embedding.""" def __init__(self, record_path=None): opts = { "input_default_bindings": False, "input_vo_keyboard": False, "osc": False, "profile": "low-latency", "cache": "no", "untimed": True, "demuxer_thread": "no", "demuxer_lavf_o": "fflags=+nobuffer", "video_sync": "display-desync", "vo": "libmpv", "hwdec": "auto", } if record_path: opts["stream_record"] = str(record_path) log.info("mpv will record stream to: %s", record_path) log.info("Creating mpv player (OpenGL render mode)") self._player = libmpv.MPV(**opts) self._ctx = None self._update_callback = None log.info("mpv player created") def init_gl(self, update_callback): """Initialize the OpenGL render context. Must be called from a thread with an active GL context (e.g. GLArea realize). Args: update_callback: called by mpv when a new frame is ready to render. Should trigger a GLArea queue_render(). """ self._update_callback = update_callback self._ctx = libmpv.MpvRenderContext( self._player, "opengl", opengl_init_params={ "get_proc_address": _get_proc_address, }, ) # Keep reference to prevent GC of the ctypes callback self._get_proc_address_ref = _get_proc_address self._ctx.update_cb = self._on_mpv_update log.info("mpv OpenGL render context initialized") def _on_mpv_update(self): """Called by mpv from any thread when a new frame is available.""" if self._update_callback: self._update_callback() def render(self, fbo, width, height): """Render the current frame to the given OpenGL FBO. Call from the GLArea render signal handler. """ if self._ctx: self._ctx.render( flip_y=True, opengl_fbo={ "fbo": fbo, "w": width, "h": height, }, ) def play(self, source): """Play from a file path or URL.""" log.info("mpv play: %s", source) self._player.play(str(source)) def play_fd(self, fd): """Play from a raw file descriptor.""" source = f"fd://{fd}" log.info("mpv play from fd: %s", source) self._player.play(source) def pause(self): self._player.pause = True def resume(self): self._player.pause = False @property def paused(self): return self._player.pause def seek(self, seconds): """Seek to absolute position in seconds.""" self._player.seek(seconds, reference="absolute") def seek_relative(self, seconds): """Seek relative to current position.""" self._player.seek(seconds, reference="relative") def stop(self): log.info("mpv stop") self._player.stop() def terminate(self): log.info("mpv terminate") try: if self._ctx: self._ctx.free() self._ctx = None self._player.terminate() except Exception as e: log.warning("mpv terminate error: %s", e) @property def idle(self): return self._player.core_idle @property def duration(self): try: return self._player.duration except Exception: return None @property def time_pos(self): try: return self._player.time_pos except Exception: return None