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
qvideoframeconverter.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 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// Qt-Security score:critical reason:data-parser
4
11#include "qcachedvalue_p.h"
12
13#include <QtCore/qcoreapplication.h>
14#include <QtCore/qsize.h>
15#include <QtCore/qhash.h>
16#include <QtCore/qfile.h>
17#include <QtGui/qimage.h>
18#include <QtCore/qloggingcategory.h>
19
20#include <QtMultimedia/private/qaudio_qspan_support_p.h>
21#include <QtMultimedia/private/qmultimedia_ranges_p.h>
22#include <QtMultimedia/private/qvideotexturehelper_p.h>
23
24#include <rhi/qrhi.h>
25
26#ifdef Q_OS_DARWIN
27#include <QtCore/private/qcore_mac_p.h>
28#endif
29
31
32namespace ranges = QtMultimediaPrivate::ranges;
33
34Q_STATIC_LOGGING_CATEGORY(qLcVideoFrameConverter, "qt.multimedia.video.frameconverter")
35
36// clang-format off
37static constexpr float g_quad[] = {
38 // Rotation 0 CW
39 1.f, -1.f, 1.f, 1.f,
40 1.f, 1.f, 1.f, 0.f,
41 -1.f, -1.f, 0.f, 1.f,
42 -1.f, 1.f, 0.f, 0.f,
43 // Rotation 90 CW
44 1.f, -1.f, 1.f, 0.f,
45 1.f, 1.f, 0.f, 0.f,
46 -1.f, -1.f, 1.f, 1.f,
47 -1.f, 1.f, 0.f, 1.f,
48 // Rotation 180 CW
49 1.f, -1.f, 0.f, 0.f,
50 1.f, 1.f, 0.f, 1.f,
51 -1.f, -1.f, 1.f, 0.f,
52 -1.f, 1.f, 1.f, 1.f,
53 // Rotation 270 CW
54 1.f, -1.f, 0.f, 1.f,
55 1.f, 1.f, 1.f, 1.f,
56 -1.f, -1.f, 0.f, 0.f,
57 -1.f, 1.f, 1.f, 0.f,
58};
59// clang-format on
60
61static bool pixelFormatHasAlpha(QVideoFrameFormat::PixelFormat format)
62{
63 switch (format) {
64 case QVideoFrameFormat::Format_ARGB8888:
65 case QVideoFrameFormat::Format_ARGB8888_Premultiplied:
66 case QVideoFrameFormat::Format_BGRA8888:
67 case QVideoFrameFormat::Format_BGRA8888_Premultiplied:
68 case QVideoFrameFormat::Format_ABGR8888:
69 case QVideoFrameFormat::Format_RGBA8888:
70 case QVideoFrameFormat::Format_AYUV:
71 case QVideoFrameFormat::Format_AYUV_Premultiplied:
72 return true;
73 default:
74 return false;
75 }
76};
77
78static QShader ensureShader(const QString &name)
79{
80 static QCachedValueMap<QString, QShader> shaderCache;
81
82 return shaderCache.ensure(name, [&name]() {
83 QFile f(name);
84 return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
85 });
86}
87
88static void rasterTransform(QImage &image, VideoTransformation transformation)
89{
90 QTransform t;
91 // QTransform composes so that the first call acts last on the point (like
92 // QPainter's nested coordinate systems), so scale() must precede rotate()
93 // here for the mirror to be applied after the rotation, as required by
94 // mirroredHorizontallyAfterRotation.
95 if (transformation.mirroredHorizontallyAfterRotation)
96 t.scale(-1., 1);
97 if (transformation.rotation != QtVideo::Rotation::None)
98 t.rotate(qreal(transformation.rotation));
99 if (!t.isIdentity())
100 image = image.transformed(t);
101}
102
103static void imageCleanupHandler(void *info)
104{
105 QByteArray *imageData = reinterpret_cast<QByteArray *>(info);
106 delete imageData;
107}
108
109static bool updateTextures(QRhi *rhi,
110 std::unique_ptr<QRhiBuffer> &uniformBuffer,
111 std::unique_ptr<QRhiSampler> &textureSampler,
112 std::unique_ptr<QRhiShaderResourceBindings> &shaderResourceBindings,
113 std::unique_ptr<QRhiGraphicsPipeline> &graphicsPipeline,
114 std::unique_ptr<QRhiRenderPassDescriptor> &renderPass,
115 QVideoFrame &frame,
116 const QVideoFrameTexturesUPtr &videoFrameTextures)
117{
118 auto format = frame.surfaceFormat();
119 auto pixelFormat = format.pixelFormat();
120
121 auto textureDesc = QVideoTextureHelper::textureDescription(pixelFormat);
122
123 QRhiShaderResourceBinding bindings[4];
124 auto *b = bindings;
125 *b++ = QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
126 uniformBuffer.get());
127 for (int i = 0; i < textureDesc->nplanes; ++i)
128 *b++ = QRhiShaderResourceBinding::sampledTexture(i + 1, QRhiShaderResourceBinding::FragmentStage,
129 videoFrameTextures->texture(i), textureSampler.get());
130 shaderResourceBindings->setBindings(bindings, b);
131 if (!shaderResourceBindings->create()) {
132 qCDebug(qLcVideoFrameConverter)
133 << Q_FUNC_INFO << ": failed to create shader resource bindings";
134 return false;
135 }
136
137 graphicsPipeline.reset(rhi->newGraphicsPipeline());
138 graphicsPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
139
140 QShader vs = ensureShader(QVideoTextureHelper::vertexShaderFileName(format));
141 if (!vs.isValid())
142 return false;
143
144 QShader fs = ensureShader(QVideoTextureHelper::fragmentShaderFileName(format, rhi));
145 if (!fs.isValid())
146 return false;
147
148 graphicsPipeline->setShaderStages({
149 { QRhiShaderStage::Vertex, vs },
150 { QRhiShaderStage::Fragment, fs }
151 });
152
153 QRhiVertexInputLayout inputLayout;
154 inputLayout.setBindings({
155 { 4 * sizeof(float) }
156 });
157 inputLayout.setAttributes({
158 { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
159 { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
160 });
161
162 graphicsPipeline->setVertexInputLayout(inputLayout);
163 graphicsPipeline->setShaderResourceBindings(shaderResourceBindings.get());
164 graphicsPipeline->setRenderPassDescriptor(renderPass.get());
165 if (!graphicsPipeline->create()) {
166 qCDebug(qLcVideoFrameConverter) << Q_FUNC_INFO << ": failed to create graphics pipeline";
167 return false;
168 }
169
170 return true;
171}
172
173QSpan<const uchar> jpegDataFromMappedVideoFrame(const QVideoFrame &mappedFrame)
174{
175 Q_ASSERT(mappedFrame.isReadable());
176 Q_ASSERT(mappedFrame.pixelFormat() == QVideoFrameFormat::PixelFormat::Format_Jpeg);
177
178 QSpan<const uchar> jpegData{
179 mappedFrame.bits(0),
180 mappedFrame.mappedBytes(0),
181 };
182
183 using namespace QtMultimediaPrivate;
184
185 constexpr std::array<uchar, 2> soiMarker{ uchar(0xff), uchar(0xd8) };
186 if (!ranges::equal(take(jpegData, 2), soiMarker, std::equal_to<void>{})) {
187 qCDebug(qLcVideoFrameConverter)
188 << Q_FUNC_INFO << ": JPEG data does not start with SOI marker";
189 return {};
190 }
191
192 constexpr std::array<uchar, 2> eoiMarker{ uchar(0xff), uchar(0xd9) };
193
194 // some JPEG cameras contain extra data after the JPEG marker. If so, we drop it to make
195 // libjpeg happy.
196 if (!ranges::equal(jpegData.last(2), eoiMarker, std::equal_to<void>{})) {
197 qCDebug(qLcVideoFrameConverter)
198 << Q_FUNC_INFO << ": JPEG data does not end with EOI marker";
199
200 auto eoi_it = std::find_end(jpegData.begin(), jpegData.end(), std::begin(eoiMarker),
201 std::end(eoiMarker));
202 if (eoi_it == jpegData.end()) {
203 qCWarning(qLcVideoFrameConverter)
204 << Q_FUNC_INFO << ": JPEG data does not contain EOI marker";
205 return {};
206 };
207
208 const size_t newSize = std::distance(jpegData.begin(), eoi_it) + std::size(eoiMarker);
209 jpegData = jpegData.first(newSize);
210 }
211
212 return jpegData;
213}
214
215static QImage convertJPEG(const QVideoFrame &frame, const VideoTransformation &transform)
216{
217 QVideoFrame varFrame = frame;
218 if (!varFrame.map(QVideoFrame::ReadOnly)) {
219 qCDebug(qLcVideoFrameConverter) << Q_FUNC_INFO << ": frame mapping failed";
220 return {};
221 }
222
223 auto unmap = std::optional(QScopeGuard([&] {
224 varFrame.unmap();
225 }));
226
227 const QSpan<const uchar> jpegData = jpegDataFromMappedVideoFrame(varFrame);
228 if (jpegData.isEmpty())
229 return QImage{};
230
231 QImage image = QImage::fromData(jpegData, "JPG");
232 unmap = std::nullopt; // Release unmap guard
233 rasterTransform(image, transform);
234 return image;
235}
236
237static QImage convertCPU(const QVideoFrame &frame, const VideoTransformation &transform)
238{
239 VideoFrameConvertFunc convert = qConverterForFormat(frame.pixelFormat());
240 if (!convert) {
241 qCDebug(qLcVideoFrameConverter) << Q_FUNC_INFO << ": unsupported pixel format" << frame.pixelFormat();
242 return {};
243 } else {
244 QVideoFrame varFrame = frame;
245 if (!varFrame.map(QVideoFrame::ReadOnly)) {
246 qCDebug(qLcVideoFrameConverter) << Q_FUNC_INFO << ": frame mapping failed";
247 return {};
248 }
249 auto format = pixelFormatHasAlpha(varFrame.pixelFormat()) ? QImage::Format_ARGB32_Premultiplied : QImage::Format_RGB32;
250 QImage image = QImage(varFrame.width(), varFrame.height(), format);
251 convert(varFrame, image.bits());
252 varFrame.unmap();
253 rasterTransform(image, transform);
254 return image;
255 }
256}
257
258QImage qImageFromVideoFrame(const QVideoFrame &frame, bool forceCpu)
259{
260 // by default, surface transformation is applied, as full transformation is used for presentation only
261 return qImageFromVideoFrame(frame, qNormalizedSurfaceTransformation(frame.surfaceFormat()),
262 forceCpu);
263}
264
265QImage qImageFromVideoFrame(const QVideoFrame &frame, const VideoTransformation &transformation,
266 bool forceCpu)
267{
268#ifdef Q_OS_DARWIN
269 QMacAutoReleasePool releasePool;
270#endif
271
272 std::unique_ptr<QRhiRenderPassDescriptor> renderPass;
273 std::unique_ptr<QRhiBuffer> vertexBuffer;
274 std::unique_ptr<QRhiBuffer> uniformBuffer;
275 std::unique_ptr<QRhiTexture> targetTexture;
276 std::unique_ptr<QRhiTextureRenderTarget> renderTarget;
277 std::unique_ptr<QRhiSampler> textureSampler;
278 std::unique_ptr<QRhiShaderResourceBindings> shaderResourceBindings;
279 std::unique_ptr<QRhiGraphicsPipeline> graphicsPipeline;
280
281 if (frame.size().isEmpty() || frame.pixelFormat() == QVideoFrameFormat::Format_Invalid)
282 return {};
283
284 if (frame.pixelFormat() == QVideoFrameFormat::Format_Jpeg)
285 return convertJPEG(frame, transformation);
286
287 if (forceCpu) // For test purposes
288 return convertCPU(frame, transformation);
289
290 QRhi *rhi = nullptr;
291
292 if (QHwVideoBuffer *buffer = QVideoFramePrivate::hwBuffer(frame))
293 rhi = buffer->associatedCurrentThreadRhi();
294
295 if (!rhi) {
296 // TODO: if a case with more the one available preferred backends appears,
297 // e.g. vulkan vs opengl, then we should:
298 // 1. Implement QHwVideoBuffer::preferredRhiBackend
299 // 2. Implement a map backend=>rhi inside qEnsureThreadLocalRhi
300 rhi = qEnsureThreadLocalRhi(/*buffer->preferredRhiBackend()*/);
301 }
302
303 if (!rhi || rhi->isRecordingFrame())
304 return convertCPU(frame, transformation);
305
306 Q_ASSERT(rhi->thread()->isCurrentThread());
307
308 // Do conversion using shaders
309
310 const QSize frameSize = qRotatedFrameSize(frame.size(), frame.surfaceFormat().rotation());
311
312 vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(g_quad)));
313 if (!vertexBuffer->create()) {
314 qCDebug(qLcVideoFrameConverter) << "Failed to create vertex buffer. Using CPU conversion.";
315 return convertCPU(frame, transformation);
316 }
317
318 uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(QVideoTextureHelper::UniformData)));
319 if (!uniformBuffer->create()) {
320 qCDebug(qLcVideoFrameConverter) << "Failed to create uniform buffer. Using CPU conversion.";
321 return convertCPU(frame, transformation);
322 }
323
324 textureSampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
325 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
326 if (!textureSampler->create()) {
327 qCDebug(qLcVideoFrameConverter)
328 << "Failed to create texture sampler. Using CPU conversion.";
329 return convertCPU(frame, transformation);
330 }
331
332 shaderResourceBindings.reset(rhi->newShaderResourceBindings());
333
334 targetTexture.reset(rhi->newTexture(QRhiTexture::RGBA8, frameSize, 1, QRhiTexture::RenderTarget));
335 if (!targetTexture->create()) {
336 qCDebug(qLcVideoFrameConverter) << "Failed to create target texture. Using CPU conversion.";
337 return convertCPU(frame, transformation);
338 }
339
340 renderTarget.reset(rhi->newTextureRenderTarget({ { targetTexture.get() } }));
341 renderPass.reset(renderTarget->newCompatibleRenderPassDescriptor());
342 renderTarget->setRenderPassDescriptor(renderPass.get());
343 if (!renderTarget->create()) {
344 qCDebug(qLcVideoFrameConverter) << "Failed to create render target. Using CPU conversion.";
345 return convertCPU(frame, transformation);
346 }
347
348 QRhiCommandBuffer *cb = nullptr;
349 QRhi::FrameOpResult r = rhi->beginOffscreenFrame(&cb);
350 if (r != QRhi::FrameOpSuccess) {
351 qCDebug(qLcVideoFrameConverter) << "Failed to set up offscreen frame. Using CPU conversion.";
352 return convertCPU(frame, transformation);
353 }
354
355 QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
356 Q_ASSERT(rub);
357
358 rub->uploadStaticBuffer(vertexBuffer.get(), g_quad);
359
360 QVideoFrame frameTmp = frame;
361 QVideoFrameTexturesUPtr texturesTmp;
362 auto videoFrameTextures = QVideoTextureHelper::createTextures(frameTmp, *rhi, *rub, texturesTmp);
363 if (!videoFrameTextures) {
364 qCDebug(qLcVideoFrameConverter) << "Failed obtain textures. Using CPU conversion.";
365 return convertCPU(frame, transformation);
366 }
367
368 if (!updateTextures(rhi, uniformBuffer, textureSampler, shaderResourceBindings,
369 graphicsPipeline, renderPass, frameTmp, videoFrameTextures)) {
370 qCDebug(qLcVideoFrameConverter) << "Failed to update textures. Using CPU conversion.";
371 return convertCPU(frame, transformation);
372 }
373
374 float xScale = transformation.mirroredHorizontallyAfterRotation ? -1.0 : 1.0;
375 // Two independent effects can each flip the rendered quad vertically before it reaches the
376 // QImage we build from the QRhiReadbackResult below:
377 // - readback row order is inverted iff isYUpInFramebuffer() (true only for OpenGL)
378 // - g_quad is authored for Y-up NDC, so it renders vertically mirrored iff the backend's NDC
379 // is not Y-up (isYUpInNDC() false; true only for Vulkan, currently)
380 // A flip is needed iff exactly one of the two applies; they cancel out otherwise.
381 const bool flipForFramebufferReadback = rhi->isYUpInFramebuffer();
382 const bool flipForClipSpaceConvention = !rhi->isYUpInNDC();
383 float yScale = (flipForFramebufferReadback != flipForClipSpaceConvention) ? -1.f : 1.f;
384
385 QMatrix4x4 transform;
386 transform.scale(xScale, yScale);
387
388 QByteArray uniformData(sizeof(QVideoTextureHelper::UniformData), Qt::Uninitialized);
389 QVideoTextureHelper::updateUniformData(&uniformData, rhi, frame.surfaceFormat(), frame,
390 transform, 1.f);
391 rub->updateDynamicBuffer(uniformBuffer.get(), 0, uniformData.size(), uniformData.constData());
392
393 cb->beginPass(renderTarget.get(), Qt::black, { 1.0f, 0 }, rub);
394 cb->setGraphicsPipeline(graphicsPipeline.get());
395
396 cb->setViewport({ 0, 0, float(frameSize.width()), float(frameSize.height()) });
397 cb->setShaderResources(shaderResourceBindings.get());
398
399 const quint32 vertexOffset = quint32(sizeof(float)) * 16 * transformation.rotationIndex();
400 const QRhiCommandBuffer::VertexInput vbufBinding(vertexBuffer.get(), vertexOffset);
401 cb->setVertexInput(0, 1, &vbufBinding);
402 cb->draw(4);
403
404 QRhiReadbackDescription readDesc(targetTexture.get());
405 QRhiReadbackResult readResult;
406 bool readCompleted = false;
407
408 readResult.completed = [&readCompleted] { readCompleted = true; };
409
410 rub = rhi->nextResourceUpdateBatch();
411 rub->readBackTexture(readDesc, &readResult);
412
413 cb->endPass(rub);
414
415 rhi->endOffscreenFrame();
416
417 if (!readCompleted) {
418 qCDebug(qLcVideoFrameConverter) << "Failed to read back texture. Using CPU conversion.";
419 return convertCPU(frame, transformation);
420 }
421
422 QByteArray *imageData = new QByteArray(readResult.data);
423
424 return QImage(reinterpret_cast<const uchar *>(imageData->constData()),
425 readResult.pixelSize.width(), readResult.pixelSize.height(),
426 QImage::Format_RGBA8888_Premultiplied, imageCleanupHandler, imageData);
427}
428
429QImage videoFramePlaneAsImage(QVideoFrame &frame, int plane, QImage::Format targetFormat,
430 QSize targetSize)
431{
432 if (plane >= frame.planeCount())
433 return {};
434
435 if (!frame.map(QVideoFrame::ReadOnly)) {
436 qWarning() << "Cannot map a video frame in ReadOnly mode!";
437 return {};
438 }
439
440 auto frameHandle = QVideoFramePrivate::handle(frame);
441
442 // With incrementing the reference counter, we share the mapped QVideoFrame
443 // with the target QImage. The function imageCleanupFunction is going to adopt
444 // the frameHandle by QVideoFrame and dereference it upon the destruction.
445 frameHandle->ref.ref();
446
447 auto imageCleanupFunction = [](void *data) {
448 QVideoFrame frame = reinterpret_cast<QVideoFramePrivate *>(data)->adoptThisByVideoFrame();
449 Q_ASSERT(frame.isMapped());
450 frame.unmap();
451 };
452
453 const auto bytesPerLine = frame.bytesPerLine(plane);
454 const auto height =
455 bytesPerLine ? qMin(targetSize.height(), frame.mappedBytes(plane) / bytesPerLine) : 0;
456
457 return QImage(reinterpret_cast<const uchar *>(frame.bits(plane)), targetSize.width(), height,
458 bytesPerLine, targetFormat, imageCleanupFunction, frameHandle);
459}
460
461QT_END_NAMESPACE
\inmodule QtGui
Definition qimage.h:38
\inmodule QtGuiPrivate \inheaderfile rhi/qrhi.h
Definition qrhi.h:809
\inmodule QtGuiPrivate \inheaderfile rhi/qrhi.h
Definition qrhi.h:443
\inmodule QtGuiPrivate \inheaderfile rhi/qrhi.h
Definition qrhi.h:325
\inmodule QtGui
Definition qshader.h:81
static QVideoFramePrivate * handle(QVideoFrame &frame)
Combined button and popup list for selecting options.
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)
QSpan< const uchar > jpegDataFromMappedVideoFrame(const QVideoFrame &mappedFrame)
static bool updateTextures(QRhi *rhi, std::unique_ptr< QRhiBuffer > &uniformBuffer, std::unique_ptr< QRhiSampler > &textureSampler, std::unique_ptr< QRhiShaderResourceBindings > &shaderResourceBindings, std::unique_ptr< QRhiGraphicsPipeline > &graphicsPipeline, std::unique_ptr< QRhiRenderPassDescriptor > &renderPass, QVideoFrame &frame, const QVideoFrameTexturesUPtr &videoFrameTextures)
static QImage convertJPEG(const QVideoFrame &frame, const VideoTransformation &transform)
static QT_BEGIN_NAMESPACE constexpr float g_quad[]
QImage qImageFromVideoFrame(const QVideoFrame &frame, const VideoTransformation &transformation, bool forceCpu)
QImage qImageFromVideoFrame(const QVideoFrame &frame, bool forceCpu)
static QShader ensureShader(const QString &name)
static bool pixelFormatHasAlpha(QVideoFrameFormat::PixelFormat format)
static QImage convertCPU(const QVideoFrame &frame, const VideoTransformation &transform)
static void rasterTransform(QImage &image, VideoTransformation transformation)
QImage videoFramePlaneAsImage(QVideoFrame &frame, int plane, QImage::Format targetFormat, QSize targetSize)
Maps the video frame and returns an image having a shared ownership for the video frame and referenci...
static void imageCleanupHandler(void *info)
\inmodule QtGuiPrivate \inheaderfile rhi/qrhi.h
Definition qrhi.h:1922