如何使用QMediaPlayer保存帧?

时间:2016-06-09 11:23:51

标签: qt qmediaplayer

我想保存QMediaPlayer的图像。阅读完文档后,我明白我应该使用QVideoProbe。我使用以下代码:

QMediaPlayer *player = new QMediaPlayer();
QVideoProbe *probe   = new QVideoProbe;

connect(probe, SIGNAL(videoFrameProbed(QVideoFrame)), this, SLOT(processFrame(QVideoFrame)));

qDebug()<<probe->setSource(player); // Returns true, hopefully.

player->setVideoOutput(myVideoSurface);
player->setMedia(QUrl::fromLocalFile("observation.mp4"));
player->play(); // Start receving frames as they get presented to myVideoSurface

但不幸的是,probe->setSource(player)始终会为我返回false,因此我的广告位processFrame不会被触发。

我做错了什么?有没有人有QVideoProbe的工作示例?

3 个答案:

答案 0 :(得分:10)

你没有做错任何事。正如@DYangu指出的那样,您的媒体对象实例不支持监控视频。我遇到了同样的问题(同样适用于QAudioProbe,但我们并不感兴趣)。我通过查看this回答和this one找到了解决方案。

主要思想是子类QAbstractVideoSurface 。完成后,它会调用您QAbstractVideoSurface::present(const QVideoFrame & frame)实施的方法QAbstractVideoSurface,然后您就可以处理视频的图片了。

