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()