embeded stream opengl

This commit is contained in:
2026-04-01 15:16:09 -03:00
parent 453601c072
commit bdc5705022
8 changed files with 407 additions and 328 deletions

160
cht/ui/mpv.py Normal file
View File

@@ -0,0 +1,160 @@
"""
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