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
qwasmvideoframegrabber.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 <QtMultimedia/qvideosink.h>
10#include <QtMultimedia/private/qmemoryvideobuffer_p.h>
11#include <QtMultimedia/private/qvideoframe_p.h>
12#include <QtGui/qopenglcontext.h>
13#include <QtGui/rhi/qrhi_platform.h>
14#include <QtCore/qdebug.h>
15#include <QtCore/private/qstdweb_p.h>
16
17#include <emscripten/emscripten.h>
18#include <emscripten/em_js.h>
19#include <emscripten/html5.h>
20#include <emscripten/val.h>
21
22#include <GLES2/gl2.h>
23
25
26using namespace emscripten;
27using namespace Qt::Literals;
28
29// EM_JS pointer arguments are BigInt on wasm64, Number() makes them usable on both targets.
30
31// Upload the current video frame to the already-bound TEXTURE_2D.
32EM_JS(void, em_texImage2DFromVideo, (const char *videoId, int *widthOut, int *heightOut), {
38 if (!video) { return; }
39 var frame;
40 try { frame = new VideoFrame(video); } catch(e) { return; }
44 frame.close();
45});
46
48 : m_videoOutput(videoOutput)
49{
50}
51
52void QWasmVideoFrameGrabber::detectWebGLContext()
53{
54 m_glContextHandle = 0;
55 m_hasWebGLContext = false;
56
57 QRhi *rhi = m_videoOutput->m_wasmSink ? m_videoOutput->m_wasmSink->rhi() : nullptr;
58 if (!rhi || rhi->backend() != QRhi::OpenGLES2)
59 return;
60
61 const auto *nativeHandles = static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles());
62 if (!nativeHandles || !nativeHandles->context)
63 return;
64 QOpenGLContext *openGLContext = nativeHandles->context;
65
66 auto tryGetHandleFromSurface = [&]() -> bool {
67 QSurface *surface = openGLContext->surface();
68 if (!surface)
69 return false;
70 // QWasmOpenGLContext::makeCurrent() ends in
71 // emscripten_webgl_make_context_current(), so the handle we want is the
72 // one that is current afterwards.
73 if (QOpenGLContext::currentContext() != openGLContext
74 && !openGLContext->makeCurrent(surface))
75 return false;
76 m_glContextHandle = emscripten_webgl_get_current_context();
77 m_hasWebGLContext = (m_glContextHandle != 0);
78 return m_hasWebGLContext;
79 };
80
81 if (!tryGetHandleFromSurface())
82 qWarning() << Q_FUNC_INFO << "could not determine the WebGL context for the current RHI";
83}
84
85// software path, copies VideoFrame data into a QMemoryVideoBuffer
86void QWasmVideoFrameGrabber::processVideoFrame()
87{
88 emscripten::val videoElement = m_videoOutput->currentVideoElement();
89
90 // The VideoFrame constructor throws InvalidStateError when the browser compositor
91 // has not yet committed the first decoded frame, even if readyState == 4 and
92 // videoWidth > 0. Use a JS try-catch so the exception does not propagate into
93 // the wasm runtime and abort the application.
94 emscripten::val oneVideoFrame = emscripten::val::take_ownership(
95 static_cast<EM_VAL>(EM_ASM_PTR({
96 try {
97 return Emval.toHandle(new VideoFrame(Emval.toValue($0)));
98 } catch(e) {
99 return Emval.toHandle(null);
100 }
101 }, videoElement.as_handle())));
102
103 if (oneVideoFrame.isNull() || oneVideoFrame.isUndefined()) {
104 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << "VideoFrame not ready yet, skipping";
105 return;
106 }
107
108 emscripten::val options = emscripten::val::object();
109 emscripten::val rectOptions = emscripten::val::object();
110
111 int displayWidth = oneVideoFrame["displayWidth"].as<int>();
112 int displayHeight = oneVideoFrame["displayHeight"].as<int>();
113
114 rectOptions.set("width", displayWidth);
115 rectOptions.set("height", displayHeight);
116 options.set("rect", rectOptions);
117
118 emscripten::val frameBytesAllocationSize = oneVideoFrame.call<emscripten::val>("allocationSize", options);
119 emscripten::val frameBuffer =
120 emscripten::val::global("Uint8Array").new_(frameBytesAllocationSize);
121 QWasmVideoOutput *videoOutput = m_videoOutput;
122
123 qstdweb::PromiseCallbacks copyToCallback;
124 copyToCallback.thenFunc = [videoOutput, oneVideoFrame, frameBuffer,
125 displayWidth, displayHeight]
126 (emscripten::val frameLayout)
127 {
128 if (frameLayout.isNull() || frameLayout.isUndefined()) {
129 qCDebug(qWasmMediaVideoOutput) << "theres no frameLayout";
130 return;
131 }
132
133 // frameBuffer now has a new frame, send to Qt
134 const QSize frameSize(displayWidth, displayHeight);
135
136 QByteArray frameBytes = QByteArray::fromEcmaUint8Array(frameBuffer);
137
138 QVideoFrameFormat::PixelFormat pixelFormat =
139 fromJsPixelFormat(oneVideoFrame["format"].as<std::string>());
140 if (pixelFormat == QVideoFrameFormat::Format_Invalid)
141 pixelFormat = QVideoFrameFormat::Format_RGBA8888;
142 QVideoFrameFormat frameFormat = QVideoFrameFormat(frameSize, pixelFormat);
143
144 if (videoOutput->m_useCameraRotation)
145 frameFormat.setRotation(videoOutput->m_rotateBy);
146 if (videoOutput->m_streamFrameRate > 0)
147 frameFormat.setStreamFrameRate(videoOutput->m_streamFrameRate);
148 auto buffer = std::make_unique<QMemoryVideoBuffer>(
149 std::move(frameBytes),
150 frameLayout[0]["stride"].as<int>());
151
152 QVideoFrame vFrame =
153 QVideoFramePrivate::createFrame(std::move(buffer), std::move(frameFormat));
154
155 if (!videoOutput->m_wasmSink) {
156 qWarning() << "ERROR ALERT!! video sink not set";
157 return;
158 }
159 videoOutput->m_wasmSink->setVideoFrame(vFrame);
160 oneVideoFrame.call<emscripten::val>("close");
161 };
162 copyToCallback.catchFunc = [oneVideoFrame](emscripten::val error)
163 {
164 qCDebug(qWasmMediaVideoOutput) << "copyTo error"
165 << QString::fromStdString(error["name"].as<std::string>())
166 << QString::fromStdString(error["message"].as<std::string>());
167 oneVideoFrame.call<emscripten::val>("close");
168 };
169
170 qstdweb::Promise::make(oneVideoFrame, u"copyTo"_s, std::move(copyToCallback), frameBuffer, options);
171}
172
173// zero-readback path, uploads the VideoFrame straight into a GL texture
174void QWasmVideoFrameGrabber::processWebGLVideoFrame()
175{
176 emscripten_webgl_make_context_current(m_glContextHandle);
177
178 GLuint rawTextureId = 0;
179 glGenTextures(1, &rawTextureId);
180 QGlTextureHandle textureHandle{ rawTextureId };
181
182 glBindTexture(GL_TEXTURE_2D, textureHandle.get());
183
184 int width = 0, height = 0;
185 em_texImage2DFromVideo(m_videoOutput->m_videoSurfaceId.c_str(), &width, &height);
186
187 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
188 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
189 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
190 glBindTexture(GL_TEXTURE_2D, 0);
191
192 if (!textureHandle || width == 0 || height == 0) {
193 qCWarning(qWasmMediaVideoOutput) << "VideoFrame upload failed";
194 return;
195 }
196
197 std::unique_ptr<QHwVideoBuffer> hwBuffer =
198 std::make_unique<QWasmGLTextureVideoBuffer>(
199 std::move(textureHandle), QSize(width, height),
200 m_glContextHandle,
201 m_videoOutput->m_wasmSink ? m_videoOutput->m_wasmSink->rhi() : nullptr);
202
203 QVideoFrameFormat frameFormat(QSize(width, height), QVideoFrameFormat::Format_RGBA8888);
204 if (m_videoOutput->m_streamFrameRate > 0)
205 frameFormat.setStreamFrameRate(m_videoOutput->m_streamFrameRate);
206 QVideoFrame vFrame =
207 QVideoFramePrivate::createFrame(std::move(hwBuffer), std::move(frameFormat));
208
209 m_videoOutput->m_wasmSink->setVideoFrame(vFrame);
210}
211
213{
214 if (!m_webGLContextChecked) {
215 m_webGLContextChecked = true;
216 detectWebGLContext();
217 }
218
219 m_videoOutput->detectIosCameraRotation();
220
221 // Single-shot callback: re-registers each frame so multiple QWasmVideoOutput
222 // instances can coexist. emscripten_request_animation_frame_loop allows only one
223 // active loop globally and would cancel another instance.
224 static EM_BOOL (*frame)(double, void *) = [](double frameTime, void *context) -> EM_BOOL {
225
226 Q_UNUSED(frameTime);
227
228 QWasmVideoFrameGrabber *frameGrabber = reinterpret_cast<QWasmVideoFrameGrabber *>(context);
229 QWasmVideoOutput *videoOutput = frameGrabber ? frameGrabber->m_videoOutput : nullptr;
230 if (!videoOutput || videoOutput->m_isStopped) {
231 qCWarning(qWasmMediaVideoOutput) << "frame loop exit: isStopped=" << (videoOutput ? videoOutput->m_isStopped : true)
232 << "mode=" << (videoOutput ? videoOutput->m_currentVideoMode : -1);
233 return false;
234 }
235
236 if (videoOutput->m_currentVideoMode == QWasmVideoOutput::VideoDisplay
237 && videoOutput->m_currentMediaStatus != QWasmVideoOutput::MediaStatus::LoadedMedia) {
238 emscripten_request_animation_frame(frame, context);
239 return true;
240 }
241
242 emscripten::val videoElement = videoOutput->currentVideoElement();
243 if (videoElement.isNull() || videoElement.isUndefined()) {
244 qCWarning(qWasmMediaVideoOutput) << "frame loop exit: video element null, mode=" << videoOutput->m_currentVideoMode;
245 return false;
246 }
247
248 if (videoElement["paused"].as<bool>() || videoElement["ended"].as<bool>()
249 || videoElement["readyState"].as<int>() < 2) {
250 qCDebug(qWasmMediaVideoOutput) << "frame loop waiting: mode=" << videoOutput->m_currentVideoMode
251 << "paused=" << videoElement["paused"].as<bool>()
252 << "ended=" << videoElement["ended"].as<bool>()
253 << "readyState=" << videoElement["readyState"].as<int>();
254 emscripten_request_animation_frame(frame, context);
255 return true;
256 }
257
258 if (frameGrabber->m_glContextHandle)
259 frameGrabber->processWebGLVideoFrame();
260 else
261 frameGrabber->processVideoFrame();
262
263 emscripten_request_animation_frame(frame, context);
264 return true;
265 };
266 if ((!m_videoOutput->m_isStopped
267 && m_videoOutput->m_video["className"].as<std::string>() == "Camera"
268 && m_videoOutput->m_cameraIsReady)
269 || (!m_videoOutput->m_isStopped
270 && m_videoOutput->m_currentVideoMode == QWasmVideoOutput::SurfaceCapture)
271 || m_videoOutput->isReady())
272 emscripten_request_animation_frame(frame, this);
273}
274
275QVideoFrameFormat::PixelFormat QWasmVideoFrameGrabber::fromJsPixelFormat(std::string_view videoFormat)
276{
277 if (videoFormat == "I420")
278 return QVideoFrameFormat::Format_YUV420P;
279 // no equivalent pixel format
280 // else if (videoFormat == "I420A") // AYUV ?
281 else if (videoFormat == "I422")
282 return QVideoFrameFormat::Format_YUV422P;
283 // no equivalent pixel format
284 // else if (videoFormat == "I444")
285 else if (videoFormat == "NV12")
286 return QVideoFrameFormat::Format_NV12;
287 else if (videoFormat == "RGBA")
288 return QVideoFrameFormat::Format_RGBA8888;
289 else if (videoFormat == "RGBX")
290 return QVideoFrameFormat::Format_RGBX8888;
291 else if (videoFormat == "BGRA")
292 return QVideoFrameFormat::Format_BGRA8888;
293 else if (videoFormat == "BGRX")
294 return QVideoFrameFormat::Format_BGRX8888;
295
296 return QVideoFrameFormat::Format_Invalid;
297}
298
299QT_END_NAMESPACE
QWasmVideoFrameGrabber(QWasmVideoOutput *videoOutput)
std::string m_videoSurfaceId
Combined button and popup list for selecting options.
#define GL_CLAMP_TO_EDGE
Definition qopenglext.h:100
EM_JS(void, em_texImage2DFromVideo,(const char *videoId, int *widthOut, int *heightOut), { videoId=Number(videoId);widthOut=Number(widthOut);heightOut=Number(heightOut);var gl=GL.currentContext.GLctx;var video=document.getElementById(UTF8ToString(videoId));if(!video) { return;} var frame;try { frame=new VideoFrame(video);} catch(e) { return;} HEAP32[widthOut/4]=frame.displayWidth;HEAP32[heightOut/4]=frame.displayHeight;gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, frame);frame.close();})