程式碼檢視器 – V-VideoPlayer-v1.py

← 返回清單
import sys
from PySide6.QtCore import Qt, QUrl
from PySide6.QtGui import QAction, QKeySequence, QPalette, QColor
from PySide6.QtWidgets import (
    QApplication, QWidget, QFileDialog, QHBoxLayout, QVBoxLayout, QLabel,
    QPushButton, QSlider, QMessageBox, QSizePolicy
)
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput
from PySide6.QtMultimediaWidgets import QVideoWidget


def apply_dark_palette(app: QApplication):
    app.setStyle("Fusion")
    p = QPalette()
    p.setColor(QPalette.Window, QColor(37, 37, 38))
    p.setColor(QPalette.WindowText, Qt.white)
    p.setColor(QPalette.Base, QColor(30, 30, 30))
    p.setColor(QPalette.AlternateBase, QColor(45, 45, 48))
    p.setColor(QPalette.ToolTipBase, Qt.white)
    p.setColor(QPalette.ToolTipText, Qt.white)
    p.setColor(QPalette.Text, Qt.white)
    p.setColor(QPalette.Button, QColor(45, 45, 48))
    p.setColor(QPalette.ButtonText, Qt.white)
    p.setColor(QPalette.BrightText, Qt.red)
    p.setColor(QPalette.Highlight, QColor(14, 99, 156))
    p.setColor(QPalette.HighlightedText, Qt.white)
    app.setPalette(p)
    app.setStyleSheet("""
        QWidget { color: #ddd; }
        QPushButton { background:#3c3c3c; border:1px solid #555; padding:6px 10px; border-radius:6px; }
        QPushButton:hover { background:#4a4a4a; }
        QPushButton:pressed { background:#2f2f2f; }
        QSlider::groove:horizontal { height:6px; background:#2b2b2b; border-radius:3px; }
        QSlider::handle:horizontal { width:14px; height:14px; margin:-5px 0; border-radius:7px; background:#aaaaaa; }
        QLabel { color:#ccc; }
    """)


def ms_to_hhmmss(ms: int) -> str:
    if ms < 0:
        ms = 0
    s = ms // 1000
    h, m, s = s // 3600, (s % 3600) // 60, s % 60
    return f"{h:02d}:{m:02d}:{s:02d}" if h else f"{m:02d}:{s:02d}"


