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