pythonpyqtpyqt5qmediaplayerqvideowidget

pyqt5: second video does not play: concurrent QMediaPlayer issue?


I am basically building a GUI with pyqt5 supposed to incorporate two videos. To do this, I use QMediaPlayer in combination with QVideoWidget, one for each class. The point is: while the first video plays as expected, the second one refuses to play. It uses exactly the same framework as the first one (one pushbuton for play/pause and one slidebar), and the same structure of code, but the screen remains desperately black when trying to play.

Worse, if I comment the code for the first video, the second now plays normally. Could that mean there is some conflict between the two QMedialPlayers? I can't make sense of that.

Any help would be greatly appreciated.

Here is my code (the GUI looks weird because I have removed most of it for clarity):

from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QLabel, QPushButton, QLineEdit, QFrame, QHBoxLayout, QCheckBox, QRadioButton, QButtonGroup, QStyle, QSlider, QStackedLayout
import sys
from tkinter import Tk
from PyQt5.QtCore import pyqtSlot, QRect, Qt, QRunnable, QThreadPool, QThread, QObject, QUrl, QSize
import time
from PyQt5 import QtMultimedia
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtGui import QFont
from PyQt5.QtGui import QImage, QPalette, QBrush, QIcon, QPixmap


class DNN_Viewer(QWidget):             
    def __init__(self, n_filters=2):
        super(DNN_Viewer, self).__init__()

        # initialise GUI
        self.init_gui()

        # initialise videos to display images
        self.mp1.play()
        self.mp1.pause()
        self.mp2.play()
        self.mp2.pause()


    def init_gui(self):

        # main window
        root = Tk()
        screen_width = root.winfo_screenwidth()                                # screen width
        screen_height = root.winfo_screenheight()                              # screen heigth
        self.width = 1900                                                      # interface width
        self.heigth = 1000                                                     # interface height
        self.left = (screen_width - self.width) / 2                            # left-center interface
        self.top = (screen_height - self.heigth) / 2                           # top-center interface
        self.setFixedSize(self.width, self.heigth)
        self.move(self.left, self.top) 
        self.setStyleSheet("background: white");                               # interface background color        



        # bottom left frame
        self.fm2 = QFrame(self)                                                # creation        
        self.fm2.setGeometry(30, 550, 850, 430)                                # left, top, width, height  
        self.fm2.setFrameShape(QFrame.Panel);                                  # use panel style for frame           
        self.fm2.setLineWidth(1)                                               # frame line width

        # video for weights and gradients
        self.vw1 = QVideoWidget(self)                                          # declare video widget
        self.vw1.move(50,555)                                                  # left, top
        self.vw1.resize(542,380)                                               # width, height
        self.vw1.setStyleSheet("background-color:black;");                     # set black background

        # wrapper for the video
        self.mp1 = QMediaPlayer(self)                                          # declare QMediaPlayer
        self.mp1.setVideoOutput(self.vw1)                                      # use video widget vw1 as output
        fileName = "path_to_video_1"                                           # local path to video
        self.mp1.setMedia(QMediaContent(QUrl.fromLocalFile(fileName)))         # path to video
        self.mp1.stateChanged.connect(self.cb_mp1_1)                           # callback on change state (play, pause, stop)
        self.mp1.positionChanged.connect(self.cb_mp1_2)                        # callback to move slider cursor
        self.mp1.durationChanged.connect(self.cb_mp1_3)                        # callback to update slider range

        # play button for video
        self.pb2 = QPushButton(self)                                           # creation 
        self.pb2.move(50,940)                                                  # left, top     
        self.pb2.resize(40,30)                                                 # width, height
        self.pb2.setIconSize(QSize(18,18))                                     # button text
        self.pb2.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))       # standard triangle icon for play
        self.pb2.clicked.connect(self.cb_pb2)                                  # callback on click (play/pause)

        # position slider for video
        self.sld1 = QSlider(Qt.Horizontal,self)                                # creation
        self.sld1.setGeometry( 110, 940, 482, 30)                              # left, top, width, height  
        self.sld1.sliderMoved.connect(self.cb_sld1)                            # callback on move                    

        # title label
        self.lb23 = QLabel(self)                                               # creation                                     
        self.lb23.setText("Loss and accuracy")                                 # label text
        self.lb23.move(980,10)                                                 # left, top
        self.lb23.setStyleSheet("font-size: 30px; font-family: \
        FreeSans; font-weight: bold")                                          # set font and size

        # top right frame
        self.fm3 = QFrame(self)                                                # creation        
        self.fm3.setGeometry(980, 50, 850, 430)                                # left, top, width, height  
        self.fm3.setFrameShape(QFrame.Panel);                                  # use panel style for frame           
        self.fm3.setLineWidth(1)                                               # frame line width

        # video for loss and accuracy
        self.vw2 = QVideoWidget(self)                                          # declare video widget
        self.vw2.move(1000,55)                                                  # left, top
        self.vw2.resize(542,380)                                               # width, height
        self.vw2.setStyleSheet("background-color:black;");                     # set black background

        # wrapper for the video
        self.mp2 = QMediaPlayer(self)                                          # declare QMediaPlayer
        self.mp2.setVideoOutput(self.vw2)                                      # use video widget vw1 as output

        fileName2 = "path_to_video_2"                                          # local path to video
        self.mp2.setMedia(QMediaContent(QUrl.fromLocalFile(fileName2)))        # path to video
        self.mp2.stateChanged.connect(self.cb_mp2_1)                           # callback on change state (play, pause, stop)
        self.mp2.positionChanged.connect(self.cb_mp2_2)                        # callback to move slider cursor
        self.mp2.durationChanged.connect(self.cb_mp2_3)                        # callback to update slider range

        # play button for video
        self.pb3 = QPushButton(self)                                           # creation 
        self.pb3.move(1000,440)                                                  # left, top     
        self.pb3.resize(40,30)                                                 # width, height
        self.pb3.setIconSize(QSize(18,18))                                     # button text
        self.pb3.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))       # standard triangle icon for play
        self.pb3.clicked.connect(self.cb_pb3)                                  # callback on click (play/pause)

        # position slider for video
        self.sld2 = QSlider(Qt.Horizontal,self)                                # creation
        self.sld2.setGeometry(1060, 440, 482, 30)                              # left, top, width, height  
        self.sld2.sliderMoved.connect(self.cb_sld2)                            # callback on move 




    def cb_mp1_1(self, state):
        if self.mp1.state() == QMediaPlayer.PlayingState:                      # if playing, switch button icon to pause
            self.pb2.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
        elif self.mp1.state() == QMediaPlayer.StoppedState:                    # if stopped, rewind to first image
            self.mp1.play()
            self.mp1.pause()
        else:
            self.pb2.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))   # if paused, switch button icon to play

    def cb_mp1_2(self, position):
        self.sld1.setValue(position)                                           # set slider position to video position     

    def cb_mp1_3(self, duration):
        self.sld1.setRange(0, duration)                                        # set slider range to video position 

    def cb_pb2(self):
        if self.mp1.state() == QMediaPlayer.PlayingState:                      # set to pause if playing
            self.mp1.pause()
        else:
            self.mp1.play()                                                    # set to play if in pause

    def cb_sld1(self, position):            
        self.mp1.setPosition(position)                                         # set video position to slider position



    def cb_mp2_1(self, state):
        if self.mp2.state() == QMediaPlayer.PlayingState:                      # if playing, switch button icon to pause
            self.pb3.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
        elif self.mp2.state() == QMediaPlayer.StoppedState:                    # if stopped, rewind to first image
            self.mp2.play()
            self.mp2.pause()
        else:
            self.pb3.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))   # if paused, switch button icon to play

    def cb_mp2_2(self, position):
        self.sld2.setValue(position)                                           # set slider position to video position     

    def cb_mp2_3(self, duration):
        self.sld2.setRange(0, duration)                                        # set slider range to video position         

    def cb_pb3(self):
        if self.mp2.state() == QMediaPlayer.PlayingState:                      # set to pause if playing
            self.mp2.pause()
        else:
            self.mp2.play()                                                    # set to play if in pause

    def cb_sld2(self, position):            
        self.mp2.setPosition(position)                                         # set video position to slider position


