1

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

eyllanesc
  • 235,170
  • 19
  • 170
  • 241

3 Answers3

3

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")
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thanks, your answer helped! – Hanna Nezrivnanna Apr 14 '21 at 15:38
  • I'm curently using PySide2 for the mensioned media player project and I can't use code below anymore: `self.mediaPlayer.setVideoOutput( [self.videoWidget.videoSurface(), self.snapshotVideoSurface] ) ` .Because PySide2 doesn't support videoWidget.videoSurface(). Do you know how can I modify my code to make itwork again? Thanks! – Hanna Nezrivnanna May 08 '21 at 21:40
  • @HannaNezrivnanna I have not tested the code with pyside2 but checking the QVideoWidget docs for PySide2 it does have a videoSurface() method, are you using the latest versions of pyside2? If you have a problem then I recommend you create a new post with an MRE (Note: the code you have provided in your post is not an MRE) – eyllanesc May 08 '21 at 22:56
2

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

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