// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include #include #include #include #include #include #include "qwasmvideooutput_p.h" #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(qWasmMediaVideoOutput, "qt.multimedia.wasm.videooutput") // TODO unique videosurface ? static std::string m_videoSurfaceId; void qtVideoBeforeUnload(emscripten::val event) { Q_UNUSED(event) // large videos will leave the unloading window // in a frozen state, so remove the video element first emscripten::val document = emscripten::val::global("document"); emscripten::val videoElement = document.call("getElementById", std::string(m_videoSurfaceId)); videoElement.call("removeAttribute", emscripten::val("src")); videoElement.call("load"); } EMSCRIPTEN_BINDINGS(video_module) { emscripten::function("mbeforeUnload", qtVideoBeforeUnload); } QWasmVideoOutput::QWasmVideoOutput(QObject *parent) : QObject{ parent } { } void QWasmVideoOutput::setVideoSize(const QSize &newSize) { if (m_pendingVideoSize == newSize) return; m_pendingVideoSize = newSize; updateVideoElementGeometry(QRect(0, 0, m_pendingVideoSize.width(), m_pendingVideoSize.height())); } void QWasmVideoOutput::setVideoMode(QWasmVideoOutput::WasmVideoMode mode) { m_currentVideoMode = mode; } void QWasmVideoOutput::start() { qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO; if (m_video.isUndefined() || m_video.isNull()) { // error emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error")); return; } if (m_currentVideoMode == QWasmVideoOutput::VideoOutput) { emscripten::val sourceObj = m_video.call("getAttribute", emscripten::val("src")); if ((sourceObj.isUndefined() || sourceObj.isNull()) && !m_source.isEmpty()) { qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << "calling load" << m_source; m_video.call("setAttribute", emscripten::val("src"), m_source.toStdString()); m_video.call("load"); } } else { emscripten::val stream = m_video["srcObject"]; if (!stream.isNull() || !stream.isUndefined()) { // camera device emscripten::val vTracks = stream.call("getVideoTracks"); if (vTracks["count"].as() > 1) { emscripten::val vSettings = vTracks[0].call("getSettings"); double fRate = vSettings["frameRate"].as(); int width = vSettings["width"].as(); int height = vSettings["height"].as(); qCDebug(qWasmMediaVideoOutput) << "frame rate" << fRate << "width" << width << "height" << height; updateVideoElementGeometry(QRect(0, 0, width, height)); } } } m_shouldStop = false; m_toBePaused = false; m_video.call("play"); if (m_currentVideoMode == QWasmVideoOutput::Camera) videoFrameTimerCallback(); } void QWasmVideoOutput::stop() { qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO; if (m_video.isUndefined() || m_video.isNull()) { // error emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error")); return; } m_shouldStop = true; if (m_toBePaused) { // we are stopped , need to reset m_toBePaused = false; m_video.call("load"); } else { m_video.call("pause"); } } void QWasmVideoOutput::pause() { qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO; if (m_video.isUndefined() || m_video.isNull()) { // error emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error")); return; } m_shouldStop = false; m_toBePaused = true; m_video.call("pause"); } void QWasmVideoOutput::reset() { // flush pending frame if (m_wasmSink) m_wasmSink->platformVideoSink()->setVideoFrame(QVideoFrame()); m_source = ""; m_video.set("currentTime", emscripten::val(0)); m_video.call("load"); } emscripten::val QWasmVideoOutput::surfaceElement() { return m_video; } void QWasmVideoOutput::setSurface(QVideoSink *surface) { qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << surface << m_wasmSink; if (surface == m_wasmSink) return; m_wasmSink = surface; } bool QWasmVideoOutput::isReady() const { if (m_video.isUndefined() || m_video.isNull()) { // error return false; } constexpr int hasCurrentData = 2; if (!m_video.isUndefined() || !m_video.isNull()) return m_video["readyState"].as() >= hasCurrentData; else return true; } void QWasmVideoOutput::setSource(const QUrl &url) { qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO << url; if (m_video.isUndefined() || m_video.isNull()) { // error emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error")); return; } if (url.isEmpty()) { stop(); return; } if (url.isLocalFile()) { qDebug() << "no local files allowed, so we need to blob"; // QFile mFile(url.toString()); // if (!mFile.open(QIODevice::readOnly)) {// some error // return; // QDataStream dStream(&mFile); // ? // setSource(dStream); return; } // is network path m_source = url.toString(); addSourceElement(m_source); } void QWasmVideoOutput::addSourceElement(const QString &urlString) { qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO; if (m_video.isUndefined() || m_video.isNull()) { // error emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error")); return; } emscripten::val document = emscripten::val::global("document"); m_videoElementSource = document.call("createElement", std::string("source")); if (m_videoElementSource.isNull() || m_videoElementSource.isUndefined()) { // error emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error")); return; } if (!urlString.isEmpty()) m_videoElementSource.set("src", m_source.toStdString()); m_video.call("appendChild", m_videoElementSource); if (!urlString.isEmpty()) m_video.call("load"); } void QWasmVideoOutput::setSource(QIODevice *stream) { Q_UNUSED(stream) if (m_video.isUndefined() || m_video.isNull() || m_videoElementSource.isUndefined() || m_videoElementSource.isNull()) { emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error")); return; } // src (depreciated) or srcObject // MediaStream, MediaSource Blob or File // TODO QIOStream to // SourceBuffer.appendBuffer() // ArrayBuffer // SourceBuffer.appendStream() // WriteableStream ReadableStream (to sink) // SourceBuffer.appendBufferAsync() // emscripten::val document = emscripten::val::global("document"); // stream to blob // Create/add video source m_video.call("setAttribute", emscripten::val("srcObject"), m_source.toStdString()); m_video.call("load"); } void QWasmVideoOutput::setVolume(qreal volume) { // between 0 - 1 volume = qBound(qreal(0.0), volume, qreal(1.0)); m_video.set("volume", volume); } void QWasmVideoOutput::setMuted(bool muted) { if (m_video.isUndefined() || m_video.isNull()) { // error emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error")); return; } m_video.set("muted", muted); } qint64 QWasmVideoOutput::getCurrentPosition() { return (!m_video.isUndefined() || !m_video.isNull()) ? (m_video["currentTime"].as() * 1000) : 0; } void QWasmVideoOutput::seekTo(qint64 positionMSecs) { if (isVideoSeekable()) { float positionToSetInSeconds = float(positionMSecs) / 1000; emscripten::val seekableTimeRange = m_video["seekable"]; if (!seekableTimeRange.isNull() || !seekableTimeRange.isUndefined()) { // range user can seek if (seekableTimeRange["length"].as() < 1) return; if (positionToSetInSeconds >= seekableTimeRange.call("start", 0).as() && positionToSetInSeconds <= seekableTimeRange.call("end", 0).as()) { m_requestedPosition = positionToSetInSeconds; m_video.set("currentTime", m_requestedPosition); } } } qCDebug(qWasmMediaVideoOutput) << "m_requestedPosition" << m_requestedPosition; } bool QWasmVideoOutput::isVideoSeekable() { if (m_video.isUndefined() || m_video.isNull()) { // error emit errorOccured(QMediaPlayer::ResourceError, QStringLiteral("video surface error")); return false; } emscripten::val seekableTimeRange = m_video["seekable"]; if (seekableTimeRange["length"].as() < 1) return false; if (!seekableTimeRange.isNull() || !seekableTimeRange.isUndefined()) { bool isit = !qFuzzyCompare(seekableTimeRange.call("start", 0).as(), seekableTimeRange.call("end", 0).as()); return isit; } return false; } void QWasmVideoOutput::createVideoElement(const std::string &id) { qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO; // Create