You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyQt6视频播放器切换下一个视频时UI卡顿问题求助

PyQt6多媒体播放器切换视频卡顿问题解决

问题概述

运行PyQt6官方多媒体播放器示例时,点击“Next”切换视频片段会导致UI无响应,只能强制关闭程序。切换逻辑如下:

@Slot()
def next_clicked(self):
    print(f"next_from {self._playlist_index}")
    self._playlist_index = (self._playlist_index + 1) % len(self._playlist)
    self._player.setSource(self._playlist[self._playlist_index])

播放列表有效且索引循环正常,但执行setSource()后UI卡死。环境为Windows 11(最新补丁)+Python 3.11。后续测试发现,短视频(约20秒)或存储在高速设备(如C盘)时问题缓解,推测是同步问题——视频未加载完成就触发播放,导致主线程阻塞。

完整可复现代码:

# see https://doc.qt.io/qtforpython-6.2/examples/example_multimedia__player.html

"""PySide6 Multimedia player example"""

import sys
import os
import glob
from PySide6.QtCore import QStandardPaths, Qt, Slot, QUrl
from PySide6.QtGui import QAction, QIcon, QKeySequence
from PySide6.QtWidgets import (QApplication, QDialog, QFileDialog,
                               QMainWindow, QSlider, QStyle, QToolBar)
from PySide6.QtMultimedia import (QAudioOutput, QMediaFormat,
                                  QMediaPlayer)
from PySide6.QtMultimediaWidgets import QVideoWidget

AVI = "video/x-msvideo"  # AVI

MP4 = 'video/mp4'


def get_supported_mime_types():
    result = []
    for f in QMediaFormat().supportedFileFormats(QMediaFormat.Decode):
        mime_type = QMediaFormat(f).mimeType()
        result.append(mime_type.name())
    return result


def is_directory(path):
    return os.path.isdir(path)


