Rendu simple sans copie de la vidéo avec accélération matérielle en QML

Présentation


Le but de cet article est de montrer comment vous pouvez vous faire des amis avec des tampons vidéo tiers et QML. L'idée principale est d'utiliser le composant QML standard de VideoOutput. Il vous permet de supprimer les sources tierces, est bien documenté et possède un backend qui prend en charge GL_OES_EGL_image_external.


L'idée que cela pourrait soudainement être utile est née après avoir essayé d'exécuter des exemples de travail avec l'appareil photo dans Qt, et sur la plate-forme intégrée, ils fonctionnaient à une vitesse de 3 à 5 images par seconde. Il est devenu clair que dès la sortie de l'emballage, il n'était pas question de copie nulle, bien que la plate-forme supporte tout cela très bien. En toute honnêteté, sur le bureau, VideoOutput et Camera fonctionnent, comme prévu, rapidement et sans copie inutile. Mais dans ma tâche, hélas, il était impossible de faire avec les classes existantes pour capturer la vidéo, et je voulais obtenir la vidéo d'une source tierce, qui pourrait être un pipeline GStreamer arbitraire pour décoder la vidéo, par exemple, à partir d'un fichier ou d'un flux RTSP, ou d'une API tierce qui peut être intégrée dans la base Les classes de Qt sont quelque peu douteuses. Bien sûr, vous pouvez à nouveau réinventer la roue et écrire votre composant en dessinant via OpenGL, mais cela a immédiatement semblé une voie délibérément sans issue et difficile.


Tout a conduit au fait que vous devez comprendre comment cela fonctionne vraiment et écrire une petite application confirmant la théorie.


Théorie


VideoOutput prend en charge la source personnalisée, à condition que


  1. l'objet passé peut accepter QAbstractVideoSurface directement via la propriété videoSurface
  2. ou via mediaObject avec QVideoRendererControl [lien] .

Une recherche dans les sources et la documentation a montré que QtMultimedia possède une classe QAbstractVideoBuffer qui prend en charge différents types de descripteurs, de QPixmap à GLTexture et EGLImage. D'autres recherches ont conduit au plugin videonode_egl, qui rend l'image qui lui est arrivée à l'aide du shader avec samplerExternalOES. Cela signifie qu'après avoir réussi à créer un QAbstractVideoBuffer avec EGLImage, il reste à trouver un moyen de passer ce tampon à videnode_egl.
Et si la plate-forme EGLImage n'est pas prise en charge, vous pouvez envelopper la mémoire et l'envoyer pour le rendu, car les shaders pour la plupart des formats de pixels sont déjà implémentés.


Implémentation


L'exemple est presque entièrement basé sur le didacticiel de présentation vidéo .


Pour que Qt fonctionne avec OpenGL ES sur le bureau, il est nécessaire de reconstruire Qt avec l'indicateur correspondant. Par défaut, il n'est pas activé pour le bureau.


Pour plus de simplicité, nous utiliserons la première méthode et prendrons un simple pipeline GStreamer comme source vidéo:


v4l2src ! appsink 

Créez une classe V4L2Source qui fournira des trames à la QAbstractVideoSurface spécifiée par celle-ci.


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

Tout est assez trivial, sauf pour le slot setWinow () - il est nécessaire d'intercepter le signal QQuickItem :: windowChanged () et de définir le rappel sur QQuickWindow :: beforeSynchronizing ().


Étant donné que le backend VideoOutput ne sait pas toujours comment travailler avec EGLImage, vous devez demander à QAbstractVideoSurface quels formats pour le QAbstractVideoBuffer :: HandleType qu'il prend en charge:


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

Créons notre pipeline et configurons les rappels nécessaires:


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

Dans le constructeur, le code standard pour lancer votre pipeline est créé par GMainContext et GMainLoop pour créer un pipeline dans un flux distinct.


Il convient de prêter attention à l'indicateur Qt :: DirectConnection dans setWindow () - il garantit que le rappel sera appelé dans le même thread que le signal, ce qui nous donne accès au contexte OpenGL actuel.


V4L2Source :: on_new_sample () qui est appelée lorsqu'une nouvelle trame de v4l2src arrive dans appsink définit simplement l'indicateur ready et déclenche le signal correspondant pour informer VideoOutput qu'il est nécessaire de redessiner le contenu.


La sonde de dissipation appsink est nécessaire pour demander à l'allocateur v4l2src d'ajouter des méta-informations sur le format vidéo à chaque tampon. Cela est nécessaire pour prendre en compte les situations où le pilote émet une mémoire tampon vidéo avec un décalage / décalage autre que standard.


La mise à jour de l'image vidéo pour VideoOutput se produit dans l'emplacement 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); } 

Dans cette fonction, nous prenons le dernier tampon disponible pour appsink, demandons à GstVideoMeta de trouver des informations sur les décalages et les pas pour chaque liste de lecture (enfin, par souci de simplicité, il n'y a pas de solution de rechange au cas où il n'y aurait pas de méta pour une raison quelconque) et créer un QAbstractVideoBuffer avec le type de tête souhaité: EGLImage (GstDmaVideoBuffer) ou None (GstVideoBuffer). Ensuite, enveloppez-le dans un QVideoFrame et placez-le dans la file d'attente de rendu.


L'implémentation de GstDmaVideoBuffer et GstVideoBuffer lui-même est assez triviale:


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

Après tout cela, nous pouvons construire une page QML de la forme suivante:


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

Conclusions


Le but de cet article était de montrer comment intégrer une API existante qui est capable de fournir de la vidéo avec accélération matérielle avec QML et utiliser les composants existants pour le rendu sans copie (enfin, ou dans le pire des cas, avec un, mais sans conversion logicielle coûteuse en RVB).


Lien de code


Les références


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


All Articles