QVideoWidget content isn't grabed from widget

491 views Asked by At

I've created a simple media player. I want to add the ability to make a snapshots of the displayed video. For this purpose I use 'self.videoWidget.grab()' function. But seems like grab() doesn't work properly for that aim because instead of snapshot I got a picture colored as a wiget background. If I substitute 'self.videoWidget.grab()' with 'snapshot = self.grab()' I got the snapshot of the widget but without videoWidget content on it (pictures are added). I went throw similar questions but found nothing. I'm a newbie with PyQt5 so I hope the solution is obvious, but I failed to find it alone.

from PyQt5.QtWidgets import QPushButton, QStyle, QVBoxLayout, QWidget, QFileDialog, QLabel, QSlider, QHBoxLayout
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from PyQt5.QtCore import QUrl, Qt

class MediaWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Media widget")
        self.initUi()
        self.show()

    def initUi(self):
        # Create media player
        self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.VideoSurface)
        self.videoWidget = QVideoWidget()
        self.mediaPlayer.setVideoOutput(self.videoWidget)
        self.mediaPlayer.durationChanged.connect(self.durationChanged)
        self.mediaPlayer.positionChanged.connect(self.positionChanged)
        self.mediaPlayer.stateChanged.connect(self.mediaStateChanged)

        # Open button configuration
        openButton = QPushButton("Open video")
        openButton.setToolTip("Open video file")
        openButton.setStatusTip("Open video file")
        openButton.setFixedHeight(24)
        openButton.clicked.connect(self.openFile)

        # Snapshot button configuration
        self.snapshotButton = QPushButton("Get snapshot")
        self.snapshotButton.setEnabled(False)
        self.snapshotButton.setShortcut("Ctrl+S")
        self.snapshotButton.setToolTip("Get snapshot (Ctrl+S)")
        self.snapshotButton.setFixedHeight(24)
        self.snapshotButton.clicked.connect(self.getSnapshot)

        # Play button configuration
        self.playButton = QPushButton()
        self.playButton.setEnabled(False)
        self.playButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
        self.playButton.clicked.connect(self.play)

        # Play button configuration
        self.videoSlider = QSlider(Qt.Horizontal)
        self.videoSlider.setRange(0, 0)
        self.videoSlider.sliderMoved.connect(self.setPosition)

        # Create layouts to place inside widget
        contentLayout = QVBoxLayout()
        controlsLayout = QHBoxLayout()
        controlsLayout.addWidget(self.playButton)
        controlsLayout.addWidget(self.snapshotButton)
        controlsLayout.addWidget(self.videoSlider)
        contentLayout.addWidget(self.videoWidget)
        contentLayout.addLayout(controlsLayout)
        contentLayout.addWidget(openButton)
        self.setLayout(contentLayout)

    def openFile(self):
        fileName = QFileDialog.getOpenFileName(self, "Open video", "/home")[0]
        if fileName != '':
            self.mediaPlayer.setMedia(QMediaContent(QUrl.fromLocalFile(fileName)))
            self.playButton.setEnabled(True)
            self.snapshotButton.setEnabled(True)

    def getSnapshot(self):
        snapshot = self.videoWidget.grab()
        snapshot.save("TestFileName", "jpg")

    def play(self):
        if self.mediaPlayer.state() == QMediaPlayer.PlayingState:
            self.mediaPlayer.pause()
        else:
            self.mediaPlayer.play()

    def mediaStateChanged(self, state):
        if state == QMediaPlayer.PlayingState:
            self.playButton.setIcon(
                    self.style().standardIcon(QStyle.SP_MediaPause))
        else:
            self.playButton.setIcon(
                    self.style().standardIcon(QStyle.SP_MediaPlay))

    def durationChanged(self, duration):
        self.videoSlider.setRange(0, duration)

    def positionChanged(self, position):
        self.videoSlider.setValue(position)

    def setPosition(self, position):
        self.mediaPlayer.setPosition(position) 

How window is grabed

What actually is going on in window

3

There are 3 answers

3
eyllanesc On BEST ANSWER

A possible solution is to implement a QAbstractVideoSurface that has the last frame shown:

class SnapshotVideoSurface(QAbstractVideoSurface):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._current_frame = QImage()

    @property
    def current_frame(self):
        return self._current_frame

    def supportedPixelFormats(self, handleType=QAbstractVideoBuffer.NoHandle):
        formats = [QVideoFrame.PixelFormat()]
        if handleType == QAbstractVideoBuffer.NoHandle:
            for f in [
                QVideoFrame.Format_RGB32,
                QVideoFrame.Format_ARGB32,
                QVideoFrame.Format_ARGB32_Premultiplied,
                QVideoFrame.Format_RGB565,
                QVideoFrame.Format_RGB555,
            ]:
                formats.append(f)
        return formats

    def present(self, frame):
        self._current_frame = frame.image()
        return True

Then

def initUi(self):
    self.snapshotVideoSurface = SnapshotVideoSurface(self)
    # Create media player
    self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.VideoSurface)
    self.videoWidget = QVideoWidget()
    # self.mediaPlayer.setVideoOutput(self.videoWidget)
    self.mediaPlayer.setVideoOutput(
        [self.videoWidget.videoSurface(), self.snapshotVideoSurface]
    )
    self.mediaPlayer.durationChanged.connect(self.durationChanged)
    self.mediaPlayer.positionChanged.connect(self.positionChanged)
    self.mediaPlayer.stateChanged.connect(self.mediaStateChanged)
    # ...
def getSnapshot(self):
    image = self.snapshotVideoSurface.current_frame
    if not image.isNull():
        image.save("TestFileName", "jpg")
0
Hanna Nezrivnanna On

The upwoted answer worked for me but after some time I've found an alternative way to solve the described problem. I hope it will help someone. I've used QGraphicsVideoItem instead of QAbstractVideoSurface implementation. It helped me to get rid of redundant QAbstractVideoSurface implementation and simplified the code. Here it's the player creation:

def initUi(self):
    # Create widget to display video frames
    self.graphicsView = QGraphicsView()
    self.scene = QGraphicsScene(self, self.graphicsView)
    self.videoItem = QGraphicsVideoItem()
    self.graphicsView.setScene(self.scene)
    self.graphicsView.scene().addItem(self.videoItem)

    # Create media player
    self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.VideoSurface)
    self.mediaPlayer.setVideoOutput(self.videoItem)
    self.mediaPlayer.durationChanged.connect(self.mediaPlayerDurationChanged)
    self.mediaPlayer.positionChanged.connect(self.mediaPlayerPositionChanged)
    self.mediaPlayer.stateChanged.connect(self.mediaPlayerStateChanged)
    #...

And snapshot is done this way:

    def getSnapshot(self):
    snapshot = self.graphicsView.grab()
0
Hanna Nezrivnanna On

One more alternative that works for me. It helps to avoid implementing QAbstractVideoSurface and keep sources clean.

QPixmap.grabWindow(self.videoWidget.winId()).save("Test4", 'jpg')