正如here所说,通常你需要重新实现两种方法:

  1. supportedPixelFormats,以便制作人可以为QVideoFrame
  2. 选择合适的格式
  3. present允许显示框架
  4. 但当时,我在Qt源代码中搜索并愉快地找到了this piece of code,这帮助我完成了一个完整的实现。因此,这里是使用“视频图像采集卡”的完整代码

    VideoFrameGrabber.cpp:

    #include "VideoFrameGrabber.h"
    
    #include <QtWidgets>
    #include <qabstractvideosurface.h>
    #include <qvideosurfaceformat.h>
    
    VideoFrameGrabber::VideoFrameGrabber(QWidget *widget, QObject *parent)
        : QAbstractVideoSurface(parent)
        , widget(widget)
        , imageFormat(QImage::Format_Invalid)
    {
    }
    
    QList<QVideoFrame::PixelFormat> VideoFrameGrabber::supportedPixelFormats(QAbstractVideoBuffer::HandleType handleType) const
    {
        Q_UNUSED(handleType);
        return QList<QVideoFrame::PixelFormat>()
            << QVideoFrame::Format_ARGB32
            << QVideoFrame::Format_ARGB32_Premultiplied
            << QVideoFrame::Format_RGB32
            << QVideoFrame::Format_RGB24
            << QVideoFrame::Format_RGB565
            << QVideoFrame::Format_RGB555
            << QVideoFrame::Format_ARGB8565_Premultiplied
            << QVideoFrame::Format_BGRA32
            << QVideoFrame::Format_BGRA32_Premultiplied
            << QVideoFrame::Format_BGR32
            << QVideoFrame::Format_BGR24
            << QVideoFrame::Format_BGR565
            << QVideoFrame::Format_BGR555
            << QVideoFrame::Format_BGRA5658_Premultiplied
            << QVideoFrame::Format_AYUV444
            << QVideoFrame::Format_AYUV444_Premultiplied
            << QVideoFrame::Format_YUV444
            << QVideoFrame::Format_YUV420P
            << QVideoFrame::Format_YV12
            << QVideoFrame::Format_UYVY
            << QVideoFrame::Format_YUYV
            << QVideoFrame::Format_NV12
            << QVideoFrame::Format_NV21
            << QVideoFrame::Format_IMC1
            << QVideoFrame::Format_IMC2
            << QVideoFrame::Format_IMC3
            << QVideoFrame::Format_IMC4
            << QVideoFrame::Format_Y8
            << QVideoFrame::Format_Y16
            << QVideoFrame::Format_Jpeg
            << QVideoFrame::Format_CameraRaw
            << QVideoFrame::Format_AdobeDng;
    }
    
    bool VideoFrameGrabber::isFormatSupported(const QVideoSurfaceFormat &format) const
    {
        const QImage::Format imageFormat = QVideoFrame::imageFormatFromPixelFormat(format.pixelFormat());
        const QSize size = format.frameSize();
    
        return imageFormat != QImage::Format_Invalid
                && !size.isEmpty()
                && format.handleType() == QAbstractVideoBuffer::NoHandle;
    }
    
    bool VideoFrameGrabber::start(const QVideoSurfaceFormat &format)
    {
        const QImage::Format imageFormat = QVideoFrame::imageFormatFromPixelFormat(format.pixelFormat());
        const QSize size = format.frameSize();
    
        if (imageFormat != QImage::Format_Invalid && !size.isEmpty()) {
            this->imageFormat = imageFormat;
            imageSize = size;
            sourceRect = format.viewport();
    
            QAbstractVideoSurface::start(format);
    
            widget->updateGeometry();
            updateVideoRect();
    
            return true;
        } else {
            return false;
        }
    }
    
    void VideoFrameGrabber::stop()
    {
        currentFrame = QVideoFrame();
        targetRect = QRect();
    
        QAbstractVideoSurface::stop();
    
        widget->update();
    }
    
    bool VideoFrameGrabber::present(const QVideoFrame &frame)
    {
        if (frame.isValid()) 
        {
            QVideoFrame cloneFrame(frame);
            cloneFrame.map(QAbstractVideoBuffer::ReadOnly);
            const QImage image(cloneFrame.bits(),
                               cloneFrame.width(),
                               cloneFrame.height(),
                               QVideoFrame::imageFormatFromPixelFormat(cloneFrame .pixelFormat()));
            emit frameAvailable(image); // this is very important
            cloneFrame.unmap();
        }
    
        if (surfaceFormat().pixelFormat() != frame.pixelFormat()
                || surfaceFormat().frameSize() != frame.size()) {
            setError(IncorrectFormatError);
            stop();
    
            return false;
        } else {
            currentFrame = frame;
    
            widget->repaint(targetRect);
    
            return true;
        }
    }
    
    void VideoFrameGrabber::updateVideoRect()
    {
        QSize size = surfaceFormat().sizeHint();
        size.scale(widget->size().boundedTo(size), Qt::KeepAspectRatio);
    
        targetRect = QRect(QPoint(0, 0), size);
        targetRect.moveCenter(widget->rect().center());
    }
    
    void VideoFrameGrabber::paint(QPainter *painter)
    {
        if (currentFrame.map(QAbstractVideoBuffer::ReadOnly)) {
            const QTransform oldTransform = painter->transform();
    
            if (surfaceFormat().scanLineDirection() == QVideoSurfaceFormat::BottomToTop) {
               painter->scale(1, -1);
               painter->translate(0, -widget->height());
            }
    
            QImage image(
                    currentFrame.bits(),
                    currentFrame.width(),
                    currentFrame.height(),
                    currentFrame.bytesPerLine(),
                    imageFormat);
    
            painter->drawImage(targetRect, image, sourceRect);
    
            painter->setTransform(oldTransform);
    
            currentFrame.unmap();
        }
    }
    

    <强> VideoFrameGrabber.h

    #ifndef VIDEOFRAMEGRABBER_H
    #define VIDEOFRAMEGRABBER_H
    
    #include <QtWidgets>
    
    class VideoFrameGrabber : public QAbstractVideoSurface
    {
        Q_OBJECT
    
    public:
        VideoFrameGrabber(QWidget *widget, QObject *parent = 0);
    
        QList<QVideoFrame::PixelFormat> supportedPixelFormats(
                QAbstractVideoBuffer::HandleType handleType = QAbstractVideoBuffer::NoHandle) const;
        bool isFormatSupported(const QVideoSurfaceFormat &format) const;
    
        bool start(const QVideoSurfaceFormat &format);
        void stop();
    
        bool present(const QVideoFrame &frame);
    
        QRect videoRect() const { return targetRect; }
        void updateVideoRect();
    
        void paint(QPainter *painter);
    
    private:
        QWidget *widget;
        QImage::Format imageFormat;
        QRect targetRect;
        QSize imageSize;
        QRect sourceRect;
        QVideoFrame currentFrame;
    
    signals:
        void frameAvailable(QImage frame);
    };
    #endif //VIDEOFRAMEGRABBER_H
    

    注意:在.h中,您会看到我添加了signal将图像作为参数。 这样您就可以在代码中的任何位置处理框架。当时,此信号以QImage作为参数,但如果您愿意,您当然可以使用QVideoFrame

    现在,我们已准备好使用此视频图像抓取器:

    QMediaPlayer* player = new QMediaPlayer(this);
    // no more QVideoProbe 
    VideoFrameGrabber* grabber = new VideoFrameGrabber(this);
    player->setVideoOutput(grabber);
    
    connect(grabber, SIGNAL(frameAvailable(QImage)), this, SLOT(processFrame(QImage)));
    

    现在您只需声明一个名为processFrame(QImage image)的广告位,每次您输入QImage的方法present时,您都会收到VideoFrameGrabber

    我希望这会对你有帮助!

