Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qohosvideooutput.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
8
9#include <private/qhwvideobuffer_p.h>
10#include <private/qplatformvideosink_p.h>
11#include <private/qvideoframe_p.h>
12#include <private/qvideoframeconverter_p.h>
13
14#include <rhi/qrhi.h>
15#include <rhi/qrhi_platform.h>
16
17#include <QtMultimedia/qvideoframe.h>
18#include <QtMultimedia/qvideoframeformat.h>
19#include <QtMultimedia/qvideosink.h>
20
21#include <QtGui/qmatrix4x4.h>
22#include <QtGui/qopenglcontext.h>
23#include <QtGui/qoffscreensurface.h>
24
25#include <QtCore/qfile.h>
26#include <QtCore/qloggingcategory.h>
27#include <QtCore/qpointer.h>
28#include <QtCore/qthread.h>
29
31
32namespace {
33
34const float g_quad[] = {
35 -1.f, -1.f, 0.f, 0.f,
36 -1.f, 1.f, 0.f, 1.f,
37 1.f, 1.f, 1.f, 1.f,
38 1.f, -1.f, 1.f, 0.f
39};
40
42{
43public:
44 QOhosVideoFrameTextures(QRhi *rhi, QSize size, quint64 handle)
45 {
46 m_tex.reset(rhi->newTexture(QRhiTexture::RGBA8, size, 1));
47 m_tex->createFrom({ handle, 0 });
48 }
49 QRhiTexture *texture(uint plane) const override { return plane == 0 ? m_tex.get() : nullptr; }
50
51private:
52 std::unique_ptr<QRhiTexture> m_tex;
53};
54
56{
57public:
58 QOhosTextureVideoBuffer(std::unique_ptr<QRhiTexture> tex, const QSize &size,
59 std::weak_ptr<QRhi> producerRhi, QPointer<QObject> producer,
60 QPointer<QOpenGLContext> producerContext)
62 , m_size(size)
63 , m_tex(std::move(tex))
67 {
68 }
69
70 MapData map(QVideoFrame::MapMode mode) override
71 {
72 MapData data;
73 if (m_mapMode != QVideoFrame::NotMapped || mode != QVideoFrame::ReadOnly)
74 return data;
75 m_mapMode = QVideoFrame::ReadOnly;
76 if (m_image.isNull())
77 m_image = readbackOnProducerThread();
78 if (m_image.isNull()) {
79 m_mapMode = QVideoFrame::NotMapped;
80 return data;
81 }
82 data.planeCount = 1;
83 data.bytesPerLine[0] = m_image.bytesPerLine();
84 data.dataSize[0] = static_cast<int>(m_image.sizeInBytes());
85 data.data[0] = m_image.bits();
86 return data;
87 }
88
90 {
91 m_image = {};
92 m_mapMode = QVideoFrame::NotMapped;
93 }
94
95 QVideoFrameTexturesUPtr mapTextures(QRhi &rhi, QVideoFrameTexturesUPtr & /*old*/) override
96 {
97 // The texture lives in the producer (texture-thread) GL context. Sampling
98 // requires the caller's RHI to share that GL context's resources. The main
99 // window RHI does (we created the producer with shareContext = main rhi
100 // context). A worker thread's QThreadLocal RHI does not — so report no
101 // textures and let qImageFromVideoFrame fall back to CPU mapping.
102 if (!isCompatibleRhi(rhi))
103 return {};
104 return std::make_unique<QOhosVideoFrameTextures>(&rhi, m_size,
105 m_tex->nativeTexture().object);
106 }
107
108private:
109 bool isCompatibleRhi(QRhi &rhi) const
110 {
111 if (rhi.backend() != QRhi::OpenGLES2)
112 return false;
113 if (!m_producerContext)
114 return false;
115 const auto *handles =
116 static_cast<const QRhiGles2NativeHandles *>(rhi.nativeHandles());
117 if (!handles || !handles->context)
118 return false;
119 return handles->context->shareGroup() == m_producerContext->shareGroup();
120 }
121
122 QImage readbackOnProducerThread() const
123 {
124 auto producerRhi = m_producerRhi.lock();
125 if (!producerRhi || !m_producer)
126 return {};
127 QImage out;
128 QRhi *rhi = producerRhi.get();
129 QRhiTexture *tex = m_tex.get();
130 QMetaObject::invokeMethod(
131 m_producer.data(),
132 [rhi, tex, &out]() {
133 QRhiReadbackResult result;
134 bool done = false;
135 result.completed = [&done] { done = true; };
136 QRhiCommandBuffer *cb = nullptr;
137 if (rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess)
138 return;
139 QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
140 rub->readBackTexture({ tex }, &result);
141 cb->resourceUpdate(rub);
142 rhi->endOffscreenFrame();
143 if (!done || result.data.isEmpty())
144 return;
145 QImage img(reinterpret_cast<const uchar *>(result.data.constData()),
146 result.pixelSize.width(), result.pixelSize.height(),
147 result.pixelSize.width() * 4, QImage::Format_RGBA8888);
148 out = img.copy();
149 },
150 Qt::BlockingQueuedConnection);
151 return out;
152 }
153
154 QSize m_size;
155 std::unique_ptr<QRhiTexture> m_tex;
156 QImage m_image;
157 QVideoFrame::MapMode m_mapMode = QVideoFrame::NotMapped;
158 std::weak_ptr<QRhi> m_producerRhi;
159 QPointer<QObject> m_producer;
160 QPointer<QOpenGLContext> m_producerContext;
161};
162
164{
165public:
166 TextureCopy(QRhi *rhi, QRhiTexture *externalTex) : m_rhi(rhi)
167 {
168 m_vertexBuffer.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer,
169 sizeof(g_quad)));
170 m_vertexBuffer->create();
171
172 m_uniformBuffer.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer,
173 64 + 64 + 4 + 4));
174 m_uniformBuffer->create();
175
176 m_sampler.reset(m_rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest,
177 QRhiSampler::None, QRhiSampler::ClampToEdge,
178 QRhiSampler::ClampToEdge));
179 m_sampler->create();
180
181 m_srb.reset(m_rhi->newShaderResourceBindings());
182 m_srb->setBindings({
183 QRhiShaderResourceBinding::uniformBuffer(
184 0,
185 QRhiShaderResourceBinding::VertexStage
186 | QRhiShaderResourceBinding::FragmentStage,
187 m_uniformBuffer.get()),
188 QRhiShaderResourceBinding::sampledTexture(
189 1, QRhiShaderResourceBinding::FragmentStage, externalTex, m_sampler.get())
190 });
191 m_srb->create();
192
193 m_vertexShader = loadShader(
194 QStringLiteral(":/qt-project.org/multimedia/shaders/externalsampler.vert.qsb"));
195 m_fragmentShader = loadShader(
196 QStringLiteral(":/qt-project.org/multimedia/shaders/externalsampler.frag.qsb"));
197 }
198
199 std::unique_ptr<QRhiTexture> copyExternalTexture(QSize size, const QMatrix4x4 &externalTexMatrix);
200
201private:
202 static QShader loadShader(const QString &name)
203 {
204 QFile f(name);
205 if (f.open(QIODevice::ReadOnly))
206 return QShader::fromSerialized(f.readAll());
207 return {};
208 }
209
210 QRhi *m_rhi{ nullptr };
211 std::unique_ptr<QRhiBuffer> m_vertexBuffer;
212 std::unique_ptr<QRhiBuffer> m_uniformBuffer;
213 std::unique_ptr<QRhiSampler> m_sampler;
214 std::unique_ptr<QRhiShaderResourceBindings> m_srb;
215 QShader m_vertexShader;
216 QShader m_fragmentShader;
217};
218
220 QRhiShaderResourceBindings *srb,
221 QRhiRenderPassDescriptor *rpd,
222 QShader vs, QShader fs)
223{
224 std::unique_ptr<QRhiGraphicsPipeline> gp(rhi->newGraphicsPipeline());
225 gp->setTopology(QRhiGraphicsPipeline::TriangleFan);
226 gp->setShaderStages({
227 { QRhiShaderStage::Vertex, vs },
228 { QRhiShaderStage::Fragment, fs }
229 });
231 layout.setBindings({ { 4 * sizeof(float) } });
232 layout.setAttributes({
233 { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
234 { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
235 });
236 gp->setVertexInputLayout(layout);
237 gp->setShaderResourceBindings(srb);
238 gp->setRenderPassDescriptor(rpd);
239 gp->create();
240 return gp;
241}
242
244TextureCopy::copyExternalTexture(QSize size, const QMatrix4x4 &externalTexMatrix)
245{
246 std::unique_ptr<QRhiTexture> tex(
247 m_rhi->newTexture(QRhiTexture::RGBA8, size, 1, QRhiTexture::RenderTarget));
248 if (!tex->create()) {
249 qCWarning(qLcOhosMediaPlugin) << "Failed to create frame texture";
250 return {};
251 }
252
253 std::unique_ptr<QRhiTextureRenderTarget> renderTarget(
254 m_rhi->newTextureRenderTarget({ { tex.get() } }));
255 std::unique_ptr<QRhiRenderPassDescriptor> rpd(
256 renderTarget->newCompatibleRenderPassDescriptor());
257 renderTarget->setRenderPassDescriptor(rpd.get());
258 renderTarget->create();
259
260 QRhiResourceUpdateBatch *rub = m_rhi->nextResourceUpdateBatch();
261 rub->uploadStaticBuffer(m_vertexBuffer.get(), g_quad);
262
263 const QMatrix4x4 identity;
264 char *p = m_uniformBuffer->beginFullDynamicBufferUpdateForCurrentFrame();
265 memcpy(p, identity.constData(), 64);
266 memcpy(p + 64, externalTexMatrix.constData(), 64);
267 const float opacity = 1.0f;
268 memcpy(p + 64 + 64, &opacity, 4);
269 m_uniformBuffer->endFullDynamicBufferUpdateForCurrentFrame();
270
271 auto pipeline = newGraphicsPipeline(m_rhi, m_srb.get(), rpd.get(), m_vertexShader,
272 m_fragmentShader);
273
274 const QRhiCommandBuffer::VertexInput vbufBinding(m_vertexBuffer.get(), 0);
275 QRhiCommandBuffer *cb = nullptr;
276 if (m_rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess)
277 return {};
278
279 cb->beginPass(renderTarget.get(), Qt::transparent, { 1.0f, 0 }, rub);
280 cb->setGraphicsPipeline(pipeline.get());
281 cb->setViewport({ 0, 0, float(size.width()), float(size.height()) });
282 cb->setShaderResources(m_srb.get());
283 cb->setVertexInput(0, 1, &vbufBinding);
284 cb->draw(4);
285 cb->endPass();
286 m_rhi->endOffscreenFrame();
287
288 return tex;
289}
290
291} // namespace
292
294{
296public:
298
305
306 void launch()
307 {
308 QThread::start();
309 moveToThread(this);
310 }
311
319
321 {
324 this,
325 [&]() {
327 if (m_surfaceImage)
329 },
331 return id;
332 }
333
339
340public slots:
341 void tearDown()
342 {
346 m_rhi.reset();
347 }
348
350 {
352 return;
354 return;
355 if (!m_textureCopy)
356 return;
357 // GL bottom-left to top-left origin flip.
358 static const QMatrix4x4 flipV(1.0f, 0.0f, 0.0f, 0.0f,
359 0.0f, -1.0f, 0.0f, 1.0f,
360 0.0f, 0.0f, 1.0f, 0.0f,
361 0.0f, 0.0f, 0.0f, 1.0f);
363 matrix *= flipV;
365 if (!rgba)
366 return;
368 if (const auto *handles =
369 static_cast<const QRhiGles2NativeHandles *>(m_rhi->nativeHandles()))
373 QPointer<QObject>(this), ctx);
377 }
378
379signals:
381
382private:
383 OHNativeWindow *ensureSurface(QRhi *rhi)
384 {
385 // Re-use the existing surface if the parent RHI matches what we built
386 // the texture thread RHI around. For headless camera (rhi == nullptr)
387 // we accept any prior standalone surface.
388 const bool reuse = m_surfaceImage && m_surfaceImage->isValid()
389 && (rhi ? m_rhi.get() == rhi : m_isHeadless);
390 if (reuse)
391 return m_surfaceImage->nativeWindow();
392
393 // Share with the main RHI's GL context so consumers can sample the
394 // texture. For headless mode we create a standalone offscreen GLES2
395 // RHI without a share context — frames are produced but consumers
396 // (cameras, recorders) only need the native surface handle.
397 QRhiGles2InitParams params;
398 const auto *nativeHandles =
399 rhi ? static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles())
400 : nullptr;
401 params.shareContext = nativeHandles ? nativeHandles->context : nullptr;
402 params.fallbackSurface = QRhiGles2InitParams::newFallbackSurface();
403 m_isHeadless = (rhi == nullptr);
404 m_rhi.reset(QRhi::create(QRhi::OpenGLES2, &params));
405 if (!m_rhi) {
406 qCWarning(qLcOhosMediaPlugin) << "Failed to create offscreen GLES2 RHI";
407 return nullptr;
408 }
409
410 m_externalTexture.reset(
411 m_rhi->newTexture(QRhiTexture::RGBA8, m_size.isEmpty() ? QSize{ 1, 1 } : m_size, 1,
412 QRhiTexture::ExternalOES));
413 if (!m_externalTexture->create()) {
414 qCWarning(qLcOhosMediaPlugin) << "External OES texture create failed";
415 m_externalTexture.reset();
416 m_rhi.reset();
417 return nullptr;
418 }
419
420 const auto nativeTex = m_externalTexture->nativeTexture();
421 m_surfaceImage = std::make_unique<QOhosSurfaceImage>(uint32_t(nativeTex.object));
422 if (!m_surfaceImage->isValid()) {
423 m_surfaceImage.reset();
424 m_externalTexture.reset();
425 m_rhi.reset();
426 return nullptr;
427 }
428
429 const quint64 index = m_surfaceImage->index();
430 connect(m_surfaceImage.get(), &QOhosSurfaceImage::frameAvailable, this,
431 [this, index]() { onFrameAvailable(index); }, Qt::QueuedConnection);
432
433 m_textureCopy = std::make_unique<TextureCopy>(m_rhi.get(), m_externalTexture.get());
434 return m_surfaceImage->nativeWindow();
435 }
436
437 std::shared_ptr<QRhi> m_rhi;
438 std::unique_ptr<QRhiTexture> m_externalTexture;
439 std::unique_ptr<QOhosSurfaceImage> m_surfaceImage;
440 std::unique_ptr<TextureCopy> m_textureCopy;
441 QSize m_size{ 1, 1 };
442 bool m_isHeadless{ false };
443};
444
445QOhosVideoOutput::QOhosVideoOutput(QVideoSink *sink, QObject *parent)
446 : QObject(parent), m_sink(sink)
447{
448 m_textureThread = std::make_shared<QOhosTextureThread>();
449 connect(m_textureThread.get(), &QOhosTextureThread::newFrame, this,
450 &QOhosVideoOutput::onNewFrame, Qt::QueuedConnection);
451 m_textureThread->launch();
452
453 if (auto *p = sink ? sink->platformVideoSink() : nullptr) {
454 connect(p, &QPlatformVideoSink::rhiChanged, this, &QOhosVideoOutput::onRhiChanged);
455 }
456}
457
458void QOhosVideoOutput::onRhiChanged()
459{
460 if (!m_sink || !m_sink->rhi())
461 return;
462 if (m_surfaceCreatedWithoutRhi) {
463 QMetaObject::invokeMethod(m_textureThread.get(), &QOhosTextureThread::tearDown,
464 Qt::BlockingQueuedConnection);
465 m_surfaceCreatedWithoutRhi = false;
466 }
467 emit surfaceReady();
468}
469
471{
472 QMetaObject::invokeMethod(m_textureThread.get(), &QOhosTextureThread::tearDown,
473 Qt::BlockingQueuedConnection);
474}
475
477{
478 auto *rhi = m_sink ? m_sink->rhi() : nullptr;
479 if (!rhi) {
480 m_surfaceCreatedWithoutRhi = true;
481 } else if (m_surfaceCreatedWithoutRhi) {
482 QMetaObject::invokeMethod(m_textureThread.get(), &QOhosTextureThread::tearDown,
483 Qt::BlockingQueuedConnection);
484 m_surfaceCreatedWithoutRhi = false;
485 }
486 return m_textureThread->nativeWindowBlocking(rhi);
487}
488
490{
491 // The camera framework needs a surface even when there's no sink or the
492 // sink isn't bound to a window yet. Spin up an offscreen RHI internally so
493 // capture can proceed headless; surfaceReady will reattach once the sink
494 // gets a real RHI.
495 auto *rhi = m_sink ? m_sink->rhi() : nullptr;
496 if (!rhi)
497 m_surfaceCreatedWithoutRhi = true;
498 else if (m_surfaceCreatedWithoutRhi) {
499 QMetaObject::invokeMethod(m_textureThread.get(), &QOhosTextureThread::tearDown,
500 Qt::BlockingQueuedConnection);
501 m_surfaceCreatedWithoutRhi = false;
502 }
503 return m_textureThread->surfaceIdBlocking(rhi);
504}
505
506void QOhosVideoOutput::setVideoSize(const QSize &size)
507{
508 if (m_videoSize == size || !size.isValid())
509 return;
510 m_videoSize = size;
511 m_textureThread->setFrameSizeBlocking(size);
512 if (m_sink) {
513 if (auto *p = m_sink->platformVideoSink())
514 p->setNativeSize(size);
515 }
516}
517
518void QOhosVideoOutput::onNewFrame(const QVideoFrame &frame)
519{
520 if (m_sink)
521 m_sink->setVideoFrame(frame);
522}
523
524QT_END_NAMESPACE
525
526#include "qohosvideooutput.moc"
527#include "moc_qohosvideooutput_p.cpp"
void setVideoSize(const QSize &size)
OHNativeWindow * nativeWindow()
\inmodule QtGuiPrivate \inheaderfile rhi/qrhi.h
Definition qrhi.h:323
QOhosTextureVideoBuffer(std::unique_ptr< QRhiTexture > tex, const QSize &size, std::weak_ptr< QRhi > producerRhi, QPointer< QObject > producer, QPointer< QOpenGLContext > producerContext)
void unmap() override
Releases the memory mapped by the map() function.
QVideoFrameTexturesUPtr mapTextures(QRhi &rhi, QVideoFrameTexturesUPtr &) override
MapData map(QVideoFrame::MapMode mode) override
Maps the planes of a video buffer to memory.
QRhiTexture * texture(uint plane) const override
QOhosVideoFrameTextures(QRhi *rhi, QSize size, quint64 handle)
std::unique_ptr< QRhiTexture > copyExternalTexture(QSize size, const QMatrix4x4 &externalTexMatrix)
TextureCopy(QRhi *rhi, QRhiTexture *externalTex)
Combined button and popup list for selecting options.
std::unique_ptr< QRhiGraphicsPipeline > newGraphicsPipeline(QRhi *rhi, QRhiShaderResourceBindings *srb, QRhiRenderPassDescriptor *rpd, QShader vs, QShader fs)