TL;DR: https://gist.github.com/JC3/a7bab65acbd7659d1e57103d2b0021ba (only file)
I had a similar issue (5.15.2; although in my case I was on Windows, was definitely using the DirectShow back-end, the probe attachment was returning true, the sample grabber was in the graph, but the callback wasn't firing).
I never figured it out but needed to get something working so I kludged one out of a QAbstractVideoSurface
, and it's been working well so far. It's a bit simpler than some of the other implementations in this post, and it's all in one file.
Note that Qt 5.15 or higher is required if you intend to both process frames and play them back with this, since the multi-surface QMediaPlayer::setVideoOutput
wasn't added until 5.15. If all you want to do is process video you can still use the code below as a template for pre-5.15, just gut the formatSource_
parts.
Code:
VideoProbeSurface.h (the only file; link is to 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
I went for the quickest-to-write implementation possible so it just forwards supported pixel formats from another surface (my intent was to both probe and play back to a QVideoWidget
) and you get whatever format you get. I just needed to grab subimages into QImage
s though, which handles most common formats. But you could modify this to force any formats you want (e.g. you might want to just return formats supported by QImage
or filter out source formats not supported by QImage)
, etc.).
Example set up:
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 });
Notes
The only really sketchy thing I had to do was const_cast
the QVideoFrame
on the receiver side (for read-only access), since QVideoFrame::map()
isn't const
:
if (const_cast<QVideoFrame&>(frame).map(QAbstractVideoBuffer::ReadOnly)) {
...;
const_cast<QVideoFrame&>(frame).unmap();
}
But the real QVideoProbe
would make you do the same thing so, I don't know what's up with that -- it's a strange API. I ran some tests with sw, native hw, and copy-back hw renderers and decoders and map
/unmap
in read mode seem to be functioning OK, so, whatever.
Performance-wise, the video will bog down if you spend too much time in the callback, so design accordingly. However, I didn't test QueuedConnection
, so I don't know if that'd still have the issue (although the fact that the signal parameter is a reference would make me wary of trying it, as well as conceivable issues with the GPU releasing the memory before the slot ends up being called). I don't know how QVideoProbe
behaves in this regard, either. I do know that, at least on my machine, I can pack and queue Full HD (1920 x 1080) resolution QImage
s to a thread pool for processing without slowing down the video.
You probably also want to implement some sort of auto-unmapper utility object for exception safe unmap()
, etc. But again, that's not unique to this, same thing you'd have to do with QVideoProbe
.
So hopefully that helps somebody else.
Example QImage Use
PS, example of packing arbitrarily-formatted QVideoFrame
s into a QImage
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();
}
}
Notes about that:
- Constructing a
QImage
directly from the data is fast and essentially free: no copies are done.
- The data buffers are only technically valid between
map
and unmap
, so if you intend to use the QImage
outside of that scope, you'll want to use copy()
(or anything else that forces a detach) to force a deep copy.
- You also probably want to ensure that the original not-copied
QImage
is destructed before calling unmap
. It's unlikely to cause problems but it's always a good idea to minimize how many invalid pointers are hanging around at any given time, and also the QImage
docs say "The buffer must remain valid throughout the life of the QImage and all copies that have not been modified or otherwise detached from the original buffer". Best to be strict about it.