Introduccion
El propósito de este artículo es demostrar cómo puedes hacer amigos con video buffers de terceros y QML. La idea principal es utilizar el componente QML estándar de VideoOutput. Le permite obtener fuentes de terceros, está bien documentado y tiene un back-end que admite GL_OES_EGL_image_external.
La idea de que esto podría ser útil repentinamente surgió después de que intenté ejecutar ejemplos de trabajo con la cámara en Qt, y en la plataforma incorporada trabajaron a una velocidad de 3-5 cuadros por segundo. Quedó claro que, desde el primer momento, no se trataba de ninguna copia cero, aunque la plataforma admite todo esto muy bien. Para ser justos, en el escritorio, VideoOutput y Camera funcionan, como se esperaba, rápidamente y sin copias innecesarias. Pero en mi tarea, por desgracia, era imposible hacerlo con las clases existentes para capturar video, y quería obtener video de una fuente de terceros, que podría ser una tubería arbitraria de GStreamer para decodificar video, por ejemplo, de un archivo o flujo RTSP, o una API de terceros que se puede integrar en la base Las clases de Qt son algo dudosas. Puede, por supuesto, reinventar una vez más la rueda y escribir su componente dibujando a través de OpenGL, pero inmediatamente pareció un callejón sin salida deliberadamente y una forma difícil.
Todo condujo al hecho de que necesita descubrir cómo funciona realmente y escribir una pequeña aplicación que confirme la teoría.
Teoría
VideoOutput admite fuentes personalizadas, siempre que
- el objeto pasado puede aceptar QAbstractVideoSurface directamente a través de la propiedad videoSurface
- o a través de mediaObject con QVideoRendererControl [enlace] .
Una búsqueda en las fuentes y la documentación mostró que QtMultimedia tiene una clase QAbstractVideoBuffer que admite varios tipos de identificadores, desde QPixmap hasta GLTexture y EGLImage. Las búsquedas posteriores condujeron al complemento videonode_egl, que representa el marco que llegó a él utilizando el sombreador con samplerExternalOES. Esto significa que después de lograr crear un QAbstractVideoBuffer con EGLImage, queda por encontrar una manera de pasar este búfer a videnode_egl.
Y si la plataforma EGLImage no es compatible, puede envolver la memoria y enviarla para renderizar, ya que los sombreadores para la mayoría de los formatos de píxeles ya se han implementado.
Implementación
El ejemplo se basa casi por completo en el tutorial Descripción general del video .
Para que Qt funcione con OpenGL ES en el escritorio, debe reconstruir Qt con el indicador correspondiente. Por defecto, no está habilitado para el escritorio.
Para simplificar, usaremos el primer método y tomaremos la simple tubería GStreamer como fuente de video:
v4l2src ! appsink
Cree una clase V4L2Source que entregará fotogramas al QAbstractVideoSurface especificado por él.
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(); ... }
Todo es bastante trivial, excepto la ranura setWinow (): es necesaria para interceptar la señal QQuickItem :: windowChanged () y establecer la devolución de llamada a QQuickWindow :: beforeSynchronizing ().
Dado que el backend VideoOutput no siempre sabe cómo trabajar con EGLImage, debe preguntarle a QAbstractVideoSurface qué formatos para el QAbstractVideoBuffer :: HandleType es compatible:
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(); } }
Creemos nuestra tubería y configuremos las devoluciones de llamada necesarias:
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; }
En el constructor, GMainContext y GMainLoop crean el código estándar para iniciar su canalización para crear una canalización en una secuencia separada.
Vale la pena prestar atención al indicador Qt :: DirectConnection en setWindow (): garantiza que la devolución de llamada se invocará en el mismo hilo que la señal, lo que nos da acceso al contexto actual de OpenGL.
V4L2Source :: on_new_sample (), que se llama cuando llega un nuevo marco de v4l2src en appsink, simplemente establece el indicador de listo y activa la señal correspondiente para informar a VideoOutput que es necesario volver a dibujar el contenido.
La sonda de sumidero de aplicaciones es necesaria para pedirle al asignador v4l2src que agregue metainformación sobre el formato de video a cada búfer. Esto es necesario para tener en cuenta situaciones en las que el controlador emite un búfer de video con una huelga / desplazamiento diferente al estándar.
La actualización del cuadro de video para VideoOutput ocurre en la ranura sync ():
En esta función, tomamos el último búfer disponible para nosotros desde appsink, le pedimos a GstVideoMeta que encuentre información sobre compensaciones y avances para cada lista de reproducción (bueno, en aras de la simplicidad, no hay respaldo en caso de que no haya meta por algún motivo) y cree un QAbstractVideoBuffer con el tipo de cabezal deseado: EGLImage (GstDmaVideoBuffer) o None (GstVideoBuffer). Luego envuélvalo en un QVideoFrame y póngalo en la cola de renderizado.
La implementación de GstDmaVideoBuffer y GstVideoBuffer es bastante 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]; };
Después de todo esto, podemos construir una página QML de la siguiente forma:
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() }
Conclusiones
El propósito de este artículo era mostrar cómo integrar una API existente que sea capaz de entregar video acelerado por hardware con QML y usar los componentes existentes para renderizar sin copiar (bueno, o en el peor de los casos, con uno, pero sin una costosa conversión de software a RGB).
Enlace de código
Referencias