You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
336 lines
11 KiB
336 lines
11 KiB
# P2P Network Communication - Media Player Widget
|
|
"""
|
|
媒体播放器界面组件
|
|
实现音频和视频播放器界面
|
|
|
|
需求: 6.3, 6.4, 6.5, 6.7
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
QSlider, QFrame, QStackedWidget, QSizePolicy
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
|
|
|
|
from client.media_player import PlaybackState
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MediaPlayerWidget(QWidget):
|
|
"""
|
|
媒体播放器界面组件
|
|
|
|
实现音频播放器界面 (需求 6.3)
|
|
实现视频播放器界面 (需求 6.4)
|
|
实现播放控制按钮 (需求 6.5)
|
|
实现全屏模式 (需求 6.7)
|
|
"""
|
|
|
|
# 信号定义
|
|
play_requested = pyqtSignal()
|
|
pause_requested = pyqtSignal()
|
|
stop_requested = pyqtSignal()
|
|
seek_requested = pyqtSignal(float) # position in seconds
|
|
volume_changed = pyqtSignal(float) # volume 0.0-1.0
|
|
fullscreen_requested = pyqtSignal(bool)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self._is_video = False
|
|
self._duration = 0.0
|
|
self._is_seeking = False
|
|
|
|
self._setup_ui()
|
|
self._connect_signals()
|
|
|
|
# 更新定时器
|
|
self._update_timer = QTimer()
|
|
self._update_timer.setInterval(100)
|
|
self._update_timer.timeout.connect(self._on_update_timer)
|
|
|
|
logger.info("MediaPlayerWidget initialized")
|
|
|
|
def _setup_ui(self) -> None:
|
|
"""设置UI"""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(0)
|
|
|
|
# 视频显示区域
|
|
self._video_frame = QFrame()
|
|
self._video_frame.setStyleSheet("background-color: black;")
|
|
self._video_frame.setMinimumHeight(200)
|
|
self._video_frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
|
|
video_layout = QVBoxLayout(self._video_frame)
|
|
video_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self._video_label = QLabel("无媒体")
|
|
self._video_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self._video_label.setStyleSheet("color: #666; font-size: 16px;")
|
|
video_layout.addWidget(self._video_label)
|
|
|
|
layout.addWidget(self._video_frame, 1)
|
|
|
|
# 控制栏
|
|
self._control_bar = self._create_control_bar()
|
|
layout.addWidget(self._control_bar)
|
|
|
|
def _create_control_bar(self) -> QWidget:
|
|
"""创建控制栏"""
|
|
bar = QFrame()
|
|
bar.setFixedHeight(80)
|
|
bar.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #2d2d2d;
|
|
}
|
|
""")
|
|
|
|
layout = QVBoxLayout(bar)
|
|
layout.setContentsMargins(10, 5, 10, 5)
|
|
layout.setSpacing(5)
|
|
|
|
# 进度条
|
|
progress_layout = QHBoxLayout()
|
|
progress_layout.setSpacing(10)
|
|
|
|
self._current_time_label = QLabel("00:00")
|
|
self._current_time_label.setStyleSheet("color: white; font-size: 12px;")
|
|
self._current_time_label.setFixedWidth(50)
|
|
progress_layout.addWidget(self._current_time_label)
|
|
|
|
self._progress_slider = QSlider(Qt.Orientation.Horizontal)
|
|
self._progress_slider.setRange(0, 1000)
|
|
self._progress_slider.setValue(0)
|
|
self._progress_slider.setStyleSheet("""
|
|
QSlider::groove:horizontal {
|
|
border: none;
|
|
height: 4px;
|
|
background: #555;
|
|
border-radius: 2px;
|
|
}
|
|
QSlider::handle:horizontal {
|
|
background: white;
|
|
width: 12px;
|
|
height: 12px;
|
|
margin: -4px 0;
|
|
border-radius: 6px;
|
|
}
|
|
QSlider::sub-page:horizontal {
|
|
background: #4a90d9;
|
|
border-radius: 2px;
|
|
}
|
|
""")
|
|
progress_layout.addWidget(self._progress_slider, 1)
|
|
|
|
self._total_time_label = QLabel("00:00")
|
|
self._total_time_label.setStyleSheet("color: white; font-size: 12px;")
|
|
self._total_time_label.setFixedWidth(50)
|
|
progress_layout.addWidget(self._total_time_label)
|
|
|
|
layout.addLayout(progress_layout)
|
|
|
|
# 控制按钮
|
|
button_layout = QHBoxLayout()
|
|
button_layout.setSpacing(10)
|
|
|
|
button_style = """
|
|
QPushButton {
|
|
background-color: transparent;
|
|
color: white;
|
|
border: none;
|
|
font-size: 18px;
|
|
padding: 5px 10px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #444;
|
|
border-radius: 5px;
|
|
}
|
|
"""
|
|
|
|
self._play_btn = QPushButton("▶")
|
|
self._play_btn.setStyleSheet(button_style)
|
|
self._play_btn.setFixedSize(40, 40)
|
|
self._play_btn.clicked.connect(self._on_play_clicked)
|
|
button_layout.addWidget(self._play_btn)
|
|
|
|
self._stop_btn = QPushButton("⏹")
|
|
self._stop_btn.setStyleSheet(button_style)
|
|
self._stop_btn.setFixedSize(40, 40)
|
|
self._stop_btn.clicked.connect(self._on_stop_clicked)
|
|
button_layout.addWidget(self._stop_btn)
|
|
|
|
button_layout.addStretch()
|
|
|
|
# 音量控制
|
|
self._volume_btn = QPushButton("🔊")
|
|
self._volume_btn.setStyleSheet(button_style)
|
|
self._volume_btn.setFixedSize(40, 40)
|
|
button_layout.addWidget(self._volume_btn)
|
|
|
|
self._volume_slider = QSlider(Qt.Orientation.Horizontal)
|
|
self._volume_slider.setRange(0, 100)
|
|
self._volume_slider.setValue(100)
|
|
self._volume_slider.setFixedWidth(100)
|
|
self._volume_slider.setStyleSheet("""
|
|
QSlider::groove:horizontal {
|
|
border: none;
|
|
height: 4px;
|
|
background: #555;
|
|
border-radius: 2px;
|
|
}
|
|
QSlider::handle:horizontal {
|
|
background: white;
|
|
width: 10px;
|
|
height: 10px;
|
|
margin: -3px 0;
|
|
border-radius: 5px;
|
|
}
|
|
QSlider::sub-page:horizontal {
|
|
background: #4a90d9;
|
|
border-radius: 2px;
|
|
}
|
|
""")
|
|
button_layout.addWidget(self._volume_slider)
|
|
|
|
# 全屏按钮
|
|
self._fullscreen_btn = QPushButton("⛶")
|
|
self._fullscreen_btn.setStyleSheet(button_style)
|
|
self._fullscreen_btn.setFixedSize(40, 40)
|
|
self._fullscreen_btn.clicked.connect(self._on_fullscreen_clicked)
|
|
button_layout.addWidget(self._fullscreen_btn)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
return bar
|
|
|
|
def _connect_signals(self) -> None:
|
|
"""连接信号"""
|
|
self._progress_slider.sliderPressed.connect(self._on_slider_pressed)
|
|
self._progress_slider.sliderReleased.connect(self._on_slider_released)
|
|
self._volume_slider.valueChanged.connect(self._on_volume_changed)
|
|
|
|
def _format_time(self, seconds: float) -> str:
|
|
"""格式化时间"""
|
|
minutes = int(seconds // 60)
|
|
secs = int(seconds % 60)
|
|
return f"{minutes:02d}:{secs:02d}"
|
|
|
|
def _on_play_clicked(self) -> None:
|
|
"""处理播放/暂停按钮点击"""
|
|
if self._play_btn.text() == "▶":
|
|
self.play_requested.emit()
|
|
else:
|
|
self.pause_requested.emit()
|
|
|
|
def _on_stop_clicked(self) -> None:
|
|
"""处理停止按钮点击"""
|
|
self.stop_requested.emit()
|
|
|
|
def _on_fullscreen_clicked(self) -> None:
|
|
"""处理全屏按钮点击"""
|
|
self.fullscreen_requested.emit(True)
|
|
|
|
def _on_slider_pressed(self) -> None:
|
|
"""进度条按下"""
|
|
self._is_seeking = True
|
|
|
|
def _on_slider_released(self) -> None:
|
|
"""进度条释放"""
|
|
self._is_seeking = False
|
|
if self._duration > 0:
|
|
position = (self._progress_slider.value() / 1000) * self._duration
|
|
self.seek_requested.emit(position)
|
|
|
|
def _on_volume_changed(self, value: int) -> None:
|
|
"""音量变化"""
|
|
volume = value / 100.0
|
|
self.volume_changed.emit(volume)
|
|
|
|
if value == 0:
|
|
self._volume_btn.setText("🔇")
|
|
elif value < 50:
|
|
self._volume_btn.setText("🔉")
|
|
else:
|
|
self._volume_btn.setText("🔊")
|
|
|
|
def _on_update_timer(self) -> None:
|
|
"""更新定时器回调"""
|
|
# 由外部调用update_position更新
|
|
pass
|
|
|
|
# ==================== 公共方法 ====================
|
|
|
|
def set_media_info(self, file_name: str, duration: float, is_video: bool = False) -> None:
|
|
"""
|
|
设置媒体信息
|
|
|
|
Args:
|
|
file_name: 文件名
|
|
duration: 时长(秒)
|
|
is_video: 是否是视频
|
|
"""
|
|
self._duration = duration
|
|
self._is_video = is_video
|
|
|
|
self._video_label.setText(file_name)
|
|
self._total_time_label.setText(self._format_time(duration))
|
|
self._progress_slider.setValue(0)
|
|
self._current_time_label.setText("00:00")
|
|
|
|
def update_position(self, position: float) -> None:
|
|
"""
|
|
更新播放位置
|
|
|
|
Args:
|
|
position: 当前位置(秒)
|
|
"""
|
|
if not self._is_seeking and self._duration > 0:
|
|
slider_value = int((position / self._duration) * 1000)
|
|
self._progress_slider.setValue(slider_value)
|
|
|
|
self._current_time_label.setText(self._format_time(position))
|
|
|
|
def update_state(self, state: PlaybackState) -> None:
|
|
"""
|
|
更新播放状态
|
|
|
|
Args:
|
|
state: 播放状态
|
|
"""
|
|
if state == PlaybackState.PLAYING:
|
|
self._play_btn.setText("⏸")
|
|
self._update_timer.start()
|
|
elif state == PlaybackState.PAUSED:
|
|
self._play_btn.setText("▶")
|
|
self._update_timer.stop()
|
|
elif state == PlaybackState.STOPPED:
|
|
self._play_btn.setText("▶")
|
|
self._update_timer.stop()
|
|
self._progress_slider.setValue(0)
|
|
self._current_time_label.setText("00:00")
|
|
|
|
def set_volume(self, volume: float) -> None:
|
|
"""
|
|
设置音量
|
|
|
|
Args:
|
|
volume: 音量 (0.0-1.0)
|
|
"""
|
|
self._volume_slider.setValue(int(volume * 100))
|
|
|
|
def clear(self) -> None:
|
|
"""清空播放器"""
|
|
self._duration = 0.0
|
|
self._video_label.setText("无媒体")
|
|
self._total_time_label.setText("00:00")
|
|
self._current_time_label.setText("00:00")
|
|
self._progress_slider.setValue(0)
|
|
self._play_btn.setText("▶")
|
|
self._update_timer.stop()
|