مقدمة
الغرض من هذه المقالة هو توضيح كيف يمكنك تكوين صداقات مع مخازن الفيديو التابعة لجهات خارجية و QML. الفكرة الرئيسية هي استخدام مكون QML القياسي في VideoOutput. يتيح لك راحة مصادر خارجية ، كما أنه موثق جيدًا وله خلفية تدعم GL_OES_EGL_image_external.
نشأت فكرة أن هذا قد يكون مفيدًا بشكل مفاجئ بعد أن حاولت تشغيل أمثلة للعمل مع الكاميرا في Qt ، وعلى المنصة المدمجة عملوا بسرعة 3-5 إطارات في الثانية. أصبح من الواضح أنه من خارج الصندوق ، لم يكن هناك أي شك في وجود أي نسخة صفرية ، على الرغم من أن النظام الأساسي يدعم كل هذا جيدًا. إنصافًا ، على سطح المكتب ، تعمل VideoOutput والكاميرا ، كما هو متوقع ، بسرعة وبدون نسخ غير ضروري. ولكن في مهمتي ، للأسف ، كان من المستحيل القيام بالصفوف الموجودة لالتقاط الفيديو ، وأردت الحصول على فيديو من مصدر تابع لجهة خارجية ، والذي يمكن أن يكون خط أنابيب GStreamer تعسفيًا لفك تشفير الفيديو ، على سبيل المثال ، من ملف أو دفق RTSP ، أو واجهة برمجة تطبيقات لجهة خارجية يمكن دمجها في الأساس فصول كيو تي مشكوك فيها إلى حد ما. يمكنك بالطبع إعادة اختراع العجلة مرة أخرى وكتابة المكون الخاص بك من خلال الرسم على OpenGL ، لكن هذا بدا على الفور أنه طريق مسدود وطريقة صعبة.
كل شيء أدى إلى حقيقة أنك تحتاج إلى معرفة كيف تعمل حقا ، وكتابة تطبيق صغير يؤكد هذه النظرية.
نظرية
VideoOutput يدعم مصدر مخصص ، شريطة أن
- يمكن للكائن الذي تم تمريره قبول QAbstractVideoSurface مباشرة من خلال خاصية videoSurface
- أو من خلال mediaObject مع QVideoRendererControl [الارتباط] .
أظهر البحث في المصادر والوثائق أن QtMultimedia لديها فئة QAbstractVideoBuffer التي تدعم أنواعًا مختلفة من المقابض ، من QPixmap إلى GLTexture و EGLImage. أدت عمليات البحث الإضافية إلى البرنامج المساعد videonode_egl ، والذي يعرض الإطار الذي جاء به باستخدام التظليل مع samplerExternalOES. هذا يعني أنه بعد أن تمكنت من إنشاء QAbstractVideoBuffer مع EGLImage ، يبقى البحث عن طريقة لتمرير هذا المخزن المؤقت إلى videnode_egl.
وإذا لم يكن نظام EGLImage مدعومًا ، فيمكنك لف الذاكرة وإرسالها للتقديم ، حيث أن تظليل معظم تنسيقات البكسل قد تم تنفيذه بالفعل.
تطبيق
يعتمد المثال بالكامل تقريبًا على البرنامج التعليمي " نظرة عامة على الفيديو" .
لكي تعمل 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 () وتعيين رد الاتصال على 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(); } }
دعنا ننشئ خط أنابيبنا وننشئ عمليات الاسترجاعات اللازمة:
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; }
في المنشئ ، يتم إنشاء الرمز القياسي لبدء تشغيل خط الأنابيب بواسطة GMainContext و GMainLoop لإنشاء خط أنابيب في دفق منفصل.
يجدر الانتباه إلى علامة Qt :: DirectConnection في setWindow () - إنه يضمن استدعاء رد الاتصال في نفس مؤشر الترابط مثل الإشارة ، مما يتيح لنا الوصول إلى سياق OpenGL الحالي.
V4L2Source :: on_new_sample () والذي يتم استدعاؤه عند وصول إطار جديد من v4l2src في appinkink يقوم ببساطة بتعيين العلم الجاهز وتشغيل الإشارة المقابلة لإبلاغ VideoOutput بأنه من الضروري إعادة رسم المحتويات.
هناك حاجة إلى مسبار التطبيق appink لطلب مُخصص v4l2src لإضافة معلومات وصفية حول تنسيق الفيديو إلى كل مخزن مؤقت. يعد ذلك ضروريًا لأخذ المواقف في الاعتبار عند قيام برنامج التشغيل بإصدار مخزن مؤقت للفيديو مع إضراب / إزاحة غير قياسي.
يحدث تحديث إطار الفيديو لـ VideoOutput في فتحة المزامنة ():
في هذه الوظيفة ، نأخذ آخر مخزن مؤقت متاح لنا من appink ، ونطلب من GstVideoMeta اكتشاف معلومات حول الإزاحات والخطوات الكبيرة لكل قائمة تشغيل (حسنًا ، من أجل البساطة ، لا توجد نسخة احتياطية في حالة عدم وجود meta لسبب ما) قم بإنشاء 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() }
النتائج
كان الغرض من هذه المقالة هو إظهار كيفية دمج واجهة برمجة تطبيقات موجودة لديها القدرة على توصيل فيديو مُسرع بالأجهزة مع QML واستخدام مكونات موجودة للتقديم دون نسخ (جيدًا ، أو في أسوأ الحالات ، مع واحد ، ولكن دون تحويل برنامج مكلف إلى RGB).
رمز الارتباط
مراجع