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