Python+PyQt5实现多屏幕协同播放功能

2025-04-01 02:50

本文主要是介绍Python+PyQt5实现多屏幕协同播放功能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Python+PyQt5实现多屏幕协同播放功能》在现代会议展示、数字广告、展览展示等场景中,多屏幕协同播放已成为刚需,下面我们就来看看如何利用Python和PyQt5开发一套功能强大的跨屏播控系统吧...

一、项目概述:突破传统播放限制

在现代会议展示、数字广告、展览展示等场景中,多屏幕协同播放已成为刚需。传统播放软件往往存在扩展屏支持不足、操作复杂、功能单一等问题。本项目基于python生态的PyQt5和VLC库,开发了一套功能强大的跨屏播控系统,实现了以下核心突破:

  • 多屏融合控制:支持主屏操作+扩展屏播放的双屏模式
  • 智能媒体识别:自动区分视频/图片格式并适配最佳播放方案
  • 专业级过渡效果:内置淡入淡出等专业转场动画
  • 低代码高扩展:采用面向对象设计,模块化程度高

系统架构图如下:

[主控制界面] ←PyQt5→ [VLC引擎] → {主屏预览/扩展屏输出}

二、核心技术解析

2.1 多屏管理机制

def init_screens(self):
    """创新性的多屏检测方案"""
    try:
        self.screens = screeninfo.get_monitors()
        if len(self.screens) > 1:
            self.ext_screen = self.screens[1]
            self._create_video_window()
            self._hide_taskbar()  # 自动隐藏扩展屏任务栏
    except Exception as e:
        self._create_fallback_window()  # 优雅降级处理

关键技术点:

  • 使用screeninfo库动态获取显示器配置
  • HWND窗口绑定实现精确到像素的跨屏控制
  • 异常情况下的单屏兼容模式

2.2 播放引擎设计

系统采用双VLC实例架构:

播放器:带音频输出的完整渲染

预览播放器:静音状态的实时同步

self.instance = vlc.Instance("--aout=directsound")
self.main_player = self.instance.media_player_new()
self.preview_player = self.instance.media_player_new()
self.preview_player.audio_set_mute(True)  # 预览静音

2.3 专业级转场动画

通过Qt动画框架实现广播级效果:

def start_fade_in_animation(self):
    """音量淡入曲线动画"""
    self.fade_animation = QPropertyAnimation(self, b"volume")
    self.fade_animation.setEasingCurve(QEasingCurve.InOutCirc)
    self.fade_animation.start()

三、功能使用详解

3.1 基础操作流程

1.添加媒体文件:

  • 支持拖拽添加/文件对话框多选
  • 自动识别视频(jpg/png等)和图片格式

2.播放模式选择:

  • 连续播放:列表循环
  • 单次播放:适合重要内容展示

3.多屏输出切换:

  • 扩展模式:主控+扩展屏输出
  • 主屏模式:仅主界面播放
  • 双屏模式:镜像输出

3.2 高级功能

定时截图预览:

def update_preview(self):
    if self.main_player.video_take_snapshot(0, temp_file, 0, 0) == 0:
        # 异步处理截图文件
        QTimer.singleShot(100, self._process_snapshot)

智能记忆播放:

  • 记录上次退出时的播放位置
  • 异常中断后自动恢复现场

四、性能优化方案

4.1 资源管理

采用懒加载策略初始化VLC实例

动态释放已完成播放的媒体资源

4.2 线程安全

pythoncom.CoInitialize()  # COM组件初始化
try:
    # VLC多线程操作
finally:
    pythoncom.CoUninitialize()

4.3 渲染优化

视频:硬件加速解码

图片:Qt原生渲染引擎

五、扩展开发方向

1.网络推流功能:

":sout=#transcode{vcodec=h264}:rtp{dst=192.168.1.100,port=1234}"

2.定时任务模块:

  • 基于cron的自动化播放计划
  • 节假日特殊排期支持
  • API接口扩展:
  • RESTful控制接口
  • WebSocket实时状态推送

六、效果展示

Python+PyQt5实现多屏幕协同播放功能

七、相关源码

import sys
import os
import json
import screeninfo
import win32gui
import win32con
import pythoncom  # 修正:使用pythoncom替代win32com.client
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QvboxLayout, QHBoxLayout, 
                            QListWidget, QPushButton, QFileDialog, QLabel, QSlider, 
                            QComboBox, QGroupBox, QSizePolicy)
