pythonvideopyqtmedia-playerpyside6

Pyqt Display Video Thumbnail At Given Time Line


I have a basic pyqt video player but I want the video time line background to be pictures of what is happing at that time of the video, what will I have to use to do this efficiently? Here is the basic player:

import sys
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from PySide6.QtMultimediaWidgets import QVideoWidget
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput

class PyVideoPlayer(QWidget):
    def __init__(self, parent=None):
        super(PyVideoPlayer, self).__init__(parent)

        self.mediaPlayer = QMediaPlayer()
        self.audioOutput = QAudioOutput()

        videoWidget = QVideoWidget()

        self.playButton = QPushButton()
        self.playButton.setIcon(QIcon("gui/images/svg_icons/icon_play.svg"))
        self.playButton.setEnabled(False)
        self.playButton.clicked.connect(self.play)

        self.volumeButton = QPushButton()
        self.volumeButton.setIcon(QIcon("gui/images/svg_icons/icon_volume.svg"))
        self.volumeButton.clicked.connect(self.showVolumeSlider)

        self.volumeSlider = QSlider(orientation=Qt.Horizontal)
        self.volumeSlider.setRange(0, 100)
        self.volumeSlider.setValue(self.audioOutput.volume() * 100)
        self.volumeSlider.setMaximumWidth(100)
        self.volumeSlider.sliderMoved.connect(self.setVolume)
        self.volumeSlider.hide()

        self.positionSlider = QSlider(orientation=Qt.Horizontal)
        self.positionSlider.setRange(0, 0)
        self.positionSlider.sliderMoved.connect(self.setPosition)

        self.timeLabel = QLabel()
        self.timeLabel.setStyleSheet("color: #4f5b6e; font-family: 'Roboto'; font-size: 9pt; font-weight: bold;")

        # Set up the layout
        videoLayout = QVBoxLayout()
        videoLayout.addWidget(videoWidget, stretch=1)

        controlLayout = QHBoxLayout()
        controlLayout.addWidget(self.playButton)
        controlLayout.addWidget(self.volumeButton)
        controlLayout.addWidget(self.volumeSlider)
        controlLayout.addWidget(self.timeLabel)

        layout = QVBoxLayout()
        layout.addLayout(videoLayout)
        layout.addWidget(self.positionSlider)
        layout.addLayout(controlLayout)

        layout.setContentsMargins(10, 0, 10, 0)

        self.setLayout(layout)

        self.mediaPlayer.setVideoOutput(videoWidget)
        self.mediaPlayer.playbackStateChanged.connect(self.mediaStateChanged)
        self.mediaPlayer.positionChanged.connect(self.positionChanged)
        self.mediaPlayer.durationChanged.connect(self.durationChanged)
        self.mediaPlayer.errorOccurred.connect(self.handleError)

    def setMedia(self, fileName):
        self.mediaPlayer.setSource(QUrl.fromLocalFile(fileName))
        self.mediaPlayer.setAudioOutput(self.audioOutput)
        self.playButton.setEnabled(True)
        self.play()

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

    def mediaStateChanged(self, state):
        if state == QMediaPlayer.PlaybackState.PlayingState:
            self.playButton.setIcon(QIcon("gui/images/svg_icons/icon_pause.svg"))
        else:
            self.playButton.setIcon(QIcon("gui/images/svg_icons/icon_play.svg"))

    def positionChanged(self, position):
        self.positionSlider.setValue(position)
        self.timeLabel.setText(f"{position // 60000:02}:{(position // 1000) % 60:02} / {self.mediaPlayer.duration() // 60000:02}:{(self.mediaPlayer.duration() // 1000) % 60:02}")

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

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

    def setVolume(self, volume):
        self.audioOutput.setVolume(volume / 100)

    def showVolumeSlider(self):
        if self.volumeSlider.isHidden():
            self.volumeSlider.show()
        else:
            self.volumeSlider.hide()

    def handleError(self):
        self.playButton.setEnabled(False)
        print("Error: " + self.mediaPlayer.errorString())

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = PyVideoPlayer()
    ex.setMedia("your_video.mp4")  # Replace with your video file path
    ex.show()
    sys.exit(app.exec_())

I have tried different things such as using QVideoFrame and then converting to an image using QImage but unsure if its supposed to be used like this.

This is an example of how I want the time line to look:

Example Image


