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 <QPointer>
14#include <QGuiApplication>
15#include <QOpenGLContext>
16
17#include <QtGui/rhi/qrhi_platform.h>
18#include <qpa/qplatformwindow_p.h>
19
20#include <GLES2/gl2.h>
21
23
24#include <qvideosink.h>
25#include <private/qplatformvideosink_p.h>
26#include <private/qstdweb_p.h>
27#include <QTimer>
28
29#include <emscripten/bind.h>
30#include <emscripten/val.h>
31
33
34
35using namespace emscripten;
36using namespace Qt::Literals;
37
38Q_LOGGING_CATEGORY(qWasmMediaVideoOutput, "qt.multimedia.wasm.videooutput")
39
40bool QWasmVideoOutput::isPlatformiOs()
41{
42 emscripten::val platformObject = emscripten::val::global("navigator")["platform"];
43 if (platformObject.call<bool>("includes", emscripten::val("iPhone"))
44 || platformObject.call<bool>("includes", emscripten::val("iPad")))
45 return true;
46 return false;
47}
48
49QWasmVideoOutput::QWasmVideoOutput(QObject *parent)
50 : QObject{ parent }, m_frameGrabber(this)
51{
52 if (isPlatformiOs()) {
53 connect(this, &QWasmVideoOutput::orientationChanged, this,
54 &QWasmVideoOutput::applyIosRotation);
55
56 emscripten_set_orientationchange_callback(this, false, &QWasmVideoOutput::orientationchangeCallback);
57 }
58}
59
61{
62 if (m_mediaInputStream) {
63 if (m_streamStarted) {
64 m_streamStarted = false;
65 m_mediaInputStream->unregisterConsumer();
66 }
67 JsMediaInputStream::releaseInstance(m_cameraId);
68 }
69}
70
71void QWasmVideoOutput::setVideoSize(const QSize &newSize)
72{
73 if (m_pendingVideoSize == newSize)
74 return;
75
76 m_pendingVideoSize = newSize;
77 updateVideoElementGeometry(QRect(0, 0, m_pendingVideoSize.width(), m_pendingVideoSize.height()));
78}
79
81{
82 m_currentVideoMode = mode;
83}
84
85// Calls play() on the video element and handles the returned promise.
86//
87// A play() that is interrupted by a new load request (e.g. a fresh srcObject or
88// a load() call) rejects with AbortError. That rejection is benign, but if the
89// promise is left unhandled the browser reports it as an uncaught rejection.
90//
91// A play() that is refused outright is not benign, the element stays unplayable
92// and fires no further media events, so nothing else would take the player out
93// of the state it is in. Report those instead of swallowing them.
94void QWasmVideoOutput::playVideoElement()
95{
96 emscripten::val promise = m_video.call<emscripten::val>("play");
97 // Older browsers return undefined from play() instead of a Promise. Only
98 // attach handlers when we actually got a Promise back.
99 if (promise.isUndefined() || promise.isNull() || promise["then"].isUndefined())
100 return;
101
102 QPointer<QWasmVideoOutput> videoOutput(this);
103 qstdweb::Promise::adoptPromise(promise, {
104 .catchFunc = [videoOutput](emscripten::val error) {
105 if (!videoOutput || videoOutput->m_isStopped)
106 return;
107
108 // The rejection value is not guaranteed to be a DOMException, so do
109 // not assume either property is there.
110 const auto stringProperty = [&error](const char *key) {
111 return error[key].isUndefined() || error[key].isNull()
112 ? QString()
113 : QString::fromStdString(error[key].as<std::string>());
114 };
115 const QString errorName = stringProperty("name");
116 const QString errorMessage = stringProperty("message");
117 // The browsers word this differently and some do not mention play()
118 // at all, so keep the origin in the string the application sees.
119 const QString errorString =
120 "video.play(): "_L1 + (errorMessage.isEmpty() ? errorName : errorMessage);
121
122 // Not being allowed to play is a policy refusal, not an interruption.
123 // The element is not sitting at a resume point, it is unplayable
124 // until the user acts, so report it regardless of what the paused
125 // attribute happens to say.
126 if (errorName == "NotAllowedError"_L1) {
127 qCWarning(qWasmMediaVideoOutput) << "video.play() not allowed:" << errorMessage;
128 emit videoOutput->readyChanged(false);
129 emit videoOutput->stateChanged(QWasmMediaPlayer::Stopped);
130 emit videoOutput->errorOccured(QMediaPlayer::AccessDeniedError, errorString);
131 return;
132 }
133
134 // Anything else is delivered a microtask late, and an AbortError from
135 // a new load request is routinely followed by a fresh play() that has
136 // already succeeded by now. Trust the element over the rejection.
137 if (!videoOutput->m_video["paused"].as<bool>()) {
138 qCDebug(qWasmMediaVideoOutput) << "video.play() rejected with" << errorName
139 << "but playback resumed, ignoring";
140 return;
141 }
142
143 qCWarning(qWasmMediaVideoOutput) << "video.play() rejected:" << errorName
144 << errorMessage;
145 emit videoOutput->readyChanged(false);
146 emit videoOutput->stateChanged(QWasmMediaPlayer::Stopped);
147 emit videoOutput->errorOccured(QMediaPlayer::ResourceError, errorString);
148 }
149 });
150}
151
153{
154 if (m_video.isUndefined() || m_video.isNull()
155 || !m_wasmSink) {
156 // error
157 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
158 return;
159 }
160
161 switch (m_currentVideoMode) {
163 emscripten::val sourceObj = m_video["src"];
164 if ((sourceObj.isUndefined() || sourceObj.isNull()) && !m_source.isEmpty()) {
165 m_video.set("src", m_source);
166 }
167 if (!isReady())
168 m_video.call<void>("load");
169 } break;
171 playVideoElement();
172 emit readyChanged(true);
173 } break;
175 {
176 emscripten::val document = emscripten::val::global("document");
177 if (m_video["parentNode"].isNull() || m_video["parentNode"].isUndefined())
178 document["body"].call<void>("appendChild", m_video);
179 }
180 if (!m_cameraIsReady) {
181 m_shouldBeStarted = true;
182 }
183
184 if (!m_connection)
185 m_connection = connect(m_mediaInputStream, &JsMediaInputStream::mediaVideoStreamReady, this,
186 [=]( ) {
187 emscripten::val newStream = m_mediaInputStream->getMediaStream();
188 // mediaVideoStreamReady may fire again for an already-attached
189 // stream (e.g. a restart on a still-active camera). Re-setting
190 // srcObject would interrupt the in-flight play() and reject its
191 // promise with AbortError, so skip it when nothing changed.
192 if (m_video["srcObject"].equals(newStream)) {
193 emit readyChanged(true);
194 return;
195 }
196 m_video.set("srcObject", newStream);
197
198 emscripten::val stream = m_video["srcObject"];
199 if (stream.isNull() || stream.isUndefined()) { // camera device
200 qCDebug(qWasmMediaVideoOutput) << "srcObject ERROR";
201 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
202 return;
203 } else {
204 emscripten::val videoTracks = stream.call<emscripten::val>("getVideoTracks");
205 if (videoTracks.isNull() || videoTracks.isUndefined()) {
206 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << "videoTracks is null";
207 emit errorOccured(QMediaPlayer::ResourceError,
208 QStringLiteral("video surface error"));
209 return;
210 }
211 if (videoTracks["length"].as<int>() == 0) {
212 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << "videoTracks count is 0";
213 emit errorOccured(QMediaPlayer::ResourceError,
214 QStringLiteral("video surface error"));
215 return;
216 }
217 emscripten::val videoSettings = videoTracks[0].call<emscripten::val>("getSettings");
218 if (!videoSettings.isNull() && !videoSettings.isUndefined()) {
219 const int width = videoSettings["width"].as<int>();
220 const int height = videoSettings["height"].as<int>();
221 updateVideoElementGeometry(QRect(0, 0, width, height));
222 if (!videoSettings["frameRate"].isUndefined())
223 m_streamFrameRate = videoSettings["frameRate"].as<double>();
224 }
225 }
226
227 playVideoElement();
228
229 emit readyChanged(true);
230
231 });
232 m_mediaInputStream->setUseAudio(false);
233 m_shouldBeStarted = true;
234 m_mediaInputStream->setVideoConstraints(m_videoResolution, m_minFrameRate, m_maxFrameRate);
235 if (!m_streamStarted) {
236 m_streamStarted = true;
237 m_mediaInputStream->registerConsumer();
238 }
239 m_mediaInputStream->setStreamDevice(m_cameraId);
240
241 } break;
242 };
243
244 m_isStopped = false;
245
246 if (m_currentVideoMode != QWasmVideoOutput::Camera
247 && m_currentVideoMode != QWasmVideoOutput::SurfaceCapture) {
248 playVideoElement();
249 }
250}
251
253{
254 if (m_isStopped)
255 return;
256 qCWarning(qWasmMediaVideoOutput) << Q_FUNC_INFO << "mode=" << m_currentVideoMode;
257 if (m_video.isUndefined() || m_video.isNull()) {
258 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("Resource error"));
259 return;
260 }
261 m_isStopped = true;
262 if (!m_toBePaused) {
263 if (m_currentVideoMode == QWasmVideoOutput::SurfaceCapture) {
264 emscripten::val stream = m_video["srcObject"];
265 if (!stream.isNull() && !stream.isUndefined()) {
266 emscripten::val tracks = stream.call<emscripten::val>("getTracks");
267 const int count = tracks["length"].as<int>();
268 for (int i = 0; i < count; ++i)
269 tracks[i].call<void>("stop");
270 }
271 } else if (m_mediaInputStream && m_streamStarted) {
272 // Only stop the shared MediaStream once the last consumer of this
273 // camera goes away; other displays of the same camera keep running.
274 m_streamStarted = false;
275 m_mediaInputStream->unregisterConsumer();
276 }
277
278
279 m_video.set("srcObject", emscripten::val::null());
280 disconnect(m_connection);
281 m_connection = {};
282 m_video.call<void>("remove");
283 } else {
284 m_video.call<void>("pause");
285 }
286}
287
289{
290 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO;
291
292 if (m_video.isUndefined() || m_video.isNull()) {
293 // error
294 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
295 return;
296 }
297 m_isStopped = false;
298 m_toBePaused = true;
299 m_video.call<void>("pause");
300}
301
303{
304 // flush pending frame
305 if (m_wasmSink)
306 m_wasmSink->platformVideoSink()->setVideoFrame(QVideoFrame());
307
308 m_source.clear();
309 m_video.set("currentTime", emscripten::val(0));
310 m_video.call<void>("load");
311}
312
314{
315 return m_video;
316}
317
318void QWasmVideoOutput::setSurface(QVideoSink *surface)
319{
320 if (!surface || surface == m_wasmSink) {
321 return;
322 }
323
324 m_wasmSink = surface;
325}
326
328{
329 if (m_video.isUndefined() || m_video.isNull()) {
330 // error
331 return false;
332 }
333
334 return m_currentMediaStatus == MediaStatus::LoadedMedia;
335 }
336
337void QWasmVideoOutput::setSource(const QUrl &url)
338{
339 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << url;
340
341 m_source = url.toString();
342
343 if (m_video.isUndefined() || m_video.isNull()) {
344 return;
345 }
346
347 if (url.isEmpty()) {
348 stop();
349 return;
350 }
351 if (url.isLocalFile()) {
352 QFile localFile(url.toLocalFile());
353 if (localFile.open(QIODevice::ReadOnly)) {
354 setSource(&localFile);
355 } else {
356 qWarning() << "Failed to open file";
357 }
358 return;
359 }
360
361 updateVideoElementSource(m_source);
362}
363
365{
366 m_video.set("src", src.toStdString());
367 m_video.call<void>("load");
368}
369
371{
372 m_cameraIsReady = false;
373 if (m_mediaInputStream)
374 JsMediaInputStream::releaseInstance(m_cameraId);
375 m_mediaInputStream = JsMediaInputStream::instance(id);
376
377 m_mediaInputStream->setUseAudio(m_hasAudio);
378 m_mediaInputStream->setUseVideo(true);
379
380 connect(m_mediaInputStream, &JsMediaInputStream::mediaVideoStreamReady, this,
381 [this]() {
382 qCDebug(qWasmMediaVideoOutput) << "mediaVideoStreamReady" << m_shouldBeStarted;
383
384 m_cameraIsReady = true;
385 if (m_shouldBeStarted) {
386 start();
387 m_shouldBeStarted = false;
388 }
389 });
390
391 m_cameraId = id;
392}
393
394void QWasmVideoOutput::setVideoConstraints(QSize resolution, float minFrameRate, float maxFrameRate)
395{
396 m_videoResolution = resolution;
397 m_minFrameRate = minFrameRate;
398 m_maxFrameRate = maxFrameRate;
399}
400
401void QWasmVideoOutput::setSource(QIODevice *stream)
402{
403 if (stream->bytesAvailable() == 0) {
404 qWarning() << "data not available";
405 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("data not available"));
406 return;
407 }
408 if (m_video.isUndefined() || m_video.isNull()) {
409 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
410 return;
411 }
412
413 QMimeDatabase db;
414 QMimeType mime = db.mimeTypeForData(stream);
415
416 QByteArray buffer = stream->readAll();
417
418 qstdweb::Blob contentBlob = qstdweb::Blob::copyFrom(buffer.data(), buffer.size(), mime.name().toStdString());
419
420 emscripten::val window = qstdweb::window();
421
422 if (window["safari"].isUndefined()) {
423 emscripten::val contentUrl = window["URL"].call<emscripten::val>("createObjectURL", contentBlob.val());
424 m_video.set("src", contentUrl);
425 m_source = QString::fromStdString(contentUrl.as<std::string>());
426 } else {
427 // only Safari currently supports Blob with srcObject
428 m_video.set("srcObject", contentBlob.val());
429 }
430}
431
432void QWasmVideoOutput::setVolume(qreal volume)
433{ // between 0 - 1
434 volume = qBound(qreal(0.0), volume, qreal(1.0));
435 m_video.set("volume", volume);
436}
437
438void QWasmVideoOutput::setMuted(bool muted)
439{
440 if (m_video.isUndefined() || m_video.isNull()) {
441 // error
442 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
443 return;
444 }
445 m_video.set("muted", muted);
446}
447
449{
450 return (!m_video.isUndefined() || !m_video.isNull())
451 ? (m_video["currentTime"].as<double>() * 1000)
452 : 0;
453}
454
455void QWasmVideoOutput::seekTo(qint64 positionMSecs)
456{
457 if (isVideoSeekable()) {
458 float positionToSetInSeconds = float(positionMSecs) / 1000;
459 emscripten::val seekableTimeRange = m_video["seekable"];
460 if (!seekableTimeRange.isNull() || !seekableTimeRange.isUndefined()) {
461 // range user can seek
462 if (seekableTimeRange["length"].as<int>() < 1)
463 return;
464 if (positionToSetInSeconds
465 >= seekableTimeRange.call<emscripten::val>("start", 0).as<double>()
466 && positionToSetInSeconds
467 <= seekableTimeRange.call<emscripten::val>("end", 0).as<double>()) {
468 m_requestedPosition = positionToSetInSeconds;
469
470 m_video.set("currentTime", m_requestedPosition);
471 }
472 }
473 }
474 qCDebug(qWasmMediaVideoOutput) << "m_requestedPosition" << m_requestedPosition;
475}
476
478{
479 if (m_video.isUndefined() || m_video.isNull()) {
480 // error
481 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error"));
482 return false;
483 }
484
485 emscripten::val seekableTimeRange = m_video["seekable"];
486 if (seekableTimeRange["length"].as<int>() < 1)
487 return false;
488 if (!seekableTimeRange.isNull() || !seekableTimeRange.isUndefined()) {
489 bool isit = !QtPrivate::fuzzyCompare(
490 seekableTimeRange.call<emscripten::val>("start", 0).as<double>(),
491 seekableTimeRange.call<emscripten::val>("end", 0).as<double>());
492 return isit;
493 }
494 return false;
495}
496
498{
499 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << this << id;
500 // Create <video> element and add it to the page body
501
502 emscripten::val document = emscripten::val::global("document");
503 emscripten::val body = document["body"];
504
505 // remove any previously created video element for this output
506 if (!m_video.isUndefined() && !m_video.isNull())
507 m_video.call<void>("remove");
508
509 m_videoSurfaceId = id;
510 m_video = document.call<emscripten::val>("createElement", std::string("video"));
511
512 m_video.set("id", m_videoSurfaceId.c_str());
513 m_video.call<void>("setAttribute", std::string("class"),
514 (m_currentVideoMode == QWasmVideoOutput::Camera ? std::string("Camera")
515 : std::string("Video")));
516 m_video.set("preload", "metadata");
517
518 // Uncaught DOMException: Failed to execute 'getImageData' on
519 // 'OffscreenCanvasRenderingContext2D': The canvas has been tainted by
520 // cross-origin data.
521 // TODO figure out somehow to let user choose between these
522 std::string originString = "anonymous"; // requires server Access-Control-Allow-Origin *
523 // std::string originString = "use-credentials"; // must not
524 // Access-Control-Allow-Origin *
525
526 m_video.call<void>("setAttribute", std::string("crossorigin"), originString);
527 body.call<void>("appendChild", m_video);
528
529 // Create/add video source
530 document.call<emscripten::val>("createElement",
531 std::string("source")).set("src", m_source.toStdString());
532
533 // Set position:absolute, which makes it possible to position the video
534 // element using x,y. coordinates, relative to its parent (the page's <body>
535 // element)
536 emscripten::val style = m_video["style"];
537 style.set("position", "absolute");
538 style.set("display", "none"); // hide
539
540 if (!m_source.isEmpty())
541 updateVideoElementSource(m_source);
542}
543
545{
546 if (!m_video.isUndefined() && !m_video.isNull())
547 m_video.call<void>("remove");
548}
549
550void QWasmVideoOutput::updateVideoElementGeometry(const QRect &windowGeometry)
551{
552 QRect videoElementRect(windowGeometry.topLeft(), windowGeometry.size());
553
554 emscripten::val style = m_video["style"];
555 style.set("left", QStringLiteral("%1px").arg(videoElementRect.left()).toStdString());
556 style.set("top", QStringLiteral("%1px").arg(videoElementRect.top()).toStdString());
557 m_video.set("width", videoElementRect.width());
558 m_video.set("height", videoElementRect.height());
559 style.set("z-index", "999");
560}
561
563{
564 // qt duration is in ms
565 // js is sec
566
567 if (m_video.isUndefined() || m_video.isNull())
568 return 0;
569 return m_video["duration"].as<double>() * 1000;
570}
571
573{
574 m_video.set("playbackRate", emscripten::val(rate));
575}
576
578{
579 return (m_video.isUndefined() || m_video.isNull()) ? 0 : m_video["playbackRate"].as<float>();
580}
581
583{
584 emscripten::val stream = m_video["srcObject"];
585 if ((!stream.isNull() && !stream.isUndefined()) && stream["active"].as<bool>()) {
586 emscripten::val tracks = stream.call<emscripten::val>("getVideoTracks");
587 if (!tracks.isUndefined()) {
588 if (tracks["length"].as<int>() == 0)
589 return emscripten::val::undefined();
590
591 emscripten::val track = tracks[0];
592 if (!track.isUndefined()) {
593 emscripten::val trackCaps = emscripten::val::undefined();
594 if (!track["getCapabilities"].isUndefined())
595 trackCaps = track.call<emscripten::val>("getCapabilities");
596 else // firefox does not support getCapabilities
597 trackCaps = track.call<emscripten::val>("getSettings");
598
599 if (!trackCaps.isUndefined())
600 return trackCaps;
601 }
602 }
603 } else {
604 // camera not started track capabilities not available
605 emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("capabilities not available"));
606 }
607
608 return emscripten::val::undefined();
609}
610
611bool QWasmVideoOutput::setDeviceSetting(const std::string &key, emscripten::val value)
612{
613 emscripten::val stream = m_video["srcObject"];
614 if (stream.isNull() || stream.isUndefined()
615 || stream["getVideoTracks"].isUndefined())
616 return false;
617
618 emscripten::val tracks = stream.call<emscripten::val>("getVideoTracks");
619 if (!tracks.isNull() || !tracks.isUndefined()) {
620 if (tracks["length"].as<int>() == 0)
621 return false;
622
623 emscripten::val track = tracks[0];
624 emscripten::val contraint = emscripten::val::object();
625 contraint.set(std::move(key), value);
626 track.call<emscripten::val>("applyConstraints", contraint);
627 return true;
628 }
629
630 return false;
631}
632
633QT_END_NAMESPACE
634
635#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 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 orientationChanged(int rotationIndex)
void setVolume(qreal volume)
void createVideoElement(const std::string &id)
void updateVideoElementSource(const QString &src)
std::string m_videoSurfaceId
void setSource(QIODevice *stream)
void setPlaybackRate(qreal rate)
Combined button and popup list for selecting options.
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")