pyqt5:第二个视频不播放:并发 QMediaPlayer 问题?

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

我基本上是用 pyqt5 构建一个 GUI,应该包含两个视频。为此,我将 QMediaPlayer 与 QVideoWidget 结合使用,每个 class。重点是:当第一个视频按预期播放时,第二个视频拒绝播放。它使用与第一个完全相同的框架(play/pause 的一个按钮和一个滑动条)和相同的代码结构,但在尝试播放时屏幕仍然拼命黑。

更糟糕的是,如果我评论第一个视频的代码,第二个现在可以正常播放。这是否意味着两个 QMediaPlayer 之间存在一些冲突?我无法理解。

如有任何帮助,我们将不胜感激。

这是我的代码(GUI 看起来很奇怪,因为为了清楚起见我删除了大部分代码):

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()

tl:dr;

使用布局管理器。

说明

好吧,看来你是因为做了一些非常错误的事情而无意中发现了一个(可能)错误。

QVideoWidget 是一个 more complex than it seems 的小部件,因为它本身与 OS 的底层图形系统接口,并且为了正确显示其内容(视频),它 主动通知其几何。

简单来说,QVideoWidget并不是直接显示QMediaPlayer显示的视频的"pictures",而是告诉Operating System来显示(嗯,不完全是,这里不讨论)。这是因为视频显示可能会利用一些硬件加速,或者需要一些处理(例如,对于 HDR 视频),类似于 3D/OpenGL 图形。

当程序要显示某些(系统管理的)视频时,它必须告诉OS该视频的可用几何结构,以便OS 能够以正确的坐标显示它,并可能应用调整大小,某种形式的 "clipping"(例如,如果另一个 window 被覆盖)或任何其他级别的 [post]处理中。


我之前谈到的 "something really wrong" 是基于您对两个视频小部件使用固定几何形状(大小和位置)的事实,我 认为 如果在 之前 视频 window 实际映射(如 "shown").

除了手头的问题,为什么真的错了?

我们的每台设备大多 独一无二:您在设备上看到的内容将以(可能完全)不同的方式显示在其他设备上。
原因有很多,包括:

  • 操作系统(和版本)及其行为;
  • 屏幕尺寸和 DPI(例如,我无法查看完整的 window 您的代码,因为我的屏幕较小);
  • default/customized系统字体大小;最重要的是,如果默认字体很大,小部件可能会重叠;
  • 进一步自定义(例如,默认边距和间距);
  • 如果界面是"adaptive",用户应该可以调整界面大小:
    • 如果用户的屏幕较小,ui 应该可以调整大小,以便所有内容都可见,而不需要将 window 移到屏幕边缘之外(有时不可能:例如,在 Windows 上,您不能将 window 移动到屏幕上边距上方);
    • 如果用户有更大的屏幕(或使用非常高的 DPI 设置),界面会太小并且某些元素可能难以阅读或与之交互;

这就是当今几乎所有网站都使用 "responsive" 布局的原因,这些布局会根据要显示的设备屏幕调整内容。

解决方案非常简单,并且还将解决有关 GUI 的 问题:避免为 GUI 使用任何固定几何图形并改用布局管理器。

请注意,您仍然可以使用固定的 大小 (不是位置,大小!):这不是什么大问题,但是使用布局管理器会帮助您很多, 通过根据可用的 space.
重新定位所有元素 原因是布局管理器确保任何调整大小的操作(在第一次显示 window 时也会发生很多次)也会通知系统,只要它是 requi红色(例如,调整 QVideoWidget 输出)。

如果您想保留 "bottom-right/top-left" 布局,您仍然可以这样做: 为小部件设置主 QGridLayout (DNN_Viewer),为每个玩家创建另一个网格布局并将这些布局添加到主布局。

结构将是这样的:

+------------------------- 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)

这将解决您的主要问题(以及您未考虑的许多其他问题)。


