VLC解码器媒体播放器源码详细说明与改进建议

【Python 开源】VLC`库实现了一个简易媒体播放器

一、源码功能与结构说明

该代码基于PyQt5VLC库实现了一个简易媒体播放器,核心功能与结构如下:

1. 核心依赖与初始化
  • 依赖库:使用PyQt5构建GUI界面,python-vlc调用VLC媒体播放能力,ctypes加载VLC动态链接库(libvlc.dlllibvlccore.dll)。
  • 环境配置:设置VLC插件路径(确保解码器正常工作),初始化VLC实例(启用硬件加速dxva2),创建媒体播放器对象。
  • 配置存储:使用QSettings保存用户最后打开的目录,提升用户体验。
2. 界面组件设计
  • 主窗口:无边框设计,黑色主题(背景色#000000,文字/边框色#00ff00#ffffff),支持窗口拖动、最小化/最大化/关闭。
  • 核心控件
    • 视频播放区域(video_frame):接收VLC视频输出,支持鼠标双击全屏、右键菜单。
    • 控制栏:包含播放/暂停、停止、上一首/下一首、打开文件/文件夹按钮,音量滑块、进度条。
    • 播放列表(playlist_widget):显示媒体文件列表,双击可播放,支持自动隐藏/显示(鼠标移至右侧时显示)。

【Python 开源】VLC`库实现了一个简易媒体播放器

3. 核心功能实现
  • 媒体控制:播放/暂停、停止、进度调整、音量控制,支持单文件播放和文件夹批量加载。
  • 播放列表管理:加载文件夹时自动生成列表,支持上一首/下一首循环播放。
  • 交互逻辑:右键菜单(打开文件、媒体信息等)、ESC键退出全屏、窗口拖动(通过鼠标事件实现)。
  • 错误处理:文件格式验证、VLC库加载失败提示、媒体播放错误捕获。

二、改进建议

基于现有功能,可从以下方面优化体验和功能完整性:

1. 图标与界面美化
  • 统一图标风格:现有图标混合了QStyle标准图标(如SP_MediaPlay)和文本按钮(如窗口控制按钮“—”“□”“×”),建议统一使用图标:

    # 示例:替换窗口控制按钮为图标
    from PyQt5.QtGui import QIcon
    self.minimize_btn.setIcon(QIcon("icons/minimize.png"))  # 加载本地图标
    self.maximize_btn.setIcon(QIcon("icons/maximize.png"))
    self.close_btn.setIcon(QIcon("icons/close.png"))
    
    # 播放列表开关图标(新增按钮)
    self.playlist_toggle_btn = QPushButton()
    self.playlist_toggle_btn.setIcon(QIcon("icons/playlist.png"))
    self.playlist_toggle_btn.clicked.connect(self.toggle_playlist)
  • 添加缺失图标:为“打开文件”“打开文件夹”“媒体信息”等功能添加直观图标(可使用Flaticon免费图标)。
  • 图标资源管理:使用Qt资源系统(.qrc文件)统一管理图标,避免路径问题:
    <!-- resources.qrc -->
    <RCC>
    <qresource prefix="/icons">
      <file>play.png</file>
      <file>pause.png</file>
      <file>stop.png</file>
    </qresource>
    </RCC>

    编译为Python文件后在代码中引用:QIcon(":/icons/play.png")

2. 功能增强
  • 播放模式切换:添加循环播放(单曲/列表)、随机播放功能,通过图标按钮控制:
    self.repeat_mode = 0  # 0:列表循环 1:单曲循环 2:随机
    self.repeat_btn = QPushButton(QIcon("icons/repeat.png"), "")
    self.repeat_btn.clicked.connect(self.toggle_repeat_mode)
  • 快捷键支持:添加常用快捷键(如空格播放/暂停、↑↓调整音量、←→跳转进度):
    def keyPressEvent(self, event):
      if event.key() == Qt.Key_Space:
          self.toggle_play()
      elif event.key() == Qt.Key_Up:
          self.volume_slider.setValue(self.volume_slider.value() + 5)
  • 媒体信息优化show_media_info目前信息较简略,可补充分辨率(视频)、比特率等细节,使用QDialog而非QMessageBox显示长文本。
  • 播放列表编辑:支持删除列表项、清空列表、拖拽排序,提升交互灵活性。
3. 稳定性与体验优化
  • 资源释放完善stop方法中释放资源的逻辑可优化,避免重复调用release()导致崩溃:
    def stop(self):
      if self.player:
          self.player.stop()
          # 仅在停止后释放,避免播放中释放导致异常
          # self.player.release()  # 建议在关闭窗口时统一释放
      self.timer.stop()
      self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
  • 全屏逻辑优化:当前“全屏”实际为窗口最大化,可改为真正全屏(隐藏任务栏):
    def toggle_fullscreen(self):
      if not self.is_fullscreen:
          self.showFullScreen()  # 真正全屏
          self.is_fullscreen = True
      else:
          self.showNormal()
          self.is_fullscreen = False
  • 加载状态提示:添加媒体加载中动画或文字提示,避免用户误以为程序无响应。

三、学习资源站点

以下站点可帮助深入学习相关技术:

  1. PyQt5基础

  2. VLC Python绑定

  3. GUI设计与图标资源

    • Qt Designer教程:可视化设计界面,可导出.ui文件供PyQt使用。
    • Flaticon:免费图标资源,可下载适合媒体播放器的图标。
  4. 实战项目参考

通过以上改进和学习资源,可进一步完善播放器功能,提升用户体验,同时深入掌握PyQt5和VLC媒体播放的核心技术。

import sys
import os
import ctypes
import json
import glob
from PyQt5.QtGui import QPalette, QColor, QFont
from PyQt5.QtCore import QSettings

# Add all required DLLs to system PATH
vlc_dir = os.path.abspath(os.path.dirname(__file__))
plugins_dir = os.path.join(vlc_dir, 'plugins')
os.environ['PATH'] = f'{vlc_dir};{plugins_dir};{os.environ["PATH"]}'

try:
    ctypes.CDLL(os.path.join(vlc_dir, 'libvlc.dll'))
    ctypes.CDLL(os.path.join(vlc_dir, 'libvlccore.dll'))
except OSError as e:
    print(f'无法加载VLC核心库: {e}')
    sys.exit(1)

# 添加DLL搜索路径
os.environ['PATH'] = os.path.dirname(__file__) + os.pathsep + os.environ['PATH']

# 显式加载核心库
libvlc_path = os.path.join(os.path.dirname(__file__), 'libvlc.dll')
ctypes.CDLL(libvlc_path)

import vlc
from PyQt5.QtCore import Qt, QTimer, QEvent, QPoint
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                            QHBoxLayout, QPushButton, QSlider, QFileDialog,
                            QLabel, QStyle, QMessageBox, QMenu, QAction, 
                            QDialog, QTextEdit, QDialogButtonBox, QGroupBox,
                            QListWidget, QListWidgetItem, QSplitter, QTableWidget,
                            QTableWidgetItem)

class MediaPlayer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('VLC 媒体播放器')
        self.setGeometry(100, 100, 1000, 700)

        # 设置无边框黑色主题
        self.setWindowFlags(Qt.FramelessWindowHint)
        self.setStyleSheet("""
            QMainWindow {
                background-color: #000000;
                color: #00ff00;
            }
            QPushButton {
                background-color: #000000;
                color: #00ff00;
                border: 1px solid #00ff00;
                border-radius: 3px;
                padding: 5px;
            }
            QPushButton:hover {
                background-color: #003300;
            }
            QPushButton:pressed {
                background-color: #00ff00;
                color: #000000;
            }
            QSlider::groove:horizontal {
                border: 1px solid #00ff00;
                height: 8px;
                background: #000000;
                border-radius: 4px;
            }
            QSlider::handle:horizontal {
                background: #00ff00;
                border: 1px solid #00ff00;
                width: 18px;
                margin: -5px 0;
                border-radius: 9px;
            }
            QSlider::sub-page:horizontal {
                background: #00ff00;
                border-radius: 4px;
            }
            QLabel {
                color: #00ff00;
            }
            QListWidget {
                background-color: #000000;
                color: #00ff00;
                border: 2px solid #00ff00;
                font-weight: bold;
            }
            QListWidget::item {
                background-color: #000000;
                color: #00ff00;
                border-bottom: 1px solid #00aa00;
            }
            QListWidget::item:selected {
                background-color: #00aa00;
                color: #000000;
            }
            QListWidget::item:hover {
                background-color: #003300;
            }
        """)

        # 设置存储
        self.settings = QSettings("VLCPlayer", "Config")
        self.last_directory = self.settings.value("last_directory", os.path.expanduser("~"))

        # 全屏状态相关
        self.is_fullscreen = False
        self.fullscreen_indicator = None  # 将在video_frame创建后初始化

        # 设置VLC插件路径
        # 验证插件目录存在
        plugins_path = os.path.join(os.path.dirname(__file__), 'plugins', 'codec')
        if os.path.exists(plugins_path):
            os.environ['VLC_PLUGIN_PATH'] = plugins_path
        else:
            QMessageBox.critical(self, '配置错误', '找不到解码器目录')
            sys.exit(1)

        # 初始化VLC实例(带硬件加速)
        try:
            self.instance = vlc.Instance('--avcodec-hw=dxva2', '--no-xlib')
            if not self.instance:
                raise Exception('无法创建VLC实例')
            self.player = self.instance.media_player_new()
            if not self.player:
                raise Exception('无法创建媒体播放器')
            self.media = None
        except Exception as e:
            QMessageBox.critical(self, '初始化错误', f'VLC初始化失败: {str(e)}')
            sys.exit(1)

        # 创建界面组件
        self.video_frame = QWidget()
        self.video_frame.setStyleSheet("background: black; border-radius: 8px;")
        # 确保视频帧能接收所有鼠标事件,无论视频是否加载
        self.video_frame.setAttribute(Qt.WA_AcceptTouchEvents, True)
        self.video_frame.setFocusPolicy(Qt.StrongFocus)
        self.video_frame.setAcceptDrops(True)
        # 强制视频帧接收鼠标事件
        self.video_frame.setAttribute(Qt.WA_TransparentForMouseEvents, False)
        # 设置视频帧为鼠标事件接收器
        self.video_frame.setMouseTracking(True)

        # 控制按钮 - 增大按钮尺寸
        self.play_btn = QPushButton()
        self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
        self.play_btn.setToolTip("播放/暂停")
        self.play_btn.setFixedSize(40, 40)  # 增大播放按钮
        self.play_btn.setStyleSheet("QPushButton { background-color: #ffffff; color: #000000; border: 1px solid #cccccc; border-radius: 3px; font-weight: bold; } QPushButton:hover { background-color: #f0f0f0; } QPushButton:pressed { background-color: #e0e0e0; }")

        self.stop_btn = QPushButton()
        self.stop_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaStop))
        self.stop_btn.setToolTip("停止")
        self.stop_btn.setFixedSize(30, 30)
        self.stop_btn.setStyleSheet("QPushButton { background-color: #ffffff; color: #000000; border: 1px solid #cccccc; border-radius: 3px; font-weight: bold; } QPushButton:hover { background-color: #f0f0f0; } QPushButton:pressed { background-color: #e0e0e0; }")

        self.open_btn = QPushButton("打开文件")
        self.open_btn.setToolTip("打开媒体文件")
        self.open_btn.setFixedHeight(30)
        self.open_btn.setStyleSheet("QPushButton { background-color: #ffffff; color: #000000; border: 1px solid #cccccc; border-radius: 3px; font-weight: bold; } QPushButton:hover { background-color: #f0f0f0; } QPushButton:pressed { background-color: #e0e0e0; }")

        self.open_folder_btn = QPushButton("打开文件夹")
        self.open_folder_btn.setToolTip("打开包含媒体文件的文件夹")
        self.open_folder_btn.setFixedHeight(30)
        self.open_folder_btn.setStyleSheet("QPushButton { background-color: #ffffff; color: #000000; border: 1px solid #cccccc; border-radius: 3px; font-weight: bold; } QPushButton:hover { background-color: #f0f0f0; } QPushButton:pressed { background-color: #e0e0e0; }")

        # 上一首/下一首按钮
        self.prev_btn = QPushButton()
        self.prev_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipBackward))
        self.prev_btn.setToolTip("上一首")
        self.prev_btn.setFixedSize(30, 30)
        self.prev_btn.setStyleSheet("QPushButton { background-color: #ffffff; color: #000000; border: 1px solid #cccccc; border-radius: 3px; font-weight: bold; } QPushButton:hover { background-color: #f0f0f0; } QPushButton:pressed { background-color: #e0e0e0; }")

        self.next_btn = QPushButton()
        self.next_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipForward))
        self.next_btn.setToolTip("下一首")
        self.next_btn.setFixedSize(30, 30)
        self.next_btn.setStyleSheet("QPushButton { background-color: #ffffff; color: #000000; border: 1px solid #cccccc; border-radius: 3px; font-weight: bold; } QPushButton:hover { background-color: #f0f0f0; } QPushButton:pressed { background-color: #e0e0e0; }")

        # 播放列表相关
        self.playlist_visible = False
        self.playlist_widget = QListWidget()
        self.playlist_widget.setMaximumWidth(300)
        self.playlist_widget.setStyleSheet("""
            QListWidget {
                background: #000000;
                border: 1px solid #ffffff;
                border-radius: 5px;
                color: #ffffff;
                font-size: 12px;
            }
            QListWidget::item {
                padding: 8px;
                border-bottom: 1px solid #666666;
                color: #ffffff;
            }
            QListWidget::item:selected {
                background: #ffffff;
                color: #000000;
            }
            QListWidget::item:hover {
                background: #333333;
                color: #ffffff;
            }
        """)
        self.playlist_widget.itemDoubleClicked.connect(self.play_selected_file)

        self.volume_slider = QSlider(Qt.Horizontal)
        self.volume_slider.setRange(0, 100)
        self.volume_slider.setValue(50)
        self.volume_slider.setToolTip("音量控制")
        # 设置音量条也能接收鼠标事件
        self.volume_slider.setMouseTracking(True)
        self.volume_slider.mousePressEvent = self.volume_slider_mouse_press_event

        # 为所有控制按钮添加右键菜单支持(在position_slider创建后设置)

        self.position_slider = QSlider(Qt.Horizontal)
        self.position_slider.setRange(0, 100)
        self.position_slider.setToolTip("播放进度")

        # 为所有控制按钮添加右键菜单支持
        for widget in [self.play_btn, self.stop_btn, self.open_btn, self.open_folder_btn, 
                      self.volume_slider, self.position_slider]:
            widget.setContextMenuPolicy(Qt.CustomContextMenu)
            widget.customContextMenuRequested.connect(lambda pos, w=widget: self.show_control_context_menu(w.mapToGlobal(pos)))

        # 窗口控制按钮
        self.minimize_btn = QPushButton('—')
        self.minimize_btn.setToolTip("最小化")
        self.minimize_btn.clicked.connect(self.showMinimized)

        self.maximize_btn = QPushButton('□')
        self.maximize_btn.setToolTip("最大化/还原")
        self.maximize_btn.clicked.connect(self.toggle_maximize)

        self.close_btn = QPushButton('×')
        self.close_btn.setToolTip("关闭")
        self.close_btn.clicked.connect(self.close)

        # 设置窗口控制按钮样式(无边框,红色图标)
        window_btn_style = """
            QPushButton {
                background-color: transparent;
                color: #ff0000;
                border: none;
                font-size: 14px;
                font-weight: bold;
                min-width: 20px;
                max-width: 20px;
                min-height: 20px;
                max-height: 20px;
            }
            QPushButton:hover {
                background-color: #330000;
            }
            QPushButton:pressed {
                background-color: #660000;
            }
        """
        self.minimize_btn.setStyleSheet(window_btn_style)
        self.maximize_btn.setStyleSheet(window_btn_style)
        self.close_btn.setStyleSheet(window_btn_style)

        # 当前播放显示
        self.current_playing_label = QLabel("当前播放: 无")
        self.current_playing_label.setStyleSheet("color: #ffffff; font-size: 12px; font-weight: bold;")

        # 状态标签
        self.status_label = QLabel("就绪")
        self.status_label.setStyleSheet("color: #ffffff; font-size: 12px;")

        # 底部控制栏布局 - 重新设计布局,音量控件在右侧
        bottom_control_layout = QHBoxLayout()

        # 左侧:播放控制按钮
        left_layout = QHBoxLayout()
        left_layout.addWidget(self.prev_btn)
        left_layout.addWidget(self.play_btn)
        left_layout.addWidget(self.stop_btn)
        left_layout.addWidget(self.next_btn)
        left_layout.addWidget(self.open_btn)
        left_layout.addWidget(self.open_folder_btn)

        # 中间:当前播放名称
        center_layout = QHBoxLayout()
        center_layout.addWidget(self.current_playing_label)
        center_layout.addStretch()

        # 右侧:音量控制和状态标签
        right_layout = QHBoxLayout()

        # 音量控制(标签和滑块分开)
        volume_label = QLabel('音量:')
        volume_label.setStyleSheet("color: #ffffff; font-size: 12px; font-weight: bold;")
        right_layout.addWidget(volume_label)
        right_layout.addWidget(self.volume_slider)
        self.volume_slider.setMaximumWidth(120)

        # 状态标签
        right_layout.addWidget(self.status_label)

        # 添加到主布局
        bottom_control_layout.addLayout(left_layout)
        bottom_control_layout.addStretch()
        bottom_control_layout.addLayout(center_layout)
        bottom_control_layout.addStretch()
        bottom_control_layout.addLayout(right_layout)

        # 窗口控制按钮在右上角
        title_layout = QHBoxLayout()
        title_layout.addStretch()
        title_layout.addWidget(self.minimize_btn)
        title_layout.addWidget(self.maximize_btn)
        title_layout.addWidget(self.close_btn)

        # 主布局 - 水平分割:视频在左侧,播放列表在右侧
        main_horizontal_layout = QHBoxLayout()

        # 左侧视频区域
        video_container = QWidget()
        video_layout = QVBoxLayout()
        video_layout.addLayout(title_layout)
        video_layout.addWidget(self.video_frame, 8)  # 视频区域占据大部分空间
        video_layout.addLayout(bottom_control_layout)
        video_container.setLayout(video_layout)

        # 右侧播放列表区域
        self.playlist_container = QWidget()
        self.playlist_container.setLayout(QVBoxLayout())
        self.playlist_container.layout().addWidget(self.playlist_widget)
        self.playlist_container.setStyleSheet("background-color: #000000; border: 2px solid #ffffff;")
        self.playlist_container.setFixedWidth(300)  # 固定宽度
        self.playlist_container.hide()  # 初始隐藏

        # 添加到水平布局
        main_horizontal_layout.addWidget(video_container, 7)  # 视频区域占7份
        main_horizontal_layout.addWidget(self.playlist_container, 3)  # 播放列表占3份

        # 创建主容器
        main_container = QWidget()
        main_container.setLayout(main_horizontal_layout)

        # 设置播放列表自动隐藏功能
        self.playlist_container.installEventFilter(self)
        self.video_frame.installEventFilter(self)

        self.setCentralWidget(main_container)

        # 右键菜单
        self.context_menu = QMenu(self)
        # 移除了媒体信息和解码器信息菜单项
        self.video_frame.setContextMenuPolicy(Qt.CustomContextMenu)
        self.video_frame.customContextMenuRequested.connect(self.show_context_menu)

        # 窗口拖动支持
        self.dragging = False
        self.drag_position = QPoint()

        # 确保视频帧能接收鼠标事件
        self.video_frame.setMouseTracking(True)
        self.video_frame.mousePressEvent = self.mouse_press_event
        self.video_frame.mouseMoveEvent = self.mouse_move_event
        self.video_frame.mouseReleaseEvent = self.mouse_release_event
        self.video_frame.mousePressEvent = self.video_mouse_press_event
        self.video_frame.mouseDoubleClickEvent = self.video_double_click_event

        # 安装事件过滤器来捕获ESC键
        self.video_frame.installEventFilter(self)

        # 全屏状态指示器(已移除以避免冲突)

        # 信号连接
        self.open_btn.clicked.connect(self.open_file)
        self.open_folder_btn.clicked.connect(self.open_folder)
        self.play_btn.clicked.connect(self.toggle_play)
        self.stop_btn.clicked.connect(self.stop)
        self.volume_slider.valueChanged.connect(self.set_volume)
        self.position_slider.sliderMoved.connect(self.set_position)
        self.prev_btn.clicked.connect(self.play_previous)
        self.next_btn.clicked.connect(self.play_next)

        # 设置视频输出
        if sys.platform == 'win32':
            self.player.set_hwnd(self.video_frame.winId())
        else:
            self.player.set_xwindow(self.video_frame.winId())

        self.timer = QTimer(self)
        self.timer.setInterval(200)
        self.timer.timeout.connect(self.update_ui)

    def toggle_play(self):
        if self.player and self.player.is_playing():
            self.player.pause()
            self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
            self.timer.stop()
        else:
            try:
                if self.player:
                    self.player.play()
                    self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
                    self.timer.start()
            except Exception as e:
                print(f'播放错误: {str(e)}')
                QMessageBox.critical(self, '播放错误', f'无法播放媒体文件:\n{str(e)}')
                self.stop()

    def stop(self):
        try:
            if self.player:
                self.player.stop()
                if self.player.is_playing():
                    self.player.stop()
                self.player.release()
            if self.instance:
                self.instance.release()
            if self.media:
                self.media.release()
        except Exception as e:
            print(f'资源释放错误: {str(e)}')
        finally:
            self.timer.stop()
            self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
            self.player = None
            self.instance = None
            self.media = None

    def check_playback_status(self):
        if not self.player.is_playing():
            QMessageBox.warning(self, '播放失败', '媒体无法正常播放,可能已损坏或格式不支持')
            self.stop()

    def set_volume(self, value):
        self.player.audio_set_volume(value)

    def set_position(self, value):
        if self.player and self.player.is_playing():
            self.player.set_position(value / 100)

    def update_ui(self):
        if self.player and self.player.is_playing():
            pos = self.player.get_position() * 100
            self.position_slider.setValue(int(pos))

        # 全屏状态处理(指示器已移除)

    def video_mouse_press_event(self, event):
        """处理视频帧鼠标点击事件"""
        print(f"鼠标点击事件,按钮: {event.button()}, 右键值: {Qt.RightButton}")
        if event.button() == Qt.RightButton:
            print("右键点击检测到,显示菜单")
            # 手动触发右键菜单,使用全局坐标确保菜单正确显示
            global_pos = self.video_frame.mapToGlobal(event.pos())
            self.show_context_menu(global_pos)
            event.accept()  # 接受事件,阻止继续传播
        else:
            # 传递其他鼠标事件
            QWidget.mousePressEvent(self.video_frame, event)

    def video_double_click_event(self, event):
        """处理视频帧鼠标双击事件 - 切换全屏"""
        print(f"双击事件触发,按钮: {event.button()}, 左键值: {Qt.LeftButton}")
        if event.button() == Qt.LeftButton:
            print("左键双击检测到,切换全屏")
            self.toggle_fullscreen()
        else:
            print(f"其他按钮双击: {event.button()}")
            # 传递其他鼠标事件
            QWidget.mouseDoubleClickEvent(self.video_frame, event)

    def volume_slider_mouse_press_event(self, event):
        """处理音量条鼠标点击事件"""
        print(f"音量条鼠标点击事件,按钮: {event.button()}, 右键值: {Qt.RightButton}")
        if event.button() == Qt.RightButton:
            print("音量条右键点击检测到,显示菜单")
            # 手动触发右键菜单,使用全局坐标确保菜单正确显示
            global_pos = self.volume_slider.mapToGlobal(event.pos())
            self.show_context_menu(global_pos)
            event.accept()  # 接受事件,阻止继续传播
        else:
            # 传递其他鼠标事件给音量条默认处理
            QSlider.mousePressEvent(self.volume_slider, event)

    def toggle_fullscreen(self):
        """切换全屏状态 - 简单的最大化窗口"""
        if not self.is_fullscreen:
            # 最大化窗口
            self.showMaximized()
            self.is_fullscreen = True
            print("窗口已最大化")
        else:
            # 恢复正常大小
            self.showNormal()
            self.is_fullscreen = False
            print("窗口恢复正常大小")

    def eventFilter(self, obj, event):
        """事件过滤器,用于捕获ESC键退出全屏和播放列表自动隐藏"""
        if obj == self.video_frame and event.type() == QEvent.KeyPress:
            if event.key() == Qt.Key_Escape and self.is_fullscreen:
                self.toggle_fullscreen()
                return True

        # 处理播放列表自动隐藏
        elif event.type() == QEvent.MouseMove:
            if obj == self.video_frame:
                # 获取鼠标在视频帧中的位置
                mouse_pos = event.pos()
                window_width = self.width()

                # 如果鼠标在窗口右侧区域,显示播放列表
                if mouse_pos.x() > window_width - 50:  # 右侧50像素区域
                    self.playlist_container.show()
                else:
                    # 只有当播放音频文件且鼠标不在右侧区域且播放列表当前可见时才隐藏
                    if not self.is_video_playing() and self.playlist_container.isVisible():
                        self.playlist_container.hide()
                return True

        return super().eventFilter(obj, event)

    def show_context_menu(self, global_pos):
        """显示右键菜单 - 使用全局坐标"""
        print(f"显示右键菜单,位置: {global_pos.x()}, {global_pos.y()}")
        menu = QMenu(self)

        # 设置菜单样式,使其在黑色背景上清晰可见
        menu.setStyleSheet("""
            QMenu {
                background-color: #2c3e50;
                border: 2px solid #3498db;
                border-radius: 8px;
                padding: 5px;
                font-family: "Microsoft YaHei", "Segoe UI";
                font-size: 12px;
            }
            QMenu::item {
                background-color: transparent;
                color: #ecf0f1;
                padding: 8px 20px;
                border-radius: 4px;
                margin: 2px;
                font-family: "Microsoft YaHei", "Segoe UI";
                font-size: 12px;
            }
            QMenu::item:selected {
                background-color: #3498db;
                color: white;
            }
            QMenu::separator {
                height: 1px;
                background-color: #7f8c8d;
                margin: 5px 0px;
            }
        """)

        open_file_action = QAction("📁 打开文件", self)
        open_file_action.triggered.connect(self.open_file)

        open_folder_action = QAction("📂 打开文件夹", self)
        open_folder_action.triggered.connect(self.open_folder)

        media_info_action = QAction("ℹ️ 媒体信息", self)
        media_info_action.triggered.connect(self.show_media_info)

        codec_info_action = QAction("🔊 解码器信息", self)
        codec_info_action.triggered.connect(self.show_codec_info)

        properties_action = QAction("⚙️ 属性参数", self)
        properties_action.triggered.connect(self.show_properties)

        menu.addAction(open_file_action)
        menu.addAction(open_folder_action)
        menu.addSeparator()
        menu.addAction(media_info_action)
        menu.addAction(codec_info_action)
        menu.addAction(properties_action)

        # 确保菜单在屏幕范围内显示,并提升到最顶层
        menu.setWindowFlags(menu.windowFlags() | Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)

        # 延迟执行菜单显示,确保VLC视频窗口不会遮挡
        QTimer.singleShot(50, lambda: menu.exec_(global_pos))

    def show_control_context_menu(self, global_pos):
        """显示控制区域的右键菜单"""
        print(f"控制区域右键菜单,位置: {global_pos.x()}, {global_pos.y()}")
        self.show_context_menu(global_pos)

    def open_file(self):
        path, _ = QFileDialog.getOpenFileName(self, '选择媒体文件', 
            self.last_directory, '媒体文件 (*.mp3 *.mp4 *.avi *.mkv *.wav *.flac *.aac *.wma *.mov *.wmv *.flv *.m4v *.3gp)')
        if not path:
            return

        # 保存最后打开的目录
        self.last_directory = os.path.dirname(path)
        self.settings.setValue("last_directory", self.last_directory)

        # 增强文件验证
        if not os.path.exists(path):
            QMessageBox.critical(self, '文件错误', '文件不存在或不可访问')
            return
        if not path.lower().endswith(('.mp4', '.avi', '.mkv', '.mp3', '.wav', '.flac', '.aac', '.wma', '.mov', '.wmv', '.flv', '.m4v', '.3gp')):
            QMessageBox.critical(self, '格式错误', '不支持的媒体格式')
            return

        # 清空播放列表并添加当前文件
        self.playlist_widget.clear()
        item = QListWidgetItem(os.path.basename(path))
        item.setData(Qt.UserRole, path)
        self.playlist_widget.addItem(item)

        # 打开单个文件时不显示播放列表
        if self.playlist_container.isVisible():
            self.playlist_container.hide()

        try:
            # 释放现有媒体资源
            if self.media:
                self.media.release()

            self.media = self.instance.media_new(path)
            if not self.media:
                raise Exception('无法创建媒体对象')

            # 预解析媒体信息(非强制验证)
            try:
                self.media.parse_with_options(1, 0)  # 同步解析基础信息
                if self.media.get_duration() == -1:
                    print('警告: 媒体时长无法获取,继续尝试播放')
            except:
                print('媒体解析跳过,直接尝试播放')

            self.player.set_media(self.media)

            # 重新设置视频输出(确保每次加载媒体后视频都能正确显示)
            if sys.platform == 'win32':
                self.player.set_hwnd(int(self.video_frame.winId()))
            else:
                self.player.set_xwindow(int(self.video_frame.winId()))

            self.toggle_play()

            # 添加播放状态监听
            QTimer.singleShot(1000, lambda: self.check_playback_status())

        except Exception as e:
            QMessageBox.critical(self, '初始化错误', f'媒体加载失败: {str(e)}')
            self.stop()

    def open_folder(self):
        """打开文件夹并显示播放列表"""
        folder = QFileDialog.getExistingDirectory(self, '选择媒体文件夹', self.last_directory)
        if not folder:
            return

        # 保存最后打开的目录
        self.last_directory = folder
        self.settings.setValue("last_directory", self.last_directory)

        # 查找文件夹中的媒体文件
        media_files = []
        for ext in ['.mp4', '.avi', '.mkv', '.mp3', '.wav', '.flac', '.aac', '.wma', '.mov', '.wmv', '.flv', '.m4v', '.3gp']:
            media_files.extend(glob.glob(os.path.join(folder, f'*{ext}')))
            media_files.extend(glob.glob(os.path.join(folder, f'*{ext.upper()}')))

        if not media_files:
            QMessageBox.information(self, '无媒体文件', '该文件夹中没有找到支持的媒体文件')
            return

        # 清空并填充播放列表
        self.playlist_widget.clear()
        for file_path in sorted(media_files):
            item = QListWidgetItem(os.path.basename(file_path))
            item.setData(Qt.UserRole, file_path)  # 存储完整路径
            self.playlist_widget.addItem(item)

        # 显示播放列表(打开文件夹时自动显示)
        if not self.playlist_container.isVisible():
            self.playlist_container.show()

        # 自动播放第一个文件
        if media_files:
            self.play_selected_file(self.playlist_widget.item(0))

    def open_file_from_path(self, path):
        """从指定路径打开文件"""
        # 保存最后打开的目录
        self.last_directory = os.path.dirname(path)
        self.settings.setValue("last_directory", self.last_directory)

        # 增强文件验证
        if not os.path.exists(path):
            QMessageBox.critical(self, '文件错误', '文件不存在或不可访问')
            return
        if not path.lower().endswith(('.mp4', '.avi', '.mkv', '.mp3', '.wav', '.flac', '.aac', '.wma', '.mov', '.wmv', '.flv', '.m4v', '.3gp')):
            QMessageBox.critical(self, '格式错误', '不支持的媒体格式')
            return

        try:
            # 释放现有媒体资源
            if self.media:
                self.media.release()

            self.media = self.instance.media_new(path)
            if not self.media:
                raise Exception('无法创建媒体对象')

            # 预解析媒体信息(非强制验证)
            try:
                self.media.parse_with_options(1, 0)  # 同步解析基础信息
                if self.media.get_duration() == -1:
                    print('警告: 媒体时长无法获取,继续尝试播放')
            except:
                print('媒体解析跳过,直接尝试播放')

            self.player.set_media(self.media)

            # 重新设置视频输出(确保每次加载媒体后视频都能正确显示)
            if sys.platform == 'win32':
                self.player.set_hwnd(int(self.video_frame.winId()))
            else:
                self.player.set_xwindow(int(self.video_frame.winId()))

            self.toggle_play()

            # 添加播放状态监听
            QTimer.singleShot(1000, lambda: self.check_playback_status())

        except Exception as e:
            QMessageBox.critical(self, '初始化错误', f'媒体加载失败: {str(e)}')
            self.stop()

    def show_media_info(self):
        """显示媒体文件详细信息"""
        if not self.media:
            QMessageBox.information(self, '媒体信息', '没有加载任何媒体文件')
            return

        try:
            info = "媒体文件信息:\n"

            # 获取媒体元数据
            meta_keys = [vlc.Meta.Title, vlc.Meta.Artist, vlc.Meta.Album, vlc.Meta.Genre, 
                        vlc.Meta.Copyright, vlc.Meta.Description, vlc.Meta.Rating]

            meta_names = {
                vlc.Meta.Title: '标题',
                vlc.Meta.Artist: '艺术家',
                vlc.Meta.Album: '专辑',
                vlc.Meta.Genre: '流派',
                vlc.Meta.Copyright: '版权',
                vlc.Meta.Description: '描述',
                vlc.Meta.Rating: '评分'
            }

            for key in meta_keys:
                value = self.media.get_meta(key)
                if value:
                    info += f"{meta_names.get(key, '未知')}: {value}\n"

            # 获取技术信息
            duration = self.media.get_duration()
            if duration > 0:
                mins, secs = divmod(duration // 1000, 60)
                info += f"时长: {mins}分{secs}秒\n"

            tracks = list(self.media.tracks_get()) if self.media.tracks_get() else []
            if tracks:
                info += f"轨道数: {len(tracks)}\n"
                for i, track in enumerate(tracks):
                    info += f"轨道 {i+1}: {track.type_name}\n"
                    if hasattr(track, 'bitrate'):
                        info += f"  码率: {track.bitrate // 1000} kbps\n"
                    if hasattr(track, 'rate'):
                        info += f"  采样率: {track.rate} Hz\n"
                    if hasattr(track, 'channels'):
                        info += f"  声道数: {track.channels}\n"

            QMessageBox.information(self, '媒体信息', info)

        except Exception as e:
            QMessageBox.warning(self, '信息错误', f'无法获取媒体信息: {str(e)}')

    def show_codec_info(self):
        """显示解码器信息"""
        if not self.player:
            QMessageBox.information(self, '解码器信息', '没有正在播放的媒体')
            return

        info = "解码器信息:\n"
        info += f"视频解码器: {self.player.video_get_spu()}\n"
        info += f"音频解码器: {self.player.audio_get_track()}\n"
        info += f"视频格式: {self.player.video_get_size()}\n"
        info += f"音频声道: {self.player.audio_get_channel()}\n"
        info += f"采样率: {self.player.audio_get_rate()}\n"

        QMessageBox.information(self, '解码器信息', info)

    def play_selected_file(self, item):
        """播放选中的文件"""
        if item:
            file_path = item.data(Qt.UserRole)
            self.open_file_from_path(file_path)
            # 更新当前播放显示
            file_name = os.path.basename(file_path)
            self.current_playing_label.setText(f"当前播放: {file_name}")
            # 注意:移除了自动隐藏播放列表的逻辑,避免需要点击两次

    def play_previous(self):
        """播放上一曲"""
        if self.playlist_widget.count() == 0:
            return

        current_row = self.playlist_widget.currentRow()
        if current_row == -1:
            # 如果没有选中项,播放第一首
            self.playlist_widget.setCurrentRow(0)
            self.play_selected_file(self.playlist_widget.item(0))
        elif current_row > 0:
            # 播放上一首
            self.playlist_widget.setCurrentRow(current_row - 1)
            self.play_selected_file(self.playlist_widget.item(current_row - 1))
        else:
            # 如果是第一首,循环到最后一首
            self.playlist_widget.setCurrentRow(self.playlist_widget.count() - 1)
            self.play_selected_file(self.playlist_widget.item(self.playlist_widget.count() - 1))

    def play_next(self):
        """播放下一曲"""
        if self.playlist_widget.count() == 0:
            return

        current_row = self.playlist_widget.currentRow()
        if current_row == -1:
            # 如果没有选中项,播放第一首
            self.playlist_widget.setCurrentRow(0)
            self.play_selected_file(self.playlist_widget.item(0))
        elif current_row < self.playlist_widget.count() - 1:
            # 播放下一首
            self.playlist_widget.setCurrentRow(current_row + 1)
            self.play_selected_file(self.playlist_widget.item(current_row + 1))
        else:
            # 如果是最后一首,循环到第一首
            self.playlist_widget.setCurrentRow(0)
            self.play_selected_file(self.playlist_widget.item(0))

    def toggle_playlist(self):
        """切换播放列表显示状态"""
        if self.playlist_container.isVisible():
            self.playlist_container.hide()
        else:
            self.playlist_container.show()

    def toggle_maximize(self):
        """切换最大化状态"""
        if self.isMaximized():
            self.showNormal()
        else:
            self.showMaximized()

    def mouse_press_event(self, event):
        """鼠标按下事件 - 用于窗口拖动"""
        if event.button() == Qt.LeftButton:
            self.dragging = True
            self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
            event.accept()

    def mouse_move_event(self, event):
        """鼠标移动事件 - 用于窗口拖动"""
        if self.dragging and event.buttons() == Qt.LeftButton:
            self.move(event.globalPos() - self.drag_position)
            event.accept()

    def mouse_release_event(self, event):
        """鼠标释放事件"""
        if event.button() == Qt.LeftButton:
            self.dragging = False
            event.accept()

    def event(self, event):
        """重写事件处理,支持在窗口顶部和底部拖动"""
        # 支持在窗口顶部和底部区域拖动
        if event.type() == QEvent.MouseButtonPress:
            if event.button() == Qt.LeftButton:
                # 检查是否在顶部或底部区域
                pos = event.pos()
                if pos.y() < 20 or pos.y() > self.height() - 20:  # 顶部或底部20像素区域
                    self.dragging = True
                    self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
                    return True

        elif event.type() == QEvent.MouseMove:
            if self.dragging and event.buttons() == Qt.LeftButton:
                self.move(event.globalPos() - self.drag_position)
                return True

        elif event.type() == QEvent.MouseButtonRelease:
            if event.button() == Qt.LeftButton:
                self.dragging = False
                return True

        return super().event(event)

    def is_video_playing(self):
        """检测当前是否正在播放视频"""
        if not self.media or not self.player:
            return False

        try:
            # 获取媒体轨道信息
            tracks = self.media.tracks_get()
            if tracks:
                for track in tracks:
                    # 如果有视频轨道,说明是视频文件
                    if hasattr(track, 'type') and track.type == 0:  # 0 表示视频轨道
                        return True

            # 如果没有视频轨道但有音频轨道,说明是音频文件
            for track in tracks:
                if hasattr(track, 'type') and track.type == 1:  # 1 表示音频轨道
                    return False

            # 备用方法:检查文件扩展名
            if hasattr(self.media, 'get_mrl'):
                mrl = self.media.get_mrl()
                if mrl and any(ext in mrl.lower() for ext in ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.m4v', '.3gp']):
                    return True
                elif mrl and any(ext in mrl.lower() for ext in ['.mp3', '.wav', '.flac', '.aac', '.wma']):
                    return False

        except Exception as e:
            print(f'检测媒体类型错误: {str(e)}')

        # 默认返回False(音频)
        return False

    def show_properties(self):
        """显示属性参数对话框"""
        dialog = QDialog(self)
        dialog.setWindowTitle('媒体属性参数')
        dialog.setFixedSize(400, 300)

        # 设置对话框样式表,确保白色文字在深色背景上清晰可见
        dialog.setStyleSheet("""
            QDialog {
                background-color: #2c3e50;
                color: #ffffff;
                font-family: "Microsoft YaHei", "Segoe UI";
                font-size: 12px;
            }
            QTableWidget {
                background-color: #34495e;
                alternate-background-color: #2c3e50;
                color: #ecf0f1;
                gridline-color: #7f8c8d;
                border: 1px solid #7f8c8d;
                border-radius: 4px;
            }
            QTableWidget::item {
                padding: 6px;
                border-bottom: 1px solid #7f8c8d;
            }
            QTableWidget::item:selected {
                background-color: #3498db;
                color: white;
            }
            QHeaderView::section {
                background-color: #2980b9;
                color: white;
                padding: 8px;
                border: none;
                font-weight: bold;
            }
            QPushButton {
                background-color: #3498db;
                color: white;
                border: none;
                padding: 10px 20px;
                border-radius: 6px;
                font-weight: bold;
                margin: 10px;
            }
            QPushButton:hover {
                background-color: #2980b9;
            }
            QPushButton:pressed {
                background-color: #21618c;
            }
        """)

        layout = QVBoxLayout()

        # 创建属性表格
        table = QTableWidget()
        table.setColumnCount(2)
        table.setHorizontalHeaderLabels(['属性', '值'])
        table.horizontalHeader().setStretchLastSection(True)
        table.setAlternatingRowColors(True)

        properties = []

        if self.media:
            try:
                # 添加媒体属性
                properties.extend([
                    ['文件路径', self.media.get_mrl()],
                    ['媒体状态', '已加载' if self.media.is_parsed() else '未解析'],
                    ['时长', f'{self.media.get_duration() // 1000} 秒'],
                ])

                # 添加轨道信息
                tracks = self.media.tracks_get()
                if tracks:
                    for i, track in enumerate(tracks):
                        properties.extend([
                            [f'轨道 {i+1} 类型', track.type_name],
                            [f'轨道 {i+1} 编码', track.codec if hasattr(track, 'codec') else '未知'],
                        ])

            except Exception as e:
                properties.append(['错误', f'获取属性失败: {str(e)}'])

        if self.player:
            try:
                properties.extend([
                    ['播放状态', '播放中' if self.player.is_playing() else '暂停/停止'],
                    ['音量', f'{self.player.audio_get_volume()}%'],
                    ['播放位置', f'{self.player.get_position() * 100:.1f}%'],
                ])
            except Exception as e:
                properties.append(['播放器错误', f'获取状态失败: {str(e)}'])

        table.setRowCount(len(properties))
        for i, (key, value) in enumerate(properties):
            table.setItem(i, 0, QTableWidgetItem(str(key)))
            table.setItem(i, 1, QTableWidgetItem(str(value)))

        layout.addWidget(table)

        # 添加关闭按钮
        close_btn = QPushButton('关闭')
        close_btn.clicked.connect(dialog.accept)
        layout.addWidget(close_btn)

        dialog.setLayout(layout)
        dialog.exec_()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    player = MediaPlayer()
    player.show()
    sys.exit(app.exec_())