Solution

  • You can do it with QVideoSink with the videoFrame() function to get frame data from QtMultimedia or in another way you can use cv2 (Open CV) to create thumbnails.

    full code:

    import sys
    import cv2
    from PySide6.QtCore import *
    from PySide6.QtGui import *
    from PySide6.QtWidgets import *
    from PySide6.QtMultimediaWidgets import QVideoWidget
    from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput
    
    
    class TimeLineImageWidget(QWidget):
        def __init__(self):
            super(TimeLineImageWidget, self).__init__()
            self.image = QLabel()
            self.lay = QVBoxLayout()
            self.resize(200, 250)
            self.setStyleSheet("background: white;")
            self.lay.addWidget(self.image)
            self.setLayout(self.lay)
            self.show()
    
        def setImage(self, img):
            pixmap = QPixmap(img)
            self.image.setPixmap(
                pixmap.scaled(200, 150, Qt.AspectRatioMode.KeepAspectRatio)
            )
    
        def destry(self):
            self.image.close()
    
    
    class PyVideoPlayer(QWidget):
        def __init__(self, parent=None):
            super(PyVideoPlayer, self).__init__(parent)
    
            self.mediaPlayer = QMediaPlayer()
            self.audioOutput = QAudioOutput()
    
            videoWidget = QVideoWidget()
    
            self.thumbnail_widget = QWidget()
            self.thumbnail = QHBoxLayout()
            self.thumbnail_widget.setLayout(self.thumbnail)
    
            self.playButton = QPushButton()
            self.playButton.setIcon(QIcon("gui/images/svg_icons/icon_play.svg"))
            self.playButton.setEnabled(False)
            self.playButton.clicked.connect(self.play)
    
            self.volumeButton = QPushButton()
            self.volumeButton.setIcon(QIcon("gui/images/svg_icons/icon_volume.svg"))
            self.volumeButton.clicked.connect(self.showVolumeSlider)
    
            self.volumeSlider = QSlider(orientation=Qt.Orientation.Horizontal)
            self.volumeSlider.setRange(0, 100)
            self.volumeSlider.setValue(int(self.audioOutput.volume() * 100))
            self.volumeSlider.setMaximumWidth(100)
            self.volumeSlider.sliderMoved.connect(self.setVolume)
            self.volumeSlider.hide()
    
            self.positionSlider = QSlider(orientation=Qt.Orientation.Horizontal)
            self.positionSlider.setRange(0, 0)
            self.positionSlider.sliderMoved.connect(self.setPosition)
    
            self.timeLabel = QLabel()
            self.timeLabel.setStyleSheet(
                "color: #4f5b6e; font-family: 'Roboto'; font-size: 9pt; font-weight: bold;"
            )
    
            self.timeLineImage = TimeLineImageWidget()
    
            # Set up the layout
            videoLayout = QVBoxLayout()
            videoLayout.addWidget(videoWidget, stretch=1)
    
            controlLayout = QHBoxLayout()
            controlLayout.addWidget(self.playButton)
            controlLayout.addWidget(self.volumeButton)
            controlLayout.addWidget(self.volumeSlider)
            controlLayout.addWidget(self.timeLabel)
    
            layout = QVBoxLayout()
            layout.addLayout(videoLayout)
            layout.addWidget(self.timeLineImage)
            layout.addWidget(self.positionSlider)
            layout.addWidget(self.thumbnail_widget)
            layout.addLayout(controlLayout)
    
            layout.setContentsMargins(10, 0, 10, 0)
    
            self.setLayout(layout)
    
            self.mediaPlayer.setVideoOutput(videoWidget)
            self.mediaPlayer.playbackStateChanged.connect(self.mediaStateChanged)
            self.mediaPlayer.positionChanged.connect(self.positionChanged)
            self.mediaPlayer.durationChanged.connect(self.durationChanged)
            self.mediaPlayer.errorOccurred.connect(self.handleError)
    
        def setMedia(self, fileName):
            self.generate_thumbnail_previews(fileName)
            self.mediaPlayer.setSource(QUrl.fromLocalFile(fileName))
            self.mediaPlayer.setAudioOutput(self.audioOutput)
            self.playButton.setEnabled(True)
            self.play()
    
        def generate_thumbnail_previews(self, url):
            video_capture = cv2.VideoCapture(url)
            total_frames = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
            interval = total_frames // 10  # Generate 10 thumbnails
            thumbnails = []
    
            for i in range(10):
                video_capture.set(cv2.CAP_PROP_POS_FRAMES, i * interval)
                ret, frame = video_capture.read()
                if ret:
                    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)  # Convert frame to RGB
                    height, width, _ = frame.shape
                    img = QPixmap.fromImage(
                        QImage(frame.data, width, height, width * 3, QImage.Format_RGB888)
                    )
                    thumbnails.append(img)
    
            video_capture.release()
    
            # Display thumbnails on the slider
            for i, thumbnail in enumerate(thumbnails):
                label = QLabel()
                label.setPixmap(thumbnail.scaled(100, 100, Qt.KeepAspectRatio))
                self.thumbnail.addWidget(label)
    
        def play(self):
            if self.mediaPlayer.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
                self.mediaPlayer.pause()
            else:
                self.mediaPlayer.play()
    
        def mediaStateChanged(self, state):
            if state == QMediaPlayer.PlaybackState.PlayingState:
                self.playButton.setIcon(QIcon("gui/images/svg_icons/icon_pause.svg"))
            else:
                self.playButton.setIcon(QIcon("gui/images/svg_icons/icon_play.svg"))
    
        def positionChanged(self, position):
            self.positionSlider.setValue(position)
            self.timeLabel.setText(
                f"{position // 60000:02}:{(position // 1000) % 60:02} / {self.mediaPlayer.duration() // 60000:02}:{(self.mediaPlayer.duration() // 1000) % 60:02}"
            )
            # update image timeline
            if self.mediaPlayer.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
                sink = self.mediaPlayer.videoSink()
                self.timeLineImage.setImage(sink.videoFrame().toImage())
    
        def durationChanged(self, duration):
            self.positionSlider.setRange(0, duration)
    
        def setPosition(self, position):
            self.mediaPlayer.setPosition(position)
            # update image timeline
            if self.mediaPlayer.playbackState() != QMediaPlayer.PlaybackState.PlayingState:
                sink = self.mediaPlayer.videoSink()
                self.timeLineImage.setImage(sink.videoFrame().toImage())
    
        def setVolume(self, volume):
            self.audioOutput.setVolume(volume / 100)
    
        def showVolumeSlider(self):
            if self.volumeSlider.isHidden():
                self.volumeSlider.show()
            else:
                self.volumeSlider.hide()
    
        def handleError(self):
            self.playButton.setEnabled(False)
            print("Error: " + self.mediaPlayer.errorString())
    
    
    if __name__ == "__main__":
        try:
            app = QApplication(sys.argv)
            ex = PyVideoPlayer()
            ex.resize(720, 480)
            ex.setMedia("video file.mp4")  # Replace with your video file path
            ex.show()
            sys.exit(app.exec())
        except KeyboardInterrupt:
            exit()