一些进一步的建议:

  • 使用更具描述性的变量名; pb2lb23 之类的东西似乎更易于使用,您可能会认为短变量等于更少的输入时间。实际上,没有最终的好处:虽然较短的变量名可能会提高编译速度(特别是对于像Python这样的解释型语言),但最后几乎没有优势;相反,你必须记住 "sld2" 的意思,而像 "player2Slider" 这样的东西更具描述性和更容易阅读(这意味着你会更快地阅读和调试,并且人们会阅读你的代码会理解它并更容易地帮助你)
  • 出于与上述相同的原因,使用更具描述性的函数名称:像 cb_mp1_3 这样的名称实际上没有任何意义;命名真的很重要,上面报告的启动速度的提高对于今天的计算机来说几乎是可以忽略的;它还可以帮助您从其他人那里获得帮助:了解您的实际问题是什么比了解您的代码的作用要花更多的时间,因为所有这些名称对我来说几乎毫无意义;在官方 Style Guide for Python Code(又名 PEP 8)上阅读更多内容;
  • 明智地使用评论:
    • 避免过度评论,它会使评论分散注意力,同时失去大部分目的(也就是说,虽然 "Let the code be the documentation" 是一个好概念,但不要过分夸大它)
    • 避免使用 "fancy" 格式的评论:它们看起来很酷,但最终处理起来很烦人;如果您想注释一个函数以更好地描述它的作用,请使用已经提供的三引号功能 Python;还要考虑到许多代码共享服务都有列限制(Whosebug 就是其中之一):人们需要滚动 每行 才能阅读相应的评论;
    • 如果您需要对单行函数的描述,则该函数可能没有描述性,正如上面所解释的那样;
  • 与函数或 classes 之间的空行分隔更加一致:Python 是在考虑可读性的情况下创建的,遵循该原则是一件好事;
  • 不要覆盖现有的属性名称:self.width() and self.height() 是所有 QWidget 的基本属性,您可能需要经常访问它们;
  • 与您使用的导入更加一致,尤其是对于像 Qt 这样的复杂模块:您应该导入子模块 (from PyQt5 import QtWidgets, ...) 或单个 classes (from PyQt5.QtWidgets import QApplication, ...);请注意,虽然后者可以被认为更多 "pythonic",但它通常对 Qt 来说很棘手,因为它有数百个 classes(每个脚本中可能需要数十个),那么你总是有记住每次需要时都添加每个 class,最终可能会导入您不再使用的不必要的 class;使用这种方法并没有太大的性能改进,至少对于 Qt 而言,尤其是如果您忘记删除不必要的导入(在您的情况下,导入单个 classes 的可能好处被完全取消,因为至少有10 个从未实际使用过的导入 classes);
  • 如果不是绝对必要,请避免从其他框架中进行不必要的导入:如果您需要了解屏幕几何形状,请使用 QApplication.screens(),不要为此导入 Tk;

所以,我又开始尝试用 musicamant 非常详尽的答案来解决这个问题。它确实解决了这个问题,但我对只适用于自适应 GUI 的解决方案不满意。所以我再次调查了这个问题,从一个只有两个视频的最小 GUI 开始。而且,令我惊讶的是,即使使用固定大小的 GUI,这两个视频也能正常播放。

所以我又开始扩充 GUI,添加所有元素,直到恢复我的初始 GUI。在某个时候,我再次遇到了这个错误,这使得确定真正的原因成为可能。

所以罪魁祸首叫做...QFrame。是的,真的。 Qframe 造成了所有这些混乱。起初我使用的是带有 setFrameShape(QFrame.Panel) 的 QFrame,以便一次创建一个矩形框架。然后我在框架内安装了视频小部件。事实证明,对于某些视频,QFrame 采取了一种奇怪的行为和一种 "covers" 视频输出,使视频查看器屏幕消失。声音不受影响。它只发生在某些视频上,而不会发生在其他视频上,这没有任何实际意义。仍然,立即删除框架可以解决问题,所以这确实是一个错误。

似乎使用 musicamante 的解决方案,框架不会采用这种奇怪的行为,因此是一个可行的解决方案。固定大小 GUI 的另一种可能解决方案是使用不覆盖视频的帧。具体来说,不是使用带有 setFrameShape(QFrame.Panel) 的单个 QFrame 在一个帧中创建一个矩形,而是必须使用一组四个帧,其中两个是带有 setFrameShape(QFrame.Hline) 的 QFrame,并且另外两个是带有 setFrameShape(QFrame.Vline) 的 QFrame,组织起来形成一个矩形。我测试了它并且它有效。框架仅覆盖它们经过的 horizontal/vertical 个表面,因此矩形的 "inside" 不是任何框架的一部分,从而避免了错误。