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
qwasmvideooutput.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
4#include <QDebug>
5#include <QUrl>
6#include <QPoint>
7#include <QRect>
8#include <QMediaPlayer>
9#include <QVideoFrame>
10#include <QFile>
11#include <QBuffer>
12#include <QMimeDatabase>
13#include <QGuiApplication>
14#include <QOpenGLContext>
15
16#include <QtGui/rhi/qrhi_platform.h>
17#include <qpa/qplatformwindow_p.h>
18
19#include <GLES2/gl2.h>
20
22
23#include <qvideosink.h>
24#include <private/qplatformvideosink_p.h>
25#include <private/qmemoryvideobuffer_p.h>
26#include <private/qvideotexturehelper_p.h>
27#include <private/qvideoframe_p.h>
28#include <private/qstdweb_p.h>
29#include <QTimer>
30
31#include <emscripten/bind.h>
32#include <emscripten/val.h>
33
34// Upload the current video frame to the already-bound TEXTURE_2D.
35// The canvas is passed as an EM_VAL handle; Emval.toValue() here refers to
36// Emscripten's internal Emval object, not Module.Emval — no EXPORTED_RUNTIME_METHODS entry needed.
37using emscripten::EM_VAL;
39 (EM_VAL canvasHandle, const char *videoId, int *pW, int *pH), {
41 var gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
44 HEAP32[pW >> 2] = frame.displayWidth; // write into the C pointer pW
47 frame.close();
48});
49
50QT_BEGIN_NAMESPACE
51
52
53using namespace emscripten;
54using namespace Qt::Literals;
55
56Q_LOGGING_CATEGORY(qWasmMediaVideoOutput, "qt.multimedia.wasm.videooutput")
57
58static bool checkForVideoFrame()
59{
60 emscripten::val videoFrame = emscripten::val::global("VideoFrame");
61 return (!videoFrame.isNull() && !videoFrame.isUndefined());
62}
63
64bool QWasmVideoOutput::isPlatformiOs()
65{
66 emscripten::val platformObject = emscripten::val::global("navigator")["platform"];
67 if (platformObject.call<bool>("includes", emscripten::val("iPhone"))
68 || platformObject.call<bool>("includes", emscripten::val("iPad")))
69 return true;
70 return false;
71}
72
73QWasmVideoOutput::QWasmVideoOutput(QObject *parent) : QObject{ parent }
74{
76
77 if (m_hasVideoFrame) {
78 if (isPlatformiOs()) {
79 // iOS has [broken] camera driver
80 connect(this, &QWasmVideoOutput::orientationChanged, this,
81 [&](int orientationIndex) {
82
83 if (orientationIndex & EMSCRIPTEN_ORIENTATION_PORTRAIT_PRIMARY) {// 1
84 m_rotateBy = QtVideo::Rotation::Clockwise90;
85 } else if (orientationIndex & EMSCRIPTEN_ORIENTATION_LANDSCAPE_PRIMARY) {// 4
86 if (m_cameraMode == QWasmVideoOutput::Front) {
87 m_rotateBy = QtVideo::Rotation::Clockwise180;
88 } else {
89 m_rotateBy = QtVideo::Rotation::None;
90 }
91 } else if (orientationIndex & EMSCRIPTEN_ORIENTATION_PORTRAIT_SECONDARY) {// 2
92 m_rotateBy = QtVideo::Rotation::Clockwise270;
93 } else if (orientationIndex & EMSCRIPTEN_ORIENTATION_LANDSCAPE_SECONDARY) {// 8
94 if (m_cameraMode == QWasmVideoOutput::Front) {
95 m_rotateBy = QtVideo::Rotation::None;
96 } else {
97 m_rotateBy = QtVideo::Rotation::Clockwise180;
98 }
99 }
100 });
101
102 emscripten_set_orientationchange_callback(this,false, &QWasmVideoOutput::orientationchangeCallback);
103
104 //get current status
105 EmscriptenOrientationChangeEvent status;
106 EMSCRIPTEN_RESULT result = emscripten_get_orientation_status(&status);
107 if (result == EMSCRIPTEN_RESULT_SUCCESS)
108 orientationChanged(status.orientationIndex);
109 }
110 }
111}
112
114
115void QWasmVideoOutput::setVideoSize(const QSize &newSize)
116{
117 if (m_pendingVideoSize == newSize)
118 return;
119
120 m_pendingVideoSize = newSize;
121 updateVideoElementGeometry(QRect(0, 0, m_pendingVideoSize.width(), m_pendingVideoSize.height()));
122}
123
124bool QWasmVideoOutput::orientationchangeCallback(int eventType,
125 const EmscriptenOrientationChangeEvent *event,
126 void *userData)
127{
128 Q_UNUSED(eventType)
129
130 QWasmVideoOutput *videoOutput = static_cast<QWasmVideoOutput *>(userData);
131 emit videoOutput->orientationChanged(event->orientationIndex);
132
133 return true;
134}
135
137{
138 m_currentVideoMode = mode;
139}
140
142{
143 if (m_video.isUndefined() || m_video.isNull()
144 || !m_wasmSink) {
145 // error
146 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
147 return;
148 }
149 switch (m_currentVideoMode) {
151 emscripten::val sourceObj = m_video["src"];
152 if ((sourceObj.isUndefined() || sourceObj.isNull()) && !m_source.isEmpty()) {
153 m_video.set("src", m_source);
154 }
155 if (!isReady())
156 m_video.call<void>("load");
157 } break;
159 m_video.call<void>("play");
160 emit readyChanged(true);
161 if (m_hasVideoFrame)
163 } break;
165 if (!m_cameraIsReady) {
166 m_shouldBeStarted = true;
167 }
168
169 if (!m_connection)
170 m_connection = connect(m_mediaInputStream, &JsMediaInputStream::mediaVideoStreamReady, this,
171 [=]( ) {
172 m_video.set("srcObject", m_mediaInputStream->getMediaStream());
173
174 emscripten::val stream = m_video["srcObject"];
175 if (stream.isNull() || stream.isUndefined()) { // camera device
176 qCDebug(qWasmMediaVideoOutput) << "srcObject ERROR";
177 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
178 return;
179 } else {
180 emscripten::val videoTracks = stream.call<emscripten::val>("getVideoTracks");
181 if (videoTracks.isNull() || videoTracks.isUndefined()) {
182 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << "videoTracks is null";
183 emit errorOccured(QMediaPlayer::ResourceError,
184 QStringLiteral("video surface error"));
185 return;
186 }
187 if (videoTracks["length"].as<int>() == 0) {
188 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << "videoTracks count is 0";
189 emit errorOccured(QMediaPlayer::ResourceError,
190 QStringLiteral("video surface error"));
191 return;
192 }
193 emscripten::val videoSettings = videoTracks[0].call<emscripten::val>("getSettings");
194 if (!videoSettings.isNull() && !videoSettings.isUndefined()) {
195 const int width = videoSettings["width"].as<int>();
196 const int height = videoSettings["height"].as<int>();
197 updateVideoElementGeometry(QRect(0, 0, width, height));
198 }
199 }
200
201 m_video.call<void>("play");
202
203 emit readyChanged(true);
204 if (m_hasVideoFrame)
205 videoFrameTimerCallback();
206
207 });
208 m_mediaInputStream->setUseAudio(false);
209 m_shouldBeStarted = true;
210 m_mediaInputStream->setStreamDevice(m_cameraId);
211
212 } break;
213 };
214
215 m_isStopped = false;
216 m_toBePaused = false;
217
218 if (m_currentVideoMode != QWasmVideoOutput::Camera
219 && m_currentVideoMode != QWasmVideoOutput::SurfaceCapture) {
220 m_video.call<void>("play");
221 }
222}
223
225{
226 if (m_isStopped)
227 return;
228 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO;
229 if (m_video.isUndefined() || m_video.isNull()) {
230 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("Resource error"));
231 return;
232 }
233 m_isStopped = true;
234 if (!m_toBePaused) {
235 if (m_currentVideoMode == QWasmVideoOutput::SurfaceCapture) {
236 emscripten::val stream = m_video["srcObject"];
237 if (!stream.isNull() && !stream.isUndefined()) {
238 emscripten::val tracks = stream.call<emscripten::val>("getTracks");
239 const int count = tracks["length"].as<int>();
240 for (int i = 0; i < count; ++i)
241 tracks[i].call<void>("stop");
242 }
243 } else if (m_mediaInputStream && m_mediaInputStream->isActive()) {
244 m_mediaInputStream->stopMediaStream(m_mediaInputStream->getMediaStream());
245 }
246 m_video.set("srcObject", emscripten::val::null());
247 disconnect(m_connection);
248 m_video.call<void>("remove");
249 } else {
250 m_video.call<void>("pause");
251 }
252}
253
255{
256 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO;
257
258 if (m_video.isUndefined() || m_video.isNull()) {
259 // error
260 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
261 return;
262 }
263 m_isStopped = false;
264 m_toBePaused = true;
265 m_video.call<void>("pause");
266}
267
269{
270 // flush pending frame
271 if (m_wasmSink)
272 m_wasmSink->platformVideoSink()->setVideoFrame(QVideoFrame());
273
274 m_source.clear();
275 m_video.set("currentTime", emscripten::val(0));
276 m_video.call<void>("load");
277}
278
280{
281 return m_video;
282}
283
284void QWasmVideoOutput::setSurface(QVideoSink *surface)
285{
286 if (!surface || surface == m_wasmSink) {
287 return;
288 }
289
290 m_wasmSink = surface;
291}
292
294{
295 if (m_video.isUndefined() || m_video.isNull()) {
296 // error
297 return false;
298 }
299
300 return m_currentMediaStatus == MediaStatus::LoadedMedia;
301 }
302
303void QWasmVideoOutput::setSource(const QUrl &url)
304{
305 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << url;
306
307 m_source = url.toString();
308
309 if (m_video.isUndefined() || m_video.isNull()) {
310 return;
311 }
312
313 if (url.isEmpty()) {
314 stop();
315 return;
316 }
317 if (url.isLocalFile()) {
318 QFile localFile(url.toLocalFile());
319 if (localFile.open(QIODevice::ReadOnly)) {
320 QDataStream buffer(&localFile); // we will serialize the data into the file
321 setSource(buffer.device());
322 } else {
323 qWarning() << "Failed to open file";
324 }
325 return;
326 }
327
328 updateVideoElementSource(m_source);
329}
330
332{
333 m_video.set("src", src.toStdString());
334 m_video.call<void>("load");
335}
336
337void QWasmVideoOutput::addCameraSourceElement(const std::string &id)
338{
339 m_cameraIsReady = false;
340 m_mediaInputStream = JsMediaInputStream::instance();
341
342 m_mediaInputStream->setUseAudio(m_hasAudio);
343 m_mediaInputStream->setUseVideo(true);
344
345 connect(m_mediaInputStream, &JsMediaInputStream::mediaVideoStreamReady, this,
346 [this]() {
347 qCDebug(qWasmMediaVideoOutput) << "mediaVideoStreamReady" << m_shouldBeStarted;
348
349 m_cameraIsReady = true;
350 if (m_shouldBeStarted) {
351 start();
352 m_shouldBeStarted = false;
353 }
354 });
355
356 m_cameraId = id;
357}
358
359void QWasmVideoOutput::setSource(QIODevice *stream)
360{
361 if (stream->bytesAvailable() == 0) {
362 qWarning() << "data not available";
363 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("data not available"));
364 return;
365 }
366 if (m_video.isUndefined() || m_video.isNull()) {
367 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
368 return;
369 }
370
371 QMimeDatabase db;
372 QMimeType mime = db.mimeTypeForData(stream);
373
374 QByteArray buffer = stream->readAll();
375
376 qstdweb::Blob contentBlob = qstdweb::Blob::copyFrom(buffer.data(), buffer.size(), mime.name().toStdString());
377
378 emscripten::val window = qstdweb::window();
379
380 if (window["safari"].isUndefined()) {
381 emscripten::val contentUrl = window["URL"].call<emscripten::val>("createObjectURL", contentBlob.val());
382 m_video.set("src", contentUrl);
383 m_source = QString::fromStdString(contentUrl.as<std::string>());
384 } else {
385 // only Safari currently supports Blob with srcObject
386 m_video.set("srcObject", contentBlob.val());
387 }
388}
389
390void QWasmVideoOutput::setVolume(qreal volume)
391{ // between 0 - 1
392 volume = qBound(qreal(0.0), volume, qreal(1.0));
393 m_video.set("volume", volume);
394}
395
396void QWasmVideoOutput::setMuted(bool muted)
397{
398 if (m_video.isUndefined() || m_video.isNull()) {
399 // error
400 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
401 return;
402 }
403 m_video.set("muted", muted);
404}
405
407{
408 return (!m_video.isUndefined() || !m_video.isNull())
409 ? (m_video["currentTime"].as<double>() * 1000)
410 : 0;
411}
412
413void QWasmVideoOutput::seekTo(qint64 positionMSecs)
414{
415 if (isVideoSeekable()) {
416 float positionToSetInSeconds = float(positionMSecs) / 1000;
417 emscripten::val seekableTimeRange = m_video["seekable"];
418 if (!seekableTimeRange.isNull() || !seekableTimeRange.isUndefined()) {
419 // range user can seek
420 if (seekableTimeRange["length"].as<int>() < 1)
421 return;
422 if (positionToSetInSeconds
423 >= seekableTimeRange.call<emscripten::val>("start", 0).as<double>()
424 && positionToSetInSeconds
425 <= seekableTimeRange.call<emscripten::val>("end", 0).as<double>()) {
426 m_requestedPosition = positionToSetInSeconds;
427
428 m_video.set("currentTime", m_requestedPosition);
429 }
430 }
431 }
432 qCDebug(qWasmMediaVideoOutput) << "m_requestedPosition" << m_requestedPosition;
433}
434
436{
437 if (m_video.isUndefined() || m_video.isNull()) {
438 // error
439 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
440 return false;
441 }
442
443 emscripten::val seekableTimeRange = m_video["seekable"];
444 if (seekableTimeRange["length"].as<int>() < 1)
445 return false;
446 if (!seekableTimeRange.isNull() || !seekableTimeRange.isUndefined()) {
447 bool isit = !QtPrivate::fuzzyCompare(
448 seekableTimeRange.call<emscripten::val>("start", 0).as<double>(),
449 seekableTimeRange.call<emscripten::val>("end", 0).as<double>());
450 return isit;
451 }
452 return false;
453}
454
455void QWasmVideoOutput::createVideoElement(const std::string &id)
456{
457 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << this << id;
458 // Create <video> element and add it to the page body
459
460 emscripten::val document = emscripten::val::global("document");
461 emscripten::val body = document["body"];
462
463 emscripten::val oldVideo = document.call<emscripten::val>("getElementById", id);
464
465 // need to remove stale element
466 if (!oldVideo.isUndefined() && !oldVideo.isNull())
467 oldVideo.call<void>("remove");
468
469 m_videoSurfaceId = id;
470 m_video = document.call<emscripten::val>("createElement", std::string("video"));
471
472 m_video.set("id", m_videoSurfaceId.c_str());
473 m_video.call<void>("setAttribute", std::string("class"),
474 (m_currentVideoMode == QWasmVideoOutput::Camera ? std::string("Camera")
475 : std::string("Video")));
476 m_video.set("data-qvideocontext",
477 emscripten::val(quintptr(reinterpret_cast<void *>(this))));
478
479 m_video.set("preload", "metadata");
480
481 // Uncaught DOMException: Failed to execute 'getImageData' on
482 // 'OffscreenCanvasRenderingContext2D': The canvas has been tainted by
483 // cross-origin data.
484 // TODO figure out somehow to let user choose between these
485 std::string originString = "anonymous"; // requires server Access-Control-Allow-Origin *
486 // std::string originString = "use-credentials"; // must not
487 // Access-Control-Allow-Origin *
488
489 m_video.call<void>("setAttribute", std::string("crossorigin"), originString);
490 body.call<void>("appendChild", m_video);
491
492 // Create/add video source
493 document.call<emscripten::val>("createElement",
494 std::string("source")).set("src", m_source.toStdString());
495
496 // Set position:absolute, which makes it possible to position the video
497 // element using x,y. coordinates, relative to its parent (the page's <body>
498 // element)
499 emscripten::val style = m_video["style"];
500 style.set("position", "absolute");
501 style.set("display", "none"); // hide
502
503 if (!m_source.isEmpty())
504 updateVideoElementSource(m_source);
505}
506
507void QWasmVideoOutput::createOffscreenElement(const QSize &offscreenSize)
508{
509 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO;
510
511 if (m_hasVideoFrame) // VideoFrame does not require offscreen canvas/context
512 return;
513
514 // create offscreen element for grabbing frames
515 // OffscreenCanvas - no safari :(
516 // https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
517
518 emscripten::val document = emscripten::val::global("document");
519
520 // TODO use correct frameBytesAllocationSize?
521 // offscreen render buffer
522 m_offscreen = emscripten::val::global("OffscreenCanvas");
523
524 if (m_offscreen.isUndefined()) {
525 // Safari OffscreenCanvas not supported, try old skool way
526 m_offscreen = document.call<emscripten::val>("createElement", std::string("canvas"));
527
528 m_offscreen.set("style",
529 "position:absolute;left:-1000px;top:-1000px"); // offscreen
530 m_offscreen.set("width", offscreenSize.width());
531 m_offscreen.set("height", offscreenSize.height());
532 m_offscreenContext = m_offscreen.call<emscripten::val>("getContext", std::string("2d"));
533 } else {
534 m_offscreen = emscripten::val::global("OffscreenCanvas")
535 .new_(offscreenSize.width(), offscreenSize.height());
536 emscripten::val offscreenAttributes = emscripten::val::array();
537 offscreenAttributes.set("willReadFrequently", true);
538 m_offscreenContext = m_offscreen.call<emscripten::val>("getContext", std::string("2d"),
539 offscreenAttributes);
540 }
541 std::string offscreenId = m_videoSurfaceId + "_offscreenOutputSurface";
542 m_offscreen.set("id", offscreenId.c_str());
543}
544
546{
547 if (!m_video.isUndefined() && !m_video.isNull())
548 m_video.call<void>("remove");
549}
550
552{
553 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO;
554
555 // event callbacks
556 // timupdate
557 auto timeUpdateCallback = [=](emscripten::val event) {
558 qCDebug(qWasmMediaVideoOutput) << "timeupdate";
559
560 // qt progress is ms
561 emit progressChanged(event["target"]["currentTime"].as<double>() * 1000);
562 };
563 m_timeUpdateEvent.reset(new QWasmEventHandler(m_video, "timeupdate", timeUpdateCallback));
564
565 // play
566 auto playCallback = [=](emscripten::val event) {
567 Q_UNUSED(event)
568 qCDebug(qWasmMediaVideoOutput) << "play" << m_video["src"].as<std::string>();
569 if (!m_isSeeking)
570 emit stateChanged(QWasmMediaPlayer::Preparing);
571 };
572 m_playEvent.reset(new QWasmEventHandler(m_video, "play", playCallback));
573
574 // ended
575 auto endedCallback = [=](emscripten::val event) {
576 Q_UNUSED(event)
577 qCDebug(qWasmMediaVideoOutput) << "ended";
578 m_currentMediaStatus = MediaStatus::EndOfMedia;
579 emit statusChanged(m_currentMediaStatus);
580 };
581 m_endedEvent.reset(new QWasmEventHandler(m_video, "ended", endedCallback));
582
583 // durationchange
584 auto durationChangeCallback = [=](emscripten::val event) {
585 qCDebug(qWasmMediaVideoOutput) << "durationChange";
586
587 // qt duration is in milliseconds.
588 qint64 dur = event["target"]["duration"].as<double>() * 1000;
589 emit durationChanged(dur);
590 };
591 m_durationChangeEvent.reset(
592 new QWasmEventHandler(m_video, "durationchange", durationChangeCallback));
593
594 // loadeddata
595 auto loadedDataCallback = [=](emscripten::val event) {
596 Q_UNUSED(event)
597 qCDebug(qWasmMediaVideoOutput) << "loaded data";
598
599 emit stateChanged(QWasmMediaPlayer::Prepared);
600 if (m_isSeekable != isVideoSeekable()) {
601 m_isSeekable = isVideoSeekable();
602 emit seekableChanged(m_isSeekable);
603 }
604 };
605 m_loadedDataEvent.reset(new QWasmEventHandler(m_video, "loadeddata", loadedDataCallback));
606
607 // error
608 auto errorCallback = [=](emscripten::val event) {
609 qCDebug(qWasmMediaVideoOutput) << "error";
610 if (event.isUndefined() || event.isNull())
611 return;
612 emit errorOccured(m_video["error"]["code"].as<int>(),
613 QString::fromStdString(m_video["error"]["message"].as<std::string>()));
614 };
615 m_errorChangeEvent.reset(new QWasmEventHandler(m_video, "error", errorCallback));
616
617 // resize
618 auto resizeCallback = [=](emscripten::val event) {
619 Q_UNUSED(event)
620 qCDebug(qWasmMediaVideoOutput) << "resize";
621
622 updateVideoElementGeometry(
623 QRect(0, 0, m_video["videoWidth"].as<int>(), m_video["videoHeight"].as<int>()));
624 emit sizeChange(m_video["videoWidth"].as<int>(), m_video["videoHeight"].as<int>());
625
626 };
627 m_resizeChangeEvent.reset(new QWasmEventHandler(m_video, "resize", resizeCallback));
628
629 // loadedmetadata
630 auto loadedMetadataCallback = [=](emscripten::val event) {
631 Q_UNUSED(event)
632 qCDebug(qWasmMediaVideoOutput) << "loaded meta data";
633
634 emit metaDataLoaded();
635 };
636 m_loadedMetadataChangeEvent.reset(
637 new QWasmEventHandler(m_video, "loadedmetadata", loadedMetadataCallback));
638
639 // loadstart
640 auto loadStartCallback = [=](emscripten::val event) {
641 Q_UNUSED(event)
642 qCDebug(qWasmMediaVideoOutput) << "load started";
643 m_currentMediaStatus = MediaStatus::LoadingMedia;
644 emit statusChanged(m_currentMediaStatus);
645 m_isStopped = false;
646 };
647 m_loadStartChangeEvent.reset(new QWasmEventHandler(m_video, "loadstart", loadStartCallback));
648
649 // canplay
650
651 auto canPlayCallback = [=](emscripten::val event) {
652 if (event.isUndefined() || event.isNull())
653 return;
654 qCDebug(qWasmMediaVideoOutput) << "can play"
655 << "m_requestedPosition" << m_requestedPosition;
656
657 if (!m_isStopped)
658 emit readyChanged(true); // sets video available
659 };
660 m_canPlayChangeEvent.reset(new QWasmEventHandler(m_video, "canplay", canPlayCallback));
661
662 // canplaythrough
663 auto canPlayThroughCallback = [=](emscripten::val event) {
664 Q_UNUSED(event)
665 qCDebug(qWasmMediaVideoOutput) << "can play through"
666 << "m_isStopped" << m_isStopped;
667
668 if (m_currentMediaStatus == MediaStatus::EndOfMedia)
669 return;
670 bool seekable = isVideoSeekable();
671 if (m_isSeekable != seekable) {
672 m_isSeekable = seekable;
673 emit seekableChanged(m_isSeekable);
674 }
675 if (!m_isSeeking && !m_isStopped) {
676 emscripten::val timeRanges = m_video["buffered"];
677 if ((!timeRanges.isNull() || !timeRanges.isUndefined())
678 && timeRanges["length"].as<int>() == 1) {
679 double buffered = m_video["buffered"].call<emscripten::val>("end", 0).as<double>();
680 const double duration = m_video["duration"].as<double>();
681
682 if (duration == buffered) {
683 m_currentBufferedValue = 100;
684 emit bufferingChanged(m_currentBufferedValue);
685 }
686 }
687 constexpr int hasEnoughData = 4;
688 if (m_video["readyState"].as<int>() == hasEnoughData) {
689 m_currentMediaStatus = MediaStatus::LoadedMedia;
690 emit statusChanged(m_currentMediaStatus);
692 }
693 } else {
694 m_isStopped = false;
695 }
696 };
697 m_canPlayThroughChangeEvent.reset(
698 new QWasmEventHandler(m_video, "canplaythrough", canPlayThroughCallback));
699
700 // seeking
701 auto seekingCallback = [=](emscripten::val event) {
702 Q_UNUSED(event)
703 qCDebug(qWasmMediaVideoOutput)
704 << "seeking started" << (m_video["currentTime"].as<double>() * 1000);
705 m_isSeeking = true;
706 };
707 m_seekingChangeEvent.reset(new QWasmEventHandler(m_video, "seeking", seekingCallback));
708
709 // seeked
710 auto seekedCallback = [=](emscripten::val event) {
711 Q_UNUSED(event)
712 qCDebug(qWasmMediaVideoOutput) << "seeked" << (m_video["currentTime"].as<double>() * 1000);
713 emit progressChanged(m_video["currentTime"].as<double>() * 1000);
714 m_isSeeking = false;
715 };
716 m_seekedChangeEvent.reset(new QWasmEventHandler(m_video, "seeked", seekedCallback));
717
718 // emptied
719 auto emptiedCallback = [=](emscripten::val event) {
720 Q_UNUSED(event)
721 qCDebug(qWasmMediaVideoOutput) << "emptied";
722 emit readyChanged(false);
723 m_currentMediaStatus = MediaStatus::EndOfMedia;
724 emit statusChanged(m_currentMediaStatus);
725 };
726 m_emptiedChangeEvent.reset(new QWasmEventHandler(m_video, "emptied", emptiedCallback));
727
728 // stalled
729 auto stalledCallback = [=](emscripten::val event) {
730 Q_UNUSED(event)
731 qCDebug(qWasmMediaVideoOutput) << "stalled";
732 m_currentMediaStatus = MediaStatus::StalledMedia;
733 emit statusChanged(m_currentMediaStatus);
734 };
735 m_stalledChangeEvent.reset(new QWasmEventHandler(m_video, "stalled", stalledCallback));
736
737 // waiting
738 auto waitingCallback = [=](emscripten::val event) {
739 Q_UNUSED(event)
740
741 qCDebug(qWasmMediaVideoOutput) << "waiting";
742 // check buffer
743 };
744 m_waitingChangeEvent.reset(new QWasmEventHandler(m_video, "waiting", waitingCallback));
745
746 // suspend
747
748 // playing
749 auto playingCallback = [=](emscripten::val event) {
750 Q_UNUSED(event)
751 qCDebug(qWasmMediaVideoOutput) << "playing";
752 if (m_isSeeking)
753 return;
754 emit stateChanged(QWasmMediaPlayer::Started);
755 if (m_toBePaused || !m_isStopped) { // paused
756 m_toBePaused = false;
757 QMetaObject::invokeMethod(this, &QWasmVideoOutput::videoFrameTimerCallback, Qt::QueuedConnection);
758 }
759 };
760 m_playingChangeEvent.reset(new QWasmEventHandler(m_video, "playing", playingCallback));
761
762 // progress (buffering progress)
763 auto progesssCallback = [=](emscripten::val event) {
764 if (event.isUndefined() || event.isNull())
765 return;
766
767 const double duration = event["target"]["duration"].as<double>();
768 if (duration < 0) // track not exactly ready yet
769 return;
770
771 emscripten::val timeRanges = event["target"]["buffered"];
772
773 if ((!timeRanges.isNull() || !timeRanges.isUndefined())
774 && timeRanges["length"].as<int>() == 1) {
775 emscripten::val dVal = timeRanges.call<emscripten::val>("end", 0);
776 if (!dVal.isNull() || !dVal.isUndefined()) {
777 double bufferedEnd = dVal.as<double>();
778
779 if (duration > 0 && bufferedEnd > 0) {
780 const double bufferedValue = (bufferedEnd / duration * 100);
781 qCDebug(qWasmMediaVideoOutput) << "progress buffered";
782 m_currentBufferedValue = bufferedValue;
783 emit bufferingChanged(m_currentBufferedValue);
784 if (bufferedEnd == duration)
785 m_currentMediaStatus = MediaStatus::BufferedMedia;
786 else
787 m_currentMediaStatus = MediaStatus::BufferingMedia;
788 emit statusChanged(m_currentMediaStatus);
789 }
790 }
791 }
792 };
793 m_progressChangeEvent.reset(new QWasmEventHandler(m_video, "progress", progesssCallback));
794
795 // pause
796 auto pauseCallback = [=](emscripten::val event) {
797 Q_UNUSED(event)
798 qCDebug(qWasmMediaVideoOutput) << "pause";
799
800 const double currentTime = m_video["currentTime"].as<double>(); // in seconds
801 const double duration = m_video["duration"].as<double>(); // in seconds
802 if ((currentTime > 0 && currentTime < duration) && (!m_isStopped && m_toBePaused)) {
803 emit stateChanged(QWasmMediaPlayer::Paused);
804 } else {
805 // stop this crazy thing!
806 m_video.set("currentTime", emscripten::val(0));
807 emit stateChanged(QWasmMediaPlayer::Stopped);
808 }
809 };
810 m_pauseChangeEvent.reset(new QWasmEventHandler(m_video, "pause", pauseCallback));
811
812 // onunload
813 // we use lower level events here as to avert a crash on activate using the
814 // qtdweb see _qt_beforeUnload
815 emscripten::val window = emscripten::val::global("window");
816
817 auto beforeUnloadCallback = [=](emscripten::val event) {
818 Q_UNUSED(event)
819 // large videos will leave the unloading window
820 // in a frozen state, so remove the video element src first
821 m_video.call<void>("removeAttribute", emscripten::val("src"));
822 m_video.call<void>("load");
823 };
824 m_beforeUnloadEvent.reset(new QWasmEventHandler(window, "beforeunload", beforeUnloadCallback));
825
826}
827
828void QWasmVideoOutput::updateVideoElementGeometry(const QRect &windowGeometry)
829{
830 QRect m_videoElementSource(windowGeometry.topLeft(), windowGeometry.size());
831
832 emscripten::val style = m_video["style"];
833 style.set("left", QStringLiteral("%1px").arg(m_videoElementSource.left()).toStdString());
834 style.set("top", QStringLiteral("%1px").arg(m_videoElementSource.top()).toStdString());
835 m_video.set("width", m_videoElementSource.width());
836 m_video.set("height", m_videoElementSource.height());
837 style.set("z-index", "999");
838
839 if (!m_hasVideoFrame) {
840 // offscreen
841 m_offscreen.set("width", m_videoElementSource.width());
842 m_offscreen.set("height", m_videoElementSource.height());
843 }
844}
845
847{
848 // qt duration is in ms
849 // js is sec
850
851 if (m_video.isUndefined() || m_video.isNull())
852 return 0;
853 return m_video["duration"].as<double>() * 1000;
854}
855
856void QWasmVideoOutput::newFrame(const QVideoFrame &frame)
857{
858 m_wasmSink->setVideoFrame(frame);
859}
860
862{
863 m_video.set("playbackRate", emscripten::val(rate));
864}
865
867{
868 return (m_video.isUndefined() || m_video.isNull()) ? 0 : m_video["playbackRate"].as<float>();
869}
870
871void QWasmVideoOutput::checkNetworkState()
872{
873 int netState = m_video["networkState"].as<int>();
874
875 qCDebug(qWasmMediaVideoOutput) << netState;
876
877 switch (netState) {
878 case QWasmMediaPlayer::QWasmMediaNetworkState::NetworkEmpty: // no data
879 break;
880 case QWasmMediaPlayer::QWasmMediaNetworkState::NetworkIdle:
881 break;
882 case QWasmMediaPlayer::QWasmMediaNetworkState::NetworkLoading:
883 break;
884 case QWasmMediaPlayer::QWasmMediaNetworkState::NetworkNoSource: // no source
885 emit errorOccured(netState, QStringLiteral("No media source found"));
886 break;
887 };
888}
889
890void QWasmVideoOutput::videoComputeFrame(void *context)
891{
892 if (m_offscreenContext.isUndefined() || m_offscreenContext.isNull()) {
893 qCDebug(qWasmMediaVideoOutput) << "offscreen canvas context could not be found";
894 return;
895 }
896 emscripten::val document = emscripten::val::global("document");
897
898 if (m_video.isUndefined() || m_video.isNull()) {
899 qCDebug(qWasmMediaVideoOutput) << "video element could not be found";
900 return;
901 }
902
903 const int videoWidth = m_video["videoWidth"].as<int>();
904 const int videoHeight = m_video["videoHeight"].as<int>();
905
906 if (videoWidth == 0 || videoHeight == 0)
907 return;
908
909 m_offscreenContext.call<void>("drawImage", m_video, 0, 0, videoWidth, videoHeight);
910
911 emscripten::val frame = // one frame, Uint8ClampedArray
912 m_offscreenContext.call<emscripten::val>("getImageData", 0, 0, videoWidth, videoHeight);
913
914 const QSize frameBytesAllocationSize(videoWidth, videoHeight);
915
916 // this seems to work ok, even though getImageData returns a Uint8ClampedArray
917 QByteArray frameBytes = qstdweb::Uint8Array(frame["data"]).copyToQByteArray();
918
919 QVideoFrameFormat frameFormat =
920 QVideoFrameFormat(frameBytesAllocationSize, QVideoFrameFormat::Format_RGBA8888);
921
922 QWasmVideoOutput *wasmVideoOutput = reinterpret_cast<QWasmVideoOutput *>(context);
923
924 if (m_useCameraRotation)
925 frameFormat.setRotation(wasmVideoOutput->m_rotateBy);
926
927 auto *textureDescription = QVideoTextureHelper::textureDescription(frameFormat.pixelFormat());
928
929 QVideoFrame vFrame = QVideoFramePrivate::createFrame(
930 std::make_unique<QMemoryVideoBuffer>(
931 std::move(frameBytes),
932 textureDescription->strideForWidth(frameFormat.frameWidth())), // width of line with padding
933 frameFormat);
934
935 if (!wasmVideoOutput->m_wasmSink) {
936 qWarning() << "ERROR ALERT!! video sink not set";
937 }
938 wasmVideoOutput->m_wasmSink->setVideoFrame(vFrame);
939}
940
941// non webgl context with VideoFrame
943{
944 QWasmVideoOutput *videoOutput = reinterpret_cast<QWasmVideoOutput *>(context);
945 if (!videoOutput || !videoOutput->isReady())
946 return;
947 emscripten::val videoElement = videoOutput->currentVideoElement();
948
949 // The VideoFrame constructor throws InvalidStateError when the browser compositor
950 // has not yet committed the first decoded frame, even if readyState == 4 and
951 // videoWidth > 0. Use a JS try-catch so the exception does not propagate into
952 // the wasm runtime and abort the application.
953 emscripten::val oneVideoFrame = emscripten::val::take_ownership(
954 (EM_VAL)EM_ASM_INT({
955 try {
956 return Emval.toHandle(new VideoFrame(Emval.toValue($0)));
957 } catch(e) {
958 return Emval.toHandle(null);
959 }
960 }, videoElement.as_handle()));
961
962 if (oneVideoFrame.isNull() || oneVideoFrame.isUndefined()) {
963 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << "VideoFrame not ready yet, skipping";
964 return;
965 }
966
967 emscripten::val options = emscripten::val::object();
968 emscripten::val rectOptions = emscripten::val::object();
969
970 int displayWidth = oneVideoFrame["displayWidth"].as<int>();
971 int displayHeight = oneVideoFrame["displayHeight"].as<int>();
972
973 rectOptions.set("width", displayWidth);
974 rectOptions.set("height", displayHeight);
975 options.set("rect", rectOptions);
976 // Request RGBA so the browser does YUV->RGB; avoids qEnsureThreadLocalRhi / makeCurrent.
977 options.set("format", std::string("RGBA"));
978
979 emscripten::val frameBytesAllocationSize = oneVideoFrame.call<emscripten::val>("allocationSize", options);
980 emscripten::val frameBuffer =
981 emscripten::val::global("Uint8Array").new_(frameBytesAllocationSize);
982 QWasmVideoOutput *wasmVideoOutput =
983 reinterpret_cast<QWasmVideoOutput*>(videoElement["data-qvideocontext"].as<quintptr>());
984
985 qstdweb::PromiseCallbacks copyToCallback;
986 copyToCallback.thenFunc = [this, wasmVideoOutput, oneVideoFrame, frameBuffer,
987 displayWidth, displayHeight]
988 (emscripten::val frameLayout)
989 {
990 if (frameLayout.isNull() || frameLayout.isUndefined()) {
991 qCDebug(qWasmMediaVideoOutput) << "theres no frameLayout";
992 return;
993 }
994
995 // frameBuffer now has a new frame, send to Qt
996 const QSize frameSize(displayWidth,
997 displayHeight);
998
999 QByteArray frameBytes = QByteArray::fromEcmaUint8Array(frameBuffer);
1000
1001 constexpr auto pixelFormat = QVideoFrameFormat::Format_RGBA8888;
1002 QVideoFrameFormat frameFormat = QVideoFrameFormat(frameSize, pixelFormat);
1003
1004 if (m_useCameraRotation)
1005 frameFormat.setRotation(wasmVideoOutput->m_rotateBy);
1006 auto buffer = std::make_unique<QMemoryVideoBuffer>(
1007 std::move(frameBytes),
1008 frameLayout[0]["stride"].as<int>());
1009
1010 QVideoFrame vFrame =
1011 QVideoFramePrivate::createFrame(std::move(buffer), std::move(frameFormat));
1012
1013 if (!wasmVideoOutput) {
1014 qCDebug(qWasmMediaVideoOutput) << "ERROR:"
1015 << "data-qvideocontext not found";
1016 return;
1017 }
1018 if (!wasmVideoOutput->m_wasmSink) {
1019 qWarning() << "ERROR ALERT!! video sink not set";
1020 return;
1021 }
1022 wasmVideoOutput->m_wasmSink->setVideoFrame(vFrame);
1023 oneVideoFrame.call<emscripten::val>("close");
1024 };
1025 copyToCallback.catchFunc = [&, wasmVideoOutput, oneVideoFrame](emscripten::val error)
1026 {
1027 qCDebug(qWasmMediaVideoOutput) << "Error"
1028 << QString::fromStdString(error["name"].as<std::string>())
1029 << QString::fromStdString(error["message"].as<std::string>()) ;
1030
1031 oneVideoFrame.call<emscripten::val>("close");
1032 wasmVideoOutput->stop();
1033 return;
1034 };
1035
1036 qstdweb::Promise::make(oneVideoFrame, u"copyTo"_s, std::move(copyToCallback), frameBuffer, options);
1037}
1038
1040{
1041 QRhi *rhi = m_wasmSink ? m_wasmSink->rhi() : nullptr;
1042 if (!rhi || rhi->backend() != QRhi::OpenGLES2)
1043 return;
1044
1045 const auto *nh = static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles());
1046 if (!nh || !nh->context)
1047 return;
1048
1049 QOpenGLContext *ctx = nh->context;
1050
1051 // QWasmOpenGLContext::makeCurrent() returns false for any surface other than
1052 // the one its WebGL context was created for, so this loop reliably identifies
1053 // the correct window without accessing GL or Emval runtime objects.
1054 for (QWindow *window : QGuiApplication::allWindows()) {
1055 if (!window->handle())
1056 continue;
1057 auto *wasmIface = window->nativeInterface<QNativeInterface::Private::QWasmWindow>();
1058 if (!wasmIface)
1059 break;
1060
1061 // Store the canvas element directly as an emscripten::val so it can be
1062 // passed to em_texImage2DFromVideo as an EM_VAL handle — avoids any
1063 // document.getElementById lookup and works inside shadow DOMs.
1064 m_glCanvas = wasmIface->canvas();
1065
1066 std::string contextType = m_glCanvas["constructor"]["name"].as<std::string>();
1067 if (contextType == "CanvasRenderingContext2D")
1068 continue; // skip windows with 2D contexts
1069
1070 if (!ctx->makeCurrent(window))
1071 continue;
1072
1073 // Capture the handle now that this context is current, for use in
1074 // emscripten_webgl_make_context_current() before the C GL calls.
1075 m_glContextHandle = emscripten_webgl_get_current_context();
1076 m_hasWebGLContext = (m_glContextHandle > 0);
1077 break;
1078 }
1079
1080 if (!m_hasWebGLContext)
1081 qWarning() << Q_FUNC_INFO << "could not locate WebGL canvas for the current RHI context";
1082}
1083
1084// framemaker for webgl context
1086{
1087 QWasmVideoOutput *wasmVideoOutput = reinterpret_cast<QWasmVideoOutput *>(context);
1088 if (!wasmVideoOutput || !wasmVideoOutput->isReady())
1089 return;
1090
1091 emscripten_webgl_make_context_current(wasmVideoOutput->m_glContextHandle);
1092
1093 GLuint rawTexId = 0;
1094 glGenTextures(1, &rawTexId);
1095 QGlTextureHandle texHandle{ rawTexId };
1096
1097 glBindTexture(GL_TEXTURE_2D, texHandle.get());
1098
1099 int w = 0, h = 0;
1100 em_texImage2DFromVideo(wasmVideoOutput->m_glCanvas.as_handle(),
1101 wasmVideoOutput->m_videoSurfaceId.c_str(),
1102 &w, &h);
1103
1104 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
1105 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
1106 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
1107 glBindTexture(GL_TEXTURE_2D, 0);
1108
1109 if (!texHandle || w == 0 || h == 0) {
1110 qCWarning(qWasmMediaVideoOutput) << "VideoFrame upload failed";
1111 return;
1112 }
1113
1114 std::unique_ptr<QHwVideoBuffer> hwBuffer =
1115 std::make_unique<QWasmGLTextureVideoBuffer>(std::move(texHandle), QSize(w, h));
1116
1117 QVideoFrame vFrame =
1118 QVideoFramePrivate::createFrame(std::move(hwBuffer),
1119 QVideoFrameFormat(QSize(w, h),
1120 QVideoFrameFormat::Format_RGBA8888));
1121
1122 wasmVideoOutput->m_wasmSink->setVideoFrame(vFrame);
1123}
1124
1125// default fallback for non VideoFrame
1127{
1130
1131 if (isPlatformiOs()) {
1132 emscripten::val stream = m_video["srcObject"];
1133 emscripten::val vTraks = stream.call<emscripten::val>("getVideoTracks");
1134
1135 if (!vTraks.isUndefined() && vTraks["length"].as<int>() > 0) {
1136 emscripten::val trak = vTraks[0];
1137 emscripten::val settings = trak.call<emscripten::val>("getSettings");
1138
1139 if (settings["facingMode"].as<std::string>() == "user")
1140 m_cameraMode = QWasmVideoOutput::Front;
1141 else
1142 m_cameraMode = QWasmVideoOutput::Back;
1143
1144 m_useCameraRotation = true;
1145 }
1146 }
1147
1148 // Single-shot callback: re-registers each frame so multiple QWasmVideoOutput
1149 // instances can coexist. emscripten_request_animation_frame_loop allows only one
1150 // active loop globally and would cancel another instance.
1151 static EM_BOOL (*frame)(double, void *) = [](double frameTime, void *context) -> EM_BOOL {
1152
1153 Q_UNUSED(frameTime);
1154
1155 QWasmVideoOutput *videoOutput = reinterpret_cast<QWasmVideoOutput *>(context);
1156 if (!videoOutput || videoOutput->m_isStopped)
1157 return false;
1158
1159 if (videoOutput->m_currentMediaStatus != MediaStatus::LoadedMedia) {
1160 emscripten_request_animation_frame(frame, context);
1161 return true;
1162 }
1163
1164 emscripten::val videoElement = videoOutput->currentVideoElement();
1165 if (videoElement.isNull() || videoElement.isUndefined())
1166 return false;
1167
1168 if (videoElement["paused"].as<bool>() || videoElement["ended"].as<bool>()
1169 || videoElement["readyState"].as<int>() < 2) {
1170 emscripten_request_animation_frame(frame, context);
1171 return true;
1172 }
1173
1174 if (videoOutput->m_hasVideoFrame) {
1175 if (videoOutput->m_glContextHandle)
1176 videoOutput->webglVideoFrameCallback(context);
1177 else
1178 videoOutput->videoFrameCallback(context);
1179 } else {
1180 videoOutput->videoComputeFrame(context);
1181 }
1182
1183 emscripten_request_animation_frame(frame, context);
1184 return true;
1185 };
1186 if ((!m_isStopped && m_video["className"].as<std::string>() == "Camera" && m_cameraIsReady)
1187 || (!m_isStopped && m_currentVideoMode == QWasmVideoOutput::SurfaceCapture)
1188 || isReady())
1189 emscripten_request_animation_frame(frame, this);
1190}
1191
1192QVideoFrameFormat::PixelFormat QWasmVideoOutput::fromJsPixelFormat(std::string_view videoFormat)
1193{
1194 if (videoFormat == "I420")
1195 return QVideoFrameFormat::Format_YUV420P;
1196 // no equivalent pixel format
1197 // else if (videoFormat == "I420A") // AYUV ?
1198 else if (videoFormat == "I422")
1199 return QVideoFrameFormat::Format_YUV422P;
1200 // no equivalent pixel format
1201 // else if (videoFormat == "I444")
1202 else if (videoFormat == "NV12")
1203 return QVideoFrameFormat::Format_NV12;
1204 else if (videoFormat == "RGBA")
1205 return QVideoFrameFormat::Format_RGBA8888;
1206 else if (videoFormat == "RGBX")
1207 return QVideoFrameFormat::Format_RGBX8888;
1208 else if (videoFormat == "BGRA")
1209 return QVideoFrameFormat::Format_BGRA8888;
1210 else if (videoFormat == "BGRX")
1211 return QVideoFrameFormat::Format_BGRX8888;
1212
1213 return QVideoFrameFormat::Format_Invalid;
1214}
1215
1217{
1218 emscripten::val stream = m_video["srcObject"];
1219 if ((!stream.isNull() && !stream.isUndefined()) && stream["active"].as<bool>()) {
1220 emscripten::val tracks = stream.call<emscripten::val>("getVideoTracks");
1221 if (!tracks.isUndefined()) {
1222 if (tracks["length"].as<int>() == 0)
1223 return emscripten::val::undefined();
1224
1225 emscripten::val track = tracks[0];
1226 if (!track.isUndefined()) {
1227 emscripten::val trackCaps = emscripten::val::undefined();
1228 if (!track["getCapabilities"].isUndefined())
1229 trackCaps = track.call<emscripten::val>("getCapabilities");
1230 else // firefox does not support getCapabilities
1231 trackCaps = track.call<emscripten::val>("getSettings");
1232
1233 if (!trackCaps.isUndefined())
1234 return trackCaps;
1235 }
1236 }
1237 } else {
1238 // camera not started track capabilities not available
1239 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("capabilities not available"));
1240 }
1241
1242 return emscripten::val::undefined();
1243}
1244
1245bool QWasmVideoOutput::setDeviceSetting(const std::string &key, emscripten::val value)
1246{
1247 emscripten::val stream = m_video["srcObject"];
1248 if (stream.isNull() || stream.isUndefined()
1249 || stream["getVideoTracks"].isUndefined())
1250 return false;
1251
1252 emscripten::val tracks = stream.call<emscripten::val>("getVideoTracks");
1253 if (!tracks.isNull() || !tracks.isUndefined()) {
1254 if (tracks["length"].as<int>() == 0)
1255 return false;
1256
1257 emscripten::val track = tracks[0];
1258 emscripten::val contraint = emscripten::val::object();
1259 contraint.set(std::move(key), value);
1260 track.call<emscripten::val>("applyConstraints", contraint);
1261 return true;
1262 }
1263
1264 return false;
1265}
1266
1267QT_END_NAMESPACE
1268
1269#include "moc_qwasmvideooutput_p.cpp"
void addCameraSourceElement(const std::string &id)
void updateVideoElementGeometry(const QRect &windowGeometry)
bool setDeviceSetting(const std::string &key, emscripten::val value)
emscripten::val surfaceElement()
emscripten::val getDeviceCapabilities()
void videoFrameCallback(void *context)
void setVideoSize(const QSize &)
void setMuted(bool muted)
void setSource(const QUrl &url)
void setVideoMode(QWasmVideoOutput::WasmVideoMode mode)
void seekTo(qint64 position)
void webglVideoFrameCallback(void *context)
void orientationChanged(int rotationIndex)
void setVolume(qreal volume)
void createVideoElement(const std::string &id)
void updateVideoElementSource(const QString &src)
void setSource(QIODevice *stream)
void setPlaybackRate(qreal rate)
void createOffscreenElement(const QSize &offscreenSize)
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")
#define GL_CLAMP_TO_EDGE
Definition qopenglext.h:100
static bool checkForVideoFrame()
EM_JS(void, qt_st_sink_loadWorkletModule,(EM_VAL ctxHandle, int callbackId, int channels), { var ctx=Emval.toValue(ctxHandle);var code=[ 'class QtSink extends AudioWorkletProcessor {', ' constructor(opts) {', ' super(opts);', ' this._numChannels=opts.processorOptions.channels|0;', ' this._queue=[];', ' this._pos=0;', ' this.port.onmessage=(e)=> { this._queue.push(e.data);};', ' }', ' process(inputs, outputs) {', ' var out=outputs[0];', ' if(!out||!out.length) return true;', ' var samplesPerChannel=out[0].length;', ' for(var i=0;i< samplesPerChannel;i++) {', ' while(this._queue.length > 0 &&this._pos >=this._queue[0].samplesPerChannel) {', ' this._queue.shift();', ' this._pos=0;', ' }', ' if(this._queue.length===0) break;', ' var frame=this._queue[0];', ' for(var channel=0;channel< out.length &&channel< frame.numChannels;channel++)', ' out[channel][i]=frame.data[channel *frame.samplesPerChannel+this._pos];', ' this._pos++;', ' }', ' this.port.postMessage(null);', ' return true;', ' }', '}', 'registerProcessor("qt-audio-sink", QtSink);'].join('\n');var blob=new Blob([code], { type:'application/javascript' });var url=URL.createObjectURL(blob);ctx.audioWorklet.addModule(url).then(function() { URL.revokeObjectURL(url);Module._qt_sinkWorkletReady(callbackId);});})