ffpyplayer+Qt,制作一个视频播放器
- 项目地址
- FFmpegMediaPlayer
- VideoWidget
项目地址
https://gitee.com/chiyaun/QtFFMediaPlayer
FFmpegMediaPlayer
按照
QMediaPlayer
的方法重写一个ffpyplayer
# coding:utf-8
import logging
from typing import Unionfrom PySide6.QtCore import QTimer, QUrl, Signal, QObject
from PySide6.QtGui import QImage
from PySide6.QtMultimedia import QMediaPlayer
from PySide6.QtWidgets import QWidget
from ffpyplayer.pic import Image
from ffpyplayer.player import MediaPlayerlogging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger('FFmpegMediaPlayer')class FFmpegMediaPlayer(QObject):"""ffmpeg media player"""sourceChanged = Signal(QUrl)mediaStatusChanged = Signal(QMediaPlayer.MediaStatus)positionChanged = Signal(int)durationChanged = Signal(int)metaDataChanged = Signal(dict)playbackStateChanged = Signal(QMediaPlayer.PlaybackState)playingChanged = Signal(bool)errorChanged = Signal(QMediaPlayer.Error)def __init__(self, parent=None):super().__init__(parent)self.__source: QUrl = QUrl()self.__playerWidget: QWidget = Noneself.__mediaStatus: QMediaPlayer.MediaStatus = QMediaPlayer.MediaStatus.NoMediaself.__position: int = 0self.__duration: int = 0self.__metaData: dict = {}self.__error: QMediaPlayer.Error = QMediaPlayer.Error.NoErrorself.__errorString: str = ''self.timer = QTimer(self)self.player: MediaPlayer = Noneself.timer.timeout.connect(self._update_frame)def setSource(self, source: Union[str, QUrl]):if isinstance(source, QUrl):source = source.toString()if self.player:self.player.close_player()self.timer.stop()self.player = Nonelogger.debug(f'set source: {source}')self.player = MediaPlayer(source,ff_opts={'paused': True,'autoexit': True,'vn': False,'sn': False,'aud': 'sdl'},loglevel='debug',callback=self.__callback)self.__source = QUrl(source)self.sourceChanged.emit(self.__source)def source(self) -> QUrl:return self.__sourcedef fps(self) -> float:fps = self.metadata()["frame_rate"][0] / self.metadata()["frame_rate"][1]return fpsdef close(self):self.player.close_player()logger.debug('player closed')def play(self):self.player.set_pause(False)self.timer.start()self.playingChanged.emit(True)self.playbackStateChanged.emit(QMediaPlayer.PlaybackState.PlayingState)logger.debug('player playing')def pause(self):self.player.set_pause(True)self.timer.stop()self.playingChanged.emit(False)self.playbackStateChanged.emit(QMediaPlayer.PlaybackState.PausedState)logger.debug('player paused')def stop(self):self.player.set_pause(True)self.timer.stop()self.playingChanged.emit(False)self.playbackStateChanged.emit(QMediaPlayer.PlaybackState.StoppedState)logger.debug('player stopped')def toggle(self):logger.debug('toggle player')self.player.toggle_pause()if self.isPaused():self.timer.stop()self.playingChanged.emit(False)self.playbackStateChanged.emit(QMediaPlayer.PlaybackState.PausedState)logger.debug('player paused')else:self.timer.start()self.playingChanged.emit(True)self.playbackStateChanged.emit(QMediaPlayer.PlaybackState.PlayingState)logger.debug('player playing')def isPlaying(self) -> bool:return not self.player.get_pause()def isPaused(self) -> bool:return self.player.get_pause()def setPosition(self, position: int):if self.player is None:returnlogger.debug(f'set position: {position}')self.player.seek(position, relative=False)def position(self) -> int:return self.player.get_pts()def duration(self) -> int:return int(self.metadata().get('duration', 0))def __setPosition(self, position: Union[float, int]):if self.player is None:returnposition = int(position)if self.__position == position:returnself.__position = positionself.positionChanged.emit(position)def metaData(self) -> dict:meta = self.player.get_metadata()if meta != self.__metaData:self.__metaData = metaself.metaDataChanged.emit(meta)return metadef setVolume(self, volume: int):if self.player is None:returnlogger.debug(f'set volume: {volume}')self.player.set_volume(volume / 100)def volume(self) -> int:return int(self.player.get_volume() * 100)def setMuted(self, muted: bool):if self.player is None:returnlogger.debug(f'set muted: {muted}')self.player.set_mute(muted)def isMuted(self) -> bool:return self.player.get_mute()def setOutputPixFormat(self, pix_fmt: str):self.player.set_output_pix_fmt(pix_fmt)def outputPixFormat(self) -> str:return self.player.get_output_pix_fmt()def metadata(self) -> dict:return self.player.get_metadata()def __setMediaStatus(self, status: QMediaPlayer.MediaStatus):if status == self.__mediaStatus:returnlogger.debug(f'set media status: {status}')self.__mediaStatus = statusself.mediaStatusChanged.emit(status)def mediaStatus(self) -> QMediaPlayer.MediaStatus:return self.__mediaStatusdef _update_frame(self):frame, val = self.player.get_frame()if frame is None:self.__setMediaStatus(QMediaPlayer.MediaStatus.LoadingMedia)if val == 'eof':# 结束状态处理self.__setMediaStatus(QMediaPlayer.MediaStatus.EndOfMedia)self.stop()returnif not frame:returnself.__setMediaStatus(QMediaPlayer.MediaStatus.LoadedMedia)img: Imagetm: intimg, tm = frameinterval = round(1000 / self.fps())if self.timer.interval() != interval:logger.debug(f'set timer interval: {interval}')self.timer.setInterval(interval)w, h = img.get_size()self.__setPosition(tm)if self.__duration != self.duration():self.durationChanged.emit(self.duration())self.metaData()qimage = QImage(img.to_bytearray(True)[0], w, h, QImage.Format.Format_RGB888)self.__playerWidget.setImage(qimage)def setVideoOutput(self, widget: QWidget):self.__playerWidget = widgetlogger.debug(f'set video output: {widget}')if not hasattr(widget, 'setImage'):logger.error('视频输出小部件必须有 `setImage` 方法')raise ValueError('视频输出小部件必须有 `setImage` 方法')def errorString(self) -> str:return self.__errorStringdef __setError(self, error: QMediaPlayer.Error):if self.__error == error:returnself.__error = errorself.errorChanged.emit(error)def error(self) -> QMediaPlayer.Error:return self.__errordef __callback(self, *args, **kwargs):tp, status = args[0].split(':')if tp == 'read':if status == 'error':self.__errorString = '资源读取错误'self.__setMediaStatus(QMediaPlayer.MediaStatus.InvalidMedia)self.__setError(QMediaPlayer.Error.ResourceError)self.stop()self.close()elif status == 'exit':self.__errorString = '播放结束'self.__setMediaStatus(QMediaPlayer.MediaStatus.EndOfMedia)self.stop()self.close()elif tp == 'audio':if status == 'error':self.__errorString = '音频播放错误'self.__setError(QMediaPlayer.Error.ResourceError)self.stop()self.close()elif status == 'exit':self.__errorString = '音频播放结束'self.stop()self.close()elif tp == 'video':if status == 'error':self.__errorString = '视频播放错误'self.__setError(QMediaPlayer.Error.ResourceError)self.stop()self.close()elif status == 'exit':self.__errorString = '视频播放结束'self.stop()self.close()
VideoWidget
# coding:utf-8
from typing import Unionfrom PySide6.QtCore import QRect, Qt, Signal, Property
from PySide6.QtGui import QImage, QPainter, QPixmap, QColor, QPainterPath, QKeyEvent
from PySide6.QtWidgets import QWidgetclass VideoWidget(QWidget):"""视频播放控件, 该控件只能作为子页面使用, 不能单独使用"""imageChanged = Signal(QImage)fullScreenChanged = Signal(bool)_topLeftRadius = 0_topRightRadius = 0_bottomLeftRadius = 0_bottomRightRadius = 0def __init__(self, parent=None):super().__init__(parent)self._transparent = Falseself._backgroundColor = Qt.GlobalColor.blackself.image = QImage()self.backgroundImage = QImage()self.setBorderRadius(5, 5, 5, 5)self.setMouseTracking(True)def setPixmap(self, pixmap: QPixmap):""" 设置显示的图像 """self.setImage(pixmap)def pixmap(self) -> QPixmap:""" 获取显示的图像 """return QPixmap.fromImage(self.image)def setImage(self, image: Union[QPixmap, QImage] = None):""" 设置显示的图像 """self.image = image or QImage()if isinstance(image, QPixmap):self.image = image.toImage()self.imageChanged.emit(self.image)self.update()def setBackgroundImage(self, image: Union[str, QPixmap, QImage] = None):""" 设置背景图像 """self.backgroundImage = image or QImage()if isinstance(image, QPixmap):self.backgroundImage = image.toImage()self.update()elif isinstance(image, str):self.backgroundImage.load(image)self.update()def backgroundImage(self) -> QImage:""" 获取背景图像 """return self.backgroundImagedef isNull(self):return self.image.isNull()def setTransparent(self, transparent: bool):""" 设置是否透明 """self._transparent = transparentself.update()def isTransparent(self) -> bool:""" 获取是否透明 """return self._transparentdef setBackgroundColor(self, color: QColor):""" 设置背景颜色 """self._backgroundColor = colorself.update()def backgroundColor(self) -> QColor:""" 获取背景颜色 """return self._backgroundColordef setBorderRadius(self, topLeft: int, topRight: int, bottomLeft: int, bottomRight: int):""" set the border radius of image """self._topLeftRadius = topLeftself._topRightRadius = topRightself._bottomLeftRadius = bottomLeftself._bottomRightRadius = bottomRightself.update()@Property(int)def topLeftRadius(self):return self._topLeftRadius@topLeftRadius.setterdef topLeftRadius(self, radius: int):self.setBorderRadius(radius, self.topRightRadius, self.bottomLeftRadius, self.bottomRightRadius)@Property(int)def topRightRadius(self):return self._topRightRadius@topRightRadius.setterdef topRightRadius(self, radius: int):self.setBorderRadius(self.topLeftRadius, radius, self.bottomLeftRadius, self.bottomRightRadius)@Property(int)def bottomLeftRadius(self):return self._bottomLeftRadius@bottomLeftRadius.setterdef bottomLeftRadius(self, radius: int):self.setBorderRadius(self.topLeftRadius, self.topRightRadius, radius, self.bottomRightRadius)@Property(int)def bottomRightRadius(self):return self._bottomRightRadius@bottomRightRadius.setterdef bottomRightRadius(self, radius: int):self.setBorderRadius(self.topLeftRadius,self.topRightRadius,self.bottomLeftRadius,radius)def paintEvent(self, event):painter = QPainter(self)painter.setRenderHints(QPainter.RenderHint.Antialiasing)painter.setRenderHint(QPainter.RenderHint.LosslessImageRendering)path = QPainterPath()w, h = self.width(), self.height()# top linepath.moveTo(self.topLeftRadius, 0)path.lineTo(w - self.topRightRadius, 0)# top right arcd = self.topRightRadius * 2path.arcTo(w - d, 0, d, d, 90, -90)# right linepath.lineTo(w, h - self.bottomRightRadius)# bottom right arcd = self.bottomRightRadius * 2path.arcTo(w - d, h - d, d, d, 0, -90)# bottom linepath.lineTo(self.bottomLeftRadius, h)# bottom left arcd = self.bottomLeftRadius * 2path.arcTo(0, h - d, d, d, -90, -90)# left linepath.lineTo(0, self.topLeftRadius)# top left arcd = self.topLeftRadius * 2path.arcTo(0, 0, d, d, -180, -90)# 裁剪路径painter.setPen(Qt.PenStyle.NoPen)painter.setClipPath(path)if not self._transparent:painter.fillRect(self.rect(), self._backgroundColor) # 填充颜色if not self.backgroundImage.isNull():painter.drawImage(self.rect(), self.backgroundImage) # 填充背景图片if self.isNull():return# draw imageimage = self.image# 保持宽高比居中显示image_ratio = image.width() / image.height()widget_ratio = self.width() / self.height()# 计算适配后的显示区域if widget_ratio > image_ratio:target_width = self.height() * image_ratiotarget_rect = QRect((self.width() - target_width) // 2, 0,target_width, self.height())else:target_height = self.width() / image_ratiotarget_rect = QRect(0, (self.height() - target_height) // 2,self.width(), target_height)painter.drawImage(target_rect, image)def fullScreen(self):""" 全屏显示 """self.setWindowFlags(Qt.WindowType.Window)self.showFullScreen()self.fullScreenChanged.emit(True)def normalScreen(self):""" 退出全屏显示 """self.setWindowFlags(Qt.WindowType.SubWindow)self.showNormal()self.fullScreenChanged.emit(False)def toggleFullScreen(self):""" 切换全屏显示 """if self.isFullScreen():self.normalScreen()else:self.fullScreen()self.setBorderRadius(0, 0, 0, 0)def setFullScreen(self, fullScreen: bool):""" 设置全屏显示 """if fullScreen:self.fullScreen()else:self.normalScreen()def keyPressEvent(self, event: QKeyEvent):""" 键盘按下事件 """if event.key() == Qt.Key.Key_Escape:self.toggleFullScreen()