Revision control

++ // 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 "qwasmaudiooutput_p.h"
++
++ #include <QtMultimedia/qaudiodevice.h>
++ #include <QtMultimedia/qaudiooutput.h>
++ #include <QtMultimedia/qmediadevices.h>
++ #include <QtCore/qfile.h>
++ #include <QtCore/qfileinfo.h>
++ #include <QtCore/qloggingcategory.h>
++ #include <QtCore/qmimedatabase.h>
++ #include <QtCore/qurl.h>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::Literals;
++
++ Q_STATIC_LOGGING_CATEGORY(qWasmMediaAudioOutput, "qt.multimedia.wasm.audiooutput");
++
++ QWasmAudioOutput::QWasmAudioOutput(QAudioOutput *parent)
++ : QPlatformAudioOutput(parent)
++ {
++ }
++
++ QWasmAudioOutput::~QWasmAudioOutput() = default;
++
++ void QWasmAudioOutput::setAudioDevice(const QAudioDevice &audioDevice)
++ {
++ qCDebug(qWasmMediaAudioOutput) << Q_FUNC_INFO << device.id();
++ device = audioDevice;
++ }
++
++ void QWasmAudioOutput::setVideoElement(emscripten::val videoElement)
++ {
++ m_videoElement = videoElement;
++ }
++
++ emscripten::val QWasmAudioOutput::videoElement()
++ {
++ return m_videoElement;
++ }
++
++ void QWasmAudioOutput::setMuted(bool muted)
++ {
++ emscripten::val realElement = videoElement();
++ if (!realElement.isUndefined()) {
++ realElement.set("muted", muted);
++ return;
++ }
++ if (m_audio.isUndefined() || m_audio.isNull()) {
++ qCDebug(qWasmMediaAudioOutput) << "Error"
++ << "Audio element could not be created";
++ Q_EMIT errorOccured(QMediaPlayer::ResourceError,
++ QStringLiteral("Media file could not be opened"));
++ return;
++ }
++ m_audio.set("mute", muted);
++ }
++
++ void QWasmAudioOutput::setVolume(float volume)
++ {
++ volume = qBound(qreal(0.0), volume, qreal(1.0));
++ emscripten::val realElement = videoElement();
++ if (!realElement.isUndefined()) {
++ realElement.set("volume", volume);
++ return;
++ }
++ if (m_audio.isUndefined() || m_audio.isNull()) {
++ qCDebug(qWasmMediaAudioOutput) << "Error"
++ << "Audio element not available";
++ Q_EMIT errorOccured(QMediaPlayer::ResourceError,
++ QStringLiteral("Media file could not be opened"));
++ return;
++ }
++
++ m_audio.set("volume", volume);
++ }
++
++ void QWasmAudioOutput::setSource(const QUrl &url)
++ {
++ qCDebug(qWasmMediaAudioOutput) << Q_FUNC_INFO << url;
++ if (url.isEmpty()) {
++ stop();
++ return;
++ }
++
++ createAudioElement(device.id().toStdString());
++
++ if (m_audio.isUndefined() || m_audio.isNull()) {
++ qCDebug(qWasmMediaAudioOutput) << "Error"
++ << "Audio element could not be created";
++ Q_EMIT errorOccured(QMediaPlayer::ResourceError,
++ QStringLiteral("Audio element could not be created"));
++ return;
++ }
++
++ emscripten::val document = emscripten::val::global("document");
++ emscripten::val body = document["body"];
++
++ m_audio.set("id", device.id().toStdString());
++
++ body.call<void>("appendChild", m_audio);
++
++
++ if (url.isLocalFile()) { // is localfile
++ qCDebug(qWasmMediaAudioOutput) << "is localfile";
++ m_source = url.toLocalFile();
++
++ QFile mediaFile(m_source);
++ if (!mediaFile.exists()) {
++ qCDebug(qWasmMediaAudioOutput) << "Error"
++ << "Media file does not exist";
++ QMetaObject::invokeMethod(this, &QWasmAudioOutput::errorOccured, Qt::QueuedConnection,
++ QMediaPlayer::ResourceError,
++ QStringLiteral("Media file does not exist"));
++
++ return;
++ }
++
++ if (!mediaFile.open(QIODevice::ReadOnly)) {
++ qCDebug(qWasmMediaAudioOutput) << "Error"
++ << "Media file could not be opened";
++ Q_EMIT errorOccured(QMediaPlayer::ResourceError,
++ QStringLiteral("Media file could not be opened"));
++ return;
++ }
++
++ // local files are relatively small due to browser filesystem being restricted
++ QByteArray content = mediaFile.readAll();
++
++ QMimeDatabase db;
++ qCDebug(qWasmMediaAudioOutput) << db.mimeTypeForData(content).name();
++
++ qstdweb::Blob contentBlob = qstdweb::Blob::copyFrom(content.constData(), content.size());
++ emscripten::val contentUrl =
++ qstdweb::window()["URL"].call<emscripten::val>("createObjectURL", contentBlob.val());
++
++ emscripten::val audioSourceElement =
++ document.call<emscripten::val>("createElement", std::string("source"));
++
++ audioSourceElement.set("src", contentUrl);
++
++ // work around Safari not being able to read audio from blob URLs.
++ QFileInfo info(m_source);
++ QMimeType mimeType = db.mimeTypeForFile(info);
++
++ audioSourceElement.set("type", mimeType.name().toStdString());
++ m_audio.call<void>("appendChild", audioSourceElement);
++
++ m_audio.call<void>("setAttribute", emscripten::val("srcObject"), contentUrl);
++
++ } else {
++ m_source = url.toString();
++ m_audio.set("src", m_source.toStdString());
++ }
++ m_audio.set("id", device.id().toStdString());
++
++ body.call<void>("appendChild", m_audio);
++ qCDebug(qWasmMediaAudioOutput) << Q_FUNC_INFO << device.id();
++
++ doElementCallbacks();
++ }
++
++ void QWasmAudioOutput::setSource(QIODevice *stream)
++ {
++ m_audioIODevice = stream;
++ }
++
++ void QWasmAudioOutput::start()
++ {
++ if (m_audio.isNull() || m_audio.isUndefined()) {
++ qCDebug(qWasmMediaAudioOutput) << "audio failed to start";
++ Q_EMIT errorOccured(QMediaPlayer::ResourceError,
++ QStringLiteral("Audio element resource error"));
++ return;
++ }
++
++ m_audio.call<void>("play");
++ }
++
++ void QWasmAudioOutput::stop()
++ {
++ if (m_audio.isNull() || m_audio.isUndefined()) {
++ qCDebug(qWasmMediaAudioOutput) << "audio failed to start";
++ Q_EMIT errorOccured(QMediaPlayer::ResourceError,
++ QStringLiteral("Audio element resource error"));
++ return;
++ }
++ if (!m_source.isEmpty()) {
++ pause();
++ m_audio.set("currentTime", emscripten::val(0));
++ }
++ if (m_audioIODevice) {
++ m_audioIODevice->close();
++ delete m_audioIODevice;
++ m_audioIODevice = 0;
++ }
++ }
++
++ void QWasmAudioOutput::pause()
++ {
++ if (m_audio.isNull() || m_audio.isUndefined()) {
++ qCDebug(qWasmMediaAudioOutput) << "audio failed to start";
++ Q_EMIT errorOccured(QMediaPlayer::ResourceError,
++ QStringLiteral("Audio element resource error"));
++ return;
++ }
++ m_audio.call<emscripten::val>("pause");
++ }
++
++ void QWasmAudioOutput::createAudioElement(const std::string &id)
++ {
++ emscripten::val document = emscripten::val::global("document");
++ m_audio = document.call<emscripten::val>("createElement", std::string("audio"));
++
++ if (id == "System output") { // defaul output, no perms needed
++ emscripten::val audioContext = emscripten::val::global("window")["AudioContext"].new_();
++ emscripten::val sourceNode = audioContext.call<emscripten::val>("createMediaElementSource", m_audio);
++
++ sourceNode.call<void>("connect", audioContext["destination"]);
++ // or control volume // is this needed?
++ // emscripten::val volumeNode = audioContext.call<emscripten::val>("createGain");
++ // sourceNode.call<void>("connect", volumeNode);
++ // volumeNode.call<void>("connect", audioContext["destination"]);
++ }
++
++ // only works in chrome and firefox.
++ // Firefox this feature is behind media.setsinkid.enabled preferences
++ // allows user to choose audio output device
++
++ if (!m_audio.hasOwnProperty("sinkId") || m_audio["sinkId"].isUndefined()) {
++ return;
++ }
++
++ std::string usableId = id;
++ if (usableId.empty() || usableId == "System output")
++ usableId = QMediaDevices::defaultAudioOutput().id();
++
++ qstdweb::PromiseCallbacks sinkIdCallbacks{
++ .thenFunc = [](emscripten::val) { qCWarning(qWasmMediaAudioOutput) << "setSinkId ok"; },
++ .catchFunc =
++ [](emscripten::val) {
++ qCWarning(qWasmMediaAudioOutput) << "Error while trying to setSinkId";
++ },
++ .finallyFunc = nullptr
++ };
++ qstdweb::Promise::make(m_audio, u"setSinkId"_s, std::move(sinkIdCallbacks), std::move(usableId));
++
++ m_audio.set("id", usableId.c_str());
++ }
++
++ void QWasmAudioOutput::doElementCallbacks()
++ {
++ // error
++ auto errorCallback = [&](emscripten::val event) {
++ qCDebug(qWasmMediaAudioOutput) << "error";
++ if (event.isUndefined() || event.isNull())
++ return;
++ Q_EMIT errorOccured(m_audio["error"]["code"].as<int>(),
++ QString::fromStdString(m_audio["error"]["message"].as<std::string>()));
++
++ QString errorMessage =
++ QString::fromStdString(m_audio["error"]["message"].as<std::string>());
++ if (errorMessage.isEmpty()) {
++ switch (m_audio["error"]["code"].as<int>()) {
++ case AudioElementError::MEDIA_ERR_ABORTED:
++ errorMessage = QStringLiteral("aborted by the user agent at the user's request.");
++ break;
++ case AudioElementError::MEDIA_ERR_NETWORK:
++ errorMessage = QStringLiteral("network error.");
++ break;
++ case AudioElementError::MEDIA_ERR_DECODE:
++ errorMessage = QStringLiteral("decoding error.");
++ break;
++ case AudioElementError::MEDIA_ERR_SRC_NOT_SUPPORTED:
++ errorMessage = QStringLiteral("src attribute not suitable.");
++ break;
++ };
++ }
++ qCDebug(qWasmMediaAudioOutput) << m_audio["error"]["code"].as<int>() << errorMessage;
++
++ Q_EMIT errorOccured(m_audio["error"]["code"].as<int>(), errorMessage);
++ };
++ m_errorChangeEvent.reset(new qstdweb::EventCallback(m_audio, "error", errorCallback));
++
++ // loadeddata
++ auto loadedDataCallback = [&](emscripten::val event) {
++ Q_UNUSED(event)
++ qCDebug(qWasmMediaAudioOutput) << "loaded data";
++ qstdweb::window()["URL"].call<emscripten::val>("revokeObjectURL", m_audio["src"]);
++ };
++ m_loadedDataEvent.reset(new qstdweb::EventCallback(m_audio, "loadeddata", loadedDataCallback));
++
++ // canplay
++ auto canPlayCallback = [&](emscripten::val event) {
++ if (event.isUndefined() || event.isNull())
++ return;
++ qCDebug(qWasmMediaAudioOutput) << "can play";
++ Q_EMIT readyChanged(true);
++ Q_EMIT stateChanged(QWasmMediaPlayer::Preparing);
++ };
++ m_canPlayChangeEvent.reset(new qstdweb::EventCallback(m_audio, "canplay", canPlayCallback));
++
++ // canplaythrough
++ auto canPlayThroughCallback = [&](emscripten::val event) {
++ Q_UNUSED(event)
++ Q_EMIT stateChanged(QWasmMediaPlayer::Prepared);
++ };
++ m_canPlayThroughChangeEvent.reset(
++ new qstdweb::EventCallback(m_audio, "canplaythrough", canPlayThroughCallback));
++
++ // play
++ auto playCallback = [&](emscripten::val event) {
++ Q_UNUSED(event)
++ qCDebug(qWasmMediaAudioOutput) << "play";
++ Q_EMIT stateChanged(QWasmMediaPlayer::Started);
++ };
++ m_playEvent.reset(new qstdweb::EventCallback(m_audio, "play", playCallback));
++
++ // durationchange
++ auto durationChangeCallback = [&](emscripten::val event) {
++ qCDebug(qWasmMediaAudioOutput) << "durationChange";
++
++ // duration in ms
++ Q_EMIT durationChanged(event["target"]["duration"].as<double>() * 1000);
++ };
++ m_durationChangeEvent.reset(
++ new qstdweb::EventCallback(m_audio, "durationchange", durationChangeCallback));
++
++ // ended
++ auto endedCallback = [&](emscripten::val event) {
++ Q_UNUSED(event)
++ qCDebug(qWasmMediaAudioOutput) << "ended";
++ m_currentMediaStatus = QMediaPlayer::EndOfMedia;
++ Q_EMIT statusChanged(m_currentMediaStatus);
++ };
++ m_endedEvent.reset(new qstdweb::EventCallback(m_audio, "ended", endedCallback));
++
++ // progress (buffering progress)
++ auto progesssCallback = [&](emscripten::val event) {
++ if (event.isUndefined() || event.isNull())
++ return;
++ qCDebug(qWasmMediaAudioOutput) << "progress";
++ float duration = event["target"]["duration"].as<int>();
++ if (duration < 0) // track not exactly ready yet
++ return;
++
++ emscripten::val timeRanges = event["target"]["buffered"];
++
++ if ((!timeRanges.isNull() || !timeRanges.isUndefined())
++ && timeRanges["length"].as<int>() == 1) {
++ emscripten::val dVal = timeRanges.call<emscripten::val>("end", 0);
++
++ if (!dVal.isNull() || !dVal.isUndefined()) {
++ double bufferedEnd = dVal.as<double>();
++
++ if (duration > 0 && bufferedEnd > 0) {
++ float bufferedValue = (bufferedEnd / duration * 100);
++ qCDebug(qWasmMediaAudioOutput) << "progress buffered" << bufferedValue;
++
++ Q_EMIT bufferingChanged(m_currentBufferedValue);
++ if (bufferedEnd == duration)
++ m_currentMediaStatus = QMediaPlayer::BufferedMedia;
++ else
++ m_currentMediaStatus = QMediaPlayer::BufferingMedia;
++
++ Q_EMIT statusChanged(m_currentMediaStatus);
++ }
++ }
++ }
++ };
++ m_progressChangeEvent.reset(new qstdweb::EventCallback(m_audio, "progress", progesssCallback));
++
++ // timupdate
++ auto timeUpdateCallback = [&](emscripten::val event) {
++ qCDebug(qWasmMediaAudioOutput)
++ << "timeupdate" << (event["target"]["currentTime"].as<double>() * 1000);
++
++ // qt progress is ms
++ Q_EMIT progressChanged(event["target"]["currentTime"].as<double>() * 1000);
++ };
++ m_timeUpdateEvent.reset(new qstdweb::EventCallback(m_audio, "timeupdate", timeUpdateCallback));
++
++ // pause
++ auto pauseCallback = [&](emscripten::val event) {
++ Q_UNUSED(event)
++ qCDebug(qWasmMediaAudioOutput) << "pause";
++
++ int currentTime = m_audio["currentTime"].as<int>(); // in seconds
++ int duration = m_audio["duration"].as<int>(); // in seconds
++ if ((currentTime > 0 && currentTime < duration)) {
++ Q_EMIT stateChanged(QWasmMediaPlayer::Paused);
++ } else {
++ Q_EMIT stateChanged(QWasmMediaPlayer::Stopped);
++ }
++ };
++ m_pauseChangeEvent.reset(new qstdweb::EventCallback(m_audio, "pause", pauseCallback));
++ }
++
++ QT_END_NAMESPACE