上一篇我写了显示播放图片的QLabel控件,本篇写显示控件的图片数据来源,涉及到了ffpyplayer的使用,它的官方API文档可参阅ffpyplayer · PyPI。大致的逻辑是:在python中引入ffpyplayer的MediaPlayer(媒体播放器),通过MediaPlayer的实例读取视频文件,获取视频图片和音频数据,然后将图片数据展示在QLabel中,音频数据由MediaPlayer播放。因为涉及到从视频文件的I/O读取操作,所以需要将该部分代码放在线程中执行。关于python的多线程编程我在后面的篇章再介绍,因为截至目前因为GIL的机制,python还没有真正实现线程的并行。上代码,代码中的注释写的比较详细了,其中self.changePixmap.emit(convertToQtFormat),就是将获取的图片数据通过信号的方式发给QLabel.
from PySide6 import QtCore, QtGui
from PySide6.QtCore import QObject
import time
import cv2
from PIL import Image, ImageQt
import os
from ffpyplayer.player import MediaPlayer
from com.xx.exceptions.FileNotFound import FileNotFound
import com.xx.log as logs
# log的作用域在当前模块
log = logs.get_logger(__name__)
"""
定义一个线程,用于视频的播放
"""
class VideoPlayerRun(QObject):
# 定义类变量。定义一个信号,用于发送图片文件,并将图片信息发送给用于显示的标签控件中。
changePixmap = QtCore.Signal(QtGui.QImage)
#定义信号,用于发送当前播放进度给Slider
sliderValue = QtCore.Signal(float)
# 视频地址
"""
filename:文件路径
"""
def __init__(self, filename, qlabel, volume=1.0):
super().__init__()
# raise是一个常用的关键字,用于引发异常。它可以通过抛出异常来阻断程序的正常执行流程,并创建自定义的异常情况。
if not os.path.exists(filename):
raise FileNotFound(filename)
self.is_running = False
self.close = False
self.state = None
self.frame = None
self.l = qlabel # QLabel,用于播放视频
self.filename = filename # 要打开的文件名称
self.skip_interval = 5 # 快进参数
# ffpyplayer是一个用于音频和视频播放的模块
# 1.创建一个MediaPlayer对象,并指定要播放的音频文件路径,设置对象参数。用于播放音频
self.player = MediaPlayer(
filename,
ff_opts={
"sync": "audio",
"paused": False,
"volume": volume,
"t": 1e7 + 1,
"ss": 0,
},
)
time.sleep(1)
# 视频能够持续时间(单位是秒)
self.duration = self.player.get_metadata()["duration"]
"""
开始播放
"""
def run(self):
log.debug("开始播放")
try:
self.is_running=True
while self.is_running:
# get_frame()用于检索下一个可用帧。返回一个元组(frame,val),其中:
# frame为一个元组类型;(image,pts),其中image为图像,pts为当前图片播放真实时间
# val的值为: 'paused', 'eof', 或者为一个float数值。如果值为:'paused'或'eof'则frame为None
# 否则,val的值是播放该视频帧的等待时间,(推迟val后播放视屏帧,在)
frame, self.val = self.player.get_frame()
self.sliderValue.emit(self.player.get_pts())
# print(self.close)
if self.val == "eof":
self.close = True # 当前文件播放结束,则设置关闭状态
# 关闭视频,循环结束
if self.close == True:
self.player.toggle_pause() # toggle_pause方法来切换音频的暂停状态
# self.player.close_player() #调用此方法后,在此实例上调用任何其他类方法都可能导致崩溃或程序损坏
self.player.seek(0)
self.sliderValue.emit(0)
time.sleep(2)
self.is_running=False
break
if isinstance(self.val, str) or self.val == 0.0:
waitkey = 32
else:
waitkey = int(self.val * 100)
# waitKey() 函数的功能是不断刷新图像 , 频率时间为delay , 单位为ms 返回值为当前键盘按键值。
# 此处图片将显示32ms,如果在此期间你按下一个键,比如键a,那么将打印出97;如果不按键,则返回-1
# 使用waitKey可以实现休眠特定时长而不影响系统消息处理
# 其中,delay表示等待的毫秒数,如果设置为0,则表示无限等待,即直到用户按下按键才会返回。如果设置了一个正整数,则表示等待指定的毫秒数。
# waitKey函数的作用是等待用户按下一个按键,如果在指定的时间内没有按键按下,则返回-1
# start_time = datetime.datetime.now()
# cv2.waitKey(delay)返回值:
# 1、等待期间有按键:返回按键的ASCII码(比如:Esc的ASCII码为27,即0001 1011);(接收值为非空,if代码块执行)
# 2、等待期间没有按键:返回 -1;(非0非空,if代码块执行)
pressed_key = (
cv2.waitKey(waitkey) & 0xFF
) # 此处会阻塞线程,等待结束后,播放当前视频帧图片(在控件上显示当前正在显示的视频帧)
# print(waitkey,pressed_key)
if frame is None:
continue # 无当前帧,则继续读取视频
# 因获取的是ffpyplayer.pic.Image,所以先将其转为PIL.Image,然后再将PIL.Image转为QImage
image, pts = (
frame # frame也是一个二元组:图片和图片显示时间.image为:ffpyplayer.pic.Image类型
)
# print(pts)
self.frame = (image, self.val) # val是多久后播放
# 获取图片尺寸
x, y = image.get_size()
# 下面代码将RGB颜色模式转为 PYQT5需要的BGR格式
# image.to_bytearray()返回的是一个List,包含四个bytearray类型数据
# data为python的bytearray类型
data = image.to_bytearray()[0]
# 调用PIL库,将图片数据转为转为PIL.Image数据类型
image = Image.frombytes("RGB", (x, y), bytes(data))
# numpy.ndarray类型数据,将图片从RGB转为BGR格式数据,因为BGR是PYQT5展示图片的格式
# image = cv2.cvtColor(numpy.array(image), cv2.COLOR_RGB2BGR)
# self.frame = frame
# 将图片展示在标签控件上
if self.l != None:
convertToQtFormat = ImageQt.ImageQt(
image
) # 将图片转换为pyside6能识别的格式
self.changePixmap.emit(convertToQtFormat)
del image
print('播放完毕',self.is_running)
return self.is_running
except Exception as e:
self.is_running=False
print(e)
def stop(self):
self.is_running = False
def seek_p(self):
if int(self.player.get_pts()) + self.skip_interval < int(self.duration):
self.player.seek(self.skip_interval, relative=True, accurate=False)
def seek_m(self):
if int(self.player.get_pts()) - self.skip_interval > 0:
self.player.seek(-self.skip_interval, relative=True, accurate=False)
#此处 relative=False设置为False,表示不是偏移。否则为偏移量
def seek_by_slider(self,value):
self.player.seek(value, relative=False, accurate=False)
# def get_codec_format(file_path):
# video = VideoFileClip(file_path)
# video_codec = video.videofile_codec
# audio_codec = video.audiofile_codec
# return video_codec, audio_codec