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