from PyQt5.QtCore import Qt, QPropertyAnimation, QEasingCurve, QTimer, pyqtProperty
from PyQt5.QtGui import QImage, QPixmap, QIcon, QColor, QLinearGradient, QPainter, QFont
import vlc
from vlc import State

class StyledGroupBox(QGroupBox):
    def __init__(self, title="Python+PyQt5实现多屏幕协同播放功能", parent=None):
        super().__init__(title, parent)
        self.setStyleSheet("""
            QGroupBox {
                border: 2px solid #2a82da;
                border-radius: 8px;
                margin-top: 10px;
                padding-top: 15px;
                background-color: rgba(20, 30, 50, 180);
                color: #ffffff;
                font-weight: bold;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 10px;
                padding: 0 5px;
            }
        """)

class StyledButton(QPushButton):
    def __init__(self, text="", parent=None):
        super().__init__(text, parent)
        self.setStyleSheet("""
            QPushButton {
                background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                                                stop:0 #3a7bd5, stop:1 #00d2ff);
                border: 1px solid #2a82da;
                border-radius: 5px;
                color: white;
                padding: 5px;
                font-weight: bold;
                min-width: 80px;
            }
            QPushButton:hover {
                background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                                                stop:0 #4a8be5, stop:1 #10e2ff);
                border: 1px solid #3a92ea;
            }
            QPushButton:pressed {
                background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                                                stop:0 #2a6bc5, stop:1 #00c2ef);
                padding-top: 6px;
                padding-bottom: 4px;
            }
        """)

