ffpyplayer+Qt,制作一个视频播放器

ffpyplayer+Qt,制作一个视频播放器

项目地址

https://gitee.com/chiyaun/QtFFMediaPlayer

FFmpegMediaPlayer

按照 QMediaPlayer的方法重写一个ffpyplayer

python 复制代码
# coding:utf-8
import logging
from typing import Union

from 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 MediaPlayer

logging.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 = None
        self.__mediaStatus: QMediaPlayer.MediaStatus = QMediaPlayer.MediaStatus.NoMedia
        self.__position: int = 0
        self.__duration: int = 0
        self.__metaData: dict = {}
        self.__error: QMediaPlayer.Error = QMediaPlayer.Error.NoError
        self.__errorString: str = ''

        self.timer = QTimer(self)
        self.player: MediaPlayer = None

        self.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 = None
        logger.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.__source

    def fps(self) -> float:
        fps = self.metadata()["frame_rate"][0] / self.metadata()["frame_rate"][1]
        return fps

    def 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:
            return
        logger.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:
            return
        position = int(position)
        if self.__position == position:
            return
        self.__position = position
        self.positionChanged.emit(position)

    def metaData(self) -> dict:
        meta = self.player.get_metadata()
        if meta != self.__metaData:
            self.__metaData = meta
            self.metaDataChanged.emit(meta)
        return meta

    def setVolume(self, volume: int):
        if self.player is None:
            return
        logger.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:
            return
        logger.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:
            return
        logger.debug(f'set media status: {status}')
        self.__mediaStatus = status
        self.mediaStatusChanged.emit(status)

    def mediaStatus(self) -> QMediaPlayer.MediaStatus:
        return self.__mediaStatus

    def _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()
            return

        if not frame:
            return
        self.__setMediaStatus(QMediaPlayer.MediaStatus.LoadedMedia)
        img: Image
        tm: int
        img, tm = frame
        interval = 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 = widget
        logger.debug(f'set video output: {widget}')
        if not hasattr(widget, 'setImage'):
            logger.error('视频输出小部件必须有 `setImage` 方法')
            raise ValueError('视频输出小部件必须有 `setImage` 方法')

    def errorString(self) -> str:
        return self.__errorString

    def __setError(self, error: QMediaPlayer.Error):
        if self.__error == error:
            return
        self.__error = error
        self.errorChanged.emit(error)

    def error(self) -> QMediaPlayer.Error:
        return self.__error

    def __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

python 复制代码
# coding:utf-8
from typing import Union

from PySide6.QtCore import QRect, Qt, Signal, Property
from PySide6.QtGui import QImage, QPainter, QPixmap, QColor, QPainterPath, QKeyEvent
from PySide6.QtWidgets import QWidget


class VideoWidget(QWidget):
    """
    视频播放控件, 该控件只能作为子页面使用, 不能单独使用
    """
    imageChanged = Signal(QImage)
    fullScreenChanged = Signal(bool)

    _topLeftRadius = 0
    _topRightRadius = 0
    _bottomLeftRadius = 0
    _bottomRightRadius = 0

    def __init__(self, parent=None):
        super().__init__(parent)
        self._transparent = False
        self._backgroundColor = Qt.GlobalColor.black
        self.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.backgroundImage

    def isNull(self):
        return self.image.isNull()

    def setTransparent(self, transparent: bool):
        """ 设置是否透明 """
        self._transparent = transparent
        self.update()

    def isTransparent(self) -> bool:
        """ 获取是否透明 """
        return self._transparent

    def setBackgroundColor(self, color: QColor):
        """ 设置背景颜色 """
        self._backgroundColor = color
        self.update()

    def backgroundColor(self) -> QColor:
        """ 获取背景颜色 """
        return self._backgroundColor

    def setBorderRadius(self, topLeft: int, topRight: int, bottomLeft: int, bottomRight: int):
        """ set the border radius of image """
        self._topLeftRadius = topLeft
        self._topRightRadius = topRight
        self._bottomLeftRadius = bottomLeft
        self._bottomRightRadius = bottomRight
        self.update()

    @Property(int)
    def topLeftRadius(self):
        return self._topLeftRadius

    @topLeftRadius.setter
    def topLeftRadius(self, radius: int):
        self.setBorderRadius(radius, self.topRightRadius, self.bottomLeftRadius, self.bottomRightRadius)

    @Property(int)
    def topRightRadius(self):
        return self._topRightRadius

    @topRightRadius.setter
    def topRightRadius(self, radius: int):
        self.setBorderRadius(self.topLeftRadius, radius, self.bottomLeftRadius, self.bottomRightRadius)

    @Property(int)
    def bottomLeftRadius(self):
        return self._bottomLeftRadius

    @bottomLeftRadius.setter
    def bottomLeftRadius(self, radius: int):
        self.setBorderRadius(self.topLeftRadius, self.topRightRadius, radius, self.bottomRightRadius)

    @Property(int)
    def bottomRightRadius(self):
        return self._bottomRightRadius

    @bottomRightRadius.setter
    def 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 line
        path.moveTo(self.topLeftRadius, 0)
        path.lineTo(w - self.topRightRadius, 0)

        # top right arc
        d = self.topRightRadius * 2
        path.arcTo(w - d, 0, d, d, 90, -90)

        # right line
        path.lineTo(w, h - self.bottomRightRadius)

        # bottom right arc
        d = self.bottomRightRadius * 2
        path.arcTo(w - d, h - d, d, d, 0, -90)

        # bottom line
        path.lineTo(self.bottomLeftRadius, h)

        # bottom left arc
        d = self.bottomLeftRadius * 2
        path.arcTo(0, h - d, d, d, -90, -90)

        # left line
        path.lineTo(0, self.topLeftRadius)

        # top left arc
        d = self.topLeftRadius * 2
        path.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 image
        image = self.image

        # 保持宽高比居中显示
        image_ratio = image.width() / image.height()
        widget_ratio = self.width() / self.height()

        # 计算适配后的显示区域
        if widget_ratio > image_ratio:
            target_width = self.height() * image_ratio
            target_rect = QRect(
                (self.width() - target_width) // 2, 0,
                target_width, self.height()
            )
        else:
            target_height = self.width() / image_ratio
            target_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()
相关推荐
第一程序员4 分钟前
Python数据结构与算法:非科班转码者的学习指南
python·github
weixin_5860614612 分钟前
如何用 event.composedPath 获取事件触发经过的所有节点
jvm·数据库·python
weixin_4087177723 分钟前
如何用 Iterator.from 将类数组转化为具备现代方法的迭代器
jvm·数据库·python
Full Stack Developme24 分钟前
MyBatis-Plus 流式查询教程
前端·python·mybatis
zzh92027 分钟前
基于51单片机的流水灯Proteus仿真按键切换 上到下下到上 2个灯(可定做)(免费代码+视频讲解)
51单片机·proteus·音视频
才兄说29 分钟前
机器人二次开发机器狗巡检?定位精度±2cm
python
2301_7826591831 分钟前
SQL视图能否用于数据仓库模型_雪花模型与视图构建
jvm·数据库·python
m0_3776182334 分钟前
CSS如何让文字超出两行显示省略号_使用line-clamp属性限制
jvm·数据库·python
m0_7436239236 分钟前
HTML5中LocalStorage存储用户自定义快捷键配置
jvm·数据库·python
2301_7735536240 分钟前
HTML5中SharedWorker生命周期与浏览器进程关闭的关系
jvm·数据库·python