Render zero-copy sederhana dari video yang dipercepat perangkat keras dalam QML

Pendahuluan


Tujuan artikel ini adalah untuk menunjukkan bagaimana Anda dapat berteman dengan buffer video pihak ketiga dan QML. Gagasan utamanya adalah menggunakan komponen QML standar dari VideoOutput. Itu memungkinkan Anda untuk menghilangkan sumber pihak ketiga, itu didokumentasikan dengan baik dan memiliki backend yang mendukung GL_OES_EGL_image_external.


Gagasan bahwa ini mungkin tiba-tiba berguna muncul setelah saya mencoba menjalankan contoh bekerja dengan kamera di Qt, dan pada platform tertanam mereka bekerja pada kecepatan 3-5 frame per detik. Menjadi jelas bahwa di luar kotak tidak ada pertanyaan tentang salinan nol, meskipun platform mendukung semua ini dengan sangat baik. Dalam keadilan, pada desktop, VideoOutput dan Kamera berfungsi, seperti yang diharapkan, cepat dan tanpa penyalinan yang tidak perlu. Namun dalam tugas saya, sayangnya, itu tidak mungkin dilakukan dengan kelas yang ada untuk merekam video, dan saya ingin mendapatkan video dari sumber pihak ketiga, yang bisa berupa pipa GStreamer sewenang-wenang untuk mendekode video, misalnya, dari file atau aliran RTSP, atau API pihak ketiga yang dapat diintegrasikan ke dalam pangkalan Kelas Qt agak meragukan. Anda dapat, tentu saja, sekali lagi menemukan kembali roda dan menulis komponen Anda dengan menggambar melalui OpenGL, tetapi segera tampak jalan buntu yang sengaja dan sulit.


Semuanya mengarah pada fakta bahwa Anda perlu mencari tahu cara kerjanya, dan menulis aplikasi kecil yang mengkonfirmasi teorinya.


Teori


VideoOutput mendukung sumber khusus, asalkan


  1. objek yang diteruskan dapat menerima QAbstractVideoSurface langsung melalui properti videoSurface
  2. atau melalui mediaObject dengan QVideoRendererControl [tautan] .

Pencarian dalam sumber dan dokumentasi menunjukkan bahwa QtMultimedia memiliki kelas QAbstractVideoBuffer yang mendukung berbagai jenis pegangan, dari QPixmap hingga GLTexture dan EGLImage. Pencarian lebih lanjut menyebabkan plugin videonode_egl, yang merender frame yang datang menggunakan shader dengan samplerExternalOES. Ini berarti bahwa setelah saya berhasil membuat QAbstractVideoBuffer dengan EGLImage, tetap menemukan cara untuk meneruskan buffer ini ke videnode_egl.
Dan jika platform EGLImage tidak didukung, maka Anda dapat membungkus memori dan mengirimkannya untuk dirender, karena shader untuk sebagian besar format piksel telah diterapkan.


Implementasi


Contohnya hampir seluruhnya didasarkan pada tutorial Tinjauan Video .


Agar Qt dapat bekerja dengan OpenGL ES pada desktop, Anda perlu membangun kembali Qt dengan flag yang sesuai. Secara default, ini tidak diaktifkan untuk desktop.


Untuk kesederhanaan, kami akan menggunakan metode pertama, dan mengambil pipa GStreamer sederhana sebagai sumber video:


v4l2src ! appsink 

Buat kelas V4L2Source yang akan mengirimkan frame ke QAbstractVideoSurface yang ditentukan olehnya.


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

Semuanya cukup sepele, kecuali slot setWinow () - diperlukan untuk mencegat sinyal QQuickItem :: windowChanged () dan mengatur panggilan balik ke QQuickWindow :: beforeSynchronizing ().


Karena backend VideoOutput tidak selalu tahu cara bekerja dengan EGLImage, Anda perlu bertanya QAbstractVideoSurface format apa untuk QAbstractVideoBuffer :: HandleType yang didukungnya:


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

Mari buat pipeline kami dan atur callback yang diperlukan:


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

Di konstruktor, kode standar untuk meluncurkan pipa Anda dibuat oleh GMainContext dan GMainLoop untuk membuat pipa dalam aliran terpisah.


Perlu memperhatikan Qt :: DirectConnection flag di setWindow () - ini menjamin bahwa panggilan balik akan dipanggil dalam utas yang sama dengan sinyal, yang memberi kita akses ke konteks OpenGL saat ini.


V4L2Source :: on_new_sample () yang dipanggil ketika frame baru dari v4l2src tiba di appsink cukup set flag yang siap dan memicu sinyal yang sesuai untuk memberi tahu VideoOutput bahwa perlu untuk menggambar ulang konten.


Probe sink aplikasi diperlukan untuk meminta pengalokasi v4l2src untuk menambahkan informasi meta tentang format video ke setiap buffer. Ini perlu untuk memperhitungkan situasi akun saat pengemudi mengeluarkan buffer video dengan teguran / offset selain standar.


Pembaruan bingkai video untuk VideoOutput terjadi di 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); } 

Dalam fungsi ini, kami mengambil buffer terakhir yang tersedia untuk kami dari appsink, meminta GstVideoMeta untuk mencari tahu informasi tentang offset dan langkah untuk setiap daftar putar (yah, demi kesederhanaan, tidak ada mundur jika tidak ada meta untuk beberapa alasan) dan buat QAbstractVideoBuffer dengan tipe kepala yang diinginkan: EGLImage (GstDmaVideoBuffer) atau None (GstVideoBuffer). Kemudian bungkus dalam QVideoFrame dan masukkan ke dalam antrian rendering.


Implementasi GstDmaVideoBuffer dan GstVideoBuffer sendiri cukup sepele:


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

Setelah semua ini, kita dapat membangun halaman QML dari formulir berikut:


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

Kesimpulan


Tujuan artikel ini adalah untuk menunjukkan bagaimana mengintegrasikan API yang ada yang mampu memberikan video akselerasi perangkat keras dengan QML dan menggunakan komponen yang ada untuk rendering tanpa menyalin (baik, atau dalam kasus terburuk, dengan satu, tetapi tanpa konversi perangkat lunak yang mahal ke RGB).


Tautan Kode


Referensi


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


All Articles