""" MonitorWidget: mpv-based live stream monitor embedded in GTK4. Uses libmpv's OpenGL render API + Gtk.GLArea for proper embedding. No X11 wid hacks — renders directly to a GL texture in the GTK layout. Works on both X11 and Wayland. """ import logging import gi gi.require_version("Gtk", "4.0") from gi.repository import Gtk, GLib, Gdk from cht.ui.mpv import Player log = logging.getLogger(__name__) class MonitorWidget(Gtk.Box): """Widget that embeds mpv video via OpenGL into the GTK4 layout.""" def __init__(self, **kwargs): super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs) self._player = None self._gl_area = Gtk.GLArea() self._gl_area.set_hexpand(True) self._gl_area.set_vexpand(True) self._gl_area.set_auto_render(False) self._gl_area.set_has_depth_buffer(False) self._gl_area.set_has_stencil_buffer(False) self._gl_area.connect("realize", self._on_realize) self._gl_area.connect("unrealize", self._on_unrealize) self._gl_area.connect("render", self._on_render) self.append(self._gl_area) log.info("MonitorWidget initialized (GLArea)") def _on_realize(self, gl_area): """GL context is ready — initialize mpv's render context.""" log.info("GLArea realized") gl_area.make_current() if gl_area.get_error(): log.error("GLArea error: %s", gl_area.get_error()) return def _on_unrealize(self, gl_area): """Clean up mpv render context.""" log.info("GLArea unrealized") if self._player: self._player.terminate() self._player = None def _on_render(self, gl_area, gl_context): """Render mpv's current frame to the GLArea.""" if not self._player: return True # Get the default FBO that GTK4 GLArea renders to fbo = gl_area.get_buffer() if hasattr(gl_area, 'get_buffer') else 0 width = gl_area.get_width() height = gl_area.get_height() # GTK4 GLArea uses its own FBO, get it from GL state import ctypes fbo_id = ctypes.c_int(0) gl = ctypes.cdll.LoadLibrary("libGL.so.1") gl.glGetIntegerv(0x8CA6, ctypes.byref(fbo_id)) # GL_DRAW_FRAMEBUFFER_BINDING self._player.render(fbo_id.value, width, height) return True def _on_mpv_update(self): """Called by mpv when a new frame is ready. Triggers re-render.""" GLib.idle_add(self._gl_area.queue_render) def start_stream(self, source, record_path=None): """Start playing from a URL and optionally record to disk. Args: source: TCP URL (tcp://...), file path, etc. record_path: if set, mpv dumps the raw stream to this file """ self._gl_area.make_current() self._player = Player(record_path=record_path) self._player.init_gl(update_callback=self._on_mpv_update) self._player.play(source) log.info("Monitor streaming from: %s (record=%s)", source, record_path) def stop(self): """Stop playback and release mpv.""" if self._player: log.info("Stopping monitor") self._player.terminate() self._player = None else: log.info("Monitor already stopped")