def list_files_with_extension(directory, extension):
    if not extension.startswith('.'):
        extension = '.' + extension
    search_path = os.path.join(directory, '*' + extension)
    return glob.glob(search_path)


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self._playlist = []  # FIXME 6.3: Replace by QMediaPlaylist?
        self._playlist_index = -1
        self._audio_output = QAudioOutput()
        self._player = QMediaPlayer()
        self._player.setAudioOutput(self._audio_output)

        self._player.errorOccurred.connect(self._player_error)

        tool_bar = QToolBar()
        self.addToolBar(tool_bar)

        file_menu = self.menuBar().addMenu("&File")
        icon = QIcon.fromTheme(QIcon.ThemeIcon.DocumentOpen)
        open_action = QAction(icon, "&Open...", self,
                              shortcut=QKeySequence.Open, triggered=self.open)
        file_menu.addAction(open_action)
        tool_bar.addAction(open_action)
        icon = QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit)
        exit_action = QAction(icon, "E&xit", self,
                              shortcut="Ctrl+Q", triggered=self.close)
        file_menu.addAction(exit_action)

        play_menu = self.menuBar().addMenu("&Play")
        style = self.style()
        icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart,
                               style.standardIcon(QStyle.SP_MediaPlay))
        self._play_action = tool_bar.addAction(icon, "Play")
        self._play_action.triggered.connect(self._player.play)
        play_menu.addAction(self._play_action)

        icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaSkipBackward,
                               style.standardIcon(QStyle.SP_MediaSkipBackward))
        self._previous_action = tool_bar.addAction(icon, "Previous")
        self._previous_action.triggered.connect(self.previous_clicked)
        play_menu.addAction(self._previous_action)

        icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackPause,
                               style.standardIcon(QStyle.SP_MediaPause))
        self._pause_action = tool_bar.addAction(icon, "Pause")
        self._pause_action.triggered.connect(self._player.pause)
        play_menu.addAction(self._pause_action)

        icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaSkipForward,
                               style.standardIcon(QStyle.SP_MediaSkipForward))
        self._next_action = tool_bar.addAction(icon, "Next")
        self._next_action.triggered.connect(self.next_clicked)
        play_menu.addAction(self._next_action)

        icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStop,
                               style.standardIcon(QStyle.SP_MediaStop))
        self._stop_action = tool_bar.addAction(icon, "Stop")
        self._stop_action.triggered.connect(self._ensure_stopped)
        play_menu.addAction(self._stop_action)

        #{ Add a Delete button
        icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaRecord,
                               style.standardIcon(QStyle.SP_MediaVolumeMuted))
        self._delete_action = tool_bar.addAction(icon, "Delete")
        self._delete_action.triggered.connect(self._delete_media)
        play_menu.addAction(self._delete_action)
        #} Add a Delete button

        self._volume_slider = QSlider()
        self._volume_slider.setOrientation(Qt.Horizontal)
        self._volume_slider.setMinimum(0)
        self._volume_slider.setMaximum(100)
        available_width = self.screen().availableGeometry().width()
        self._volume_slider.setFixedWidth(available_width / 10)
        self._volume_slider.setValue(self._audio_output.volume())
        self._volume_slider.setTickInterval(10)
        self._volume_slider.setTickPosition(QSlider.TicksBelow)
        self._volume_slider.setToolTip("Volume")
        self._volume_slider.valueChanged.connect(self._audio_output.setVolume)
        tool_bar.addWidget(self._volume_slider)

        icon = QIcon.fromTheme(QIcon.ThemeIcon.HelpAbout)
        about_menu = self.menuBar().addMenu("&About")
        about_qt_act = QAction(icon, "About &Qt", self, triggered=qApp.aboutQt)  # noqa: F821
        about_menu.addAction(about_qt_act)

        self._video_widget = QVideoWidget()
        self.setCentralWidget(self._video_widget)
        self._player.playbackStateChanged.connect(self.update_buttons)
        self._player.setVideoOutput(self._video_widget)

        self.update_buttons(self._player.playbackState())
        self._mime_types = []

    def closeEvent(self, event):
        self._ensure_stopped()
        event.accept()

    @Slot()
    def open(self):
        self._ensure_stopped()
        file_dialog = QFileDialog(self)

        is_windows = sys.platform == 'win32'
        if not self._mime_types:
            self._mime_types = get_supported_mime_types()
            if (is_windows and AVI not in self._mime_types):
                self._mime_types.append(AVI)
            elif MP4 not in self._mime_types:
                self._mime_types.append(MP4)

        file_dialog.setMimeTypeFilters(self._mime_types)

        #efault_mimetype = AVI if is_windows else MP4
        default_mimetype = MP4
        if default_mimetype in self._mime_types:
            file_dialog.selectMimeTypeFilter(default_mimetype)

        movies_location = QStandardPaths.writableLocation(QStandardPaths.MoviesLocation)
        file_dialog.setDirectory(movies_location)
        if file_dialog.exec() == QDialog.Accepted:
            url = file_dialog.selectedUrls()[0]
            print(f"Opening {url.toString()}")
            if url.isLocalFile():
                file_url = url.toLocalFile()
                if is_directory(file_url):
                    self.append_files(file_url)
                else:
                    dir_url = os.path.dirname(file_url)
                    self.append_files(dir_url)
            self._player.play()

    def append_file(self, url):
        self._playlist.append(url)
        self._playlist_index = len(self._playlist) - 1
        self._player.setSource(url)


    def append_files(self, dir_url):
        files = list_files_with_extension(dir_url, 'mp4')
        urls = [QUrl.fromLocalFile(file) for file in files]
        for url in urls:
            self.append_file(url)

    @Slot()
    def _ensure_stopped(self):
        if self._player.playbackState() != QMediaPlayer.StoppedState:
            self._player.stop()

    @Slot()
    def _delete_media(self):
        playing_now = self._playlist[self._playlist_index]
        print(f"Delete {playing_now}")

    @Slot()
    def previous_clicked(self):
        # Go to previous track if we are within the first 5 seconds of playback
        # Otherwise, seek to the beginning.
        if self._player.position() <= 5000 and self._playlist_index > 0:
            self._playlist_index -= 1
            self._playlist.previous()
            self._player.setSource(self._playlist[self._playlist_index])
        else:
            self._player.setPosition(0)

    @Slot()
    def next_clicked(self):
        print(f"next_from {self._playlist_index}")
        self._playlist_index = (self._playlist_index + 1) % len(self._playlist)
        self._player.setSource(self._playlist[self._playlist_index])
        #self._player.play()


    @Slot("QMediaPlayer::PlaybackState")
    def update_buttons(self, state):
        media_count = len(self._playlist)
        self._play_action.setEnabled(media_count > 0 and state != QMediaPlayer.PlayingState)
        self._pause_action.setEnabled(state == QMediaPlayer.PlayingState)
        self._stop_action.setEnabled(state != QMediaPlayer.StoppedState)
        self._previous_action.setEnabled(self._player.position() > 0)
        self._next_action.setEnabled(media_count > 1)

    def show_status_message(self, message):
        self.statusBar().showMessage(message, 5000)

    @Slot("QMediaPlayer::Error", str)
    def _player_error(self, error, error_string):
        print(error_string, file=sys.stderr)
        self.show_status_message(error_string)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_win = MainWindow()
    available_geometry = main_win.screen().availableGeometry()
    main_win.resize(available_geometry.width() / 3,
                    available_geometry.height() / 2)
    main_win.show()
    sys.exit(app.exec())

