Простой zero-copy рендеринг аппаратно ускоренного видео в QML

от автора

Введение

Целью данной статьи является продемонстрировать способ как можно подружить сторонние видео буфера и QML. Основная идея — использовать стандартный QML компонент VideoOutput. Он позволяет подсовывать сторонние источники, неплохо документирован и имеет бекэнд поддерживающий GL_OES_EGL_image_external.

Мысль, что это вдруг может быть полезно, возникла после того, как я попытался запустить примеры работы с камерой в Qt, и на embedded платформе они работали со скоростью 3-5 кадра в секунду. Стало ясно, что из коробки ни о каком zero-copy речи не идет, хотя платформа все это отлично поддерживает. Справедливости ради, на десктопе VideoOutput и Camera работают, как и положено, быстро и без лишних копирований. Но в моей задаче, увы, нельзя было обойтись существующими классами для захвата видео, и хотелось завести видео из стороннего источника, каким может быть произвольный GStreamer пайплайн для декодирования видео, к примеру, из файла или RTSP стрим, или сторонний API который интегрировать в базовые классы Qt несколько сомнительно. Можно еще, конечно, в очередной раз переизобрести велосипед и написать свой компонент с рисованием через OpenGL, но это сразу показалось заведомо тупиковым и сложным путем.

Все вело к тому, что нужно разобраться как же оно устроено на самом деле, и написать небольшое приложение подтверждающее теорию.

Теория

VideoOutput поддерживает пользовательские source, при условии что

  1. переданный объект может принять QAbstractVideoSurface напрямую через свойство videoSurface
  2. или через 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/486062/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *