Python+PyQt5實(shí)現(xiàn)多屏幕協(xié)同播放功能
一、項(xiàng)目概述:突破傳統(tǒng)播放限制
在現(xiàn)代會(huì)議展示、數(shù)字廣告、展覽展示等場(chǎng)景中,多屏幕協(xié)同播放已成為剛需。傳統(tǒng)播放軟件往往存在擴(kuò)展屏支持不足、操作復(fù)雜、功能單一等問(wèn)題。本項(xiàng)目基于Python生態(tài)的PyQt5和VLC庫(kù),開(kāi)發(fā)了一套功能強(qiáng)大的跨屏播控系統(tǒng),實(shí)現(xiàn)了以下核心突破:
- 多屏融合控制:支持主屏操作+擴(kuò)展屏播放的雙屏模式
- 智能媒體識(shí)別:自動(dòng)區(qū)分視頻/圖片格式并適配最佳播放方案
- 專業(yè)級(jí)過(guò)渡效果:內(nèi)置淡入淡出等專業(yè)轉(zhuǎn)場(chǎng)動(dòng)畫(huà)
- 低代碼高擴(kuò)展:采用面向?qū)ο笤O(shè)計(jì),模塊化程度高
系統(tǒng)架構(gòu)圖如下:
[主控制界面] ←PyQt5→ [VLC引擎] → {主屏預(yù)覽/擴(kuò)展屏輸出}
二、核心技術(shù)解析
2.1 多屏管理機(jī)制
def init_screens(self):
"""創(chuàng)新性的多屏檢測(cè)方案"""
try:
self.screens = screeninfo.get_monitors()
if len(self.screens) > 1:
self.ext_screen = self.screens[1]
self._create_video_window()
self._hide_taskbar() # 自動(dòng)隱藏?cái)U(kuò)展屏任務(wù)欄
except Exception as e:
self._create_fallback_window() # 優(yōu)雅降級(jí)處理
關(guān)鍵技術(shù)點(diǎn):
- 使用screeninfo庫(kù)動(dòng)態(tài)獲取顯示器配置
- HWND窗口綁定實(shí)現(xiàn)精確到像素的跨屏控制
- 異常情況下的單屏兼容模式
2.2 播放引擎設(shè)計(jì)
系統(tǒng)采用雙VLC實(shí)例架構(gòu):
主播放器:帶音頻輸出的完整渲染
預(yù)覽播放器:靜音狀態(tài)的實(shí)時(shí)同步
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è)級(jí)轉(zhuǎn)場(chǎng)動(dòng)畫(huà)
通過(guò)Qt動(dòng)畫(huà)框架實(shí)現(xiàn)廣播級(jí)效果:
def start_fade_in_animation(self):
"""音量淡入曲線動(dòng)畫(huà)"""
self.fade_animation = QPropertyAnimation(self, b"volume")
self.fade_animation.setEasingCurve(QEasingCurve.InOutCirc)
self.fade_animation.start()
三、功能使用詳解
3.1 基礎(chǔ)操作流程
1.添加媒體文件:
- 支持拖拽添加/文件對(duì)話框多選
- 自動(dòng)識(shí)別視頻(jpg/png等)和圖片格式
2.播放模式選擇:
- 連續(xù)播放:列表循環(huán)
- 單次播放:適合重要內(nèi)容展示
3.多屏輸出切換:
- 擴(kuò)展模式:主控+擴(kuò)展屏輸出
- 主屏模式:僅主界面播放
- 雙屏模式:鏡像輸出
3.2 高級(jí)功能
定時(shí)截圖預(yù)覽:
def update_preview(self):
if self.main_player.video_take_snapshot(0, temp_file, 0, 0) == 0:
# 異步處理截圖文件
QTimer.singleShot(100, self._process_snapshot)
智能記憶播放:
- 記錄上次退出時(shí)的播放位置
- 異常中斷后自動(dòng)恢復(fù)現(xiàn)場(chǎng)
四、性能優(yōu)化方案
4.1 資源管理
采用懶加載策略初始化VLC實(shí)例
動(dòng)態(tài)釋放已完成播放的媒體資源
4.2 線程安全
pythoncom.CoInitialize() # COM組件初始化
try:
# VLC多線程操作
finally:
pythoncom.CoUninitialize()
4.3 渲染優(yōu)化
視頻:硬件加速解碼
圖片:Qt原生渲染引擎
五、擴(kuò)展開(kāi)發(fā)方向
1.網(wǎng)絡(luò)推流功能:
":sout=#transcode{vcodec=h264}:rtp{dst=192.168.1.100,port=1234}"
2.定時(shí)任務(wù)模塊:
- 基于cron的自動(dòng)化播放計(jì)劃
- 節(jié)假日特殊排期支持
- API接口擴(kuò)展:
- RESTful控制接口
- WebSocket實(shí)時(shí)狀態(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()
# 初始化定時(shí)器
self.media_timer = QTimer(self)
self.media_timer.timeout.connect(self.update_media_status)
self.media_timer.start(200)
# 初始化動(dòng)畫(huà)相關(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, '返回首頁(yè)畫(huà)面'),
('?', self.prev_item, '播放上一項(xiàng)'),
('?', self.toggle_play, '播放/暫停'),
('?', self.stop, '停止播放'),
('?', self.next_item, '播放下一項(xiàng)')
]
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('警告:未檢測(cè)到擴(kuò)展屏幕,將使用主屏幕播放!')
self._create_fallback_window()
except Exception as e:
self.status_bar.setText(f'屏幕檢測(cè)失敗: {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 '單個(gè)播放')
def play_selected_item(self, item):
"""處理雙擊播放列表項(xiàng)事件"""
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'錯(cuò)誤: {str(e)}')
self.show_home_screen()
return
else:
# 如果是單個(gè)播放模式且不是圖片,先顯示首頁(yè)
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è)置(靜音且獨(dú)立)
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)
# 同步啟動(dòng)播放
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īng)顟B(tài)和列表選擇
self.status_bar.setText(f'正在播放: {os.path.basename(self.playlist[index])}')
self.playlist_widget.setCurrentRow(index)
def fade_process(self):
"""處理音量漸變過(guò)程"""
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):
"""啟動(dòng)淡入動(dòng)畫(huà)"""
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ù)覽畫(huà)面"""
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, "主畫(huà)面播放中")
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ù)播放時(shí)保持當(dāng)前位置
self.main_player.set_pause(0)
self.playback_paused = False
else:
# 新增播放時(shí)保持位置
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):
"""顯示首頁(yè)畫(huà)面"""
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):
"""隱藏?cái)U(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):
"""播放上一項(xiàng)"""
if self.playlist:
new_index = (self.current_index - 1) % len(self.playlist)
self.play_item(new_index)
def next_item(self):
"""播放下一項(xiàng)"""
if self.playlist:
new_index = (self.current_index + 1) % len(self.playlist)
self.play_item(new_index)
def remove_selected(self):
"""刪除選中項(xiàng)"""
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_())
八、項(xiàng)目總結(jié)
本系統(tǒng)通過(guò)創(chuàng)新的技術(shù)架構(gòu)解決了多屏播控領(lǐng)域的三大痛點(diǎn):
? 操作復(fù)雜性:直觀的GUI界面降低使用門(mén)檻
? 功能單一性:融合播放控制、轉(zhuǎn)場(chǎng)特效、多屏管理
? 穩(wěn)定性不足:完善的異常處理機(jī)制
實(shí)際應(yīng)用場(chǎng)景:
企業(yè)展廳的自動(dòng)導(dǎo)覽系統(tǒng)
會(huì)議中心的數(shù)字會(huì)標(biāo)管理
零售門(mén)店的廣告輪播系統(tǒng)
項(xiàng)目完整代碼已開(kāi)源,開(kāi)發(fā)者可基于此進(jìn)行二次開(kāi)發(fā)。未來(lái)計(jì)劃增加AI內(nèi)容分析模塊,實(shí)現(xiàn)智能播控。
到此這篇關(guān)于Python+PyQt5實(shí)現(xiàn)多屏幕協(xié)同播放功能的文章就介紹到這了,更多相關(guān)Python多屏幕協(xié)同播放內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
django中send_mail功能實(shí)現(xiàn)詳解
這篇文章主要給大家介紹了關(guān)于django中send_mail功能實(shí)現(xiàn)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-02-02
詳解python 3.6 安裝json 模塊(simplejson)
這篇文章主要介紹了python 3.6 安裝json 模塊(simplejson),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Python利用緩存流實(shí)現(xiàn)壓縮PDF文件
在Python中,有許多庫(kù)可以用來(lái)壓縮PDF文件,其中最常用的是PyPDF2和PDFMiner,本文將為大家介紹一個(gè)新的方法,即使用緩存流壓縮PDF文件,感興趣的可以了解下2023-08-08
Python函數(shù)式編程模塊functools的使用與實(shí)踐
本文主要介紹了Python函數(shù)式編程模塊functools的使用與實(shí)踐,教你如何使用?functools.partial、functools.wraps、functools.lru_cache?和?functools.reduce,感興趣的可以了解一下2024-03-03