答案 1 :(得分:1)

Qt QVideoProbe documentation之后:

bool QVideoProbe::setSource(QMediaObject *mediaObject)
     

开始监控给定的mediaObject。

     

如果没有与mediaObject关联的媒体对象,或者它是   零,此探针将被停用,此功能将返回   真。

     

如果媒体对象实例不支持监控视频,请执行此操作   函数将返回false。

     

将不再监视任何先前监视的对象。通过   在同一个对象中将被忽略,但监控将继续。

所以看来你的“媒体对象实例不支持监控视频”

答案 2 :(得分:0)

TL;DR: https://gist.github.com/JC3/a7bab65acbd7659d1e57103d2b0021ba(仅文件)


我有一个类似的问题(5.15.2;虽然在我的情况下我在 Windows 上,肯定使用 DirectShow 后端,探针附件返回 true,样本采集器在图中,但回调不是不开火)。

never figured it out 但需要让一些东西正常工作,所以我从 QAbstractVideoSurface 中挤出了一个,到目前为止它运行良好。它比本文中的其他一些实现要简单一些,而且都在一个文件中。

请注意,如果您打算同时处理帧,则需要 Qt 5.15 或更高版本,因为直到 5.15 才添加多表面 QMediaPlayer::setVideoOutput。如果您只想处理视频,您仍然可以使用下面的代码作为 5.15 之前的模板,只需删除 formatSource_ 部分。

代码:

VideoProbeSurface.h(唯一的文件;链接是 Gist)

#ifndef VIDEOPROBESURFACE_H
#define VIDEOPROBESURFACE_H

#include <QAbstractVideoSurface>
#include <QVideoSurfaceFormat>

class VideoProbeSurface : public QAbstractVideoSurface {
    Q_OBJECT
public:
    VideoProbeSurface (QObject *parent = nullptr)
        : QAbstractVideoSurface(parent)
        , formatSource_(nullptr)
    {
    }
    void setFormatSource (QAbstractVideoSurface *source) {
        formatSource_ = source;
    }
    QList<QVideoFrame::PixelFormat> supportedPixelFormats (QAbstractVideoBuffer::HandleType type) const override {
        return formatSource_ ? formatSource_->supportedPixelFormats(type)
                             : QList<QVideoFrame::PixelFormat>();
    }
    QVideoSurfaceFormat nearestFormat (const QVideoSurfaceFormat &format) const override {
        return formatSource_ ? formatSource_->nearestFormat(format)
                             : QAbstractVideoSurface::nearestFormat(format);
    }
    bool present (const QVideoFrame &frame) override {
        emit videoFrameProbed(frame);
        return true;
    }
signals:
    void videoFrameProbed (const QVideoFrame &frame);
private:
    QAbstractVideoSurface *formatSource_;
};

