Python+PyQt5實現(xiàn)多屏幕協(xié)同播放功能
一、項目概述:突破傳統(tǒng)播放限制
在現(xiàn)代會議展示、數(shù)字廣告、展覽展示等場景中,多屏幕協(xié)同播放已成為剛需。傳統(tǒng)播放軟件往往存在擴(kuò)展屏支持不足、操作復(fù)雜、功能單一等問題。本項目基于Python生態(tài)的PyQt5和VLC庫,開發(fā)了一套功能強(qiáng)大的跨屏播控系統(tǒng),實現(xiàn)了以下核心突破:
- 多屏融合控制:支持主屏操作+擴(kuò)展屏播放的雙屏模式
- 智能媒體識別:自動區(qū)分視頻/圖片格式并適配最佳播放方案
- 專業(yè)級過渡效果:內(nèi)置淡入淡出等專業(yè)轉(zhuǎn)場動畫
- 低代碼高擴(kuò)展:采用面向?qū)ο笤O(shè)計,模塊化程度高
系統(tǒng)架構(gòu)圖如下:
[主控制界面] ←PyQt5→ [VLC引擎] → {主屏預(yù)覽/擴(kuò)展屏輸出}
二、核心技術(shù)解析
2.1 多屏管理機(jī)制
def init_screens(self): """創(chuàng)新性的多屏檢測方案""" try: self.screens = screeninfo.get_monitors() if len(self.screens) > 1: self.ext_screen = self.screens[1] self._create_video_window() self._hide_taskbar() # 自動隱藏擴(kuò)展屏任務(wù)欄 except Exception as e: self._create_fallback_window() # 優(yōu)雅降級處理
關(guān)鍵技術(shù)點:
- 使用screeninfo庫動態(tài)獲取顯示器配置
- HWND窗口綁定實現(xiàn)精確到像素的跨屏控制
- 異常情況下的單屏兼容模式
2.2 播放引擎設(shè)計
系統(tǒng)采用雙VLC實例架構(gòu):
主播放器:帶音頻輸出的完整渲染
預(yù)覽播放器:靜音狀態(tài)的實時同步
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) # 預(yù)覽靜音
2.3 專業(yè)級轉(zhuǎn)場動畫
通過Qt動畫框架實現(xiàn)廣播級效果:
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 基礎(chǔ)操作流程
1.添加媒體文件:
- 支持拖拽添加/文件對話框多選
- 自動識別視頻(jpg/png等)和圖片格式
2.播放模式選擇:
- 連續(xù)播放:列表循環(huán)
- 單次播放:適合重要內(nèi)容展示
3.多屏輸出切換:
- 擴(kuò)展模式:主控+擴(kuò)展屏輸出
- 主屏模式:僅主界面播放
- 雙屏模式:鏡像輸出
3.2 高級功能
定時截圖預(yù)覽:
def update_preview(self): if self.main_player.video_take_snapshot(0, temp_file, 0, 0) == 0: # 異步處理截圖文件 QTimer.singleShot(100, self._process_snapshot)
智能記憶播放:
- 記錄上次退出時的播放位置
- 異常中斷后自動恢復(fù)現(xiàn)場
四、性能優(yōu)化方案
4.1 資源管理
采用懶加載策略初始化VLC實例
動態(tài)釋放已完成播放的媒體資源
4.2 線程安全
pythoncom.CoInitialize() # COM組件初始化 try: # VLC多線程操作 finally: pythoncom.CoUninitialize()
4.3 渲染優(yōu)化
視頻:硬件加速解碼
圖片:Qt原生渲染引擎
五、擴(kuò)展開發(fā)方向
1.網(wǎng)絡(luò)推流功能:
":sout=#transcode{vcodec=h264}:rtp{dst=192.168.1.100,port=1234}"
2.定時任務(wù)模塊:
- 基于cron的自動化播放計劃
- 節(jié)假日特殊排期支持
- API接口擴(kuò)展:
- RESTful控制接口
- WebSocket實時狀態(tài)推送
六、效果展示
七、相關(guān)源碼
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="", 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=Qt.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進(jìn)行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 = "擴(kuò)展模式" self.screen_modes = ["擴(kuò)展模式", "主屏模式", "雙屏模式"] 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) # 初始化動畫相關(guān) 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 # 新增暫停狀態(tài)標(biāo)記 self.current_media_position = 0 # 記錄當(dāng)前播放位置 def setup_ui_style(self): """設(shè)置全局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; } """) # 設(shè)置全局字體 font = QFont() font.setFamily("Arial") font.setPointSize(10) QApplication.setFont(font) def init_ui(self): """初始化用戶界面""" self.setWindowTitle('大屏播控系統(tǒng)') 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) # 左側(cè)控制面板 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) # 進(jìn)度條 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('連續(xù)播放') 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) # 右側(cè)預(yù)覽區(qū)域 preview_panel = StyledGroupBox("預(yù)覽") preview_layout = QVBoxLayout() preview_panel.setLayout(preview_layout) # 視頻預(yù)覽窗口 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) # 狀態(tài)欄 self.status_bar = QLabel('大屏準(zhǔn)備就緒') 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('警告:未檢測到擴(kuò)展屏幕,將使用主屏幕播放!') self._create_fallback_window() except Exception as e: self.status_bar.setText(f'屏幕檢測失敗: {str(e)}') self._create_fallback_window() def _create_video_window(self): """創(chuàng)建擴(kuò)展屏播放窗口""" self.video_window = QWidget() self.video_window.setWindowTitle('擴(kuò)展屏幕播放器') 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): """創(chuàng)建集成到主界面右側(cè)的監(jiān)看窗口""" self.video_window = self.preview_window self.preview_player.set_hwnd(0) def change_mode(self, mode): """切換播放模式""" self.mode = mode if mode == "擴(kuò)展模式" 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('連續(xù)播放' 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("圖片加載失敗") # 在主預(yù)覽窗口顯示 scaled_pixmap = pixmap.scaled( QSize(800, 600), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.preview_window.setPixmap(scaled_pixmap) # 擴(kuò)展屏顯示邏輯 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]) # 主播放器設(shè)置 self.main_player.stop() self.main_player.set_media(media) # 預(yù)覽播放器設(shè)置(靜音且獨立) self.preview_player.stop() self.preview_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: # 雙屏模式:主輸出到擴(kuò)展屏,預(yù)覽輸出到主界面 self.main_player.set_hwnd(self.video_window.winId()) self.preview_player.set_hwnd(self.preview_window.winId()) else: # 單屏模式:主播放器輸出到預(yù)覽窗口 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() # 更新狀態(tài)和列表選擇 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): """更新預(yù)覽畫面""" 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"預(yù)覽更新失敗: {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): """更新媒體狀態(tài)""" 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 != "擴(kuò)展模式" 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: # 恢復(fù)播放時保持當(dāng)前位置 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): """隱藏擴(kuò)展屏任務(wù)欄""" 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"隱藏任務(wù)欄失敗: {str(e)}") def closeEvent(self, event): """窗口關(guān)閉事件""" 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_index = 0 except FileNotFoundError: self.status_bar.setText('播放列表文件不存在') def get_volume(self): return self.main_player.audio_get_volume() def set_volume(self, volume): """設(shè)置音量""" 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]) if self.current_index == -1: self.current_index = 0 if __name__ == '__main__': app = QApplication(sys.argv) player = ExtendedScreenPlayer() player.show() sys.exit(app.exec_())
八、項目總結(jié)
本系統(tǒng)通過創(chuàng)新的技術(shù)架構(gòu)解決了多屏播控領(lǐng)域的三大痛點:
? 操作復(fù)雜性:直觀的GUI界面降低使用門檻
? 功能單一性:融合播放控制、轉(zhuǎn)場特效、多屏管理
? 穩(wěn)定性不足:完善的異常處理機(jī)制
實際應(yīng)用場景:
企業(yè)展廳的自動導(dǎo)覽系統(tǒng)
會議中心的數(shù)字會標(biāo)管理
零售門店的廣告輪播系統(tǒng)
項目完整代碼已開源,開發(fā)者可基于此進(jìn)行二次開發(fā)。未來計劃增加AI內(nèi)容分析模塊,實現(xiàn)智能播控。
到此這篇關(guān)于Python+PyQt5實現(xiàn)多屏幕協(xié)同播放功能的文章就介紹到這了,更多相關(guān)Python多屏幕協(xié)同播放內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解python 3.6 安裝json 模塊(simplejson)
這篇文章主要介紹了python 3.6 安裝json 模塊(simplejson),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Python函數(shù)式編程模塊functools的使用與實踐
本文主要介紹了Python函數(shù)式編程模塊functools的使用與實踐,教你如何使用?functools.partial、functools.wraps、functools.lru_cache?和?functools.reduce,感興趣的可以了解一下2024-03-03