Как сохранить кадр, используя 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?

4 ответа

Решение

Вы не делаете ничего плохого. Как отметил @DYangu, ваш экземпляр медиа-объекта не поддерживает мониторинг видео. У меня была такая же проблема (и такая же для QAudioProbe но это нас здесь не интересует). Я нашел решение, посмотрев на этот ответ и этот.

Основная идея заключается в создании подкласса QAbstractVideoSurface. Как только вы это сделаете, он вызовет метод QAbstractVideoSurface::present(const QVideoFrame & frame) вашей реализации QAbstractVideoSurface и вы сможете обрабатывать кадры вашего видео.

Как здесь сказано, обычно вам просто нужно переопределить два метода:

  1. supportPixelFormats, чтобы производитель мог выбрать подходящий формат для QVideoFrame
  2. подарок, который позволяет отображать кадр

Но в то время я искал в исходном коде 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 вы увидите, что я добавил сигнал, принимая изображение в качестве параметра. Это позволит вам обрабатывать ваш фрейм в любом месте вашего кода. В то время этот сигнал занял 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 каждый раз, когда вы будете вводить метод настоящего вашего VideoFrameGrabber,

Я надеюсь, что это поможет вам!

TL;DR: (только файл)


У меня была аналогичная проблема (5.15.2; хотя в моем случае я был в Windows, определенно использовал серверную часть DirectShow, вложение зонда возвращало истину, сборщик образцов был на графике, но обратный вызов не запускался ).

Я но так и не понял,мне нужно было что-то заработать, поэтому я выбрал один из , и пока он работает хорошо. Это немного проще, чем некоторые другие реализации в этом посте, и все это в одном файле.

Обратите внимание, что Qt 5.15 или выше требуется, если вы собираетесь обрабатывать оба кадра и воспроизводить их с этим, поскольку многоповерхностный не был добавлен до 5.15. Если все, что вы хотите сделать, это обработать видео, вы все равно можете использовать приведенный ниже код в качестве шаблона для версий до 5.15, просто выпотрошите части.

Код:

https://gist.github.com/JC3/a7bab65acbd7659d1e57103d2b0021baVideoProbeSurface.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

Я выбрал самую быструю возможную реализацию, поэтому он просто перенаправляет поддерживаемые форматы пикселей с другой поверхности (я намеревался как проверить, так и воспроизвести на ), и вы получите любой формат. Мне просто нужно было захватить фрагменты изображения в s, который обрабатывает наиболее распространенные форматы. Но вы можете изменить это, чтобы принудительно использовать любые форматы, которые вы хотите (например, вы можете просто вернуть форматы, поддерживаемые или отфильтровать исходные форматы, не поддерживаемые , так далее.).

Пример настройки:

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

Заметки

Единственное, что мне нужно было сделать по-настоящему схематично, это на стороне получателя (для доступа только для чтения), поскольку не :

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

Но настоящий заставит вас сделать то же самое, поэтому я не знаю, что с этим делать - это странный API. Я провел несколько тестов с рендерерами и декодерами sw, native hw и copy-back hw и / в режиме чтения, похоже, работает нормально, так что что угодно.

С точки зрения производительности видео будет тормозить, если вы потратите слишком много времени на обратный вызов, поэтому проектируйте соответственно. Однако я не тестировал , поэтому я не знаю, будет ли это по-прежнему проблема (хотя тот факт, что параметр сигнала является ссылкой, заставит меня опасаться его попытки, а также возможные проблемы с освобождением памяти графическим процессором до того, как слот закончится называется). Я тоже не знаю, как себя ведет в этом плане. Я знаю, что, по крайней мере, на моей машине, я могу упаковать и поставить в очередь Full HD (1920 x 1080) разрешение s в пул потоков для обработки без замедления видео.

Вы, вероятно, также захотите реализовать какой-то служебный объект auto-unmapper для безопасного исключения и т. д. Но опять же, это не уникальное явление, то же самое, что и вам. .

Надеюсь, это поможет кому-то другому.

Пример использования QImage

PS, пример упаковки произвольно отформатированный s в in:

      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 и всех копий, которые не были изменены или иным образом не были отсоединены от исходного буфера». Лучше отнестись к этому строго.

После документации Qt QVideoProbe:

bool QVideoProbe::setSource(QMediaObject *mediaObject)

Запускает мониторинг заданного mediaObject.

Если нет медиа-объекта, связанного с mediaObjectили, если он равен нулю, этот зонд будет деактивирован, и эта функция вернет true.

Если экземпляр медиа-объекта не поддерживает мониторинг видео, эта функция вернет false.

Любые ранее отслеживаемые объекты больше не будут отслеживаться. Передача в том же объекте будет игнорироваться, но мониторинг будет продолжен.

Так что, похоже, ваш "экземпляр медиа-объекта не поддерживает мониторинг видео"

Сейчас 2023 год, и последняя версия Qt — Qt6.6. По официальной информации .

Модуль Qt Multimedia в Qt 6 заменяет модуль Qt Multimedia из Qt 5.x. QVideoProbe был удален.

Но теперь у нас есть более простой способ сохранять кадры, выводимые QMediaPlayer. Нам следует использовать QVideoSink.

Вот мой пример:

      QMediaPlayer* backgroundVideoPlayer = new QMediaPlayer;
QVideoSink* backgroundVideoSink = new QVideoSink;

backgroundVideoPlayer->setLoops(QMediaPlayer::Infinite);
backgroundVideoPlayer->setVideoOutput(backgroundVideoSink);

connect(backgroundVideoSink, &QVideoSink::videoFrameChanged, this, [&](const QVideoFrame &frame){
    /*Add your frame handling code here*/
    QPixmap backgroundVideoFrame = QPixmap::fromImage(frame.toImage());
});

backgroundVideoPlayer->setSource(QUrl::fromLocalFile("test.mp4"));
backgroundVideoPlayer->play();
Другие вопросы по тегам