#endif // VIDEOPROBESURFACE_H

我选择了最快的写入实现,所以它只是从另一个表面转发支持的像素格式(我的目的是探测播放到QVideoWidget)和你得到任何你得到的格式。我只需要将子图像抓取到 QImage 中,它可以处理最常见的格式。但是您可以修改它以强制使用您想要的任何格式(例如,您可能只想返回 QImage 支持的格式或过滤掉 QImage) 不支持的源格式,等等。 ).

示例设置:

 QMediaPlayer *player = ...;
 QVideoWidget *widget = ...;

 // forward surface formats provided by the video widget:
 VideoProbeSurface *probe = new VideoProbeSurface(...);
 probe->setFormatSource(widget->videoSurface());

 // same signal signature as QVideoProbe's signal:
 connect(probe, &VideoProbeSurface::videoFrameProbed, ...);

 // the key move is to render to both the widget (for viewing)
 // and probe (for processing). fortunately, QMediaPlayer can
 // take a list:
 player->setVideoOutput({ widget->videoSurface(), probe });

注意事项

我必须做的唯一粗略的事情是接收方的 const_cast QVideoFrame(用于只读访问),因为 QVideoFrame::map() 不是 const :

    if (const_cast<QVideoFrame&>(frame).map(QAbstractVideoBuffer::ReadOnly)) {
        ...;
        const_cast<QVideoFrame&>(frame).unmap();
    }

但真正的 QVideoProbe 会让你做同样的事情,所以我不知道这是怎么回事——这是一个奇怪的 API。我对软件、原生硬件和复写硬件渲染器和解码器进行了一些测试,读取模式下的 map/unmap 似乎运行正常,所以,无论如何。

在性能方面,如果您在回调中花费太多时间,视频将陷入停滞,因此请相应地进行设计。但是,我没有测试 QueuedConnection,所以我不知道这是否仍然存在问题(尽管信号参数是参考的事实会让我对尝试它持谨慎态度,并且可以想象GPU 在插槽最终被调用之前释放内存的问题)。我也不知道 QVideoProbe 在这方面的表现如何。我确实知道,至少在我的机器上,我可以将全高清 (1920 x 1080) 分辨率QImage打包并排队到线程池中进行处理,而不会减慢视频速度。

您可能还想为异常安全 unmap() 等实现某种自动取消映射器实用程序对象。但同样,这不是唯一的,您必须使用 {{1} }.

所以希望这能帮助其他人。

示例 QImage 使用

PS,将任意格式的 QVideoProbe 打包到 QVideoFrame 中的示例:

QImage

注意事项:

  • 直接从数据构建 void MyVideoProcessor::onFrameProbed(const QVideoFrame &frame) { if (const_cast<QVideoFrame&>(frame).map(QAbstractVideoBuffer::ReadOnly)) { auto imageFormat = QVideoFrame::imageFormatFromPixelFormat(frame.pixelFormat()); QImage image(frame.bits(), frame.width(), frame.height(), frame.bytesPerLine(), imageFormat); // *if* you want to use this elsewhere you must force detach: image = image.copy(); // but if you don't need to use it past unmap(), you can just // use the original image instead of a copy. // <---- now do whatever with the image, e.g. save() it. // if you *haven't* copied the image, then, before unmapping, // kill any internal data pointers just to be safe: image = QImage(); const_cast<QVideoFrame&>(frame).unmap(); } } 是快速且基本免费的:无需复制。
  • 数据缓冲区仅在 QImagemap 之间在技术上有效,因此如果您打算在该范围之外使用 unmap,您需要使用 {{1} }(或任何其他强制分离的东西)强制深度复制。
  • 您可能还想确保在调用 QImage 之前销毁原始未复制的 copy()。这不太可能导致问题,但在任何给定时间最小化无效指针的数量总是一个好主意,而且 QImage 文档说“缓冲区必须在 QImage 和所有副本的整个生命周期中保持有效尚未修改或以其他方式与原始缓冲区分离”。最好严格点。