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