Files
mitus/cht/ui/monitor.py
2026-04-01 15:16:09 -03:00

101 lines
3.3 KiB
Python

"""
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")