You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FFpyplayer结合QPixmap播放UDP MPEG-TS流的追帧缓冲卡顿问题

问题描述

基于FFpyplayer库结合PyQt构建UDP流播放应用时,出现追帧式缓冲异常:视频先大幅加速播放后恢复正常,部分视频还存在卡顿现象。但使用ffplay直接播放同一UDP流无异常,且FFpyplayer结合OpenCV的播放代码也能正常运行,怀疑问题出在PyQt的帧获取与显示环节。

测试环境与代码

推流命令(FFmpeg)

ffmpeg -re -stream_loop -1 -i .\2099536-hd_1920_1080_30fps.mp4 -f mpegts udp://127.0.0.1:51234?localaddr=127.0.0.1

正常播放命令(ffplay)

ffplay udp://127.0.0.1:51234

存在问题的FFpyplayer+PyQt代码

from PyQt6 import QtGui, QtWidgets, QtCore
from ffpyplayer.player import MediaPlayer
import time
from threading import Thread

class VideoDisplayWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.player = None
        self.layout = QtWidgets.QVBoxLayout(self)
        self.video_frame_widget = QtWidgets.QLabel()
        self.layout.addWidget(self.video_frame_widget)
        self.frame_rate = 30

        self.latest_frame = QtGui.QPixmap()

        self.timer = QtCore.QTimer()
        self.timer.setTimerType(QtCore.Qt.TimerType.PreciseTimer)
        self.timer.timeout.connect(self.nextFrameSlot)
        self.timer.start(int(1000.0/self.frame_rate))

        self.play()
        self.setLayout(self.layout)

    def play(self):
        play_thread = Thread(target=self._play, daemon=True)
        play_thread.start()
    
    def _play(self):
        player = MediaPlayer("udp://127.0.0.1:51234")
        val = ''
        while val != 'eof':
            frame, val = player.get_frame()
            if val != 'eof' and frame is not None:
                img, t = frame
                w = img.get_size()[0] 
                h = img.get_size()[1]
                data = img.to_bytearray()[0]

                qimage = QtGui.QImage(data, w, h, QtGui.QImage.Format.Format_RGB888)
                self.latest_frame = QtGui.QPixmap.fromImage(qimage)
                time.sleep(0.001)
    
    def nextFrameSlot(self):
        self.video_frame_widget.setPixmap(self.latest_frame)

if __name__ == "__main__":
    current_port = 51234
    app = QtWidgets.QApplication([])
    player = VideoDisplayWidget()
    player.show()

    exit(app.exec())

正常运行的FFpyplayer+OpenCV代码

from ffpyplayer.player import MediaPlayer
import time
from pathlib import Path
import numpy as np
import cv2

player = MediaPlayer("udp://127.0.0.1:51234")
val = ''
while val != 'eof':
    frame, val = player.get_frame()
    if val != 'eof' and frame is not None:
        img, t = frame
        w = img.get_size()[0] 
        h = img.get_size()[1]
        arr = np.uint8(np.array(img.to_bytearray()[0]).reshape(h,w,3))
        arr = cv2.cvtColor(arr, cv2.COLOR_BGR2RGB)
        
        cv2.imshow('test', arr)
        if cv2.waitKey(25) & 0xFF == ord('q'):
            cv2.destroyAllWindows()
            break
解决方案

1. 修复跨线程GUI操作问题

Qt的GUI对象(如QPixmap)禁止在非主线程直接修改/赋值,原代码中后台线程直接操作self.latest_frame会引发线程安全问题,导致显示异常。需通过信号槽机制在后台线程发送帧数据,主线程接收后更新UI。

2. 基于帧时间戳控制显示节奏

原代码硬编码30fps定时器,无法匹配视频实际帧率或UDP流的波动。应利用FFpyplayer返回的帧时间戳t,计算每帧的显示间隔,动态调整播放节奏,避免累积帧导致的追帧现象。

3. 优化FFpyplayer的UDP流参数

给MediaPlayer添加ff_opts参数,优化UDP流的缓冲策略,避免丢帧或帧堆积:

player = MediaPlayer("udp://127.0.0.1:51234", ff_opts={"buffer_size": "1024000"})

4. 优化帧转换效率

确保QImage的格式与FFpyplayer输出的像素格式严格匹配,减少不必要的格式转换开销。FFpyplayer默认输出RGB24格式,对应QtGui.QImage.Format.Format_RGB888是正确的,可直接传递字节数据减少中间拷贝。

修改后的完整代码
from PyQt6 import QtGui, QtWidgets, QtCore
from ffpyplayer.player import MediaPlayer
from threading import Thread
import time

class VideoDisplayWidget(QtWidgets.QWidget):
    # 定义信号:传递QImage帧数据
    frame_ready = QtCore.pyqtSignal(QtGui.QImage)

    def __init__(self):
        super().__init__()
        self.layout = QtWidgets.QVBoxLayout(self)
        self.video_frame_widget = QtWidgets.QLabel()
        self.layout.addWidget(self.video_frame_widget)
        self.latest_pixmap = QtGui.QPixmap()

        # 绑定信号槽:主线程接收帧数据
        self.frame_ready.connect(self.update_pixmap)

        self.play()
        self.setLayout(self.layout)

    def play(self):
        play_thread = Thread(target=self._play, daemon=True)
        play_thread.start()
    
    def _play(self):
        # 优化FFpyplayer缓冲参数
        player = MediaPlayer("udp://127.0.0.1:51234", ff_opts={"buffer_size": "1024000"})
        val = ''
        prev_timestamp = 0
        while val != 'eof':
            frame, val = player.get_frame()
            if val != 'eof' and frame is not None:
                img, timestamp = frame
                w, h = img.get_size()
                data = img.to_bytearray()[0]
                # 创建QImage并发送到主线程
                qimage = QtGui.QImage(data, w, h, QtGui.QImage.Format.Format_RGB888)
                self.frame_ready.emit(qimage.copy())
                
                # 根据时间戳控制播放间隔
                if prev_timestamp > 0:
                    delta = timestamp - prev_timestamp
                    time.sleep(max(0, delta - 0.001))
                prev_timestamp = timestamp
    
    def update_pixmap(self, qimage):
        # 主线程安全更新QPixmap并刷新UI
        self.latest_pixmap = QtGui.QPixmap.fromImage(qimage)
        self.video_frame_widget.setPixmap(self.latest_pixmap)

if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    player = VideoDisplayWidget()
    player.show()
    exit(app.exec())

内容的提问来源于stack exchange,提问作者kyrlon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 01:32:04