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