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

# 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()