Pyqt Display Video Thumbnail At Given Time Line

44 Views Asked by At

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

1

There are 1 best solutions below

3
duckzio On BEST ANSWER

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