class VideoWindow(QWidget):
    def __init__(self, path: str | None = None):
        super().__init__()
        self.setWindowTitle("影片播放器")
        self.resize(960, 540)

        # 視訊與媒體核心
        self.video = QVideoWidget()
        self.player = QMediaPlayer(self)
        self.audio = QAudioOutput(self)
        self.player.setVideoOutput(self.video)
        self.player.setAudioOutput(self.audio)
        self.audio.setVolume(0.6)

        # 控制列元件(單列)
        self.btn_open = QPushButton("開啟")
        self.btn_play = QPushButton("播放")
        self.btn_stop = QPushButton("停止")
        self.btn_mute = QPushButton("靜音")

        self.progress = QSlider(Qt.Horizontal)
        self.progress.setRange(0, 0)
        self.progress.setSingleStep(1000)  # 1 秒
        self.progress.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

        self.time_label = QLabel("00:00 / 00:00")
        self.time_label.setMinimumWidth(140)

        self.vol_label = QLabel("音量")
        self.vol = QSlider(Qt.Horizontal)
        self.vol.setRange(0, 100)
        self.vol.setValue(int(self.audio.volume() * 100))
        self.vol.setFixedWidth(120)

        self.btn_full = QPushButton("全螢幕")

        # 版面配置:上方影片、下方單列控制
        root = QVBoxLayout(self)
        root.setContentsMargins(8, 8, 8, 8)
        root.addWidget(self.video)

        bar = QHBoxLayout()
        bar.setSpacing(8)
        bar.addWidget(self.btn_open)
        bar.addWidget(self.btn_play)
        bar.addWidget(self.btn_stop)
        bar.addSpacing(6)

        bar.addWidget(self.progress, 1)
        bar.addWidget(self.time_label)

        bar.addSpacing(6)
        bar.addWidget(self.btn_mute)
        bar.addWidget(self.vol_label)
        bar.addWidget(self.vol)
        bar.addSpacing(6)
        bar.addWidget(self.btn_full)

        root.addLayout(bar)

        # 事件連結
        self.btn_open.clicked.connect(self.open_file)
        self.btn_play.clicked.connect(self.toggle_play)
        self.btn_stop.clicked.connect(self.stop)
        self.btn_mute.clicked.connect(self.toggle_mute)
        self.btn_full.clicked.connect(self.toggle_fullscreen)

        self.progress.sliderMoved.connect(self.seek_to)
        self.progress.sliderPressed.connect(self._pause_for_scrub)
        self.progress.sliderReleased.connect(self._resume_after_scrub)

        self.vol.valueChanged.connect(self.on_volume)

        self.player.positionChanged.connect(self.on_position)
        self.player.durationChanged.connect(self.on_duration)
        self.player.errorOccurred.connect(self.on_error)

        # 快捷鍵 & 視窗雙擊全螢幕
        self._make_shortcuts()
        self.video.mouseDoubleClickEvent = lambda e: self.toggle_fullscreen()

        if path:
            self.load_and_play(path)

    # ---- 控制 ----
    def open_file(self):
        path, _ = QFileDialog.getOpenFileName(
            self, "選擇影片檔案", "", "影片檔案 (*.mp4 *.mov *.mkv *.avi *.m4v);;所有檔案 (*)"
        )
        if path:
            self.load_and_play(path)

    def load_and_play(self, path: str):
        self.player.setSource(QUrl.fromLocalFile(path))
        self.player.play()
        self.btn_play.setText("暫停")
        self.setWindowTitle(f"影片播放器 - {path}")

    def toggle_play(self):
        if self.player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
            self.player.pause()
            self.btn_play.setText("播放")
        else:
            self.player.play()
            self.btn_play.setText("暫停")

    def stop(self):
        self.player.stop()
        self.btn_play.setText("播放")

    def toggle_mute(self):
        self.audio.setMuted(not self.audio.isMuted())
        self.btn_mute.setText("解除靜音" if self.audio.isMuted() else "靜音")

    def on_volume(self, v: int):
        self.audio.setVolume(v / 100.0)
        if v == 0 and not self.audio.isMuted():
            self.audio.setMuted(True)
            self.btn_mute.setText("解除靜音")
        elif v > 0 and self.audio.isMuted():
            self.audio.setMuted(False)
            self.btn_mute.setText("靜音")

    def seek_to(self, slider_value: int):
        self.player.setPosition(slider_value)

    def _pause_for_scrub(self):
        self._was_playing = (
            self.player.playbackState() == QMediaPlayer.PlaybackState.PlayingState
        )
        if self._was_playing:
            self.player.pause()

    def _resume_after_scrub(self):
        if getattr(self, "_was_playing", False):
            self.player.play()

    def on_position(self, pos_ms: int):
        if not self.progress.isSliderDown():
            self.progress.setValue(pos_ms)
        self.time_label.setText(
            f"{ms_to_hhmmss(pos_ms)} / {ms_to_hhmmss(self.player.duration())}"
        )

    def on_duration(self, dur_ms: int):
        self.progress.setRange(0, dur_ms)

    def on_error(self, err, what):
        if err != QMediaPlayer.Error.NoError:
            QMessageBox.critical(self, "播放錯誤", f"發生錯誤:{what}")

    def toggle_fullscreen(self):
        self.video.setFullScreen(not self.video.isFullScreen())

    # ---- 快捷鍵 ----
    def _make_shortcuts(self):
        def add_shortcut(key, func):
            a = QAction(self); a.setShortcut(QKeySequence(key)); a.triggered.connect(func); self.addAction(a)
        add_shortcut(Qt.Key_Space, self.toggle_play)
        add_shortcut(Qt.Key_F, self.toggle_fullscreen)
        add_shortcut(Qt.Key_Left, lambda: self.player.setPosition(max(0, self.player.position() - 5000)))
        add_shortcut(Qt.Key_Right, lambda: self.player.setPosition(min(self.player.duration(), self.player.position() + 5000)))
        add_shortcut(Qt.Key_Up, lambda: self.vol.setValue(min(100, self.vol.value() + 5)))
        add_shortcut(Qt.Key_Down, lambda: self.vol.setValue(max(0, self.vol.value() - 5)))


if __name__ == "__main__":
    app = QApplication(sys.argv)
    apply_dark_palette(app)
    path = sys.argv[1] if len(sys.argv) > 1 else None
    w = VideoWindow(path)
    w.show()
    sys.exit(app.exec())