class StyledListWidget(QListWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setStyleSheet("""
            QListWidget {
                background-color: rgba(30, 40, 60, 200);
                border: 1px solid #2a82da;
                border-radius: 5px;
                color: #ffffff;
                font-size: 12px;
                padding: 5px;
            }
            QListWidget::item {
                border-bottom: 1px solid rgba(42, 130, 218, 50);
                padding: 5px;
            }
            QListWidget::item:selected {
                background-color: rgba(42, 130, 218, 150);
                color: white;
            }
            QScrollBar:vertical {
                border: none;
                background: rgba(30, 40, 60, 200);
                width: 10px;
                margin: 0px;
            }
            QScrollBar::handle:vertical {
                background: #2a82da;
                min-height: 20px;
                border-radius: 4px;
            }
            QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
                height: 0px;
            }
        """)

class StyledSlider(QSlider):
    def __init__(self, orientation=Qphpt.Horizontal, parent=None):
        super().__init__(orientation, parent)
        if orientation == Qt.Horizontal:
            self.setStyleSheet("""
                QSlider::groove:horizontal {
                    height: 6px;
                    background: rgba(30, 40, 60, 200);
                    border-radius: 3px;
                }
                QSlider::sub-page:horizontal {
                    background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
                                              stop:0 #3a7bd5, stop:1 #00d2ff);
                    border-radius: 3px;
                }
                QSlider::add-page:horizontal {
                    background: rgba(42, 130, 218, 50);
                    border-radius: 3px;
                }
                QSlider::handle:horizontal {
                    width: 14px;
                    margin: -4px 0;
                    background: qradialgradient(cx:0.5, cy:0.5, radius:0.5,
                                              fx:0.5, fy:0.5,
                                              stop:0 #ffffff, stop:1 #2a82da);
                    border-radius: 7px;
                }
            """)
        else:
            self.setStyleSheet("""
                QSlider::groove:vertical {
                    width: 6px;
                    background: rgba(30, 40, 60, 200);
                    border-radius: 3px;
                }
                QSlider::sub-page:vertical {
                    background: qlineargradient(x1:0, y1:1, x2:0, y2:0,
                                              stop:0 #3a7bd5, stop:1 #00d2ff);
                    border-radius: 3px;
                }
                QSlider::add-page:vertical {
                    background: rgba(42, 130, 218, 50);
                    border-radius: 3px;
                }
                QSlider::handle:vertical {
                    height: 14px;
                    margin: 0 -4px;
                    background: qradialgradient(cx:0.5, cy:0.5, radius:0.5,
                                              fx:0.5, fy:0.5,
                                              stop:0 #ffffff, stop:1 #2a82da);
                    border-radius: 7px;
                }
            """)

class StyledComboBox(QComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setStyleSheet("""
            QComboBox {
                background-color: rgba(30, 40, 60, 200);
                border: 1px solid #2a82da;
                border-radius: 5px;
                color: white;
                padding: 5px;
                padding-left: 10px;
                min-width: 100px;
            }
            QComboBox:hover {
                border: 1px solid #3a92ea;
            }
            QComboBox::drop-down {
                subcontrol-origin: padding;
                subcontrol-position: top right;
                width: 20px;
                border-left: 1px solid #2a82da;
                border-top-right-radius: 5px;
                border-bottom-right-radius: 5px;
            }
            QComboBox::down-arrow {
                image: url(none);
                width: 10px;
                height: 10px;
            }
            QComboBox QAbstractItemView {
                background-color: rgba(30, 40, 60, 200);
                border: 1px solid #2a82da;
                selection-background-color: rgba(42, 130, 218, 150);
                color: white;
            }
        """)

class ExtendedScreenPlayer(QMainWindow):
    def __init__(self):
        super().__init__()
        pythoncom.CoInitialize()  # 修正:使用pythoncom进行COM初始化
        
        # 初始化变量
        self.playlist = []
        self.current_index = -1
        self.instance = vlc.Instance("--aout=directsound")
        self.main_player = self.instance.media_player_new("--aout=directsound")
        self.preview_player = self.instance.media_player_new("--aout=directsound")
        self.mode = "扩展模式"
        self.screen_modes = ["扩展模式", "主屏模式", "双屏模式"]
        self.play_mode = True
        self.current_volume = 100
        self._volume = 100
        
        # 初始化UI
        self.setup_ui_style()
        self.init_ui()
        self.init_screens()
        
        # 初始化定时器
        self.media_timer = QTimer(self)
        self.media_timer.timeout.connect(self.update_media_status)
        self.media_timer.start(200)
        
        # 初始化动画相关
        self.fade_timer = QTimer(self)
        self.fade_timer.timeout.connect(self.fade_process)
        self.fade_duration = 8000
        self.fade_steps = 30
        self.fade_step_interval = self.fade_duration // self.fade_steps
        self.fade_animation = None
        self.fading_out = False
        self.fading_in = False
        
        # 显示初始界面
        self.show()
        self.show_home_screen()
        self.setAcceptDrops(True)
        
        self.playback_paused = False  # 新增暂停状态标记
        self.current_media_position = 0  # 记录当前播放位置

    def setup_ui_style(self):
        """设置全局UI样式"""
        self.setStyleSheet("""
            QMainWindow {
                background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1,
                                                stop:0 #0f2027, stop:1 #2c5364);
                color: #ffffff;
            }
            QLabel {
                color: #ffffff;
                font-size: 12px;
            }
            QLabel#status_label {
                font-size: 14px;
                font-weight: bold;
                padding: 5px;
                background-color: rgba(20, 30, 50, 180);
                border-radius: 5px;
                border: 1px solid #2a82da;
            }
        """)
        
        # 设置全局字体
        font = QFont()
        font.setFamily("Arial")
        font.setPointSize(10)
        QApplication.setFont(font)

    def init_ui(self):
        """初始化用户界面"""
        self.setWindowTitle('大屏播控系统')
        self.setWindowIcon(QIcon('icon.png')) if os.path.exists('icon.png') else None
        self.setGeometry(100, 100, 1200, 800)
        
        # 主布局
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QHBoxLayout()
        main_widget.setLayout(main_layout)
        
        # 左侧控制面板
        control_panel = StyledGroupBox("控制面板")
        control_layout = QVBoxLayout()
        control_panel.setLayout(control_layout)
        control_panel.setFixedwidth(450)
        
        # 播放列表
        self.playlist_widget = StyledListWidget()
        self.playlist_widget.itemDoubleClicked.connect(self.play_selected_item)
        control_layout.addWidget(QLabel("播放列表:"))
        control_layout.addWidget(self.playlist_widget)
                
        # 播放控制按钮
        btn_layout = QHBoxLayout()
        controls = [
            ('', self.show_home_screen, '返回首页画面'),
            ('⏮', self.prev_item, '播放上一项'),
            ('⏯', self.toggle_play, '播放/暂停'),
            ('⏹', self.stop, '停止播放'),
            ('⏭', self.next_item, '播放下一项')
        ]
        for text, callback, tip in controls:
            btn = StyledButton(text)
            btn.clicked.connect(callback)
            btn.setFixedSize(70, 50)
            btn.setToolTip(tip)
            btn.setStyleSheet("""
                QPushButton {
                    font-size: 20px;
                    min-width: 30px;
                }
            """)
            btn_layout.addWidget(btn)
        control_layout.addLayout(btn_layout)
        
        # 进度条
        self.position_slider = StyledSlider(Qt.Horizontal)
        self.position_slider.setRange(0, 1000)
        self.position_slider.sliderMoved.connect(self.set_position)
        control_layout.addWidget(self.position_slider)
        
        # 音量控制
        volume_layout = QHBoxLayout()
        volume_layout.addWidget(QLabel("音量:"))
        self.volume_slider = StyledSlider(Qt.Horizontal)
        self.volume_slider.setRange(0, 100)
        self.volume_slider.setValue(100)
        self.volume_slider.valueChanged.connect(self.set_volume)
        volume_layout.addWidget(self.volume_slider)
        control_layout.addLayout(volume_layout)
        
        # 文件操作按钮
        file_btn_layout = QHBoxLayout()
        file_controls = [
            ('添加文件', self.add_files),
            ('删除选中', self.remove_selected),
            ('清空列表', self.clear_playlist)
        ]
        for text, callback in file_controls:
            btn = StyledButton(text)
            btn.clicked.connect(callback)
            file_btn_layout.addWidget(btn)
        control_layout.addLayout(file_btn_layout)
        
        # 播放模式选择
        self.mode_combo = StyledComboBox()
        self.mode_combo.addItems(self.screen_modes)
        self.mode_combo.currentTextChanged.connect(self.change_mode)
        control_layout.addWidget(QLabel("播放模式:"))
        control_layout.addWidget(self.mode_combo)
        
        # 播放模式切换按钮
        self.play_mode_btn = StyledButton('连续播放')
        self.play_mode_btn.clicked.connect(self.toggle_play_mode)
        control_layout.addWidget(self.play_mode_btn)
        
        # 列表管理按钮
        list_btn_layout = QHBoxLayout()
        list_controls = [
            ('保存列表', self.save_playlist),
            ('加载列表', self.load_playlist)
        ]
        for text, callback in list_controls:
            btn = StyledButton(text)
            btn.clicked.connect(callback)
            list_btn_layout.addWidget(btn)
        control_layout.addLayout(list_btn_layout)
        
        # 右侧预览区域
        preview_panel = StyledGroupBox("预览")
        preview_layout = QVBoxLayout()
        preview_panel.setLayout(preview_layout)
        
        # 视频预览窗口
        self.preview_window = QLabel()
        self.preview_window.setAlignment(Qt.AlignCenter)
        self.preview_window.setStyleSheet("""
            QLabel {
                background-color: black;
                border: 2px solid #2a82da;
                border-radius: 5px;
            }
        """)
        self.preview_window.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
        preview_layout.addWidget(self.preview_window)
        
        # 状态栏
        self.status_bar = QLabel('大屏准备就绪')
        self.status_bar.setObjectName("status_label")
        preview_layout.addWidget(self.status_bar)
        
        # 主布局添加组件
        main_layout.addWidget(control_panel)
        main_layout.addWidget(preview_panel)

    def init_screens(self):
        """初始化屏幕配置"""
        try:
            self.screens = screeninfo.get_monitors()
            if len(self.screens) > 1:
                self.ext_screen = self.screens[1]
                self._create_video_window()
                self._hide_taskbar()
            else:
                self.status_bar.setText('警告:未检测到扩展屏幕,将使用主屏幕播放!')
                self._create_fallback_window()
        except Exception as e:
            self.status_bar.setText(f'屏幕检测失败: {str(e)}')
            self._create_fallback_window()

    def _create_video_window(self):
        """创建扩展屏播放窗口"""
        self.video_window = QWidget()
        self.video_window.setWindowTitle('扩展屏幕播放器')
        self.video_window.setGeometry(
            self.ext_screen.x, self.ext_screen.y,
            self.ext_screen.width, self.ext_screen.height
        )
        self.video_window.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool)
        self.video_window.setStyleSheet("background-color: black;")
        self.video_window.showFullScreen()

    def _create_fallback_window(self):
        """创建集成到主界面右侧的监看窗口"""
        self.video_window = self.preview_window
        self.preview_player.set_hwnd(0)

    def change_mode(self, mode):
        """切换播放模式"""
        self.mode = mode
        if mode == "扩展模式" and hasattr(self, 'ext_screen'):
            self._create_video_window()
        else:
            if hasattr(self, 'video_window') and self.video_window != self.preview_window:
                self.video_window.close()
            self.video_window = self.preview_window

    def toggle_play_mode(self):
        """切换播放模式"""
        self.play_mode = not self.play_mode
        self.play_mode_btn.setText('连续播放' if self.play_mode else '单个播放')

    def play_selected_item(self, item):
        """处理双击播放列表项事件"""
        row = self.playlist_widget.row(item)
        self.play_item(row)

    def play_item(self, index):
        """播放指定索引的媒体"""
        if 0 <= index < len(self.playlist):
            self.current_index = index
            file_path = self.playlist[index]
            is_image = file_path.lower().endswith(('.jpg', '.jpeg', '.png'))
            
            if not self.play_mode and is_image:
                self._setup_single_image_playback()
                if hasattr(self, 'video_window') and self.video_window != self.preview_window:
                    for child in self.video_window.findChildren(QLabel):
                        child.deleteLater()
                try:
                    pixmap = QPixmap(file_path)
                    if pixmap.isNull():
                        raise ValueError("图片加载失败")
                        
                    # 在主预览窗口显示
                    scaled_pixmap = pixmap.scaled(
                        QSize(800, 600),
                        Qt.KeepASPectRatio,
                        Qt.SmoothTransformation
                    )
                    self.preview_window.setPixmap(scaled_pixmap)
                    
                    # 扩展屏显示逻辑
                    if hasattr(self, 'video_window') and self.video_window != self.preview_window:
                        ext_label = QLabel(self.video_window)
                        ext_pixmap = pixmap.scaled(
                            QSize(800, 600),
                            Qt.KeepAspectRatio,
                            Qt.SmoothTransformation
                        )
                        ext_label.setPixmap(ext_pixmap)
                        ext_label.setAlignment(Qt.AlignCenter)
                        ext_label.show()
                    
                    self.status_bar.setText(f'正在显示: {os.path.basename(file_path)}')
                    return
                except Exception as e:
                    self.status_bar.setText(f'错误: {str(e)}')
                    self.show_home_screen()
                    return
            else:
                # 如果是单个播放模式且不是图片,先显示首页
                if not self.play_mode and not is_image:
                    self.show_home_screen()
                if not self.play_mode:
                    self.main_player.event_manager().event_attach(
                        vlc.EventType.MediaPlayerEndReached, 
                        self._on_single_play_end
                    )
            
            media = self.instance.media_new(self.playlist[index])
            # 主播放器设置
            self.main_player.stop()
            self.main_player.set_media(media)
            
            # 预览播放器设置(静音且独立)
            self.preview_player.stop()
            self.previephpw_player.set_media(media)
            self.preview_player.audio_set_mute(True)
            
            # 窗口绑定
            self.main_player.set_hwnd(0)
            self.preview_player.set_hwnd(0)
            
            if self.video_window and self.video_window != self.preview_window:
                # 双屏模式:主输出到扩展屏,预览输出到主界面
                self.main_player.set_hwnd(self.video_window.winId())
                self.preview_player.set_hwnd(self.preview_window.winId())
            elhttp://www.chinasem.cnse:
                # 单屏模式:主播放器输出到预览窗口
                self.main_player.set_hwnd(self.preview_window.winId())
                self.preview_player.set_hwnd(0)
            
            # 同步启动播放
            self.main_player.play()
            if self.video_window != self.preview_window:
                self.preview_player.play()
            self.fading_in = True
            self.fade_timer.start(self.fade_step_interval)
            self.start_fade_in_animation()
            
            # 更新状态和列表选择
            self.status_bar.setText(f'正在播放: {os.path.basename(self.playlist[index])}')
            self.playlist_widget.setCurrentRow(index)

    def fade_process(self):
        """处理音量渐变过程"""
        if self.fading_in:
            progress = self.fade_timer.remainingTime() / self.fade_duration
            new_volume = int(100 * (1 - progress) ** 3)
            self.set_volume(new_volume)
            if progress <= 0:
                self.fading_in = False
                self.fade_timer.stop()
        elif self.fading_out:
            progress = self.fade_timer.remainingTime() / self.fade_duration
            new_volume = int(100 * progress ** 3)
            self.set_volume(new_volume)
            if progress <= 0:
                self.fading_out = False
                self.fade_timer.stop()
                QTimer.singleShot(200, lambda: [self.main_player.stop(), self.preview_player.stop()])

    def start_fade_in_animation(self):
        """启动淡入动画"""
        self.fade_animation = QPropertyAnimation(self, b"volume")
        self.fade_animation.setDuration(self.fade_duration)
        self.fade_animation.setStartValue(0)
        self.fade_animation.setEndValue(100)
        self.fade_animation.setEasingCurve(QEasingCurve.InOutCirc)
        self.fade_animation.start()

    def update_preview(self):
        """更新预览画面"""
        if hasattr(self, 'video_window') and self.video_window != self.preview_window:
            if self.main_player.is_playing():
                try:
                    if self.main_player.video_get_size()[0] > 0:
                        temp_file = f"preview_{id(self)}.jpg"
                        if self.main_player.video_take_snapshot(0, temp_file, 0, 0) == 0:
                            retry = 3
                            while retry > 0 and not os.path.exists(temp_file):
                                QApplication.processEvents()
                                retry -= 1
                            
                            if os.path.exists(temp_file):
                                pixmap = QPixmap(temp_file)
                                if not pixmap.isNull():
                                    target_size = QSize(800, 600)
                                    scaled_pixmap = pixmap.scaled(
                                        target_size,
                                        Qt.KeepAspectRatio,
                                        Qt.SmoothTransformation
                                    )
                                    self.preview_window.setPixmap(scaled_pixmap)
                                os.remove(temp_file)
                except Exception as e:
                    print(f"预览更新失败: {str(e)}")
        else:
            grad = QLinearGradient(0, 0, self.preview_window.width(), 0)
            grad.setColorAt(0, QColor(42, 130, 218))
            grad.setColorAt(1, QColor(0, 210, 255))
            
            placeholder = QPixmap(self.preview_window.size())
            placeholder.fill(Qt.transparent)
            painter = QPainter(placeholder)
            painter.setPen(Qt.NoPen)
            painter.setBrush(grad)
            painter.drawRoundedRect(placeholder.rect(), 10, 10)
            painter.setFont(QFont("微软雅黑", 14))
            painter.drawText(placeholder.rect(), Qt.AlignCenter, "主画面播放中")
            painter.end()
            self.preview_window.setPixmap(placeholder)
        
        QTimer.singleShot(500, self.update_preview)

    def set_position(self, position):
        if self.main_player.is_playing():
            self.current_media_position = position / 1000.0
            self.main_player.set_position(self.current_media_position)

    def _ensure_media_loaded(self):
        if not self.main_player.get_media():
            media = self.instance.media_new(self.playlist[self.current_index])
            self.main_player.set_media(media)
            self.preview_player.set_media(media)

    def update_media_status(self):
        """更新媒体状态"""
        if self.main_player.is_playing():
            position = self.main_player.get_position() * 1000
            self.position_slider.setValue(int(position))
            
            if abs(self.preview_player.get_position() - self.main_player.get_position()) > 0.01:
                self.preview_player.set_position(self.main_player.get_position())
            
            if self.mode != "扩展模式" or not hasattr(self, 'ext_screen'):
                self.update_preview()
        else:
            if self.main_player.get_state() == vlc.State.Ended and self.playlist:
                if self.play_mode:
                    self.next_item()
                else:
                    self.stop()
                    self.show_home_screen()

    def toggle_play(self):
        if self.main_player.is_playing():
            self.main_player.pause()
            self.playback_paused = True
            self.status_bar.setText('已暂停')
        else:
            if self.playlist:
                if self.playback_paused:
                    # 恢复播放时保持当前位置
                    self.main_player.set_pause(0)
                    self.playback_paused = False
                else:
                    # 新增播放时保持位置
                    self._ensure_media_loaded()
                self.main_player.play()
                self.status_bar.setText('正在播放')
                #selected = self.playlist_widget.currentRow()
                #self.play_item(selected if selected != -1 else 0)

    def stop(self):
        self.main_player.stop()
        self.preview_player.stop()
        self.current_index = -1
        self.show_home_screen()

    def show_home_screen(self):
        """显示首页画面"""
        self.main_player.stop()
        self.preview_player.stop()
        
        if os.path.exists('index.jpg'):
            pixmap = QPixmap('index.jpg')
            if not pixmap.isNull():
                if len(self.screens) > 1:
                    scaled_pixmap = pixmap.scaled(QSize(800, 600), Qt.KeepAspectRatio, Qt.SmoothTransformation)
                else:
                    scaled_pixmap = pixmap.scaled(self.preview_window.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
                self.preview_window.setPixmap(scaled_pixmap)
                
                if hasattr(self, 'video_window') and self.video_window != self.preview_window:
                    if len(self.screens) > 1:
                        ext_pixmap = pixmap.scaled(QSize(800, 600), Qt.KeepAspectRatio, Qt.SmoothTransformation)
                    else:
                        ext_pixmap = pixmap.scaled(self.video_window.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
                    
                    if hasattr(self.video_window, 'setPixmap'):
                        self.video_window.setPixmap(ext_pixmap)
                    else:
                        for child in self.video_window.children():
                            if isinstance(child, QLabel):
                                child.setPixmap(ext_pixmap)
                    label = QLabel(self.video_window)
                    label.setPixmap(pixmap.scaled(
                        self.video_window.size(), 
                        Qt.KeepAspectRatio, 
                        Qt.SmoothTransformation
                    ))
                    label.setAlignment(Qt.AlignCenter)
                    label.show()

    def _hide_taskbar(self):
        """隐藏扩展屏任务栏"""
        try:
            def callback(hwnd, extra):
                class_name = win32gui.GetClassName(hwnd)
                rect = win32gui.GetWindowRect(hwnd)
                if class_name == "Shell_TrayWnd" and self.ext_screen.x <= rect[0] < self.ext_screen.x + self.ext_screen.width:
                    win32gui.ShowWindow(hwnd, win32con.SW_HIDE)
                    
            win32gui.EnumWindows(callback, None)
        except Exception as e:
            print(f"隐藏任务栏失败: {str(e)}")

    def closeEvent(self, event):
        """窗口关闭事件"""
        def restore_callback(hwnd, extra):
            if win32gui.GetClassName(hwnd) == "Shell_TrayWnd":
                win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
        
        win32gui.EnumWindows(restore_callback, None)
        
        self.main_player.stop()
        if hasattr(self, 'video_window') and self.video_window != self.preview_window:
            self.video_window.close()
        event.accept()

    def _setup_single_image_playback(self):
        """配置单张图片播放"""
        self.main_player.stop()
        self.preview_player.stop()

    def _on_single_play_end(self, event):
        try:
            self.stop()
            self.show_home_screen()
        finally:
            self.main_player.event_manager().event_detach(
                vlc.EventType.MediaPlayerEndReached
            )

    def prev_item(self):
        """播放上一项"""
        if self.playlist:
            new_index = (self.current_index - 1) % len(self.playlist)
            self.play_item(new_index)

    def next_item(self):
        """播放下一项"""
        if self.playlist:
            new_index = (self.current_index + 1) % len(self.playlist)
            self.play_item(new_index)

    def remove_selected(self):
        """删除选中项"""
        selected = self.playlist_widget.currentRow()
        if selected != -1:
            self.playlist.pop(selected)
            self.playlist_widget.takeItem(selected)
            if not self.playlist:
                self.current_index = -1

    def clear_playlist(self):
        """清空播放列表"""
        self.playlist.clear()
        self.playlist_widget.clear()
        self.current_index = -1

    def save_playlist(self):
        """保存播放列表"""
        file_name, _ = QFileDialog.getSaveFileName(self, "保存播放列表", os.getcwd(), "列表文件 (*.list)")
        if file_name:
            if not file_name.endswith('.list'):
                file_name += '.list'
            with open(file_name, 'w', encoding='utf-8') as f:
                json.dump(self.playlist, f, ensure_ascii=False)

    def load_playlist(self):
        """加载播放列表"""
        file_name, _ = QFileDialog.getOpenFileName(self, "加载播放列表", os.getcwd(), "列表文件 (*.list)")
        if file_name:
            try:
                with open(file_name, 'r', encoding='utf-8') as f:
                    self.playlist = json.load(f)
                    self.playlist_widget.clear()
                    self.playlist_widget.addItems([os.path.basename(f) for f in self.playlist])
                    if self.playlist:
                        self.current_inpythondex = 0
            except FileNotFoundError:
                self.status_bar.setText('播放列表文件不存在')

    def get_volume(self):
        return self.main_player.audio_get_volume()
    
    def set_volume(self, volume):
        """设置音量"""
        self.current_volume = volume
        self.main_player.audio_set_volume(volume)

    volume = pyqtProperty(int, get_volume, set_volume)

    def add_files(self):
        files, _ = QFileDialog.getOpenFileNames(
            self, '选择媒体文件', '',
            '媒体文件 (*.mp4 *.avi *.mov *.mkv *.mp3 *.wav *.jpg *.jpeg *.png)')
            
        if files:
            self.playlist.extend(files)
            self.playlist_widget.addItems([os.path.basename(f) for f in files])
            ipythonf self.current_index == -1:
                self.current_index = 0

if __name__ == '__main__':
    app = QApplication(sys.argv)
    player = ExtendedScreenPlayer()
    player.show()
    sys.exit(app.exec_())

八、项目总结

本系统通过创新的技术架构解决了多屏播控领域的三大痛点:

✅ 操作复杂性:直观的GUI界面降低使用门槛

✅ 功能单一性:融合播放控制、转场特效、多屏管理

✅ 稳定性不足:完善的异常处理机制

实际应用场景:

企业展厅的自动导览系统

会议中心的数字会标管理

零售门店的广告轮播系统

项目完整代码已开源,开发者可基于此进行二次开发。未来计划增加AI内容分析模块,实现智能播控。

到此这篇关于Python+PyQt5实现多屏幕协同播放功能的文章就介绍到这了,更多相关Python多屏幕协同播放内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!

这篇关于Python+PyQt5实现多屏幕协同播放功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154035

相关文章

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

Python中随机休眠技术原理与应用详解

《Python中随机休眠技术原理与应用详解》在编程中,让程序暂停执行特定时间是常见需求,当需要引入不确定性时,随机休眠就成为关键技巧,下面我们就来看看Python中随机休眠技术的具体实现与应用吧... 目录引言一、实现原理与基础方法1.1 核心函数解析1.2 基础实现模板1.3 整数版实现二、典型应用场景2

一文详解SpringBoot响应压缩功能的配置与优化

《一文详解SpringBoot响应压缩功能的配置与优化》SpringBoot的响应压缩功能基于智能协商机制,需同时满足很多条件,本文主要为大家详细介绍了SpringBoot响应压缩功能的配置与优化,需... 目录一、核心工作机制1.1 自动协商触发条件1.2 压缩处理流程二、配置方案详解2.1 基础YAML

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

idea中创建新类时自动添加注释的实现

《idea中创建新类时自动添加注释的实现》在每次使用idea创建一个新类时,过了一段时间发现看不懂这个类是用来干嘛的,为了解决这个问题,我们可以设置在创建一个新类时自动添加注释,帮助我们理解这个类的用... 目录前言:详细操作:步骤一:点击上方的 文件(File),点击&nbmyHIgsp;设置(Setti

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

MySQL大表数据的分区与分库分表的实现

《MySQL大表数据的分区与分库分表的实现》数据库的分区和分库分表是两种常用的技术方案,本文主要介绍了MySQL大表数据的分区与分库分表的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. mysql大表数据的分区1.1 什么是分区?1.2 分区的类型1.3 分区的优点1.4 分

一文详解如何从零构建Spring Boot Starter并实现整合

《一文详解如何从零构建SpringBootStarter并实现整合》SpringBoot是一个开源的Java基础框架,用于创建独立、生产级的基于Spring框架的应用程序,:本文主要介绍如何从... 目录一、Spring Boot Starter的核心价值二、Starter项目创建全流程2.1 项目初始化(

Mysql删除几亿条数据表中的部分数据的方法实现

《Mysql删除几亿条数据表中的部分数据的方法实现》在MySQL中删除一个大表中的数据时,需要特别注意操作的性能和对系统的影响,本文主要介绍了Mysql删除几亿条数据表中的部分数据的方法实现,具有一定... 目录1、需求2、方案1. 使用 DELETE 语句分批删除2. 使用 INPLACE ALTER T

MySQL INSERT语句实现当记录不存在时插入的几种方法

《MySQLINSERT语句实现当记录不存在时插入的几种方法》MySQL的INSERT语句是用于向数据库表中插入新记录的关键命令,下面:本文主要介绍MySQLINSERT语句实现当记录不存在时... 目录使用 INSERT IGNORE使用 ON DUPLICATE KEY UPDATE使用 REPLACE