Введение
Целью данной статьи является продемонстрировать способ как можно подружить сторонние видео буфера и QML. Основная идея — использовать стандартный QML компонент VideoOutput. Он позволяет подсовывать сторонние источники, неплохо документирован и имеет бекэнд поддерживающий GL_OES_EGL_image_external.
Мысль, что это вдруг может быть полезно, возникла после того, как я попытался запустить примеры работы с камерой в Qt, и на embedded платформе они работали со скоростью 3-5 кадра в секунду. Стало ясно, что из коробки ни о каком zero-copy речи не идет, хотя платформа все это отлично поддерживает. Справедливости ради, на десктопе VideoOutput и Camera работают, как и положено, быстро и без лишних копирований. Но в моей задаче, увы, нельзя было обойтись существующими классами для захвата видео, и хотелось завести видео из стороннего источника, каким может быть произвольный GStreamer пайплайн для декодирования видео, к примеру, из файла или RTSP стрим, или сторонний API который интегрировать в базовые классы Qt несколько сомнительно. Можно еще, конечно, в очередной раз переизобрести велосипед и написать свой компонент с рисованием через OpenGL, но это сразу показалось заведомо тупиковым и сложным путем.
Все вело к тому, что нужно разобраться как же оно устроено на самом деле, и написать небольшое приложение подтверждающее теорию.
Теория
VideoOutput поддерживает пользовательские source, при условии что
- переданный объект может принять QAbstractVideoSurface напрямую через свойство videoSurface
- или через mediaObject с контролом QVideoRendererControl [ссылка].
Поиск по исходникам и документации показал, что в QtMultimedia есть класс QAbstractVideoBuffer который поддерживает различные типы хэндлов, начиная от QPixmap и заканчивая GLTexture и EGLImage. Дальнейшие поиски привели к плагину videonode_egl, который отрисовывает пришедший ему кадр при помощи шейдера с samplerExternalOES. Это означает, что после того как мне удасться создать QAbstractVideoBuffer с EGLImage, остается найти способ передать этот буфер в videnode_egl.
А если EGLImage платформой не поддерживается, то можно обернуть пришедшую память и отправить на отрисовку, благо шейдеры для большинства пиксельных форматов уже реализованы.
Реализация
Пример почти целиком основывается на туториале Video Overview.
Для того чтобы Qt заработало с OpenGL ES на десктопе, необходимо пересобрать Qt с соответствующим флагом. По умолчанию, для десктопа он не включен.
Для простоты мы воспользуемся первым способом, а в качестве источника видео возьмем простой GStreamer пайплайн:
v4l2src ! appsink
Создадим класс V4L2Source, который будет поставлять фреймы в заданную ему QAbstractVideoSurface.
class V4L2Source : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QAbstractVideoSurface* videoSurface READ videoSurface WRITE
setVideoSurface)
Q_PROPERTY(QString device MEMBER m_device READ device WRITE setDevice)
Q_PROPERTY(QString caps MEMBER m_caps)
public:
V4L2Source(QQuickItem* parent = nullptr);
virtual ~V4L2Source();
void setVideoSurface(QAbstractVideoSurface* surface);
void setDevice(QString device);
public slots:
void start();
void stop();
private slots:
void setWindow(QQuickWindow* win);
void sync();
signals:
void frameReady();
...
}
Все достаточно тривиально, кроме слота setWinow() — он нужен чтобы перехватить сигнал QQuickItem::windowChanged() и установить callback на QQuickWindow::beforeSynchronizing().
Так как бэкэнд VideoOutput не всегда умеет работать с EGLImage, то необходимо запросить у QAbstractVideoSurface какие форматы для заданного QAbstractVideoBuffer::HandleType она поддерживает:
void V4L2Source::setVideoSurface(QAbstractVideoSurface* surface)
{
if (m_surface != surface && m_surface && m_surface->isActive()) {
m_surface->stop();
}
m_surface = surface;
if (surface
->supportedPixelFormats(
QAbstractVideoBuffer::HandleType::EGLImageHandle)
.size() > 0) {
EGLImageSupported = true;
} else {
EGLImageSupported = false;
}
if (m_surface && m_device.length() > 0) {
start();
}
}
Создадим наш пайплайн, и установим необходимые callback'и:
GstAppSinkCallbacks V4L2Source::callbacks = {.eos = nullptr,
.new_preroll = nullptr,
.new_sample =
&V4L2Source::on_new_sample};
V4L2Source::V4L2Source(QQuickItem* parent) : QQuickItem(parent)
{
m_surface = nullptr;
connect(this, &QQuickItem::windowChanged, this, &V4L2Source::setWindow);
pipeline = gst_pipeline_new("V4L2Source::pipeline");
v4l2src = gst_element_factory_make("v4l2src", nullptr);
appsink = gst_element_factory_make("appsink", nullptr);
GstPad* pad = gst_element_get_static_pad(appsink, "sink");
gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_QUERY_BOTH, appsink_pad_probe,
nullptr, nullptr);
gst_object_unref(pad);
gst_app_sink_set_callbacks(GST_APP_SINK(appsink), &callbacks, this,
nullptr);
gst_bin_add_many(GST_BIN(pipeline), v4l2src, appsink, nullptr);
gst_element_link(v4l2src, appsink);
context = g_main_context_new();
loop = g_main_loop_new(context, FALSE);
}
void V4L2Source::setWindow(QQuickWindow* win)
{
if (win) {
connect(win, &QQuickWindow::beforeSynchronizing, this,
&V4L2Source::sync, Qt::DirectConnection);
}
}
GstFlowReturn V4L2Source::on_new_sample(GstAppSink* sink, gpointer data)
{
Q_UNUSED(sink)
V4L2Source* self = (V4L2Source*)data;
QMutexLocker locker(&self->mutex);
self->ready = true;
self->frameReady();
return GST_FLOW_OK;
}
// Request v4l2src allocator to add GstVideoMeta to buffers
static GstPadProbeReturn
appsink_pad_probe(GstPad* pad, GstPadProbeInfo* info, gpointer user_data)
{
if (info->type & GST_PAD_PROBE_TYPE_QUERY_BOTH) {
GstQuery* query = gst_pad_probe_info_get_query(info);
if (GST_QUERY_TYPE(query) == GST_QUERY_ALLOCATION) {
gst_query_add_allocation_meta(query, GST_VIDEO_META_API_TYPE, NULL);
}
}
return GST_PAD_PROBE_OK;
}
В конструкторе стандартный код для запуска своего пайплайна, создается GMainContext и GMainLoop чтобы завести пайплайн в отдельном потоке.
Стоит обратить внимание на флаг Qt::DirectConnection в setWindow() — он гарантирует что callback будет вызван в том же потоке что и сигнал, что дает нам доступ к текущему OpenGL контексту.
V4L2Source::on_new_sample() который вызывается когда новый кадр из v4l2src приходит в appsink просто выставляет флаг готовности и вызывает соответствующий сигнал, чтобы проинформировать VideoOutput что требуется перерисовать содержимое.
Пробник на sink паде у appsink необходим, чтобы попросить аллокатор v4l2src добавлять мета информацию о видео формате к каждому буферу. Это необходимо чтобы учесть ситуации, когда драйвер выдает видео буфер со страйдом/оффсетом отличными от стандартных.
Обновление видеокадра для VideoOutput происходит в слоте sync():
// Make sure this callback is invoked from rendering thread
void V4L2Source::sync()
{
{
QMutexLocker locker(&mutex);
if (!ready) {
return;
}
// reset ready flag
ready = false;
}
// pull available sample and convert GstBuffer into a QAbstractVideoBuffer
GstSample* sample = gst_app_sink_pull_sample(GST_APP_SINK(appsink));
GstBuffer* buffer = gst_sample_get_buffer(sample);
GstVideoMeta* videoMeta = gst_buffer_get_video_meta(buffer);
// if memory is DMABUF and EGLImage is supported by the backend,
// create video buffer with EGLImage handle
videoFrame.reset();
if (EGLImageSupported && buffer_is_dmabuf(buffer)) {
videoBuffer.reset(new GstDmaVideoBuffer(buffer, videoMeta));
} else {
// TODO: support other memory types, probably GL textures?
// just map memory
videoBuffer.reset(new GstVideoBuffer(buffer, videoMeta));
}
QSize size = QSize(videoMeta->width, videoMeta->height);
QVideoFrame::PixelFormat format =
gst_video_format_to_qvideoformat(videoMeta->format);
videoFrame.reset(new QVideoFrame(
static_cast<QAbstractVideoBuffer*>(videoBuffer.get()), size, format));
if (!m_surface->isActive()) {
m_format = QVideoSurfaceFormat(size, format);
Q_ASSERT(m_surface->start(m_format) == true);
}
m_surface->present(*videoFrame);
gst_sample_unref(sample);
}
В этой функции мы забираем последний доступный нам буфер из appsink, запрашиваем GstVideoMeta чтобы узнать информацию об оффсетах и страйдах для каждого плейна (что ж, для простоты примера, никакого fallback на случай, если по какой-то причине меты нет, не предусмотрено) и создаем QAbstractVideoBuffer с нужным типом хедла: EGLImage (GstDmaVideoBuffer) или None (GstVideoBuffer). Затем оборачиваем это в QVideoFrame и отправляем в очередь на отрисовку.
Сама реализация GstDmaVideoBuffer и GstVideoBuffer достаточно тривиальна:
#define GST_BUFFER_GET_DMAFD(buffer, plane)
(((plane) < gst_buffer_n_memory((buffer))) ?
gst_dmabuf_memory_get_fd(gst_buffer_peek_memory((buffer), (plane))) :
gst_dmabuf_memory_get_fd(gst_buffer_peek_memory((buffer), 0)))
class GstDmaVideoBuffer : public QAbstractVideoBuffer
{
public:
// This should be called from renderer thread
GstDmaVideoBuffer(GstBuffer* buffer, GstVideoMeta* videoMeta) :
QAbstractVideoBuffer(HandleType::EGLImageHandle),
buffer(gst_buffer_ref(buffer)), m_videoMeta(videoMeta)
{
static PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR =
reinterpret_cast<PFNEGLCREATEIMAGEKHRPROC>(
eglGetProcAddress("eglCreateImageKHR"));
int idx = 0;
EGLint attribs[MAX_ATTRIBUTES_COUNT];
attribs[idx++] = EGL_WIDTH;
attribs[idx++] = m_videoMeta->width;
attribs[idx++] = EGL_HEIGHT;
attribs[idx++] = m_videoMeta->height;
attribs[idx++] = EGL_LINUX_DRM_FOURCC_EXT;
attribs[idx++] = gst_video_format_to_drm_code(m_videoMeta->format);
attribs[idx++] = EGL_DMA_BUF_PLANE0_FD_EXT;
attribs[idx++] = GST_BUFFER_GET_DMAFD(buffer, 0);
attribs[idx++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
attribs[idx++] = m_videoMeta->offset[0];
attribs[idx++] = EGL_DMA_BUF_PLANE0_PITCH_EXT;
attribs[idx++] = m_videoMeta->stride[0];
if (m_videoMeta->n_planes > 1) {
attribs[idx++] = EGL_DMA_BUF_PLANE1_FD_EXT;
attribs[idx++] = GST_BUFFER_GET_DMAFD(buffer, 1);
attribs[idx++] = EGL_DMA_BUF_PLANE1_OFFSET_EXT;
attribs[idx++] = m_videoMeta->offset[1];
attribs[idx++] = EGL_DMA_BUF_PLANE1_PITCH_EXT;
attribs[idx++] = m_videoMeta->stride[1];
}
if (m_videoMeta->n_planes > 2) {
attribs[idx++] = EGL_DMA_BUF_PLANE2_FD_EXT;
attribs[idx++] = GST_BUFFER_GET_DMAFD(buffer, 2);
attribs[idx++] = EGL_DMA_BUF_PLANE2_OFFSET_EXT;
attribs[idx++] = m_videoMeta->offset[2];
attribs[idx++] = EGL_DMA_BUF_PLANE2_PITCH_EXT;
attribs[idx++] = m_videoMeta->stride[2];
}
attribs[idx++] = EGL_NONE;
auto m_qOpenGLContext = QOpenGLContext::currentContext();
QEGLNativeContext qEglContext =
qvariant_cast<QEGLNativeContext>(m_qOpenGLContext->nativeHandle());
EGLDisplay dpy = qEglContext.display();
Q_ASSERT(dpy != EGL_NO_DISPLAY);
image = eglCreateImageKHR(dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT,
(EGLClientBuffer) nullptr, attribs);
Q_ASSERT(image != EGL_NO_IMAGE_KHR);
}
...
// This should be called from renderer thread
~GstDmaVideoBuffer() override
{
static PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR =
reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(
eglGetProcAddress("eglDestroyImageKHR"));
auto m_qOpenGLContext = QOpenGLContext::currentContext();
QEGLNativeContext qEglContext =
qvariant_cast<QEGLNativeContext>(m_qOpenGLContext->nativeHandle());
EGLDisplay dpy = qEglContext.display();
Q_ASSERT(dpy != EGL_NO_DISPLAY);
eglDestroyImageKHR(dpy, image);
gst_buffer_unref(buffer);
}
private:
EGLImage image;
GstBuffer* buffer;
GstVideoMeta* m_videoMeta;
};
class GstVideoBuffer : public QAbstractPlanarVideoBuffer
{
public:
GstVideoBuffer(GstBuffer* buffer, GstVideoMeta* videoMeta) :
QAbstractPlanarVideoBuffer(HandleType::NoHandle),
m_buffer(gst_buffer_ref(buffer)), m_videoMeta(videoMeta),
m_mode(QAbstractVideoBuffer::MapMode::NotMapped)
{
}
QVariant handle() const override
{
return QVariant();
}
void release() override
{
}
int map(MapMode mode,
int* numBytes,
int bytesPerLine[4],
uchar* data[4]) override
{
int size = 0;
const GstMapFlags flags =
GstMapFlags(((mode & ReadOnly) ? GST_MAP_READ : 0) |
((mode & WriteOnly) ? GST_MAP_WRITE : 0));
if (mode == NotMapped || m_mode != NotMapped) {
return 0;
} else {
for (int i = 0; i < m_videoMeta->n_planes; i++) {
gst_video_meta_map(m_videoMeta, i, &m_mapInfo[i],
(gpointer*)&data[i], &bytesPerLine[i],
flags);
size += m_mapInfo[i].size;
}
}
m_mode = mode;
*numBytes = size;
return m_videoMeta->n_planes;
}
MapMode mapMode() const override
{
return m_mode;
}
void unmap() override
{
if (m_mode != NotMapped) {
for (int i = 0; i < m_videoMeta->n_planes; i++) {
gst_video_meta_unmap(m_videoMeta, i, &m_mapInfo[i]);
}
}
m_mode = NotMapped;
}
~GstVideoBuffer() override
{
unmap();
gst_buffer_unref(m_buffer);
}
private:
GstBuffer* m_buffer;
MapMode m_mode;
GstVideoMeta* m_videoMeta;
GstMapInfo m_mapInfo[4];
};
После всего этого мы можем собрать QML страничку следующего вида:
import QtQuick 2.10
import QtQuick.Window 2.10
import QtQuick.Layouts 1.10
import QtQuick.Controls 2.0
import QtMultimedia 5.10
import v4l2source 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("qml zero copy rendering")
color: "black"
CameraSource {
id: camera
device: "/dev/video0"
onFrameReady: videoOutput.update()
}
VideoOutput {
id: videoOutput
source: camera
anchors.fill: parent
}
onClosing: camera.stop()
}
Выводы
Целью данной статьи было показать, как интегрировать существующий API, который способен выдавать аппаратно-ускоренное видео, с QML и использовать существующие компоненты для отрисовки без копирования (ну или в худшем случае с одним, но без дорогостоящего софтверного конвертирования в RGB).
Ссылки
- https://habr.com/ru/post/481540/
- https://habr.com/ru/post/254625/
- https://doc.qt.io/qt-5/videooverview.html
- https://doc.qt.io/qt-5/qml-qtmultimedia-videooutput.html
- https://doc.qt.io/qt-5/qtquick-visualcanvas-scenegraph.html
- https://doc.qt.io/qt-5.12/qtquick-scenegraph-openglunderqml-example.html
Автор: Rambden