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