Einfaches Zero-Copy-Rendering hardwarebeschleunigter Videos in QML

Einleitung


In diesem Artikel wird veranschaulicht, wie Sie sich mit Videopuffern und QML von Drittanbietern anfreunden können. Die Hauptidee ist, die Standard-QML-Komponente von VideoOutput zu verwenden. Sie können damit Quellen von Drittanbietern abrufen, sind gut dokumentiert und verfügen über ein Backend, das GL_OES_EGL_image_external unterstützt.


Die Idee, dass dies plötzlich nützlich sein könnte, entstand, nachdem ich versuchte, Beispiele für die Arbeit mit der Kamera in Qt auszuführen, und auf der eingebetteten Plattform arbeiteten sie mit einer Geschwindigkeit von 3-5 Bildern pro Sekunde. Es stellte sich heraus, dass von einer Nullkopie keine Rede war, obwohl die Plattform dies alles sehr gut unterstützt. Fairerweise funktionieren VideoOutput und Camera auf dem Desktop wie erwartet schnell und ohne unnötiges Kopieren. Bei meiner Aufgabe war es leider unmöglich, mit den vorhandenen Klassen für die Videoerfassung fertig zu werden, und ich wollte ein Video von einer Quelle eines Drittanbieters erhalten, bei der es sich um eine beliebige GStreamer-Pipeline zum Decodieren von Videos handeln könnte, beispielsweise um eine Datei oder einen RTSP-Stream oder eine Drittanbieter-API, die in die Basis integriert werden kann Qts Klassen sind etwas zweifelhaft. Natürlich können Sie das Rad noch einmal neu erfinden und Ihre Komponente mit OpenGL zeichnen. Dies schien jedoch sofort eine bewusste Sackgasse und ein schwieriger Weg zu sein.


Alles hat dazu geführt, dass Sie herausfinden müssen, wie es wirklich funktioniert, und eine kleine Anwendung schreiben müssen, die die Theorie bestätigt.


Theorie


VideoOutput unterstützt benutzerdefinierte Quellen, sofern diese vorhanden sind


  1. Das übergebene Objekt kann QAbstractVideoSurface direkt über die videoSurface-Eigenschaft akzeptieren
  2. oder über mediaObject mit QVideoRendererControl [link] .

Eine Suche in den Quellen und in der Dokumentation ergab, dass QtMultimedia über eine QAbstractVideoBuffer-Klasse verfügt, die verschiedene Arten von Handles unterstützt, von QPixmap bis GLTexture und EGLImage. Weitere Suchanfragen führten zum videonode_egl-Plugin, das den Frame, der zu ihm kam, mithilfe des Shaders mit samplerExternalOES wiedergibt. Dies bedeutet, dass ich nach dem Erstellen eines QAbstractVideoBuffers mit EGLImage einen Weg finden muss, diesen Puffer an videnode_egl zu übergeben.
Wenn die EGLImage-Plattform nicht unterstützt wird, können Sie den Speicher umbrechen und zum Rendern senden, da die Shader für die meisten Pixelformate bereits implementiert sind.


Implementierung


Das Beispiel basiert fast ausschließlich auf dem Tutorial Videoübersicht .


Damit Qt mit OpenGL ES auf dem Desktop funktioniert, müssen Sie Qt mit dem entsprechenden Flag neu erstellen. Standardmäßig ist es nicht für den Desktop aktiviert.


Der Einfachheit halber verwenden wir die erste Methode und verwenden die einfache GStreamer-Pipeline als Videoquelle:


v4l2src ! appsink 

Erstellen Sie eine V4L2Source-Klasse, die Frames an das von ihr angegebene QAbstractVideoSurface liefert.


 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(); ... } 

Bis auf den Slot setWinow () ist alles trivial genug - er wird benötigt, um das Signal QQuickItem :: windowChanged () abzufangen und den Rückruf auf QQuickWindow :: beforeSynchronizing () zu setzen.


Da das VideoOutput-Backend nicht immer weiß, wie mit EGLImage gearbeitet wird, müssen Sie QAbstractVideoSurface fragen, welche Formate für den angegebenen QAbstractVideoBuffer :: HandleType unterstützt werden:


 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(); } } 

Lassen Sie uns unsere Pipeline erstellen und die erforderlichen Rückrufe einrichten:


 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; } 

Im Konstruktor wird der Standardcode zum Starten Ihrer Pipeline von GMainContext und GMainLoop erstellt, um eine Pipeline in einem separaten Stream zu erstellen.


Es lohnt sich, auf das Qt :: DirectConnection-Flag in setWindow () zu achten - es garantiert, dass der Rückruf im selben Thread wie das Signal aufgerufen wird, wodurch wir auf den aktuellen OpenGL-Kontext zugreifen können.


V4L2Source :: on_new_sample (), das aufgerufen wird, wenn ein neuer Frame von v4l2src in appsink eintrifft, setzt einfach das Ready-Flag und löst das entsprechende Signal aus, um VideoOutput darüber zu informieren, dass der Inhalt neu gezeichnet werden muss.


Der appsink-Sink-Test wird benötigt, um den v4l2src-Allokator aufzufordern, jedem Puffer Metainformationen zum Videoformat hinzuzufügen. Dies ist erforderlich, um Situationen zu berücksichtigen, in denen der Treiber einen Videopuffer mit einem anderen Strike / Offset als dem Standard ausgibt.


Das Video-Frame-Update für VideoOutput erfolgt im Slot 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); } 

In dieser Funktion nehmen wir den letzten verfügbaren Puffer von appsink, fordern GstVideoMeta auf, Informationen zu Offsets und Schritten für jede Wiedergabeliste herauszufinden (der Einfachheit halber gibt es keinen Fallback, falls aus irgendeinem Grund kein Meta vorhanden ist) und Erstellen Sie einen QAbstractVideoBuffer mit dem gewünschten Kopftyp: EGLImage (GstDmaVideoBuffer) oder None (GstVideoBuffer). Wickeln Sie es dann in einen QVideoFrame und stellen Sie es in die Renderwarteschlange.


Die Implementierung von GstDmaVideoBuffer und GstVideoBuffer selbst ist ziemlich trivial:


 #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]; }; 

Nach alledem können wir eine QML-Seite der folgenden Form erstellen:


 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() } 

Schlussfolgerungen


In diesem Artikel wurde gezeigt, wie eine vorhandene API integriert wird, die hardwarebeschleunigtes Video mit QML liefert, und vorhandene Komponenten zum Rendern ohne Kopieren verwendet werden können (oder im schlimmsten Fall mit einer, aber ohne teure Software-Konvertierung in RGB).


Code Link


Referenzen


Source: https://habr.com/ru/post/de486062/


All Articles