pyqt5: второе видео не воспроизводится: проблема с одновременным QMediaPlayer?

Я в основном создаю графический интерфейс с pyqt5, который должен включать два видео. Для этого я использую QMediaPlayer в сочетании с QVideoWidget, по одному для каждого класса. Дело в том, что пока первое видео воспроизводится как положено, второе отказывается воспроизводиться. Он использует ту же структуру, что и первый (одна кнопка для воспроизведения / паузы и одна ползунок), и ту же структуру кода, но экран остается отчаянно черным при попытке воспроизведения.

Хуже того, если я прокомментирую код для первого видео, второе теперь воспроизводится нормально. Может ли это означать конфликт между двумя QMedialPlayers? Я не могу понять этого.

Любая помощь будет принята с благодарностью.

Вот мой код (графический интерфейс выглядит странно, потому что я удалил большую его часть для ясности):

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

2 ответа

Решение

tl:dr;

Используйте менеджеры по расположению.

Объяснение

Что ж, похоже, вы случайно обнаружили (возможную) ошибку, сделав что-то действительно не так.

QVideoWidget - более сложный виджет, чем кажется, поскольку он взаимодействует с базовой графической системой ОС, и для правильного отображения его содержимого (видео) он должен быть активно уведомлен о его геометрии.

Проще говоря, QVideoWidget не показывает напрямую "картинки" видео, которое показывает QMediaPlayer, но сообщает об этом операционной системе (ну, не совсем так, но мы не будем обсуждать это здесь). Это связано с тем, что для отображения видео может использоваться некоторое аппаратное ускорение или требуется некоторая обработка (например, для HDR-видео), аналогично тому, что делает графика 3D/OpenGL.

Когда программа будет отображать некоторые (систему управляемой) видео, он должен сказать OS об имеющейся геометрии для этого видео, так что ОС может показать его в правильных координатах, и, возможно, применить изменение размеров, некоторые формы "отсечение" (например, если другое окно перекрывается) или любой другой уровень [пост] обработки.


"Что-то действительно не так", о котором я говорил раньше, основано на том факте, что вы используете фиксированную геометрию (размеры и положение) для обоих виджетов видео, и я думаю, что Qt не может уведомлять систему об этих геометриях более чем виджет сразу, если это происходит до фактического отображения окна видео (как в "показано").

Почему это действительно неправильно, помимо рассматриваемой проблемы?

Каждое из наших устройств в основном уникально: то, что вы видите на своем устройстве, будет отображаться (возможно, радикально) по-другому на других.
Для этого есть много причин, в том числе:

  • Операционная система (и версии) и ее поведение;
  • размер экрана и DPI (например, я не смог просмотреть все окно вашего кода, так как у меня экран меньшего размера);
  • размер системного шрифта по умолчанию / индивидуальный; самое главное, если шрифт по умолчанию очень большой, виджеты могут перекрываться;
  • дальнейшая настройка (например, поля и интервалы по умолчанию);
  • если интерфейс "адаптивный", пользователь должен иметь возможность изменять размер интерфейса:
    • если у пользователя экран меньшего размера, размер пользовательского интерфейса следует изменять, чтобы все было видно, вместо того, чтобы перемещать окно за пределы экрана (что иногда невозможно: например, в Windows вы не можете переместить окно над верхним краем экрана);
    • если у пользователя экран большего размера (или используется очень высокая настройка DPI), интерфейс будет слишком маленьким, и некоторые элементы могут быть трудно читать или взаимодействовать с ними;

Это причина, по которой почти любой современный веб-сайт использует "отзывчивые" макеты, которые адаптируют содержимое в соответствии с экраном устройства, на котором они будут отображаться.

Решение очень простое и также решит большую проблему с вашим графическим интерфейсом: избегайте фиксированной геометрии графического интерфейса и используйте вместо этого менеджеры компоновки.

Обратите внимание, что вы по-прежнему можете использовать фиксированные размеры (не позиции, размеры!): Это не такая уж большая проблема, но использование менеджеров компоновки очень поможет вам в этом, переместив все элементы в соответствии с доступным пространством.
Причина в том, что менеджеры компоновки гарантируют, что любая операция изменения размера (что также происходит много раз, когда окно отображается в первый раз) также уведомляется системе всякий раз, когда это требуется (например, адаптация QVideoWidget выход).

Если вы хотите сохранить макет "нижний правый / верхний левый", вы все равно можете сделать это: установите основной QGridLayout для виджета (DNN_Viewer), создайте другой макет сетки для каждого игрока и добавьте этот макет к основному.

Структура будет примерно такой:

+------------------------- DNN_Viewer -------------------------+
|                              | +------ player2Layout ------+ |
|                              | |                           | |
|                              | |            vw2            | |
|                              | |                           | |
|                              | +-------+-------------------+ |
|                              | |  pb2  |        sld1       | |
|                              | +-------+-------------------+ |
+------------------------------+-------------------------------+
| +------ player1Layout------+ |                               |
| |                          | |                               |
| |            vw1           | |                               |
| |                          | |                               |
| +-------+------------------+ |                               |
| |  pb1  |        sld2      | |                               |
| +-------+------------------+ |                               |
+------------------------------+-------------------------------+
class DNN_Viewer(QWidget):             
    # ...
    def init_gui(self):
        # create agrid 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)

Это решит вашу основную проблему (и многие другие, которые вы не рассматривали).


