Pyqt在给定时间线上显示视频缩略图

0 投票
1 回答
41 浏览
提问于 2025-04-14 15:30

我有一个简单的pyqt视频播放器,但我想让视频时间轴的背景显示视频中那个时刻发生的画面。我需要用什么方法才能高效地实现这个呢?下面是我基本的播放器代码:

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

我尝试过不同的方法,比如使用QVideoFrame,然后用QImage把它转换成图片,但我不太确定这样用是否正确。

这是我想要的时间轴效果的例子:

示例图片

1 个回答

0

你可以使用 QVideoSink,通过 videoFrame() 这个函数来获取来自 QtMultimedia 的帧数据。另一种方法是使用 cv2(Open CV)来创建缩略图。

完整代码:

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

撰写回答