4

我想从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

4

3 回答 3

15

你没有做错什么。正如@DYangu 指出的,您的媒体对象实例不支持监控视频。我遇到了同样的问题(同样的问题,QAudioProbe但我们在这里不感兴趣)。我通过查看这个答案和这个找到了一个解决方案。

主要思想是继承QAbstractVideoSurface。完成后,它将调用QAbstractVideoSurface::present(const QVideoFrame & frame)您的实现方法,QAbstractVideoSurface您将能够处理视频的帧。

正如这里所说,通常您只需要重新实现两种方法:

  1. supportedPixelFormats以便制作者可以为QVideoFrame
  2. present允许显示框架

但当时,我在Qt源代码中搜索,很高兴地找到了这段代码,它帮助我做了一个完整的实现。因此,这里是使用“视频图像采集卡”的完整代码。

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 中,您会看到我添加了一个将图像作为参数的信号。这将允许您在代码中的任何位置处理您的框架。当时,这个信号以 aQImage作为参数,但QVideoFrame如果你愿意,当然可以带 a。


现在,我们准备好使用这个视频帧采集器了:

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每次你输入你VideoFrameGrabber.

我希望这会对你有所帮助!

于 2016-06-09T13:00:38.647 回答
1

Qt QVideoProbe 文档之后:

bool QVideoProbe::setSource(QMediaObject *mediaObject)

开始监视给定的 mediaObject。

如果没有与 关联的媒体对象mediaObject,或者它为零,则此探测器将被停用,此函数将返回 true。

如果媒体对象实例不支持监控视频,该函数将返回false。

任何先前被监控的对象将不再被监控。传入相同的对象将被忽略,但监视将继续。

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

于 2016-06-09T12:06:58.473 回答
1

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


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

从来没有想出来,但需要让一些东西工作,所以我从 a 中拼凑出一个QAbstractVideoSurface,到目前为止它运行良好。它比本文中的其他一些实现要简单一些,而且都在一个文件中。

请注意,如果您打算同时处理帧以此回放它们,则需要 Qt 5.15 或更高版本,因为QMediaPlayer::setVideoOutput直到 5.15 才添加多表面。如果您只想处理视频,您仍然可以使用下面的代码作为 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

我选择了最快的编写实现,所以它只是从另一个表面转发支持的像素格式(我的目的是探测回放到 a QVideoWidget),你会得到你得到的任何格式。我只需要将子图像抓取到QImages 中,它可以处理最常见的格式。但是您可以修改它以强制使用您想要的任何格式(例如,您可能只想返回支持的格式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_castQVideoFrame接收方(用于只读访问),因为QVideoFrame::map()不是const

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

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

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

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

所以希望这对其他人有帮助。

示例 QImage 使用

PS,将任意格式QVideoFrame的s打包成QImagein的例子:

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

}

关于这一点的注意事项:

  • 直接从数据构建一个QImage快速且基本上免费的:没有复制。
  • 数据缓冲区仅在mapand之间在技术上有效unmap,因此如果您打算使用该QImage范围的外部,您将希望使用copy()(或任何其他强制分离的东西)来强制进行深层复制。
  • 您可能还想确保QImage在调用unmap. 这不太可能引起问题,但在任何给定时间尽量减少挂起的无效指针的数量总是一个好主意,而且QImage文档还说“缓冲区必须在 QImage 的整个生命周期以及所有未修改的副本中保持有效或以其他方式与原始缓冲区分离”。最好严格一点。
于 2021-06-22T18:23:49.360 回答