Еще несколько предложений:

  • используйте более информативные имена переменных; вещи какpb2 или lb23кажется более простым в использовании, и вы можете подумать, что короткие переменные означают меньше времени, затрачиваемого на ввод. На самом деле, в этом нет окончательного преимущества: хотя может быть и правда, что более короткие имена переменных могут улучшить скорость компиляции (особенно для интерпретируемых языков, таких как Python), в конечном итоге преимущества почти нет; напротив, вам нужно помнить, что означает "sld2", в то время как что-то вроде "player2Slider" более наглядно и легче читается (что означает, что вы будете читать и отлаживать быстрее, а люди, читающие ваш код, поймут это и поможет вам намного легче)
  • по той же причине, указанной выше, используйте более описательные имена функций: такие имена, как cb_mp1_3буквально ничего не значат; именование действительно важно, и вышеописанное улучшение скорости запуска практически недопустимо для современных компьютеров; это также помогает вам получить помощь от других: потребовалось больше времени, чтобы понять, в чем была ваша реальная проблема, чем понять, что делает ваш код, поскольку все эти имена были для меня почти бессмысленными; подробнее читайте в официальном руководстве по стилю кода Python (также известном как PEP 8);
  • используйте комментарии с умом:
    • избегать чрезмерного комментирования, он делает комментарии отвлекающими, теряя при этом большую часть своей цели (при этом "Пусть код будет документацией" - хорошая концепция, не переоценивайте ее)
    • избегайте комментариев в "причудливом" формате: они могут показаться классными, но в конце концов они просто раздражают; если вы хотите прокомментировать функцию, чтобы лучше описать, что она делает, используйте функцию тройных кавычек, уже предоставляемую Python; также учтите, что многие службы совместного использования кода имеют ограничения на столбцы (и Stackru среди них): людям нужно будет прокручивать каждую строку, чтобы прочитать соответствующий комментарий;
    • если вам нужно описание однострочной функции, возможно, функция не является описательной, как это могло или должно быть, как объяснено выше;
  • быть более последовательным с разделением пустых строк между функциями или классами: Python был создан с учетом удобочитаемости, и следует придерживаться этого принципа;
  • не перезаписывайте существующие имена атрибутов: self.width() а также self.height() являются базовыми свойствами всех QWidgets, и вам может потребоваться частый доступ к ним;
  • быть более последовательным с импортом, который вы используете, особенно со сложными модулями, такими как Qt: вы должны либо импортировать подмодули (from PyQt5 import QtWidgets, ...) или одиночные классы (from PyQt5.QtWidgets import QApplication, ...); обратите внимание, что, хотя последний может считаться более "питоническим", с Qt обычно сложно, так как он имеет сотни классов (и вам могут понадобиться десятки из них в каждом скрипте), тогда вы всегда должны помнить, чтобы добавлять каждый класс каждый раз вам это понадобится, и вы можете импортировать ненужные классы, которые больше не используете; при использовании этого подхода не так много улучшений производительности, по крайней мере, с Qt, особенно если вы забыли удалить ненужный импорт (в вашем случае возможное преимущество импорта отдельных классов полностью отменяется тем фактом, что существует как минимум 10 импортированных классов, которые являются фактически никогда не использовался);
  • избегайте ненужного импорта из других фреймворков, если он не является абсолютно необходимым: если вам нужно знать геометрию экрана, используйте QApplication.screens(), не импортируйте Tk только для этого;

Итак, я снова начал пытаться решить вопрос с очень исчерпывающим ответом музыканта. Это действительно решило проблему, но мне не понравилось решение, которое работало бы только с адаптивным графическим интерфейсом. Поэтому я снова исследовал проблему, начав с минимального графического интерфейса, в котором присутствовали бы только два видео. И, к моему величайшему изумлению, оба видео воспроизводились нормально, даже с графическим интерфейсом фиксированного размера.

Поэтому я снова начал раздувать графический интерфейс, добавляя все элементы, пока не восстановил свой первоначальный графический интерфейс. И в какой-то момент я снова столкнулся с ошибкой, что позволило определить настоящую причину.

Итак, виновник называется... QFrame. Да, по-настоящему. Qframe вызвал весь этот беспорядок. Сначала я использовал QFrame с setFrameShape(QFrame.Panel), так что прямоугольный фрейм создавался сразу. Затем я установил виджет видео внутри рамки. Оказывается, с некоторыми видео QFrame ведет себя странно и как бы "закрывает" вывод видео, заставляя экран средства просмотра видео исчезать. Звук остается неизменным. Это происходит только с некоторыми видео, а не с другими, что не имеет никакого реального смысла. Тем не менее, удаление рамки мгновенно решает проблему, так что это действительно ошибка.

Кажется, что с решением musicamante фрейм не принимает этого странного поведения, следовательно, рабочее решение. Еще одно возможное решение с графическим интерфейсом пользователя фиксированного размера - использовать кадры, не закрывающие видео. Конкретно, вместо использования одного QFrame с setFrameShape(QFrame.Panel), который создает прямоугольник в одном кадре, необходимо использовать набор из четырех кадров, два из которых являются QFrame с setFrameShape(QFrame.Hline), а два других - QFrame с setFrameShape(QFrame.Vline), организованный в виде прямоугольника. Я протестировал, и он работает. Рамки закрывают только горизонтальные / вертикальные поверхности, через которые они проходят, поэтому "внутренняя часть" прямоугольника не является частью какого-либо кадра, что позволяет избежать ошибки.

Другие вопросы по тегам