9#include <QOpenGLContext>
10#include <QtGui/rhi/qrhi_platform.h>
11#include <qpa/qplatformwindow_p.h>
15#include <qvideosink.h>
16#include <private/qmemoryvideobuffer_p.h>
17#include <private/qvideoframe_p.h>
18#include <private/qstdweb_p.h>
20#include <emscripten/emscripten.h>
21#include <emscripten/em_js.h>
22#include <emscripten/html5.h>
23#include <emscripten/val.h>
27using namespace emscripten;
28using namespace Qt::Literals;
36 if (!
video) {
return; }
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)
56 : m_videoOutput(videoOutput)
62 m_glContextHandle = 0;
63 m_hasWebGLContext =
false;
65 QRhi *rhi = m_videoOutput->m_wasmSink ? m_videoOutput->m_wasmSink->rhi() :
nullptr;
66 if (!rhi || rhi->backend() != QRhi::OpenGLES2)
69 const auto *nativeHandles =
static_cast<
const QRhiGles2NativeHandles *>(rhi->nativeHandles());
70 if (!nativeHandles || !nativeHandles->context)
72 QOpenGLContext *openGLContext = nativeHandles->context;
74 auto tryGetHandleFromSurface = [&]() ->
bool {
75 QSurface *surface = openGLContext->surface();
76 if (!surface || surface->surfaceClass() != QSurface::Window)
78 QWindow *window =
static_cast<QWindow *>(surface);
79 if (!window->handle())
81 auto *wasmIface = window->nativeInterface<QNativeInterface::Private::QWasmWindow>();
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())
90 m_glContextHandle = qwasm_find_webgl_context_for_canvas(canvas.as_handle());
91 m_hasWebGLContext = (m_glContextHandle > 0);
92 return m_hasWebGLContext;
95 if (!tryGetHandleFromSurface())
96 qWarning() << Q_FUNC_INFO <<
"could not locate WebGL canvas for the current RHI context";
102 emscripten::val videoElement = m_videoOutput->currentVideoElement();
108 emscripten::val oneVideoFrame = emscripten::val::take_ownership(
111 return Emval.toHandle(
new VideoFrame(Emval.toValue($0)));
113 return Emval.toHandle(null);
115 }, videoElement.as_handle()));
117 if (oneVideoFrame.isNull() || oneVideoFrame.isUndefined()) {
118 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO <<
"VideoFrame not ready yet, skipping";
122 emscripten::val options = emscripten::val::object();
123 emscripten::val rectOptions = emscripten::val::object();
125 int displayWidth = oneVideoFrame[
"displayWidth"].as<
int>();
126 int displayHeight = oneVideoFrame[
"displayHeight"].as<
int>();
128 rectOptions.set(
"width", displayWidth);
129 rectOptions.set(
"height", displayHeight);
130 options.set(
"rect", rectOptions);
132 emscripten::val frameBytesAllocationSize = oneVideoFrame.call<emscripten::val>(
"allocationSize", options);
133 emscripten::val frameBuffer =
134 emscripten::val::global(
"Uint8Array").new_(frameBytesAllocationSize);
137 qstdweb::PromiseCallbacks copyToCallback;
138 copyToCallback.thenFunc = [videoOutput, oneVideoFrame, frameBuffer,
139 displayWidth, displayHeight]
140 (emscripten::val frameLayout)
142 if (frameLayout.isNull() || frameLayout.isUndefined()) {
143 qCDebug(qWasmMediaVideoOutput) <<
"theres no frameLayout";
148 const QSize frameSize(displayWidth, displayHeight);
150 QByteArray frameBytes = QByteArray::fromEcmaUint8Array(frameBuffer);
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);
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>());
167 QVideoFramePrivate::createFrame(std::move(buffer), std::move(frameFormat));
169 if (!videoOutput->m_wasmSink) {
170 qWarning() <<
"ERROR ALERT!! video sink not set";
173 videoOutput->m_wasmSink->setVideoFrame(vFrame);
174 oneVideoFrame.call<emscripten::val>(
"close");
176 copyToCallback.catchFunc = [oneVideoFrame](emscripten::val error)
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");
184 qstdweb::Promise::make(oneVideoFrame, u"copyTo"_s, std::move(copyToCallback), frameBuffer, options);
190 emscripten_webgl_make_context_current(m_glContextHandle);
192 GLuint rawTextureId = 0;
193 glGenTextures(1, &rawTextureId);
194 QGlTextureHandle textureHandle{ rawTextureId };
196 glBindTexture(GL_TEXTURE_2D, textureHandle.get());
198 int width = 0, height = 0;
201 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
204 glBindTexture(GL_TEXTURE_2D, 0);
206 if (!textureHandle || width == 0 || height == 0) {
207 qCWarning(qWasmMediaVideoOutput) <<
"VideoFrame upload failed";
211 std::unique_ptr<QHwVideoBuffer> hwBuffer =
212 std::make_unique<QWasmGLTextureVideoBuffer>(
213 std::move(textureHandle), QSize(width, height),
215 m_videoOutput->m_wasmSink ? m_videoOutput->m_wasmSink->rhi() :
nullptr);
217 QVideoFrameFormat frameFormat(QSize(width, height), QVideoFrameFormat::Format_RGBA8888);
218 if (m_videoOutput->m_streamFrameRate > 0)
219 frameFormat.setStreamFrameRate(m_videoOutput->m_streamFrameRate);
221 QVideoFramePrivate::createFrame(std::move(hwBuffer), std::move(frameFormat));
223 m_videoOutput->m_wasmSink->setVideoFrame(vFrame);
228 if (!m_webGLContextChecked) {
229 m_webGLContextChecked =
true;
230 detectWebGLContext();
233 m_videoOutput->detectIosCameraRotation();
238 static EM_BOOL (*frame)(
double,
void *) = [](
double frameTime,
void *context) -> EM_BOOL {
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);
250 if (videoOutput->m_currentVideoMode == QWasmVideoOutput::VideoDisplay
251 && videoOutput->m_currentMediaStatus != QWasmVideoOutput::MediaStatus::LoadedMedia) {
252 emscripten_request_animation_frame(frame, context);
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;
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);
272 qCDebug(qWasmMediaVideoOutput) <<
"frame loop render: mode=" << videoOutput->m_currentVideoMode
273 <<
"glHandle=" << frameGrabber->m_glContextHandle;
275 if (frameGrabber->m_glContextHandle)
276 frameGrabber->processWebGLVideoFrame();
278 frameGrabber->processVideoFrame();
280 emscripten_request_animation_frame(frame, context);
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
289 emscripten_request_animation_frame(frame,
this);
294 if (videoFormat ==
"I420")
295 return QVideoFrameFormat::Format_YUV420P;
298 else if (videoFormat ==
"I422")
299 return QVideoFrameFormat::Format_YUV422P;
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;
313 return QVideoFrameFormat::Format_Invalid;
QWasmVideoFrameGrabber(QWasmVideoOutput *videoOutput)
std::string m_videoSurfaceId
Combined button and popup list for selecting options.
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);})