使用python实现视频播放器(支持拖动播放位置跳转)
使用python实现视频播放器(支持拖动播放位置跳转)
Python实现视频播放器,在我早期的博文中介绍或作为资料记录过
Python实现视频播放器 https://blog.csdn.net/cnds123/article/details/145926189
Python实现本地视频/音频播放器https://blog.csdn.net/cnds123/article/details/137874107
Python简单GUI程序示例 中 “四、视频播放器” https://blog.csdn.net/cnds123/article/details/122903311
但是,一直不尽人意。现在,再介绍一个。
这是一个基于 PyQt6 和 python-vlc 开发的视频播放器,主要实现了我多次试图实现未果的功能
——带有播放进度条,不仅显示播放进度,还支持拖动播放位置跳转。
普通版视频播放器
主要特点:
播放画面随窗口缩放
支持常见格式(MP4、AVI、MKV 等)
通过文件对话框加载视频文件
播放/暂停、停止、播放进度跳转、音量调节
显示视频文件名、当前播放时间、总时长
播放进度条:显示播放进度并支持拖动播放位置跳转。
音量滑块:音量滑块调整音量大小。
需安装以下 Python第三方库:
python-vlc、 PyQt6
Windows中,还要安装 VLC 播放器,其下载 地址 https://www.videolan.org/vlc/ 。否则,将报错:缺少 libvlc.dll。
运行效果界面如下:
基本使用操作:
打开文件:点击菜单栏 文件 > 打开文件(快捷键 Ctrl+O) 或底部 打开文件 按钮
播放/暂停:空格键 或 点击 按钮切换
停止:停止 按钮
进度跳转:拖动进度条
音量调节:拖动底部音量滑块
源码如下:
import sys
import time
import vlc
import os
from PyQt6 import QtWidgets, QtCore, QtGui
class VLCPlayer(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.instance = vlc.Instance()
self.player = self.instance.media_player_new()
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.update_ui)
self.current_file = None
self.media_loaded = False
self.init_ui()
def init_ui(self):
# 主窗口设置
self.setWindowTitle("PyQt6 VLC Player")
self.resize(800, 600)
# 创建菜单栏
menubar = self.menuBar()
file_menu = menubar.addMenu("文件(&F)")
# 添加"打开"动作
open_action = QtGui.QAction("打开文件...", self)
open_action.setShortcut("Ctrl+O")
open_action.triggered.connect(self.open_file)
file_menu.addAction(open_action)
# 创建主容器和布局
central_widget = QtWidgets.QWidget(self)
self.setCentralWidget(central_widget)
main_layout = QtWidgets.QVBoxLayout(central_widget)
main_layout.setContentsMargins(0, 0, 0, 0)
# 视频标题标签
self.title_label = QtWidgets.QLabel("当前未选择媒体文件")
self.title_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.title_label.setStyleSheet("font-size: 14px; color: #666; margin: 5px;")
main_layout.addWidget(self.title_label)
# 视频显示区域
self.video_widget = QtWidgets.QWidget()
self.video_widget.setStyleSheet("background-color: black;")
main_layout.addWidget(self.video_widget, stretch=1)
# 控制面板
control_panel = QtWidgets.QWidget()
control_layout = QtWidgets.QVBoxLayout(control_panel)
# 进度条
self.progress_bar = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
self.progress_bar.setMinimum(0)
self.progress_bar.sliderMoved.connect(self.set_position)
control_layout.addWidget(self.progress_bar)
# 时间标签
self.time_label = QtWidgets.QLabel("00:00:00 / 00:00:00")
control_layout.addWidget(self.time_label)
# 控制按钮
button_layout = QtWidgets.QHBoxLayout()
self.play_btn = QtWidgets.QPushButton("播放")
self.play_btn.clicked.connect(self.toggle_play)
self.stop_btn = QtWidgets.QPushButton("停止")
self.stop_btn.clicked.connect(self.stop)
self.open_btn = QtWidgets.QPushButton("打开文件")
self.open_btn.clicked.connect(self.open_file)
# 音量控制
self.volume_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
self.volume_slider.setRange(0, 100)
self.volume_slider.setValue(100)
self.volume_slider.valueChanged.connect(self.set_volume)
# 添加控件
button_layout.addWidget(self.open_btn)
button_layout.addWidget(self.play_btn)
button_layout.addWidget(self.stop_btn)
button_layout.addWidget(QtWidgets.QLabel("音量:"))
button_layout.addWidget(self.volume_slider)
control_layout.addLayout(button_layout)
main_layout.addWidget(control_panel)
# 设置VLC渲染
if sys.platform == "win32":
self.player.set_hwnd(int(self.video_widget.winId()))
elif sys.platform == "linux":
self.player.set_xwindow(self.video_widget.winId())
elif sys.platform == "darwin":
from PyQt6.QtGui import QCocoaNativeContext
self.player.set_nsobject(int(QCocoaNativeContext(self.video_widget.winId()).nsview()))
self.timer.start(200)
def open_file(self):
# 修复1:打开文件前先停止播放
if self.player.is_playing():
self.player.stop()
self.media_loaded = False
self.play_btn.setText("播放")
file_dialog = QtWidgets.QFileDialog(self)
file_dialog.setNameFilter("视频文件 (*.mp4 *.avi *.mkv *.mov *.flv)")
if file_dialog.exec():
selected_files = file_dialog.selectedFiles()
if selected_files:
self.load_media(selected_files[0])
def load_media(self, file_path):
try:
# 新增:停止定时器避免冲突
self.timer.stop()
# 修复2:确保彻底释放旧媒体资源
self.player.stop()
self.player.set_media(None) # 清除旧媒体引用
# 重置状态
self.media_loaded = False
self.play_btn.setText("播放")
self.progress_bar.setValue(0)
self.time_label.setText("00:00:00 / 00:00:00")
# 加载新文件
self.current_file = file_path
media = self.instance.media_new(file_path)
self.player.set_media(media)
# 更新标题
file_name = os.path.basename(file_path)
self.title_label.setText(f"当前播放: {file_name}")
# 修复3:异步解析媒体信息(避免阻塞UI)
media.parse_with_options(vlc.MediaParseFlag.network, 1000)
# 设置进度条最大值
self.progress_bar.setMaximum(media.get_duration())
self.media_loaded = True
# 显示总时长
total_time = time.strftime("%H:%M:%S", time.gmtime(media.get_duration() // 1000))
self.time_label.setText(f"00:00:00 / {total_time}")
# 新增:确保媒体加载完成后再启定时器
self.timer.start(200)
except Exception as e:
QtWidgets.QMessageBox.critical(self, "错误", f"无法加载文件:\n{str(e)}")
self.media_loaded = False
self.title_label.setText("媒体加载失败")
def toggle_play(self):
if not self.media_loaded:
self.open_file()
return
# 修复4:强制同步按钮状态
if self.player.is_playing():
self.player.pause()
self.play_btn.setText("播放")
else:
self.player.play()
self.play_btn.setText("暂停")
def stop(self):
self.player.stop()
self.progress_bar.setValue(0)
self.time_label.setText("00:00:00 / 00:00:00")
self.play_btn.setText("播放")
self.title_label.setText("播放已停止")
def set_volume(self, value):
self.player.audio_set_volume(value)
def set_position(self, value):
if self.player.is_seekable():
self.player.set_position(value / self.progress_bar.maximum())
def update_ui(self):
if not self.media_loaded:
return # 新增:防止在无媒体时更新
media_length = self.player.get_length()
if media_length > 0:
current_time = self.player.get_time()
# 新增:检测播放结束
if current_time >= media_length - 500: # 留50ms容差
self.stop()
return
self.progress_bar.setMaximum(media_length)
self.progress_bar.setValue(current_time)
total_time = time.strftime("%H:%M:%S", time.gmtime(media_length // 1000))
current_time_str = time.strftime("%H:%M:%S", time.gmtime(current_time // 1000))
self.time_label.setText(f"{current_time_str} / {total_time}")
def closeEvent(self, event):
self.player.stop()
event.accept()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
player = VLCPlayer()
player.show()
sys.exit(app.exec())
专用版视频播放控制器(视频播控器)
主要添加视频“加密”、“解密”功能
运行条件除了和上面的一样外,还需安装第三方库pycryptodome
pycryptodome 是一个强大的加密库,用于实现加密算法。
界面如下:
1)加密视频
菜单栏点击 安全 → 加密视频
优先当前播放文件路径,即:
如果当前正在播放视频且未加密 → 弹出确认对话框
否则 → 弹出文件选择对话框
加密文件的后缀 .vef,放在原文将后缀之后。命名规则为:原文件名.vef(如 video.mp4 → video.mp4.vef)
2)解密视频
菜单栏点击 安全 → 解密视频
文件过滤:仅显示 .vef 文件
解密文件,出现保存对话框,默认放置在原位置,默认用文件名(可改),若存放处有同名文件,提示是否替换。
命名规则:自动去除 .vef 后缀 → video.mp4
解密时,若输入的密码和加密时不一致,提示:密码错误或文件损坏 → 立即终止并提示
源码如下:
import sys
import time
import hashlib
import secrets
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import vlc
import os
from PyQt6 import QtWidgets, QtCore, QtGui
class VLCPlayer(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.instance = vlc.Instance()
self.player = self.instance.media_player_new()
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.update_ui) # 连接定时器到更新方法
self.current_file = None
self.media_loaded = False
self.temp_files = set() #用于跟踪临时解密文件
self.init_ui()
self.init_crypto()
def init_ui(self):
# 主窗口设置
self.setWindowTitle("视频播控器(特别专用版)")
self.resize(800, 600)
# 初始化菜单
menubar = self.menuBar()
file_menu = menubar.addMenu("文件(&F)")
crypto_menu = menubar.addMenu("安全(&S)")
# 文件操作
open_action = self.create_action("打开文件...", "Ctrl+O", self.open_file)
# exit_action = self.create_action("退出", "Ctrl+Q", lambda: self.close()) # 播放退出太慢
# 加密解密操作
encrypt_action = self.create_action("加密视频...", "Ctrl+E", self.encrypt_video)
decrypt_action = self.create_action("解密视频...", "Ctrl+D", self.decrypt_video)
# 添加菜单项
file_menu.addAction(open_action)
file_menu.addSeparator()
# file_menu.addAction(exit_action)
crypto_menu.addAction(encrypt_action)
crypto_menu.addAction(decrypt_action)
# 创建主容器和布局
central_widget = QtWidgets.QWidget(self)
self.setCentralWidget(central_widget)
main_layout = QtWidgets.QVBoxLayout(central_widget)
main_layout.setContentsMargins(0, 0, 0, 0)
# 视频标题标签
self.title_label = QtWidgets.QLabel("当前未选择媒体文件")
self.title_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.title_label.setStyleSheet("font-size: 14px; color: #666; margin: 5px;")
main_layout.addWidget(self.title_label)
# 视频显示区域
self.video_widget = QtWidgets.QWidget()
self.video_widget.setStyleSheet("background-color: black;")
main_layout.addWidget(self.video_widget, stretch=1)
# 控制面板
control_panel = QtWidgets.QWidget()
control_layout = QtWidgets.QVBoxLayout(control_panel)
# 进度条
self.progress_bar = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
self.progress_bar.setMinimum(0)
self.progress_bar.sliderMoved.connect(self.set_position)
control_layout.addWidget(self.progress_bar)
# 时间标签
self.time_label = QtWidgets.QLabel("00:00:00 / 00:00:00")
control_layout.addWidget(self.time_label)
# 控制按钮
button_layout = QtWidgets.QHBoxLayout()
self.play_btn = QtWidgets.QPushButton("播放")
self.play_btn.clicked.connect(self.toggle_play)
self.stop_btn = QtWidgets.QPushButton("停止")
self.stop_btn.clicked.connect(self.stop)
self.open_btn = QtWidgets.QPushButton("打开文件")
self.open_btn.clicked.connect(self.open_file)
# 音量控制
self.volume_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
self.volume_slider.setRange(0, 100)
self.volume_slider.setValue(100)
self.volume_slider.valueChanged.connect(self.set_volume)
# 添加控件
button_layout.addWidget(self.open_btn)
button_layout.addWidget(self.play_btn)
button_layout.addWidget(self.stop_btn)
button_layout.addWidget(QtWidgets.QLabel("音量:"))
button_layout.addWidget(self.volume_slider)
control_layout.addLayout(button_layout)
main_layout.addWidget(control_panel)
# 设置VLC渲染
if sys.platform == "win32":
self.player.set_hwnd(int(self.video_widget.winId()))
elif sys.platform == "linux":
self.player.set_xwindow(self.video_widget.winId())
elif sys.platform == "darwin":
from PyQt6.QtGui import QCocoaNativeContext
self.player.set_nsobject(int(QCocoaNativeContext(self.video_widget.winId()).nsview()))
self.timer.start(200)
def init_crypto(self):
"""初始化加密参数"""
self.key_derivation_iterations = 100000
self.salt_size = 16
self.nonce_size = 16
self.tag_size = 16
self.chunk_size = 64 * 1024 # 64KB块处理
def create_action(self, text, shortcut, callback):
"""创建标准化菜单动作"""
action = QtGui.QAction(text, self)
action.setShortcut(shortcut)
action.triggered.connect(callback)
return action
def open_file(self):
# 修复1:打开文件前先停止播放
if self.player.is_playing():
self.player.stop()
self.media_loaded = False
self.play_btn.setText("播放")
file_dialog = QtWidgets.QFileDialog(self)
file_dialog.setNameFilter("视频文件 (*.mp4 *.avi *.mkv *.mov *.flv)")
if file_dialog.exec():
selected_files = file_dialog.selectedFiles()
if selected_files:
self.load_media(selected_files[0])
def load_media(self, file_path):
try:
# 如果是加密文件需要特殊处理
if file_path.lower().endswith('.vef'):
QtWidgets.QMessageBox.warning(
self, "警告",
"请使用菜单中的解密功能打开加密文件"
)
return
# 新增:停止定时器避免冲突
self.timer.stop()
# 修复2:确保彻底释放旧媒体资源
self.player.stop()
self.player.set_media(None) # 清除旧媒体引用
# 重置状态
self.media_loaded = False
self.play_btn.setText("播放")
self.progress_bar.setValue(0)
self.time_label.setText("00:00:00 / 00:00:00")
# 加载新文件
self.current_file = file_path
media = self.instance.media_new(file_path)
self.player.set_media(media)
# 更新标题
file_name = os.path.basename(file_path)
self.title_label.setText(f"当前播放: {file_name}")
# 修复3:异步解析媒体信息(避免阻塞UI)
media.parse_with_options(vlc.MediaParseFlag.network, 1000)
# 设置进度条最大值
self.progress_bar.setMaximum(media.get_duration())
self.media_loaded = True
# 显示总时长
total_time = time.strftime("%H:%M:%S", time.gmtime(media.get_duration() // 1000))
self.time_label.setText(f"00:00:00 / {total_time}")
# 新增:确保媒体加载完成后再启定时器
self.timer.start(200)
except Exception as e:
QtWidgets.QMessageBox.critical(self, "错误", f"无法加载文件:\n{str(e)}")
self.media_loaded = False
self.title_label.setText("媒体加载失败")
def toggle_play(self):
if not self.media_loaded:
self.open_file()
return
# 修复4:强制同步按钮状态
if self.player.is_playing():
self.player.pause()
self.play_btn.setText("播放")
else:
self.player.play()
self.play_btn.setText("暂停")
# ---------- 加密解密功能 ----------
def _get_encrypted_filename(self, src_path):
"""生成加密文件名(原文件名+.vef)"""
return src_path + ".vef" # 直接在原文件名后追加.vef
def encrypt_video(self):
"""智能加密方法:优先处理当前播放文件"""
# 自动检测当前播放文件
src_path = None
if self.current_file and os.path.exists(self.current_file):
# 检查是否已经是加密文件
if not self.current_file.lower().endswith('.vef'):
reply = QtWidgets.QMessageBox.question(
self, '加密确认',
f"是否加密当前播放的文件?\n{os.path.basename(self.current_file)}",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
src_path = self.current_file
# 如果无当前可用文件,则选择文件
if not src_path:
src_path, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "选择要加密的视频文件", "",
"视频文件 (*.mp4 *.avi *.mkv *.mov *.flv)"
)
if not src_path:
return
# 处理加密文件特殊情况
if src_path.lower().endswith('.vef'):
QtWidgets.QMessageBox.warning(self, "警告", "不能加密已加密文件")
return
# 获取保存路径
default_name = self._get_encrypted_filename(os.path.basename(src_path))
dest_path, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "保存加密文件",
os.path.join(os.path.dirname(src_path), default_name), # 默认原目录
"加密视频 (*.vef)"
)
if not dest_path:
return
# 获取密码
password, ok = QtWidgets.QInputDialog.getText(
self, "输入密码", "设置加密密码:",
QtWidgets.QLineEdit.EchoMode.Password
)
if not ok or not password:
return
# 执行加密流程
try:
# 如果是当前播放文件,停止播放
was_playing = False
if src_path == self.current_file:
was_playing = self.player.is_playing()
self.player.stop()
self.media_loaded = False
self._encrypt_file(src_path, dest_path, password)
# 成功提示
msg = f"加密成功!\n原文件: {os.path.basename(src_path)}\n加密文件: {os.path.basename(dest_path)}"
QtWidgets.QMessageBox.information(self, "完成", msg)
# 如果加密的是当前文件,询问是否加载加密文件
if src_path == self.current_file:
choice = QtWidgets.QMessageBox.question(
self, "加载文件", "是否立即加载加密后的文件?",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
)
if choice == QtWidgets.QMessageBox.StandardButton.Yes:
self.load_media(dest_path)
except Exception as e:
self.show_error(f"加密失败: {str(e)}")
finally:
# 恢复原始状态(如果需要)
if was_playing and src_path != dest_path:
self.load_media(src_path)
def _encrypt_file(self, src_path, dest_path, password):
"""执行文件加密(新增)"""
try:
# 生成加密参数
salt = secrets.token_bytes(self.salt_size)
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
self.key_derivation_iterations,
dklen=32
)
cipher = AES.new(key, AES.MODE_GCM)
cipher.update(salt)
# 分块加密
with open(src_path, 'rb') as fin, open(dest_path, 'wb') as fout:
# 写入加密头
fout.write(salt)
fout.write(cipher.nonce)
while True:
chunk = fin.read(self.chunk_size)
if not chunk:
break
encrypted = cipher.encrypt(pad(chunk, AES.block_size))
fout.write(encrypted)
# 写入认证标签
fout.write(cipher.digest())
except PermissionError:
raise RuntimeError("文件被其他程序占用,请关闭后重试")
except Exception as e:
raise RuntimeError(f"加密失败: {str(e)}")
def _get_decrypted_filename(self, src_path):
"""生成解密文件名(去除.vef后缀)"""
if src_path.lower().endswith('.vef'):
return src_path[:-4] # 去除.vef
return src_path + "_decrypted"
def decrypt_video(self):
"""增强型解密方法"""
try:
# 选择加密文件
src_file, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "选择要解密的文件", "",
"加密视频 (*.vef)"
)
if not src_file:
return
# 生成默认保存路径
default_path = self._get_decrypted_filename(src_file)
dest_file, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "保存解密文件",
default_path, # 默认原目录+去后缀
"视频文件 (*.*)"
)
if not dest_file:
return
# 检查文件是否已存在
if os.path.exists(dest_file):
reply = QtWidgets.QMessageBox.question(
self, "文件存在",
f"目标文件已存在,是否覆盖?\n{dest_file}",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No
)
if reply != QtWidgets.QMessageBox.StandardButton.Yes:
return
# 获取密码
password, ok = QtWidgets.QInputDialog.getText(
self, "输入密码", "解密密码:",
QtWidgets.QLineEdit.EchoMode.Password
)
if not ok or not password:
return
# 执行解密
decrypted_path = self._decrypt_file(src_file, password, dest_file)
if decrypted_path:
QtWidgets.QMessageBox.information(
self, "成功",
f"文件解密成功!\n保存路径: {decrypted_path}"
)
self.load_media(decrypted_path)
except Exception as e:
self.show_error(f"解密失败: {str(e)}")
def _decrypt_file(self, src_path, password, dest_path):
"""增强型解密方法(保存到指定路径)"""
try:
with open(src_path, 'rb') as fin:
salt = fin.read(self.salt_size)
nonce = fin.read(self.nonce_size)
key = self._derive_key(password, salt)
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
cipher.update(salt)
with open(dest_path, 'wb') as fout:
encrypted = fin.read()
ciphertext, tag = encrypted[:-self.tag_size], encrypted[-self.tag_size:]
# 分块解密写入
chunk_size = self.chunk_size + AES.block_size
for i in range(0, len(ciphertext), chunk_size):
chunk = ciphertext[i:i+chunk_size]
decrypted = unpad(cipher.decrypt(chunk), AES.block_size)
fout.write(decrypted)
# 验证标签
cipher.verify(tag)
return dest_path
except ValueError as ve:
# 清理已写入的部分文件
if os.path.exists(dest_path):
try:
os.remove(dest_path)
except:
pass
raise ValueError("解密失败 - 密码错误或文件损坏") from ve
except Exception as e:
if os.path.exists(dest_path):
try:
os.remove(dest_path)
except:
pass
raise RuntimeError(f"解密过程错误: {str(e)}") from e
def _derive_key(self, password, salt):
"""生成加密密钥"""
return hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
self.key_derivation_iterations,
dklen=32 # AES-256需要32字节密钥
)
# ---------- 辅助功能 ----------
def show_error(self, message):
"""显示错误提示"""
QtWidgets.QMessageBox.critical(self, "错误", message)
def closeEvent(self, event):
"""关闭处理,清理临时文件"""
for temp_file in self.temp_files:
try:
if os.path.exists(temp_file):
os.remove(temp_file)
except Exception as e:
print(f"删除临时文件失败: {str(e)}")
self.player.stop() # 停止播放器
event.accept() #
def stop(self):
self.player.stop()
self.progress_bar.setValue(0)
self.time_label.setText("00:00:00 / 00:00:00")
self.play_btn.setText("播放")
self.title_label.setText("播放已停止")
def set_volume(self, value):
self.player.audio_set_volume(value)
def set_position(self, value):
if self.player.is_seekable():
self.player.set_position(value / self.progress_bar.maximum())
def update_ui(self):
if not self.media_loaded:
return # 新增:防止在无媒体时更新
media_length = self.player.get_length()
if media_length > 0:
current_time = self.player.get_time()
# 新增:检测播放结束
if current_time >= media_length - 500: # 留50ms容差
self.stop()
return
self.progress_bar.setMaximum(media_length)
self.progress_bar.setValue(current_time)
total_time = time.strftime("%H:%M:%S", time.gmtime(media_length // 1000))
current_time_str = time.strftime("%H:%M:%S", time.gmtime(current_time // 1000))
self.time_label.setText(f"{current_time_str} / {total_time}")
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
player = VLCPlayer()
player.show()
sys.exit(app.exec())