解决方案

1. 改用QMediaPlaylist管理播放列表

原代码注释已提示推荐使用QMediaPlaylist,它内置了播放状态管理、自动切换逻辑,能避免手动切换时的主线程阻塞问题。

修改步骤:

  1. 导入QMediaPlaylist:
from PySide6.QtMultimedia import QMediaPlaylist
  1. 在__init__中替换手动播放列表:
def __init__(self):
    super().__init__()

    # 替换手动列表为QMediaPlaylist
    self._playlist = QMediaPlaylist()
    self._playlist.setPlaybackMode(QMediaPlaylist.Loop)  # 设置循环播放
    self._playlist_index = 0
    self._audio_output = QAudioOutput()
    self._player = QMediaPlayer()
    self._player.setAudioOutput(self._audio_output)
    self._player.setPlaylist(self._playlist)  # 将播放列表绑定到播放器

    self._player.errorOccurred.connect(self._player_error)
    # 新增:监听播放列表当前索引变化,同步本地索引
    self._playlist.currentIndexChanged.connect(self.update_playlist_index)
  1. 实现索引同步方法:
@Slot(int)
def update_playlist_index(self, index):
    self._playlist_index = index
  1. 修改append_file和append_files方法:
def append_file(self, url):
    self._playlist.addMedia(url)
    # 如果是第一个媒体,自动设置为当前播放项
    if self._playlist.mediaCount() == 1:
        self._playlist.setCurrentIndex(0)
        self._playlist_index = 0

def append_files(self, dir_url):
    files = list_files_with_extension(dir_url, 'mp4')
    urls = [QUrl.fromLocalFile(file) for file in files]
    for url in urls:
        self._playlist.addMedia(url)
    # 初始化播放索引
    if self._playlist.mediaCount() > 0:
        self._playlist.setCurrentIndex(0)
        self._playlist_index = 0
  1. 简化next_clicked和previous_clicked:
@Slot()
def next_clicked(self):
    self._playlist.next()

@Slot()
def previous_clicked(self):
    if self._player.position() <= 5000 and self._playlist.currentIndex() > 0:
        self._playlist.previous()
    else:
        self._player.setPosition(0)

2. 监听媒体加载状态,避免提前播放

如果坚持手动维护播放列表,需监听mediaStatusChanged信号,在媒体加载完成后再触发播放,防止主线程阻塞。

修改步骤:

  1. 在__init__中添加信号连接:
def __init__(self):
    # ... 其他代码 ...
    self._player.mediaStatusChanged.connect(self.on_media_loaded)
  1. 实现加载状态处理方法:
@Slot(QMediaPlayer.MediaStatus)
def on_media_loaded(self, status):
    # 媒体加载完成或缓冲完成后再播放
    if status in (QMediaPlayer.LoadedMedia, QMediaPlayer.BufferedMedia):
        self._player.play()
  1. 修改next_clicked方法,先停止当前播放再设置源,不手动调用play():
@Slot()
def next_clicked(self):
    print(f"next_from {self._playlist_index}")
    self._player.stop()  # 先停止当前播放,释放资源
    self._playlist_index = (self._playlist_index + 1) % len(self._playlist)
    self._player.setSource(self._playlist[self._playlist_index])
    # 播放逻辑交给状态信号处理

3. 额外优化建议

  • 切换视频前必须调用self._player.stop(),释放当前媒体资源
  • 避免在主线程中处理大文件的IO操作,可考虑用QThread预加载下一个视频
  • 确保所有媒体路径使用QUrl.fromLocalFile()转换,减少格式转换开销

内容的提问来源于stack exchange,提问作者radumanolescu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 07:28:10