Skip to content

When trying to use libmpv in a QOpenGLWidget, fullscreen leads to black/no video #299

@SumOys

Description

@SumOys

I was taking a look at the PySide/opengl render example that was linked from the Gtk4/Wayland issue:

https://github.com/trin94/qtquick-mpv

Unfortunately my application doesn't use QQuick, but I was able to figure out how to implement a basic QOpenGLWidget using the MpvRenderContext. I wrote this minimal example (it expects a test.mp4) that maps the fullscreen/window to the left right mouse buttons. When going full screen, on both X11 (i3) and Wayland (KDE), the full screen window is black. mpv is still running and you can hear the audio playing, but there's no video rendering.

import sys

from OpenGL.GL import glClearColor
from PySide6.QtGui import QOpenGLContext, Qt
from PySide6.QtWidgets import QPushButton, QVBoxLayout
from PySide6.QtCore import Signal, Slot
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtWidgets import QApplication, QWidget
from mpv import MPV, MpvRenderContext, MpvGlGetProcAddressFn


def get_process_address(_, name):
    print("get_process_address", name.decode('utf-8'))
    glctx = QOpenGLContext.currentContext()
    if glctx is None:
        return 0
    return int(glctx.getProcAddress(name))


class MPVWidget(QOpenGLWidget):

    onUpdate = Signal()

    def __init__(self):
        super().__init__()

        self.setWindowTitle("OpenGL ES 2.0, PySide6, Python")
        self.resize(350, 350)
        print("MpvObject.init")

        # This is necessary since PyQT stomps over the locale settings needed by libmpv.
        # This needs to happen after importing PyQT before creating the first mpv.MPV instance.
        import locale  # noqa
        locale.setlocale(locale.LC_NUMERIC, 'C')

        self.mpv = MPV(vo="libmpv",
                   log_handler=print,
                   input_default_bindings=False,
                   input_vo_keyboard=False,
                   keep_open='yes',
                   msg_level="all=v")
        self._ctx = None
        self._get_proc_address_resolver = MpvGlGetProcAddressFn(get_process_address)
        self.onUpdate.connect(self.update_frame)

    @Slot(str)
    def play(self, url):
        print("MpvObject.play")
        self.mpv.play(url)

    def initializeGL(self):
        glClearColor(0.0, 0.0, 0.0, 1.0)
        self._ctx = MpvRenderContext(
            self.mpv,
            api_type="opengl",
            opengl_init_params={"get_proc_address": self._get_proc_address_resolver},
        )
        self._ctx.update_cb = self.onUpdate.emit

    def resizeGL(self, w, h):
        print(f'MpvObject.resizeGL {w}x{h}')

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.RightButton:
            self.showFullScreen()
        elif event.button() == Qt.MouseButton.LeftButton:
            self.showNormal()

    def paintGL(self):
        if self._ctx:
            rect = self.size()
            scale = self.devicePixelRatio()

            width = int(rect.width() * scale)
            height = int(rect.height() * scale)
            fbo = int(self.defaultFramebufferObject())

            self._ctx.render(
                flip_y=True,
                opengl_fbo={"w": width, "h": height, "fbo": fbo},
            )

    @Slot()
    def update_frame(self):
        self.update()

class MainWidget(QWidget):

    def __init__(self):
        super().__init__()
        self. play_button = QPushButton("Play")
        self.fullscreen_button = QPushButton("Fullscreen")
        self.play_button.clicked.connect(self.click_play)
        self.fullscreen_button.clicked.connect(self.click_fullscreen)
        import locale
        locale.setlocale(locale.LC_NUMERIC, 'C')
        self.mpv_widget = MPVWidget()

        layout = QVBoxLayout()
        layout.addWidget(self.play_button)
        layout.addWidget(self.fullscreen_button)
        layout.addWidget(self.mpv_widget)
        self.setLayout(layout.layout())

    @Slot()
    def click_play(self):
        print("click")
        self.mpv_widget.play("test.mp4")

    @Slot()
    def click_fullscreen(self):
        print("fullscreen")
        self.mpv_widget.setParent(None)
        self.mpv_widget.showFullScreen()

if __name__ == "__main__":
    # Doesn't seem to be needed or have an effect
    # QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseDesktopOpenGL)
    app = QApplication(sys.argv)
    main_widget = MainWidget()
    main_widget.show()
    sys.exit(app.exec())

I found this very basic OpenGL box demo and used the same technique to fullscreen it. It seems to render fine, transitioning between fullscreen and window.

import sys

import numpy as np
from OpenGL.GL import (GL_COLOR_BUFFER_BIT, GL_FLOAT, GL_TRIANGLE_STRIP,
                       glClear, glClearColor, glDrawArrays)
from PySide6.QtCore import Qt
from PySide6.QtOpenGL import QOpenGLBuffer, QOpenGLShader, QOpenGLShaderProgram
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtWidgets import QApplication

# Original: 
# https://github.com/8Observer8/examples-opengles2-webgl-box2d-bulletphysics-openal-web-audio-api/blob/main/shapes/simple-square/qopenglwidget-pyside6-python/main.py

class OpenGLWindow(QOpenGLWidget):

    def __init__(self):
        super().__init__()

        self.setWindowTitle("OpenGL ES 2.0, PySide6, Python")
        self.resize(350, 350)

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.RightButton:
            self.showFullScreen()
        elif event.button() == Qt.MouseButton.LeftButton:
            self.showNormal()


    def initializeGL(self):
        glClearColor(48 / 255, 56 / 255, 65 / 255, 1)

        vertShaderSrc = """
            attribute vec2 aPosition;
            void main()
            {
                gl_Position = vec4(aPosition, 0.0, 1.0);
            }
        """

        fragShaderSrc = """
            #ifdef GL_ES
            precision mediump float;
            #endif
            void main()
            {
                gl_FragColor = vec4(0.2, 0.7, 0.3, 1.0);
            }
        """

        self.program = QOpenGLShaderProgram(self)
        self.program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Vertex, vertShaderSrc)
        self.program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Fragment, fragShaderSrc)
        self.program.link()
        self.program.bind()

        vertPositions = np.array([
            -0.5, -0.5,
            0.5, -0.5,
            -0.5, 0.5,
            0.5, 0.5], dtype=np.float32)
        self.vertPosBuffer = QOpenGLBuffer()
        self.vertPosBuffer.create()
        self.vertPosBuffer.bind()
        self.vertPosBuffer.allocate(vertPositions, len(vertPositions) * 4)

    def resizeGL(self, w, h):
        pass

    def paintGL(self):
        glClear(GL_COLOR_BUFFER_BIT)
        self.program.bind()
        self.vertPosBuffer.bind()
        self.program.setAttributeBuffer("aPosition", GL_FLOAT, 0, 2)
        self.program.enableAttributeArray("aPosition")
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

if __name__ == "__main__":
    QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseDesktopOpenGL)
    app = QApplication(sys.argv)
    w = OpenGLWindow()
    w.show()
    sys.exit(app.exec())

I'm not very familiar with OpenGL or rendering contexts. Is there something I need to be doing specifically with the MpvRenderContext so it can continue rendering video correctly when the widget goes to fullscreen?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions