Renderização simples de cópia zero de vídeo acelerado por hardware em QML

1. Introdução


O objetivo deste artigo é demonstrar como você pode fazer amizade com buffers de vídeo de terceiros e QML. A idéia principal é usar o componente QML padrão do VideoOutput. Ele permite descartar fontes de terceiros, está bem documentado e possui um back-end que suporta GL_OES_EGL_image_external.


A ideia de que isso pode ser útil repentinamente surgiu depois que tentei executar exemplos de trabalho com a câmera no Qt, e na plataforma incorporada eles trabalhavam a uma velocidade de 3-5 quadros por segundo. Ficou claro que fora da caixa não havia dúvida de nenhuma cópia zero, embora a plataforma suporte tudo isso muito bem. Para ser justo, na área de trabalho, o VideoOutput e a Câmera funcionam, como esperado, rapidamente e sem cópias desnecessárias. Mas na minha tarefa, infelizmente, era impossível fazer parte das classes existentes para captura de vídeo, e eu queria obter vídeo de uma fonte de terceiros, que poderia ser um pipeline arbitrário do GStreamer para decodificar vídeo, por exemplo, de um arquivo ou fluxo RTSP ou uma API de terceiros que pode ser integrada à base As aulas de Qt são um tanto duvidosas. Você pode, é claro, mais uma vez reinventar a roda e escrever seu componente desenhando no OpenGL, mas imediatamente pareceu um beco sem saída e uma maneira difícil.


Tudo levou ao fato de que você precisa descobrir como realmente funciona e escrever um pequeno aplicativo confirmando a teoria.


Teoria


VideoOutput suporta fonte personalizada, desde que


  1. o objeto transmitido pode aceitar QAbstractVideoSurface diretamente através da propriedade videoSurface
  2. ou através do mediaObject com QVideoRendererControl [link] .

Uma pesquisa nas fontes e na documentação mostrou que QtMultimedia possui uma classe QAbstractVideoBuffer que suporta vários tipos de identificadores, de QPixmap a GLTexture e EGLImage. Outras pesquisas levaram ao plug-in videonode_egl, que renderiza o quadro que veio a ele usando o shader com samplerExternalOES. Isso significa que, depois de conseguir criar um QAbstractVideoBuffer com EGLImage, resta encontrar uma maneira de passar esse buffer para videnode_egl.
E se a plataforma EGLImage não for suportada, você poderá agrupar a memória e enviá-la para renderização, pois os shaders para a maioria dos formatos de pixel já foram implementados.


Implementação


O exemplo é quase inteiramente baseado no tutorial Visão geral do vídeo .


Para que o Qt funcione com o OpenGL ES na área de trabalho, é necessário reconstruir o Qt com o sinalizador correspondente. Por padrão, ele não está ativado para a área de trabalho.


Para simplificar, usaremos o primeiro método e usaremos um pipeline simples do GStreamer como fonte de vídeo:


v4l2src ! appsink 

Crie uma classe V4L2Source que fornecerá quadros para o QAbstractVideoSurface especificado por ele.


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

Tudo é trivial o suficiente, exceto o slot setWinow () - é necessário interceptar o sinal QQuickItem :: windowChanged () e definir o retorno de chamada como QQuickWindow :: beforeSynchronizing ().


Como o back-end VideoOutput nem sempre sabe trabalhar com o EGLImage, você precisa perguntar ao QAbstractVideoSurface quais formatos para o QAbstractVideoBuffer :: HandleType que ele oferece:


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

Vamos criar nosso pipeline e configurar os retornos de chamada necessários:


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

No construtor, o código padrão para iniciar seu pipeline é criado por GMainContext e GMainLoop para criar um pipeline em um fluxo separado.


Vale a pena prestar atenção no sinalizador Qt :: DirectConnection em setWindow () - garante que o retorno de chamada será chamado no mesmo segmento que o sinal, o que nos dá acesso ao contexto atual do OpenGL.


V4L2Source :: on_new_sample () que é chamado quando um novo quadro da v4l2src chega no appsink simplesmente define o sinalizador pronto e aciona o sinal correspondente para informar ao VideoOutput que é necessário redesenhar o conteúdo.


O probe coletor appsink é necessário para solicitar ao alocador v4l2src para adicionar meta informações sobre o formato de vídeo a cada buffer. Isso é necessário para levar em consideração as situações em que o driver emite um buffer de vídeo com um strike / offset diferente do padrão.


A atualização do quadro de vídeo do VideoOutput ocorre no 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); } 

Nesta função, pegamos o último buffer disponível a partir do appsink, pedimos ao GstVideoMeta para descobrir informações sobre desvios e avanços para cada lista de reprodução (bem, por uma questão de simplicidade, não há fallback no caso de não haver meta por algum motivo) e crie um QAbstractVideoBuffer com o tipo de cabeçalho desejado: EGLImage (GstDmaVideoBuffer) ou Nenhum (GstVideoBuffer). Em seguida, envolva-o em um QVideoFrame e coloque-o na fila de renderização.


A implementação do GstDmaVideoBuffer e do próprio GstVideoBuffer é 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]; }; 

Depois de tudo isso, podemos criar uma página QML do seguinte formulário:


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

Conclusões


O objetivo deste artigo foi mostrar como integrar uma API existente capaz de produzir vídeo acelerado por hardware com QML e usar componentes existentes para renderizar sem copiar (bem, ou, na pior das hipóteses, com um, mas sem conversão dispendiosa de software para RGB).


Código Link


Referências


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


All Articles