183 lines
5.7 KiB
Python
183 lines
5.7 KiB
Python
# from https://gitlab.com/robozman/python-mpv-qml-example/-/tree/master/
|
|
# (c) Robozman 2020 - Apache 2.0
|
|
|
|
from PySide2.QtCore import QUrl, QSize, Signal, Slot, Property
|
|
from PySide2.QtGui import QOpenGLFramebufferObject
|
|
import PySide2.QtWidgets as QtWidgets
|
|
|
|
from PySide2.QtQuick import QQuickFramebufferObject, QQuickView
|
|
from PySide2.QtQml import qmlRegisterType
|
|
|
|
import ctypes
|
|
|
|
|
|
from OpenGL import GL, GLX
|
|
|
|
from mpv import MPV, MpvRenderContext, MpvGlGetProcAddressFn
|
|
|
|
|
|
def get_process_address(_, name):
|
|
"""This function allows looking up OpenGL functions."""
|
|
address = GLX.glXGetProcAddress(name.decode("utf-8"))
|
|
return ctypes.cast(address, ctypes.c_void_p).value
|
|
|
|
|
|
class MpvObject(QQuickFramebufferObject):
|
|
"""MpvObject:
|
|
This is a QML widget that can be used to embed the output of a mpv instance.
|
|
It extends the QQuickFramebufferObject class to implement this functionality."""
|
|
|
|
def __init__(self, parent=None):
|
|
print("Creating MpvObject")
|
|
super(MpvObject, self).__init__(parent)
|
|
self.mpv = MPV(ytdl=True, log_handler=print, ytdl_format="bestvideo[height<=?1080][fps<=?30][vcodec!=?vp9]+bestaudio/best")
|
|
self.mpv.pause = True
|
|
self.mpv_gl = None
|
|
self._renderer = None
|
|
self._proc_addr_wrapper = MpvGlGetProcAddressFn(get_process_address)
|
|
self.onUpdate.connect(self.doUpdate)
|
|
self.registered = False
|
|
self.urlToLoad = None
|
|
|
|
self._time_pos = 0
|
|
self._duration = 0
|
|
self._seeking = False
|
|
|
|
def initPlayer(self):
|
|
print('init player')
|
|
self.mpv.observe_property('time-pos', self.observe_time_pos_handler)
|
|
self.mpv.observe_property('duration', self.observe_duration_handler)
|
|
self.mpv.observe_property('seeking', self.observe_seeking_handler)
|
|
self.load(self.urlToLoad)
|
|
|
|
@Signal
|
|
def onUpdate():
|
|
pass
|
|
|
|
def on_update(self):
|
|
"""Function for mpv to call to trigger a framebuffer update"""
|
|
self.onUpdate.emit()
|
|
|
|
def observe_time_pos_handler(self, prop_name, value):
|
|
self._time_pos = value
|
|
self.onTimePosChanged.emit()
|
|
|
|
def observe_duration_handler(self, prop_name, value):
|
|
self._duration = value
|
|
self.onDurationChanged.emit()
|
|
|
|
def observe_seeking_handler(self, prop_name, value):
|
|
self._seeking = value
|
|
self.onSeekingChanged.emit()
|
|
|
|
@Slot(result=None)
|
|
def doUpdate(self):
|
|
"""Slot for receiving the update event on the correct thread"""
|
|
self.update()
|
|
|
|
def createRenderer(self) -> 'QQuickFramebufferObject.Renderer':
|
|
"""Overrides the default createRenderer function to create a
|
|
MpvRenderer instance"""
|
|
print("Calling overridden createRenderer")
|
|
self._renderer = MpvRenderer(self)
|
|
return self._renderer
|
|
|
|
@Slot(str, result=None)
|
|
def load(self, url):
|
|
"""Temporary adapter fuction that allowing playing media from QML"""
|
|
if not self.registered:
|
|
self.urlToLoad = url
|
|
elif url is not None:
|
|
self.mpv.loadfile(url)
|
|
self.pauseChanged.emit()
|
|
|
|
@Slot(int, result=None)
|
|
def seek(self, position):
|
|
if self.registered:
|
|
self.mpv.seek(position, reference='absolute')
|
|
|
|
@Slot(result=None)
|
|
def togglePause(self):
|
|
self.mpv.pause = not self.mpv.pause
|
|
self.pauseChanged.emit()
|
|
|
|
def _paused(self):
|
|
return self.mpv.pause
|
|
|
|
@Signal
|
|
def pauseChanged(self):
|
|
pass
|
|
|
|
paused = Property(bool, _paused, notify=pauseChanged)
|
|
|
|
def _time_pos_get(self):
|
|
return self._time_pos
|
|
|
|
@Signal
|
|
def onTimePosChanged(self):
|
|
pass
|
|
|
|
timePos = Property(int, _time_pos_get, notify=onTimePosChanged)
|
|
|
|
def _duration_get(self):
|
|
return self._duration
|
|
|
|
@Signal
|
|
def onDurationChanged(self):
|
|
pass
|
|
|
|
duration = Property(int, _duration_get, notify=onDurationChanged)
|
|
|
|
|
|
def _seeking_get(self):
|
|
return self._seeking
|
|
|
|
@Signal
|
|
def onSeekingChanged(self):
|
|
pass
|
|
|
|
seeking = Property(bool, _seeking_get, notify=onSeekingChanged)
|
|
|
|
|
|
|
|
class MpvRenderer(QQuickFramebufferObject.Renderer):
|
|
"""MpvRenderer:
|
|
This class implements the QQuickFramebufferObject's Renderer subsystem.
|
|
It augments the base renderer with an instance of mpv's render API."""
|
|
def __init__(self, parent = None):
|
|
print("Creating MpvRenderer")
|
|
super(MpvRenderer, self).__init__()
|
|
self.obj = parent
|
|
self.ctx = None
|
|
self._fbo = None
|
|
|
|
def createFramebufferObject(self, size: QSize) -> QOpenGLFramebufferObject:
|
|
"""Overrides the base createFramebufferObject function, augmenting it to
|
|
create an MpvRenderContext using opengl"""
|
|
if self.obj.mpv_gl is None:
|
|
print("Creating mpv gl")
|
|
self.ctx = MpvRenderContext(self.obj.mpv, 'opengl',
|
|
opengl_init_params={
|
|
'get_proc_address': self.obj._proc_addr_wrapper
|
|
})
|
|
self.ctx.update_cb = self.obj.on_update
|
|
self.obj.registered = True
|
|
self.obj.initPlayer()
|
|
|
|
self._fbo = QQuickFramebufferObject.Renderer.createFramebufferObject(self, size)
|
|
return self._fbo
|
|
|
|
|
|
def render(self):
|
|
"""Overrides the base render function, calling mpv's render functions instead"""
|
|
if self.ctx:
|
|
factor = self.obj.scale()
|
|
rect = self.obj.size()
|
|
|
|
# width and height are floats
|
|
width = int(rect.width() * factor)
|
|
height = int(rect.height() * factor)
|
|
|
|
fbo = GL.glGetIntegerv(GL.GL_DRAW_FRAMEBUFFER_BINDING)
|
|
self.ctx.render(flip_y=False, opengl_fbo={'w': width, 'h': height, 'fbo': fbo})
|