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