Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
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 "qwasmvideooutput_p.h"
14
15#include <qvideosink.h>
16#include <private/qabstractvideobuffer_p.h>
17#include <private/qplatformvideosink_p.h>
18#include <private/qmemoryvideobuffer_p.h>
19#include <private/qvideotexturehelper_p.h>
20#include <private/qstdweb_p.h>
21#include <QTimer>
22
23#include <emscripten/bind.h>
24#include <emscripten/html5.h>
25#include <emscripten/val.h>
26
27
29
30
31using namespace emscripten;
32
33Q_LOGGING_CATEGORY(qWasmMediaVideoOutput, "qt.multimedia.wasm.videooutput")
34
35// TODO unique videosurface ?
36static std::string m_videoSurfaceId;
37
39{
41 // large videos will leave the unloading window
42 // in a frozen state, so remove the video element first
43 emscripten::val document = emscripten::val::global("document");
44 emscripten::val videoElement =
45 document.call<emscripten::val>("getElementById", std::string(m_videoSurfaceId));
46 videoElement.call<void>("removeAttribute", emscripten::val("src"));
47 videoElement.call<void>("load");
48}
49
51{
52 emscripten::function("mbeforeUnload", qtVideoBeforeUnload);
53}
54
55static bool checkForVideoFrame()
56{
57 emscripten::val videoFrame = emscripten::val::global("VideoFrame");
58 return (!videoFrame.isNull() && !videoFrame.isUndefined());
59}
60
62
64{
65}
66
68{
69 if (m_pendingVideoSize == newSize)
70 return;
71
72 m_pendingVideoSize = newSize;
73 updateVideoElementGeometry(QRect(0, 0, m_pendingVideoSize.width(), m_pendingVideoSize.height()));
74}
75
80
82{
83 if (m_video.isUndefined() || m_video.isNull()
84 || !m_wasmSink) {
85 // error
87 return;
88 }
89
90 switch (m_currentVideoMode) {
92 emscripten::val sourceObj = m_video["src"];
93 if ((sourceObj.isUndefined() || sourceObj.isNull()) && !m_source.isEmpty()) {
94 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << "calling load" << m_source;
95 m_video.set("src", m_source);
96 m_video.call<void>("load");
97 }
98 } break;
100 if (!m_cameraIsReady) {
101 m_shouldBeStarted = true;
102 }
103
104 emscripten::val stream = m_video["srcObject"];
105 if (stream.isNull() || stream.isUndefined()) { // camera device
106 qCDebug(qWasmMediaVideoOutput) << "ERROR";
108 return;
109 } else {
110 emscripten::val videoTracks = stream.call<emscripten::val>("getVideoTracks");
111 if (videoTracks.isNull() || videoTracks.isUndefined()) {
112 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << "videoTracks is null";
114 QStringLiteral("video surface error"));
115 return;
116 }
117 if (videoTracks["length"].as<int>() == 0) {
118 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << "videoTracks count is 0";
120 QStringLiteral("video surface error"));
121 return;
122 }
123 emscripten::val videoSettings = videoTracks[0].call<emscripten::val>("getSettings");
124 if (!videoSettings.isNull() || !videoSettings.isUndefined()) {
125 // double fRate = videoSettings["frameRate"].as<double>(); TODO
126 const int width = videoSettings["width"].as<int>();
127 const int height = videoSettings["height"].as<int>();
128
129 qCDebug(qWasmMediaVideoOutput)
130 << "width" << width << "height" << height;
131
133 }
134 }
135 } break;
136 };
137
138 m_shouldStop = false;
139 m_toBePaused = false;
140 m_video.call<void>("play");
141
142 if (m_currentVideoMode == QWasmVideoOutput::Camera) {
143 if (m_hasVideoFrame) {
144 m_video.call<emscripten::val>("requestVideoFrameCallback",
145 emscripten::val::module_property("qtVideoFrameTimerCallback"));
146 } else {
148 }
149 }
150}
151
153{
154 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO;
155
156 if (m_video.isUndefined() || m_video.isNull()) {
157 // error
159 return;
160 }
161 m_shouldStop = true;
162 if (m_toBePaused) {
163 // we are stopped , need to reset
164 m_toBePaused = false;
165 m_video.call<void>("load");
166 } else {
167 m_video.call<void>("pause");
168 }
169}
170
172{
173 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO;
174
175 if (m_video.isUndefined() || m_video.isNull()) {
176 // error
178 return;
179 }
180 m_shouldStop = false;
181 m_toBePaused = true;
182 m_video.call<void>("pause");
183}
184
186{
187 // flush pending frame
188 if (m_wasmSink)
190
191 m_source = "";
192 m_video.set("currentTime", emscripten::val(0));
193 m_video.call<void>("load");
194}
195
197{
198 return m_video;
199}
200
202{
203 if (!surface || surface == m_wasmSink) {
204 qWarning() << "Surface not ready";
205 return;
206 }
207
208 m_wasmSink = surface;
209}
210
212{
213 if (m_video.isUndefined() || m_video.isNull()) {
214 // error
215 return false;
216 }
217
218 constexpr int hasCurrentData = 2;
219 if (!m_video.isUndefined() || !m_video.isNull())
220 return m_video["readyState"].as<int>() >= hasCurrentData;
221 else
222 return true;
223}
224
226{
227 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << url;
228
229 if (m_video.isUndefined() || m_video.isNull()) {
230 return;
231 }
232
233 m_source = url.toString();
234 if (url.isEmpty()) {
235 stop();
236 return;
237 }
238 if (url.isLocalFile()) {
239 QFile localFile(url.toLocalFile());
240 if (localFile.open(QIODevice::ReadOnly)) {
241 QDataStream buffer(&localFile); // we will serialize the data into the file
242 setSource(buffer.device());
243 } else {
244 qWarning() << "Failed to open file";
245 }
246 return;
247 }
248
249 updateVideoElementSource(m_source);
250}
251
253{
254 m_video.set("src", src.toStdString());
255 m_video.call<void>("load");
256}
257
259{
260 m_cameraIsReady = false;
261 emscripten::val navigator = emscripten::val::global("navigator");
262 emscripten::val mediaDevices = navigator["mediaDevices"];
263
264 if (mediaDevices.isNull() || mediaDevices.isUndefined()) {
265 qWarning() << "No media devices found";
267 return;
268 }
269
270 qstdweb::PromiseCallbacks getUserMediaCallback{
271 .thenFunc =
272 [this](emscripten::val stream) {
273 qCDebug(qWasmMediaVideoOutput) << "getUserMediaSuccess";
274
275 m_video.set("srcObject", stream);
276 m_cameraIsReady = true;
277 if (m_shouldBeStarted) {
278 start();
279 m_shouldBeStarted = false;
280 }
281 },
282 .catchFunc =
283 [](emscripten::val error) {
284 qCDebug(qWasmMediaVideoOutput)
285 << "getUserMedia fail"
286 << QString::fromStdString(error["name"].as<std::string>())
287 << QString::fromStdString(error["message"].as<std::string>());
288 }
289 };
290
291 emscripten::val constraints = emscripten::val::object();
292
293 constraints.set("audio", m_hasAudio);
294
295 emscripten::val videoContraints = emscripten::val::object();
296 videoContraints.set("exact", id);
297 videoContraints.set("deviceId", id);
298 constraints.set("video", videoContraints);
299
300 // we do it this way as this prompts user for mic/camera permissions
301 qstdweb::Promise::make(mediaDevices, QStringLiteral("getUserMedia"),
302 std::move(getUserMediaCallback), constraints);
303}
304
306{
307 if (stream->bytesAvailable() == 0) {
308 qWarning() << "data not available";
310 return;
311 }
312 if (m_video.isUndefined() || m_video.isNull()) {
314 return;
315 }
316
319
320 QByteArray buffer = stream->readAll();
321
322 qstdweb::Blob contentBlob = qstdweb::Blob::copyFrom(buffer.data(), buffer.size(), mime.name().toStdString());
323
324 emscripten::val window = qstdweb::window();
325
326 if (window["safari"].isUndefined()) {
327 emscripten::val contentUrl = window["URL"].call<emscripten::val>("createObjectURL", contentBlob.val());
328 m_video.set("src", contentUrl);
329 m_source = QString::fromStdString(contentUrl.as<std::string>());
330 } else {
331 // only Safari currently supports Blob with srcObject
332 m_video.set("srcObject", contentBlob.val());
333 }
334}
335
337{ // between 0 - 1
338 volume = qBound(qreal(0.0), volume, qreal(1.0));
339 m_video.set("volume", volume);
340}
341
343{
344 if (m_video.isUndefined() || m_video.isNull()) {
345 // error
347 return;
348 }
349 m_video.set("muted", muted);
350}
351
353{
354 return (!m_video.isUndefined() || !m_video.isNull())
355 ? (m_video["currentTime"].as<double>() * 1000)
356 : 0;
357}
358
360{
361 if (isVideoSeekable()) {
362 float positionToSetInSeconds = float(positionMSecs) / 1000;
363 emscripten::val seekableTimeRange = m_video["seekable"];
364 if (!seekableTimeRange.isNull() || !seekableTimeRange.isUndefined()) {
365 // range user can seek
366 if (seekableTimeRange["length"].as<int>() < 1)
367 return;
368 if (positionToSetInSeconds
369 >= seekableTimeRange.call<emscripten::val>("start", 0).as<double>()
370 && positionToSetInSeconds
371 <= seekableTimeRange.call<emscripten::val>("end", 0).as<double>()) {
372 m_requestedPosition = positionToSetInSeconds;
373
374 m_video.set("currentTime", m_requestedPosition);
375 }
376 }
377 }
378 qCDebug(qWasmMediaVideoOutput) << "m_requestedPosition" << m_requestedPosition;
379}
380
382{
383 if (m_video.isUndefined() || m_video.isNull()) {
384 // error
386 return false;
387 }
388
389 emscripten::val seekableTimeRange = m_video["seekable"];
390 if (seekableTimeRange["length"].as<int>() < 1)
391 return false;
392 if (!seekableTimeRange.isNull() || !seekableTimeRange.isUndefined()) {
393 bool isit = !qFuzzyCompare(seekableTimeRange.call<emscripten::val>("start", 0).as<double>(),
394 seekableTimeRange.call<emscripten::val>("end", 0).as<double>());
395 return isit;
396 }
397 return false;
398}
399
400void QWasmVideoOutput::createVideoElement(const std::string &id)
401{
402 // TODO: there can be more than one element !!
403 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << id;
404 // Create <video> element and add it to the page body
405 emscripten::val document = emscripten::val::global("document");
406 emscripten::val body = document["body"];
407
408 emscripten::val oldVideo = document.call<emscripten::val>("getElementsByClassName",
409 (m_currentVideoMode == QWasmVideoOutput::Camera
410 ? std::string("Camera")
411 : std::string("Video")));
412
413 // we don't provide alternate tracks
414 // but need to remove stale track
415 if (oldVideo["length"].as<int>() > 0)
416 oldVideo[0].call<void>("remove");
417
419 m_video = document.call<emscripten::val>("createElement", std::string("video"));
420
421 m_video.set("id", m_videoSurfaceId.c_str());
422 m_video.call<void>("setAttribute", std::string("class"),
423 (m_currentVideoMode == QWasmVideoOutput::Camera ? std::string("Camera")
424 : std::string("Video")));
425 m_video.set("data-qvideocontext",
426 emscripten::val(quintptr(reinterpret_cast<void *>(this))));
427
428 // if video
429 m_video.set("preload", "metadata");
430
431 // Uncaught DOMException: Failed to execute 'getImageData' on
432 // 'OffscreenCanvasRenderingContext2D': The canvas has been tainted by
433 // cross-origin data.
434 // TODO figure out somehow to let user choose between these
435 std::string originString = "anonymous"; // requires server Access-Control-Allow-Origin *
436 // std::string originString = "use-credentials"; // must not
437 // Access-Control-Allow-Origin *
438
439 m_video.call<void>("setAttribute", std::string("crossorigin"), originString);
440 body.call<void>("appendChild", m_video);
441
442 // Create/add video source
443 document.call<emscripten::val>("createElement",
444 std::string("source")).set("src", m_source.toStdString());
445
446 // Set position:absolute, which makes it possible to position the video
447 // element using x,y. coordinates, relative to its parent (the page's <body>
448 // element)
449 emscripten::val style = m_video["style"];
450 style.set("position", "absolute");
451 style.set("display", "none"); // hide
452
453 if (!m_source.isEmpty())
454 updateVideoElementSource(m_source);
455}
456
458{
459 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO;
460
461 if (m_hasVideoFrame) // VideoFrame does not require offscreen canvas/context
462 return;
463
464 // create offscreen element for grabbing frames
465 // OffscreenCanvas - no safari :(
466 // https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
467
468 emscripten::val document = emscripten::val::global("document");
469
470 // TODO use correct frameBytesAllocationSize?
471 // offscreen render buffer
472 m_offscreen = emscripten::val::global("OffscreenCanvas");
473
474 if (m_offscreen.isUndefined()) {
475 // Safari OffscreenCanvas not supported, try old skool way
476 m_offscreen = document.call<emscripten::val>("createElement", std::string("canvas"));
477
478 m_offscreen.set("style",
479 "position:absolute;left:-1000px;top:-1000px"); // offscreen
480 m_offscreen.set("width", offscreenSize.width());
481 m_offscreen.set("height", offscreenSize.height());
482 m_offscreenContext = m_offscreen.call<emscripten::val>("getContext", std::string("2d"));
483 } else {
484 m_offscreen = emscripten::val::global("OffscreenCanvas")
485 .new_(offscreenSize.width(), offscreenSize.height());
486 emscripten::val offscreenAttributes = emscripten::val::array();
487 offscreenAttributes.set("willReadFrequently", true);
488 m_offscreenContext = m_offscreen.call<emscripten::val>("getContext", std::string("2d"),
489 offscreenAttributes);
490 }
491 std::string offscreenId = m_videoSurfaceId + "_offscreenOutputSurface";
492 m_offscreen.set("id", offscreenId.c_str());
493}
494
496{
497 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO;
498
499 // event callbacks
500 // timupdate
501 auto timeUpdateCallback = [=](emscripten::val event) {
502 qCDebug(qWasmMediaVideoOutput) << "timeupdate";
503
504 // qt progress is ms
505 emit progressChanged(event["target"]["currentTime"].as<double>() * 1000);
506 };
507 m_timeUpdateEvent.reset(new qstdweb::EventCallback(m_video, "timeupdate", timeUpdateCallback));
508
509 // play
510 auto playCallback = [=](emscripten::val event) {
512 qCDebug(qWasmMediaVideoOutput) << "play" << m_video["src"].as<std::string>();
513 if (!m_isSeeking)
515 };
516 m_playEvent.reset(new qstdweb::EventCallback(m_video, "play", playCallback));
517
518 // ended
519 auto endedCallback = [=](emscripten::val event) {
521 qCDebug(qWasmMediaVideoOutput) << "ended";
522 m_currentMediaStatus = QMediaPlayer::EndOfMedia;
523 emit statusChanged(m_currentMediaStatus);
524 m_shouldStop = true;
525 stop();
526 };
527 m_endedEvent.reset(new qstdweb::EventCallback(m_video, "ended", endedCallback));
528
529 // durationchange
530 auto durationChangeCallback = [=](emscripten::val event) {
531 qCDebug(qWasmMediaVideoOutput) << "durationChange";
532
533 // qt duration is in milliseconds.
534 qint64 dur = event["target"]["duration"].as<double>() * 1000;
536 };
537 m_durationChangeEvent.reset(
538 new qstdweb::EventCallback(m_video, "durationchange", durationChangeCallback));
539
540 // loadeddata
541 auto loadedDataCallback = [=](emscripten::val event) {
543 qCDebug(qWasmMediaVideoOutput) << "loaded data";
544
546 };
547 m_loadedDataEvent.reset(new qstdweb::EventCallback(m_video, "loadeddata", loadedDataCallback));
548
549 // error
550 auto errorCallback = [=](emscripten::val event) {
551 qCDebug(qWasmMediaVideoOutput) << "error";
552 if (event.isUndefined() || event.isNull())
553 return;
554 emit errorOccured(m_video["error"]["code"].as<int>(),
555 QString::fromStdString(m_video["error"]["message"].as<std::string>()));
556 };
557 m_errorChangeEvent.reset(new qstdweb::EventCallback(m_video, "error", errorCallback));
558
559 // resize
560 auto resizeCallback = [=](emscripten::val event) {
562 qCDebug(qWasmMediaVideoOutput) << "resize";
563
565 QRect(0, 0, m_video["videoWidth"].as<int>(), m_video["videoHeight"].as<int>()));
566 emit sizeChange(m_video["videoWidth"].as<int>(), m_video["videoHeight"].as<int>());
567
568 };
569 m_resizeChangeEvent.reset(new qstdweb::EventCallback(m_video, "resize", resizeCallback));
570
571 // loadedmetadata
572 auto loadedMetadataCallback = [=](emscripten::val event) {
574 qCDebug(qWasmMediaVideoOutput) << "loaded meta data";
575
577 };
578 m_loadedMetadataChangeEvent.reset(
579 new qstdweb::EventCallback(m_video, "loadedmetadata", loadedMetadataCallback));
580
581 // loadstart
582 auto loadStartCallback = [=](emscripten::val event) {
584 qCDebug(qWasmMediaVideoOutput) << "load started";
585 m_currentMediaStatus = QMediaPlayer::LoadingMedia;
586 emit statusChanged(m_currentMediaStatus);
587 m_shouldStop = false;
588 };
589 m_loadStartChangeEvent.reset(new qstdweb::EventCallback(m_video, "loadstart", loadStartCallback));
590
591 // canplay
592
593 auto canPlayCallback = [=](emscripten::val event) {
594 if (event.isUndefined() || event.isNull())
595 return;
596 qCDebug(qWasmMediaVideoOutput) << "can play"
597 << "m_requestedPosition" << m_requestedPosition;
598
599 if (!m_shouldStop)
600 emit readyChanged(true); // sets video available
601 };
602 m_canPlayChangeEvent.reset(new qstdweb::EventCallback(m_video, "canplay", canPlayCallback));
603
604 // canplaythrough
605 auto canPlayThroughCallback = [=](emscripten::val event) {
607 qCDebug(qWasmMediaVideoOutput) << "can play through"
608 << "m_shouldStop" << m_shouldStop;
609
610 if (m_currentMediaStatus == QMediaPlayer::EndOfMedia)
611 return;
612 if (!m_isSeeking && !m_shouldStop) {
613 emscripten::val timeRanges = m_video["buffered"];
614 if ((!timeRanges.isNull() || !timeRanges.isUndefined())
615 && timeRanges["length"].as<int>() == 1) {
616 double buffered = m_video["buffered"].call<emscripten::val>("end", 0).as<double>();
617 const double duration = m_video["duration"].as<double>();
618
619 if (duration == buffered) {
620 m_currentBufferedValue = 100;
621 emit bufferingChanged(m_currentBufferedValue);
622 }
623 }
624 m_currentMediaStatus = QMediaPlayer::LoadedMedia;
625 emit statusChanged(m_currentMediaStatus);
626 if (m_hasVideoFrame) {
627 m_video.call<emscripten::val>("requestVideoFrameCallback",
628 emscripten::val::module_property("qtVideoFrameTimerCallback"));
629 } else {
631 }
632 } else {
633 m_shouldStop = false;
634 }
635 };
636 m_canPlayThroughChangeEvent.reset(
637 new qstdweb::EventCallback(m_video, "canplaythrough", canPlayThroughCallback));
638
639 // seeking
640 auto seekingCallback = [=](emscripten::val event) {
642 qCDebug(qWasmMediaVideoOutput)
643 << "seeking started" << (m_video["currentTime"].as<double>() * 1000);
644 m_isSeeking = true;
645 };
646 m_seekingChangeEvent.reset(new qstdweb::EventCallback(m_video, "seeking", seekingCallback));
647
648 // seeked
649 auto seekedCallback = [=](emscripten::val event) {
651 qCDebug(qWasmMediaVideoOutput) << "seeked" << (m_video["currentTime"].as<double>() * 1000);
652 emit progressChanged(m_video["currentTime"].as<double>() * 1000);
653 m_isSeeking = false;
654 };
655 m_seekedChangeEvent.reset(new qstdweb::EventCallback(m_video, "seeked", seekedCallback));
656
657 // emptied
658 auto emptiedCallback = [=](emscripten::val event) {
660 qCDebug(qWasmMediaVideoOutput) << "emptied";
661 emit readyChanged(false);
662 m_currentMediaStatus = QMediaPlayer::EndOfMedia;
663 emit statusChanged(m_currentMediaStatus);
664 };
665 m_emptiedChangeEvent.reset(new qstdweb::EventCallback(m_video, "emptied", emptiedCallback));
666
667 // stalled
668 auto stalledCallback = [=](emscripten::val event) {
670 qCDebug(qWasmMediaVideoOutput) << "stalled";
671 m_currentMediaStatus = QMediaPlayer::StalledMedia;
672 emit statusChanged(m_currentMediaStatus);
673 };
674 m_stalledChangeEvent.reset(new qstdweb::EventCallback(m_video, "stalled", stalledCallback));
675
676 // waiting
677 auto waitingCallback = [=](emscripten::val event) {
679
680 qCDebug(qWasmMediaVideoOutput) << "waiting";
681 // check buffer
682 };
683 m_waitingChangeEvent.reset(new qstdweb::EventCallback(m_video, "waiting", waitingCallback));
684
685 // suspend
686
687 // playing
688 auto playingCallback = [=](emscripten::val event) {
690 qCDebug(qWasmMediaVideoOutput) << "playing";
691 if (m_isSeeking)
692 return;
694 if (m_toBePaused || !m_shouldStop) { // paused
695 m_toBePaused = false;
696
697 if (m_hasVideoFrame) {
698 m_video.call<emscripten::val>("requestVideoFrameCallback",
699 emscripten::val::module_property("qtVideoFrameTimerCallback"));
700 } else {
701 videoFrameTimerCallback(); // get the ball rolling
702 }
703 }
704 };
705 m_playingChangeEvent.reset(new qstdweb::EventCallback(m_video, "playing", playingCallback));
706
707 // progress (buffering progress)
708 auto progesssCallback = [=](emscripten::val event) {
709 if (event.isUndefined() || event.isNull())
710 return;
711
712 const double duration = event["target"]["duration"].as<double>();
713 if (duration < 0) // track not exactly ready yet
714 return;
715
716 emscripten::val timeRanges = event["target"]["buffered"];
717
718 if ((!timeRanges.isNull() || !timeRanges.isUndefined())
719 && timeRanges["length"].as<int>() == 1) {
720 emscripten::val dVal = timeRanges.call<emscripten::val>("end", 0);
721 if (!dVal.isNull() || !dVal.isUndefined()) {
722 double bufferedEnd = dVal.as<double>();
723
724 if (duration > 0 && bufferedEnd > 0) {
725 const double bufferedValue = (bufferedEnd / duration * 100);
726 qCDebug(qWasmMediaVideoOutput) << "progress buffered";
727 m_currentBufferedValue = bufferedValue;
728 emit bufferingChanged(m_currentBufferedValue);
729 if (bufferedEnd == duration)
730 m_currentMediaStatus = QMediaPlayer::BufferedMedia;
731 else
732 m_currentMediaStatus = QMediaPlayer::BufferingMedia;
733 emit statusChanged(m_currentMediaStatus);
734 }
735 }
736 }
737 };
738 m_progressChangeEvent.reset(new qstdweb::EventCallback(m_video, "progress", progesssCallback));
739
740 // pause
741 auto pauseCallback = [=](emscripten::val event) {
743 qCDebug(qWasmMediaVideoOutput) << "pause";
744
745 const double currentTime = m_video["currentTime"].as<double>(); // in seconds
746 const double duration = m_video["duration"].as<double>(); // in seconds
747 if ((currentTime > 0 && currentTime < duration) && (!m_shouldStop && m_toBePaused)) {
749 } else {
750 // stop this crazy thing!
751 m_video.set("currentTime", emscripten::val(0));
753 }
754 };
755 m_pauseChangeEvent.reset(new qstdweb::EventCallback(m_video, "pause", pauseCallback));
756
757 // onunload
758 // we use lower level events here as to avert a crash on activate using the
759 // qtdweb see _qt_beforeUnload
760 emscripten::val window = emscripten::val::global("window");
761 window.call<void>("addEventListener", std::string("beforeunload"),
762 emscripten::val::module_property("mbeforeUnload"));
763}
764
766{
767 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << windowGeometry;
768 QRect m_videoElementSource(windowGeometry.topLeft(), windowGeometry.size());
769
770 emscripten::val style = m_video["style"];
771 style.set("left", QString("%1px").arg(m_videoElementSource.left()).toStdString());
772 style.set("top", QString("%1px").arg(m_videoElementSource.top()).toStdString());
773 style.set("width", QString("%1px").arg(m_videoElementSource.width()).toStdString());
774 style.set("height", QString("%1px").arg(m_videoElementSource.height()).toStdString());
775 style.set("z-index", "999");
776
777 if (!m_hasVideoFrame) {
778 // offscreen
779 m_offscreen.set("width", m_videoElementSource.width());
780 m_offscreen.set("height", m_videoElementSource.height());
781 }
782}
783
785{
786 // qt duration is in ms
787 // js is sec
788
789 if (m_video.isUndefined() || m_video.isNull())
790 return 0;
791 return m_video["duration"].as<double>() * 1000;
792}
793
798
800{
801 m_video.set("playbackRate", emscripten::val(rate));
802}
803
805{
806 return (m_video.isUndefined() || m_video.isNull()) ? 0 : m_video["playbackRate"].as<float>();
807}
808
809void QWasmVideoOutput::checkNetworkState()
810{
811 int netState = m_video["networkState"].as<int>();
812
813 qCDebug(qWasmMediaVideoOutput) << netState;
814
815 switch (netState) {
817 break;
819 break;
821 break;
823 emit errorOccured(netState, QStringLiteral("No media source found"));
824 break;
825 };
826}
827
828void QWasmVideoOutput::videoComputeFrame(void *context)
829{
830 if (m_offscreenContext.isUndefined() || m_offscreenContext.isNull()) {
831 qCDebug(qWasmMediaVideoOutput) << "offscreen canvas context could not be found";
832 return;
833 }
834 emscripten::val document = emscripten::val::global("document");
835
836 emscripten::val videoElement =
837 document.call<emscripten::val>("getElementById", std::string(m_videoSurfaceId));
838
839 if (videoElement.isUndefined() || videoElement.isNull()) {
840 qCDebug(qWasmMediaVideoOutput) << "video element could not be found";
841 return;
842 }
843
844 const int videoWidth = videoElement["videoWidth"].as<int>();
845 const int videoHeight = videoElement["videoHeight"].as<int>();
846
847 if (videoWidth == 0 || videoHeight == 0)
848 return;
849
850 m_offscreenContext.call<void>("drawImage", videoElement, 0, 0, videoWidth, videoHeight);
851
852 emscripten::val frame = // one frame, Uint8ClampedArray
853 m_offscreenContext.call<emscripten::val>("getImageData", 0, 0, videoWidth, videoHeight);
854
855 const QSize frameBytesAllocationSize(videoWidth, videoHeight);
856
857 // this seems to work ok, even though getImageData returns a Uint8ClampedArray
859
860 QVideoFrameFormat frameFormat =
861 QVideoFrameFormat(frameBytesAllocationSize, QVideoFrameFormat::Format_RGBA8888);
862
864
865 QVideoFrame vFrame(
866 new QMemoryVideoBuffer(frameBytes,
868 frameFormat);
869 QWasmVideoOutput *wasmVideoOutput = reinterpret_cast<QWasmVideoOutput *>(context);
870
871 if (!wasmVideoOutput->m_wasmSink) {
872 qWarning() << "ERROR ALERT!! video sink not set";
873 }
874 wasmVideoOutput->m_wasmSink->setVideoFrame(vFrame);
875}
876
877
878void QWasmVideoOutput::videoFrameCallback(emscripten::val now, emscripten::val metadata)
879{
881 Q_UNUSED(metadata)
882
883 emscripten::val videoElement =
884 emscripten::val::global("document").
885 call<emscripten::val>("getElementById",
886 std::string(m_videoSurfaceId));
887
888 emscripten::val oneVideoFrame = val::global("VideoFrame").new_(videoElement);
889
890 if (oneVideoFrame.isNull() || oneVideoFrame.isUndefined()) {
891 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO
892 << "ERROR" << "failed to construct VideoFrame";
893 return;
894 }
895 emscripten::val frameBytesAllocationSize = oneVideoFrame.call<emscripten::val>("allocationSize");
896
897 emscripten::val frameBuffer =
898 emscripten::val::global("Uint8Array").new_(frameBytesAllocationSize);
899 QWasmVideoOutput *wasmVideoOutput =
900 reinterpret_cast<QWasmVideoOutput*>(videoElement["data-qvideocontext"].as<quintptr>());
901
902 qstdweb::PromiseCallbacks copyToCallback;
903 copyToCallback.thenFunc = [wasmVideoOutput, oneVideoFrame, frameBuffer, videoElement]
904 (emscripten::val frameLayout)
905 {
906 if (frameLayout.isNull() || frameLayout.isUndefined()) {
907 qCDebug(qWasmMediaVideoOutput) << "theres no frameLayout";
908 return;
909 }
910
911 // frameBuffer now has a new frame, send to Qt
912 const QSize frameSize(oneVideoFrame["displayWidth"].as<int>(),
913 oneVideoFrame["displayHeight"].as<int>());
914
915
916 QByteArray frameBytes = QByteArray::fromEcmaUint8Array(frameBuffer);
917
918 QVideoFrameFormat::PixelFormat pixelFormat = fromJsPixelFormat(oneVideoFrame["format"].as<std::string>());
919 if (pixelFormat == QVideoFrameFormat::Format_Invalid) {
920 qWarning() << "Invalid pixel format";
921 return;
922 }
923 QVideoFrameFormat frameFormat = QVideoFrameFormat(frameSize, pixelFormat);
924
925 auto *textureDescription = QVideoTextureHelper::textureDescription(frameFormat.pixelFormat());
926
927 QVideoFrame vFrame(
928 new QMemoryVideoBuffer(frameBytes,
929 textureDescription->strideForWidth(frameFormat.frameWidth())),
930 frameFormat);
931
932 if (!wasmVideoOutput) {
933 qCDebug(qWasmMediaVideoOutput) << "ERROR:"
934 << "data-qvideocontext not found";
935 return;
936 }
937 if (!wasmVideoOutput->m_wasmSink) {
938 qWarning() << "ERROR ALERT!! video sink not set";
939 return;
940 }
941 wasmVideoOutput->m_wasmSink->setVideoFrame(vFrame);
942 oneVideoFrame.call<emscripten::val>("close");
943 };
944 copyToCallback.catchFunc = [&, wasmVideoOutput, oneVideoFrame, videoElement](emscripten::val error)
945 {
946 qCDebug(qWasmMediaVideoOutput) << "Error"
947 << QString::fromStdString(error["name"].as<std::string>())
948 << QString::fromStdString(error["message"].as<std::string>()) ;
949
950 oneVideoFrame.call<emscripten::val>("close");
951 wasmVideoOutput->stop();
952 return;
953 };
954
955 qstdweb::Promise::make(oneVideoFrame, "copyTo", std::move(copyToCallback), frameBuffer);
956
957 videoElement.call<emscripten::val>("requestVideoFrameCallback",
958 emscripten::val::module_property("qtVideoFrameTimerCallback"));
959
960}
961
963{
964 static auto frame = [](double frameTime, void *context) -> int {
965 Q_UNUSED(frameTime);
966 QWasmVideoOutput *videoOutput = reinterpret_cast<QWasmVideoOutput *>(context);
967
968 emscripten::val document = emscripten::val::global("document");
969 emscripten::val videoElement =
970 document.call<emscripten::val>("getElementById", std::string(m_videoSurfaceId));
971
972 if (videoElement["paused"].as<bool>() || videoElement["ended"].as<bool>())
973 return false;
974
975 videoOutput->videoComputeFrame(context);
976
977 return true;
978 };
979
980 emscripten_request_animation_frame_loop(frame, this);
981 // about 60 fps
982}
983
984
985QVideoFrameFormat::PixelFormat QWasmVideoOutput::fromJsPixelFormat(std::string videoFormat)
986{
987 if (videoFormat == "I420")
989 // no equivalent pixel format
990 // else if (videoFormat == "I420A")
991 else if (videoFormat == "I422")
993 // no equivalent pixel format
994 // else if (videoFormat == "I444")
995 else if (videoFormat == "NV12")
997 else if (videoFormat == "RGBA")
999 else if (videoFormat == "I420")
1001 else if (videoFormat == "RGBX")
1003 else if (videoFormat == "BGRA")
1005 else if (videoFormat == "BGRX")
1007
1009}
1010
1011
1013{
1014 emscripten::val stream = m_video["srcObject"];
1015 if (!stream.isUndefined() || !stream["getVideoTracks"].isUndefined()) {
1016 emscripten::val tracks = stream.call<emscripten::val>("getVideoTracks");
1017 if (!tracks.isUndefined()) {
1018 if (tracks["length"].as<int>() == 0)
1019 return emscripten::val::undefined();
1020
1021 emscripten::val track = tracks[0];
1022 if (!track.isUndefined()) {
1023 emscripten::val trackCaps = emscripten::val::undefined();
1024 if (!track["getCapabilities"].isUndefined())
1025 trackCaps = track.call<emscripten::val>("getCapabilities");
1026 else // firefox does not support getCapabilities
1027 trackCaps = track.call<emscripten::val>("getSettings");
1028
1029 if (!trackCaps.isUndefined())
1030 return trackCaps;
1031 }
1032 }
1033 } else {
1034 // camera not started track capabilities not available
1035 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("capabilities not available"));
1036 }
1037
1038 return emscripten::val::undefined();
1039}
1040
1041bool QWasmVideoOutput::setDeviceSetting(const std::string &key, emscripten::val value)
1042{
1043 emscripten::val stream = m_video["srcObject"];
1044 if (stream.isNull() || stream.isUndefined()
1045 || stream["getVideoTracks"].isUndefined())
1046 return false;
1047
1048 emscripten::val tracks = stream.call<emscripten::val>("getVideoTracks");
1049 if (!tracks.isNull() || !tracks.isUndefined()) {
1050 if (tracks["length"].as<int>() == 0)
1051 return false;
1052
1053 emscripten::val track = tracks[0];
1054 emscripten::val contraint = emscripten::val::object();
1055 contraint.set(std::move(key), value);
1056 track.call<emscripten::val>("applyConstraints", contraint);
1057 return true;
1058 }
1059
1060 return false;
1061}
1062
1063EMSCRIPTEN_BINDINGS(qtwasmvideooutput) {
1064 emscripten::function("qtVideoFrameTimerCallback", &QWasmVideoOutput::videoFrameCallback);
1065}
1066
1068
1069#include "moc_qwasmvideooutput_p.cpp"
\inmodule QtCore
Definition qbytearray.h:57
\inmodule QtCore\reentrant
Definition qdatastream.h:46
\inmodule QtCore
Definition qfile.h:93
\inmodule QtCore \reentrant
Definition qiodevice.h:34
The QMemoryVideoBuffer class provides a system memory allocated video data buffer.
\inmodule QtCore
QMimeType mimeTypeForData(const QByteArray &data) const
Returns a MIME type for data.
\inmodule QtCore
Definition qmimetype.h:25
\inmodule QtCore
Definition qobject.h:103
virtual void setVideoFrame(const QVideoFrame &frame)
\inmodule QtCore\reentrant
Definition qrect.h:30
constexpr int height() const noexcept
Returns the height of the rectangle.
Definition qrect.h:239
constexpr QPoint topLeft() const noexcept
Returns the position of the rectangle's top-left corner.
Definition qrect.h:221
constexpr int top() const noexcept
Returns the y-coordinate of the rectangle's top edge.
Definition qrect.h:176
constexpr int left() const noexcept
Returns the x-coordinate of the rectangle's left edge.
Definition qrect.h:173
constexpr QSize size() const noexcept
Returns the size of the rectangle.
Definition qrect.h:242
constexpr int width() const noexcept
Returns the width of the rectangle.
Definition qrect.h:236
void reset(T *other=nullptr) noexcept(noexcept(Cleanup::cleanup(std::declval< T * >())))
Deletes the existing object it is pointing to (if any), and sets its pointer to other.
\inmodule QtCore
Definition qsize.h:25
constexpr int height() const noexcept
Returns the height.
Definition qsize.h:133
constexpr int width() const noexcept
Returns the width.
Definition qsize.h:130
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
static QString fromStdString(const std::string &s)
Definition qstring.h:1447
bool isEmpty() const noexcept
Returns true if the string has no characters; otherwise returns false.
Definition qstring.h:192
std::string toStdString() const
Returns a std::string object with the data contained in this QString.
Definition qstring.h:1444
\inmodule QtCore
Definition qurl.h:94
bool isLocalFile() const
Definition qurl.cpp:3445
bool isEmpty() const
Returns true if the URL has no data; otherwise returns false.
Definition qurl.cpp:1896
QString toString(FormattingOptions options=FormattingOptions(PrettyDecoded)) const
Returns a string representation of the URL.
Definition qurl.cpp:2831
QString toLocalFile() const
Returns the path of this URL formatted as a local file path.
Definition qurl.cpp:3425
The QVideoFrameFormat class specifies the stream format of a video presentation surface.
PixelFormat
Enumerates video data types.
QVideoFrameFormat::PixelFormat pixelFormat() const
Returns the pixel format of frames in a video stream.
int frameWidth() const
Returns the width of frames in a video stream.
The QVideoFrame class represents a frame of video data.
Definition qvideoframe.h:27
The QVideoSink class represents a generic sink for video data.
Definition qvideosink.h:22
void setVideoFrame(const QVideoFrame &frame)
Sets the current video frame.
QPlatformVideoSink * platformVideoSink() const
void addCameraSourceElement(const std::string &id)
void readyChanged(bool)
void updateVideoElementGeometry(const QRect &windowGeometry)
bool setDeviceSetting(const std::string &key, emscripten::val value)
void sizeChange(qint32 width, qint32 height)
void newFrame(const QVideoFrame &newFrame)
emscripten::val surfaceElement()
void durationChanged(qint64 duration)
emscripten::val getDeviceCapabilities()
static void videoFrameCallback(emscripten::val now, emscripten::val metadata)
void setVideoSize(const QSize &)
void setMuted(bool muted)
QVideoSink * m_wasmSink
void setSource(const QUrl &url)
void setVideoMode(QWasmVideoOutput::WasmVideoMode mode)
void bufferingChanged(qint32 percent)
void seekTo(qint64 position)
void setVolume(qreal volume)
void createVideoElement(const std::string &id)
void stateChanged(QWasmMediaPlayer::QWasmMediaPlayerState newState)
void setSurface(QVideoSink *surface)
void updateVideoElementSource(const QString &src)
void statusChanged(QMediaPlayer::MediaStatus status)
void progressChanged(qint32 position)
void errorOccured(qint32 code, const QString &message)
void setPlaybackRate(qreal rate)
void createOffscreenElement(const QSize &offscreenSize)
static Blob copyFrom(const char *buffer, uint32_t size, std::string mimeType)
Definition qstdweb.cpp:425
QByteArray copyToQByteArray() const
Definition qstdweb.cpp:672
Combined button and popup list for selecting options.
const TextureDescription * textureDescription(QVideoFrameFormat::PixelFormat format)
emscripten::val document()
Definition qwasmdom.h:49
void make(emscripten::val target, QString methodName, PromiseCallbacks callbacks, Args... args)
Definition qstdweb_p.h:221
emscripten::val window()
Definition qstdweb_p.h:278
static void * context
#define Q_FUNC_INFO
DBusConnection const char DBusError * error
EGLStreamKHR stream
EGLOutputLayerEXT EGLint EGLAttrib value
[5]
bool qFuzzyCompare(qfloat16 p1, qfloat16 p2) noexcept
Definition qfloat16.h:333
#define Q_GLOBAL_STATIC_WITH_ARGS(TYPE, NAME, ARGS)
#define qWarning
Definition qlogging.h:166
#define Q_LOGGING_CATEGORY(name,...)
#define qCDebug(category,...)
constexpr const T & qBound(const T &min, const T &val, const T &max)
Definition qminmax.h:44
GLenum mode
GLuint64 key
GLint GLsizei GLsizei height
GLenum GLuint id
[7]
GLenum src
GLenum GLuint buffer
GLint GLsizei width
struct _cl_event * event
GLuint GLfloat * val
GLuint GLenum * rate
static constexpr QSize frameSize(const T &frame)
SSL_CTX int void * arg
#define QStringLiteral(str)
#define emit
#define Q_UNUSED(x)
size_t quintptr
Definition qtypes.h:167
long long qint64
Definition qtypes.h:60
double qreal
Definition qtypes.h:187
static double currentTime()
static bool checkForVideoFrame()
EMSCRIPTEN_BINDINGS(video_module)
static std::string m_videoSurfaceId
void qtVideoBeforeUnload(emscripten::val event)
QFuture< QSet< QChar > > set
[10]
QUrl url("example.com")
[constructor-url-reference]
application x qt windows mime
[2]
QMimeDatabase db
[0]
aWidget window() -> setWindowTitle("New Window Title")
[2]
QFrame frame
[0]
std::function< void(emscripten::val)> thenFunc
Definition qstdweb_p.h:212