Как сохранить кадр, используя 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
и вы сможете обрабатывать кадры вашего видео.
Как здесь сказано, обычно вам просто нужно переопределить два метода:
- supportPixelFormats, чтобы производитель мог выбрать подходящий формат для
QVideoFrame
- подарок, который позволяет отображать кадр
Но в то время я искал в исходном коде 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 или выше требуется, если вы собираетесь обрабатывать оба кадра и воспроизводить их с этим, поскольку многоповерхностный
Код:
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
Я выбрал самую быструю возможную реализацию, поэтому он просто перенаправляет поддерживаемые форматы пикселей с другой поверхности (я намеревался как проверить, так и воспроизвести на
Пример настройки:
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 и / в режиме чтения, похоже, работает нормально, так что что угодно.
С точки зрения производительности видео будет тормозить, если вы потратите слишком много времени на обратный вызов, поэтому проектируйте соответственно. Однако я не тестировал
Вы, вероятно, также захотите реализовать какой-то служебный объект auto-unmapper для безопасного исключения
Надеюсь, это поможет кому-то другому.
Пример использования QImage
PS, пример упаковки произвольно отформатированный
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();