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#include <QtMultimedia/qtvideo.h>
21
22#include <QtGui/qmatrix4x4.h>
23#include <QtGui/qopenglcontext.h>
24#include <QtGui/qoffscreensurface.h>
25#include <QtGui/qguiapplication.h>
26#include <QtGui/qscreen.h>
27
28#include <QtCore/qfile.h>
29#include <QtCore/qloggingcategory.h>
30
31#include <atomic>
32#include <QtCore/qpointer.h>
33#include <QtCore/qthread.h>
34
36
37namespace {
38
39const float g_quad[] = {
40 -1.f, -1.f, 0.f, 0.f,
41 -1.f, 1.f, 0.f, 1.f,
42 1.f, 1.f, 1.f, 1.f,
43 1.f, -1.f, 1.f, 0.f
44};
45
47{
48public:
49 QOhosVideoFrameTextures(QRhi *rhi, QSize size, quint64 handle)
50 {
51 m_tex.reset(rhi->newTexture(QRhiTexture::RGBA8, size, 1));
52 m_tex->createFrom({ handle, 0 });
53 }
54 QRhiTexture *texture(uint plane) const override { return plane == 0 ? m_tex.get() : nullptr; }
55
56private:
57 std::unique_ptr<QRhiTexture> m_tex;
58};
59
61{
62public:
63 QOhosTextureVideoBuffer(std::unique_ptr<QRhiTexture> tex, const QSize &size,
64 std::weak_ptr<QRhi> producerRhi, QPointer<QObject> producer,
65 QPointer<QOpenGLContext> producerContext)
67 , m_size(size)
68 , m_tex(std::move(tex))
72 {
73 }
74
75 MapData map(QVideoFrame::MapMode mode) override
76 {
77 MapData data;
78 if (m_mapMode != QVideoFrame::NotMapped || mode != QVideoFrame::ReadOnly)
79 return data;
80 m_mapMode = QVideoFrame::ReadOnly;
81 if (m_image.isNull())
82 m_image = readbackOnProducerThread();
83 if (m_image.isNull()) {
84 m_mapMode = QVideoFrame::NotMapped;
85 return data;
86 }
87 data.planeCount = 1;
88 data.bytesPerLine[0] = m_image.bytesPerLine();
89 data.dataSize[0] = static_cast<int>(m_image.sizeInBytes());
90 data.data[0] = m_image.bits();
91 return data;
92 }
93
95 {
96 m_image = {};
97 m_mapMode = QVideoFrame::NotMapped;
98 }
99
100 QVideoFrameTexturesUPtr mapTextures(QRhi &rhi, QVideoFrameTexturesUPtr & /*old*/) override
101 {
102 // The texture lives in the producer (texture-thread) GL context. Sampling
103 // requires the caller's RHI to share that GL context's resources. The main
104 // window RHI does (we created the producer with shareContext = main rhi
105 // context). A worker thread's QThreadLocal RHI does not — so report no
106 // textures and let qImageFromVideoFrame fall back to CPU mapping.
107 if (!isCompatibleRhi(rhi))
108 return {};
109 return std::make_unique<QOhosVideoFrameTextures>(&rhi, m_size,
110 m_tex->nativeTexture().object);
111 }
112
113private:
114 bool isCompatibleRhi(QRhi &rhi) const
115 {
116 if (rhi.backend() != QRhi::OpenGLES2)
117 return false;
118 if (!m_producerContext)
119 return false;
120 const auto *handles =
121 static_cast<const QRhiGles2NativeHandles *>(rhi.nativeHandles());
122 if (!handles || !handles->context)
123 return false;
124 return handles->context->shareGroup() == m_producerContext->shareGroup();
125 }
126
127 QImage readbackOnProducerThread() const
128 {
129 auto producerRhi = m_producerRhi.lock();
130 if (!producerRhi || !m_producer)
131 return {};
132 QImage out;
133 QRhi *rhi = producerRhi.get();
134 QRhiTexture *tex = m_tex.get();
135 QMetaObject::invokeMethod(
136 m_producer.data(),
137 [rhi, tex, &out]() {
138 QRhiReadbackResult result;
139 bool done = false;
140 result.completed = [&done] { done = true; };
141 QRhiCommandBuffer *cb = nullptr;
142 if (rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess)
143 return;
144 QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
145 rub->readBackTexture({ tex }, &result);
146 cb->resourceUpdate(rub);
147 rhi->endOffscreenFrame();
148 if (!done || result.data.isEmpty())
149 return;
150 QImage img(reinterpret_cast<const uchar *>(result.data.constData()),
151 result.pixelSize.width(), result.pixelSize.height(),
152 result.pixelSize.width() * 4, QImage::Format_RGBA8888);
153 out = img.copy();
154 },
155 Qt::BlockingQueuedConnection);
156 return out;
157 }
158
159 QSize m_size;
160 std::unique_ptr<QRhiTexture> m_tex;
161 QImage m_image;
162 QVideoFrame::MapMode m_mapMode = QVideoFrame::NotMapped;
163 std::weak_ptr<QRhi> m_producerRhi;
164 QPointer<QObject> m_producer;
165 QPointer<QOpenGLContext> m_producerContext;
166};
167
169{
170public:
171 TextureCopy(QRhi *rhi, QRhiTexture *externalTex) : m_rhi(rhi)
172 {
173 m_vertexBuffer.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer,
174 sizeof(g_quad)));
175 m_vertexBuffer->create();
176
177 m_uniformBuffer.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic,
178 QRhiBuffer::UniformBuffer, 160));
179 m_uniformBuffer->create();
180
181 m_sampler.reset(m_rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest,
182 QRhiSampler::None, QRhiSampler::ClampToEdge,
183 QRhiSampler::ClampToEdge));
184 m_sampler->create();
185
186 m_srb.reset(m_rhi->newShaderResourceBindings());
187 m_srb->setBindings({
188 QRhiShaderResourceBinding::uniformBuffer(
189 0,
190 QRhiShaderResourceBinding::VertexStage
191 | QRhiShaderResourceBinding::FragmentStage,
192 m_uniformBuffer.get()),
193 QRhiShaderResourceBinding::sampledTexture(
194 1, QRhiShaderResourceBinding::FragmentStage, externalTex, m_sampler.get())
195 });
196 m_srb->create();
197
198 m_vertexShader = loadShader(
199 QStringLiteral(":/qt-project.org/multimedia/shaders/externalsampler.vert.qsb"));
200 m_fragmentShader = loadShader(
201 QStringLiteral(":/qt-project.org/multimedia/shaders/externalsampler.frag.qsb"));
202 }
203
204 std::unique_ptr<QRhiTexture> copyExternalTexture(QSize size, const QMatrix4x4 &externalTexMatrix);
205
206private:
207 static QShader loadShader(const QString &name)
208 {
209 QFile f(name);
210 if (f.open(QIODevice::ReadOnly))
211 return QShader::fromSerialized(f.readAll());
212 return {};
213 }
214
215 QRhi *m_rhi{ nullptr };
216 std::unique_ptr<QRhiBuffer> m_vertexBuffer;
217 std::unique_ptr<QRhiBuffer> m_uniformBuffer;
218 std::unique_ptr<QRhiSampler> m_sampler;
219 std::unique_ptr<QRhiShaderResourceBindings> m_srb;
220 QShader m_vertexShader;
221 QShader m_fragmentShader;
222};
223
225 QRhiShaderResourceBindings *srb,
226 QRhiRenderPassDescriptor *rpd,
227 QShader vs, QShader fs)
228{
229 std::unique_ptr<QRhiGraphicsPipeline> gp(rhi->newGraphicsPipeline());
230 gp->setTopology(QRhiGraphicsPipeline::TriangleFan);
231 gp->setShaderStages({
232 { QRhiShaderStage::Vertex, vs },
233 { QRhiShaderStage::Fragment, fs }
234 });
236 layout.setBindings({ { 4 * sizeof(float) } });
237 layout.setAttributes({
238 { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
239 { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
240 });
241 gp->setVertexInputLayout(layout);
242 gp->setShaderResourceBindings(srb);
243 gp->setRenderPassDescriptor(rpd);
244 gp->create();
245 return gp;
246}
247
249TextureCopy::copyExternalTexture(QSize size, const QMatrix4x4 &externalTexMatrix)
250{
251 std::unique_ptr<QRhiTexture> tex(
252 m_rhi->newTexture(QRhiTexture::RGBA8, size, 1, QRhiTexture::RenderTarget));
253 if (!tex->create()) {
254 qCWarning(qLcOhosMediaPlugin) << "Failed to create frame texture";
255 return {};
256 }
257
258 std::unique_ptr<QRhiTextureRenderTarget> renderTarget(
259 m_rhi->newTextureRenderTarget({ { tex.get() } }));
260 std::unique_ptr<QRhiRenderPassDescriptor> rpd(
261 renderTarget->newCompatibleRenderPassDescriptor());
262 renderTarget->setRenderPassDescriptor(rpd.get());
263 renderTarget->create();
264
265 QRhiResourceUpdateBatch *rub = m_rhi->nextResourceUpdateBatch();
266 rub->uploadStaticBuffer(m_vertexBuffer.get(), g_quad);
267
268 const QMatrix4x4 identity;
269 char *p = m_uniformBuffer->beginFullDynamicBufferUpdateForCurrentFrame();
270 memcpy(p, identity.constData(), 64);
271 memcpy(p + 64, externalTexMatrix.constData(), 64);
272 const float opacity = 1.0f;
273 memcpy(p + 64 + 64, &opacity, 4);
274 m_uniformBuffer->endFullDynamicBufferUpdateForCurrentFrame();
275
276 auto pipeline = newGraphicsPipeline(m_rhi, m_srb.get(), rpd.get(), m_vertexShader,
277 m_fragmentShader);
278
279 const QRhiCommandBuffer::VertexInput vbufBinding(m_vertexBuffer.get(), 0);
280 QRhiCommandBuffer *cb = nullptr;
281 if (m_rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess)
282 return {};
283
284 cb->beginPass(renderTarget.get(), Qt::transparent, { 1.0f, 0 }, rub);
285 cb->setGraphicsPipeline(pipeline.get());
286 cb->setViewport({ 0, 0, float(size.width()), float(size.height()) });
287 cb->setShaderResources(m_srb.get());
288 cb->setVertexInput(0, 1, &vbufBinding);
289 cb->draw(4);
290 cb->endPass();
291 m_rhi->endOffscreenFrame();
292
293 return tex;
294}
295
296} // namespace
297
299{
301public:
303
310
311 void launch()
312 {
313 QThread::start();
314 moveToThread(this);
315 }
316
324
326 {
329 this,
330 [&]() {
332 if (m_surfaceImage)
334 },
336 return id;
337 }
338
344
345public slots:
346 void tearDown()
347 {
351 m_rhi.reset();
352 m_shareContext = nullptr;
353 }
354
355 // Sampled on the GUI thread; QScreen must not be touched from this thread.
360
362 {
364 return;
366 return;
367 if (!m_textureCopy)
368 return;
369 // GL bottom-left to top-left origin flip.
370 static const QMatrix4x4 flipV(1.0f, 0.0f, 0.0f, 0.0f,
371 0.0f, -1.0f, 0.0f, 1.0f,
372 0.0f, 0.0f, 1.0f, 0.0f,
373 0.0f, 0.0f, 0.0f, 1.0f);
375 // The sensor transform swaps x/y on 90/270; transpose the target to match aspect.
376 const bool axesSwapped = std::abs(matrix(0, 0)) < std::abs(matrix(0, 1));
379 matrix *= flipV;
381 if (!rgba)
382 return;
384 if (const auto *handles =
385 static_cast<const QRhiGles2NativeHandles *>(m_rhi->nativeHandles()))
389 QPointer<QObject>(this), ctx);
394 }
395
396signals:
398
399private:
400 OHNativeWindow *ensureSurface(QRhi *rhi)
401 {
402 const auto *nativeHandles =
403 rhi ? static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles())
404 : nullptr;
405 QOpenGLContext *shareContext = nativeHandles ? nativeHandles->context : nullptr;
406
407 // Re-use the existing surface only while it still shares the caller's
408 // GL context. For headless camera (rhi == nullptr) we accept any prior
409 // standalone surface.
410 const bool reuse = m_surfaceImage && m_surfaceImage->isValid()
411 && (rhi ? !m_isHeadless && m_shareContext == shareContext : m_isHeadless);
412 if (reuse)
413 return m_surfaceImage->nativeWindow();
414
415 // Order matters: the texture and the copy pipeline outlive m_rhi otherwise.
416 tearDown();
417
418 // Share with the main RHI's GL context so consumers can sample the
419 // texture. For headless mode we create a standalone offscreen GLES2
420 // RHI without a share context — frames are produced but consumers
421 // (cameras, recorders) only need the native surface handle.
422 QRhiGles2InitParams params;
423 params.shareContext = shareContext;
424 params.fallbackSurface = QRhiGles2InitParams::newFallbackSurface();
425 m_isHeadless = (rhi == nullptr);
426 m_shareContext = shareContext;
427 m_rhi.reset(QRhi::create(QRhi::OpenGLES2, &params));
428 if (!m_rhi) {
429 qCWarning(qLcOhosMediaPlugin) << "Failed to create offscreen GLES2 RHI";
430 return nullptr;
431 }
432
433 m_externalTexture.reset(
434 m_rhi->newTexture(QRhiTexture::RGBA8, m_size.isEmpty() ? QSize{ 1, 1 } : m_size, 1,
435 QRhiTexture::ExternalOES));
436 if (!m_externalTexture->create()) {
437 qCWarning(qLcOhosMediaPlugin) << "External OES texture create failed";
438 m_externalTexture.reset();
439 m_rhi.reset();
440 return nullptr;
441 }
442
443 const auto nativeTex = m_externalTexture->nativeTexture();
444 m_surfaceImage = std::make_unique<QOhosSurfaceImage>(uint32_t(nativeTex.object));
445 if (!m_surfaceImage->isValid()) {
446 m_surfaceImage.reset();
447 m_externalTexture.reset();
448 m_rhi.reset();
449 return nullptr;
450 }
451
452 const quint64 index = m_surfaceImage->index();
453 connect(m_surfaceImage.get(), &QOhosSurfaceImage::frameAvailable, this,
454 [this, index]() { onFrameAvailable(index); }, Qt::QueuedConnection);
455
456 m_textureCopy = std::make_unique<TextureCopy>(m_rhi.get(), m_externalTexture.get());
457 return m_surfaceImage->nativeWindow();
458 }
459
460 std::shared_ptr<QRhi> m_rhi;
461 std::unique_ptr<QRhiTexture> m_externalTexture;
462 std::unique_ptr<QOhosSurfaceImage> m_surfaceImage;
463 std::unique_ptr<TextureCopy> m_textureCopy;
464 QSize m_size{ 1, 1 };
465 std::atomic<QtVideo::Rotation> m_displayRotation{ QtVideo::Rotation::None };
466 QPointer<QOpenGLContext> m_shareContext;
467 bool m_isHeadless{ false };
468};
469
470QOhosVideoOutput::QOhosVideoOutput(QVideoSink *sink, ContentSource contentSource,
471 QObject *parent)
473{
474 m_textureThread = std::make_shared<QOhosTextureThread>();
475 connect(m_textureThread.get(), &QOhosTextureThread::newFrame, this,
476 &QOhosVideoOutput::onNewFrame, Qt::QueuedConnection);
477 m_textureThread->launch();
478
479 if (auto *p = sink ? sink->platformVideoSink() : nullptr) {
480 connect(p, &QPlatformVideoSink::rhiChanged, this, &QOhosVideoOutput::onRhiChanged);
481 }
482
483 if (m_contentSource == ContentSource::Camera) {
484 if (QScreen *screen = QGuiApplication::primaryScreen()) {
485 connect(screen, &QScreen::orientationChanged, this,
486 &QOhosVideoOutput::updateDisplayRotation);
487 }
488 updateDisplayRotation();
489 }
490}
491
492void QOhosVideoOutput::updateDisplayRotation()
493{
494 QtVideo::Rotation rotation = QtVideo::Rotation::None;
495 if (const QScreen *screen = QGuiApplication::primaryScreen()) {
496 switch (screen->angleBetween(screen->orientation(), screen->nativeOrientation())) {
497 case 90:
498 rotation = QtVideo::Rotation::Clockwise90;
499 break;
500 case 180:
501 rotation = QtVideo::Rotation::Clockwise180;
502 break;
503 case 270:
504 rotation = QtVideo::Rotation::Clockwise270;
505 break;
506 default:
507 break;
508 }
509 }
510 m_textureThread->setDisplayRotation(rotation);
511}
512
513void QOhosVideoOutput::onRhiChanged()
514{
515 if (!m_sink || !m_sink->rhi())
516 return;
517 if (m_surfaceCreatedWithoutRhi) {
518 tearDownTextureThread();
519 m_surfaceCreatedWithoutRhi = false;
520 }
521 emit surfaceReady();
522}
523
525{
526 tearDownTextureThread();
527}
528
529void QOhosVideoOutput::tearDownTextureThread()
530{
531 // Delivered frames own textures from the RHI that is about to go away.
532 if (m_sink)
533 m_sink->setVideoFrame({});
534 QMetaObject::invokeMethod(m_textureThread.get(), &QOhosTextureThread::tearDown,
535 Qt::BlockingQueuedConnection);
536}
537
539{
540 auto *rhi = m_sink ? m_sink->rhi() : nullptr;
541 if (!rhi) {
542 m_surfaceCreatedWithoutRhi = true;
543 } else if (m_surfaceCreatedWithoutRhi) {
544 tearDownTextureThread();
545 m_surfaceCreatedWithoutRhi = false;
546 }
547 return m_textureThread->nativeWindowBlocking(rhi);
548}
549
551{
552 // The camera framework needs a surface even when there's no sink or the
553 // sink isn't bound to a window yet. Spin up an offscreen RHI internally so
554 // capture can proceed headless; surfaceReady will reattach once the sink
555 // gets a real RHI.
556 auto *rhi = m_sink ? m_sink->rhi() : nullptr;
557 if (!rhi)
558 m_surfaceCreatedWithoutRhi = true;
559 else if (m_surfaceCreatedWithoutRhi) {
560 tearDownTextureThread();
561 m_surfaceCreatedWithoutRhi = false;
562 }
563 return m_textureThread->surfaceIdBlocking(rhi);
564}
565
566void QOhosVideoOutput::setVideoSize(const QSize &size)
567{
568 if (m_videoSize == size || !size.isValid())
569 return;
570 m_videoSize = size;
571 m_textureThread->setFrameSizeBlocking(size);
572 if (m_sink) {
573 if (auto *p = m_sink->platformVideoSink())
574 p->setNativeSize(size);
575 }
576}
577
578void QOhosVideoOutput::onNewFrame(const QVideoFrame &frame)
579{
580 if (m_sink)
581 m_sink->setVideoFrame(frame);
582}
583
584QT_END_NAMESPACE
585
586#include "qohosvideooutput.moc"
587#include "moc_qohosvideooutput_p.cpp"
void setVideoSize(const QSize &size)
QOhosVideoOutput(QVideoSink *sink, ContentSource contentSource, QObject *parent=nullptr)
OHNativeWindow * nativeWindow()
\inmodule QtGuiPrivate \inheaderfile rhi/qrhi.h
Definition qrhi.h:325
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)