# run GUI


def dnn_viewer():
    app = QApplication(sys.argv)                  # initiate app; sys.argv argument is only for OS-specific settings
    viewer = DNN_Viewer()                         # create instance of Fil_Rouge_Dashboard class
    viewer.show()                                 # display dashboard
    sys.exit(app.exec_())                         # allow exit of the figure by clicking on the top right cross


# call window function
dnn_viewer()

Solution

  • tl:dr;

    Use layout managers.

    Explanation

    Well, it seems you accidentally found a (possible) bug by doing something really wrong.

    QVideoWidget is a widget that is more complex than it seems, since it interfaces itself with the underlying graphics system of the OS, and, in order to correctly show its contents (the video), it has to be actively notified of its geometry.

    Simply speaking, QVideoWidget does not directly show the "pictures" of the video QMediaPlayer shows, but tells the Operating System to do so (well, not exactly, but we won't discuss it here). This is because video displaying might take advantage of some hardware acceleration, or require some processing (for example, for HDR videos), similarly to what 3D/OpenGL graphics does.

    When a program is going to display some (system managed) video, it has to tell the OS about the available geometry for that video, so that the OS is able to show it at the correct coordinates, and possibly apply resizing, some form of "clipping" (if another window is overlayed, for example) or any other level of [post]processing.


    The "something really wrong" I was talking about before is based on the fact that you are using fixed geometries (sizes and positions) for both video widgets, and I think that Qt is not able to notify the system about those geometries for more than a widget at once if that happens before the video window is actually mapped (as in "shown").

    Why is it really wrong, besides the issue at hand?

    Each one of our devices is mostly unique: what you see on your device will be shown in a (possibly radically) different way on other's.
    There are many reasons for that, including:

    That's the reason for which almost any nowadays website uses "responsive" layouts, which adapt the contents according to the screen of the device they're going to be displayed into.

    The solution is very simple, and will also solve the big issue about your GUI: avoid any fixed geometry for your GUI and use layout managers instead.

    Note that you can still use fixed sizes (not positions, sizes!): that's not that big of an issue, but using layout managers will help you a lot with it, by repositioning all elements according to the available space.
    The reason for it is that layout managers ensure that any resizing operation (something that also happens many times as soon as a window is shown the first time) is also notified to the system, whenever it's required (like, for instance, adapting the QVideoWidget output).

    If you want to keep the "bottom-right/top-left" layout, you can still do that: set a main QGridLayout for the widget (DNN_Viewer), create another grid layout for each player and add those layout to the main one.

    The structure will be something like this:

    +------------------------- DNN_Viewer -------------------------+
    |                              | +------ player2Layout ------+ |
    |                              | |                           | |
    |                              | |            vw2            | |
    |                              | |                           | |
    |                              | +-------+-------------------+ |
    |                              | |  pb2  |        sld1       | |
    |                              | +-------+-------------------+ |
    +------------------------------+-------------------------------+
    | +------ player1Layout------+ |                               |
    | |                          | |                               |
    | |            vw1           | |                               |
    | |                          | |                               |
    | +-------+------------------+ |                               |
    | |  pb1  |        sld2      | |                               |
    | +-------+------------------+ |                               |
    +------------------------------+-------------------------------+
    
    class DNN_Viewer(QWidget):             
        # ...
        def init_gui(self):
            # create a grid layout for the widget and automatically set it for it
            layout = QtWidgets.QGridLayout(self)
    
            player1Layout = QtWidgets.QGridLayout()
            # add the layout to the second row, at the first column
            layout.addLayout(player1Layout, 1, 0)
    
            # video for weights and gradients
            self.vw1 = QVideoWidget(self)
            # add the video widget at the first row and column, but set its column
            # span to 2: we'll need to add two widgets in the second row, the play
            # button and the slider
            player1Layout.addWidget(self.vw1, 0, 0, 1, 2)
    
            # ...
    
            self.pb2 = QPushButton(self)
            # add the button to the layout; if you don't specify rows and columns it
            # normally means that the widget is added to a new grid row
            player1Layout.addWidget(self.pb2)
    
            # ...
    
            self.sld1 = QSlider(Qt.Horizontal,self)
            # add the slider to the second row, besides the button
            player1Layout.addWidget(self.sld1, 1, 1)
    
            # ...
    
            player2Layout = QtWidgets.QGridLayout()
            # add the second player layout to the first row, second column
            layout.addLayout(player2Layout, 0, 1)
    
            self.vw2 = QVideoWidget(self)
            # same column span as before
            player2Layout.addWidget(self.vw2, 0, 0, 1, 2)
    
            # ...
    
            self.pb3 = QPushButton(self)
            player2Layout.addWidget(self.pb3, 1, 0)
    
            # ...
    
            self.sld2 = QSlider(Qt.Horizontal,self)
            player2Layout.addWidget(self.sld2, 1, 1)
    

    This will solve your main issue (and lots of others you didn't consider).


    Some further suggestions: