embeded stream opengl
This commit is contained in:
160
cht/ui/mpv.py
Normal file
160
cht/ui/mpv.py
Normal 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
|
||||
Reference in New Issue
Block a user