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