Введение
Целью данной статьи является продемонстрировать способ как можно подружить сторонние видео буфера и 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/486062/
Добавить комментарий