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
qwasmaudiooutput.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/qaudiodevice.h>
7#include <QtMultimedia/qaudiooutput.h>
8#include <QtMultimedia/qmediadevices.h>
9#include <QtCore/qfile.h>
10#include <QtCore/qfileinfo.h>
11#include <QtCore/qloggingcategory.h>
12#include <QtCore/qmimedatabase.h>
13#include <QtCore/qurl.h>
14
16
17using namespace Qt::Literals;
18
19Q_STATIC_LOGGING_CATEGORY(qWasmMediaAudioOutput, "qt.multimedia.wasm.audiooutput");
20
21QWasmAudioOutput::QWasmAudioOutput(QAudioOutput *parent)
22 : QPlatformAudioOutput(parent)
23{
24}
25
26QWasmAudioOutput::~QWasmAudioOutput() = default;
27
28void QWasmAudioOutput::setAudioDevice(const QAudioDevice &audioDevice)
29{
30 qCDebug(qWasmMediaAudioOutput) << Q_FUNC_INFO << device.id();
31 device = audioDevice;
32}
33
34void QWasmAudioOutput::setVideoElement(emscripten::val videoElement)
35{
36 m_videoElement = videoElement;
37}
38
39emscripten::val QWasmAudioOutput::videoElement()
40{
41 return m_videoElement;
42}
43
44void QWasmAudioOutput::setMuted(bool muted)
45{
46 emscripten::val realElement = videoElement();
47 if (!realElement.isUndefined()) {
48 realElement.set("muted", muted);
49 return;
50 }
51 if (m_audio.isUndefined() || m_audio.isNull()) {
52 qCDebug(qWasmMediaAudioOutput) << "Error"
53 << "Audio element could not be created";
54 Q_EMIT errorOccured(QMediaPlayer::ResourceError,
55 QStringLiteral("Media file could not be opened"));
56 return;
57 }
58 m_audio.set("mute", muted);
59}
60
61void QWasmAudioOutput::setVolume(float volume)
62{
63 volume = qBound(qreal(0.0), volume, qreal(1.0));
64 emscripten::val realElement = videoElement();
65 if (!realElement.isUndefined()) {
66 realElement.set("volume", volume);
67 return;
68 }
69 if (m_audio.isUndefined() || m_audio.isNull()) {
70 qCDebug(qWasmMediaAudioOutput) << "Error"
71 << "Audio element not available";
72 Q_EMIT errorOccured(QMediaPlayer::ResourceError,
73 QStringLiteral("Media file could not be opened"));
74 return;
75 }
76
77 m_audio.set("volume", volume);
78}
79
80void QWasmAudioOutput::setSource(const QUrl &url)
81{
82 qCDebug(qWasmMediaAudioOutput) << Q_FUNC_INFO << url;
83 if (url.isEmpty()) {
84 stop();
85 return;
86 }
87
88 createAudioElement(device.id().toStdString());
89
90 if (m_audio.isUndefined() || m_audio.isNull()) {
91 qCDebug(qWasmMediaAudioOutput) << "Error"
92 << "Audio element could not be created";
93 Q_EMIT errorOccured(QMediaPlayer::ResourceError,
94 QStringLiteral("Audio element could not be created"));
95 return;
96 }
97
98 emscripten::val document = emscripten::val::global("document");
99 emscripten::val body = document["body"];
100
101 m_audio.set("id", device.id().toStdString());
102
103 body.call<void>("appendChild", m_audio);
104
105
106 if (url.isLocalFile()) { // is localfile
107 qCDebug(qWasmMediaAudioOutput) << "is localfile";
108 m_source = url.toLocalFile();
109
110 QFile mediaFile(m_source);
111 if (!mediaFile.exists()) {
112 qCDebug(qWasmMediaAudioOutput) << "Error"
113 << "Media file does not exist";
114 QMetaObject::invokeMethod(this, &QWasmAudioOutput::errorOccured, Qt::QueuedConnection,
115 QMediaPlayer::ResourceError,
116 QStringLiteral("Media file does not exist"));
117
118 return;
119 }
120
121 if (!mediaFile.open(QIODevice::ReadOnly)) {
122 qCDebug(qWasmMediaAudioOutput) << "Error"
123 << "Media file could not be opened";
124 Q_EMIT errorOccured(QMediaPlayer::ResourceError,
125 QStringLiteral("Media file could not be opened"));
126 return;
127 }
128
129 // local files are relatively small due to browser filesystem being restricted
130 QByteArray content = mediaFile.readAll();
131
132 QMimeDatabase db;
133 qCDebug(qWasmMediaAudioOutput) << db.mimeTypeForData(content).name();
134
135 qstdweb::Blob contentBlob = qstdweb::Blob::copyFrom(content.constData(), content.size());
136 emscripten::val contentUrl =
137 qstdweb::window()["URL"].call<emscripten::val>("createObjectURL", contentBlob.val());
138
139 emscripten::val audioSourceElement =
140 document.call<emscripten::val>("createElement", std::string("source"));
141
142 audioSourceElement.set("src", contentUrl);
143
144 // work around Safari not being able to read audio from blob URLs.
145 QFileInfo info(m_source);
146 QMimeType mimeType = db.mimeTypeForFile(info);
147
148 audioSourceElement.set("type", mimeType.name().toStdString());
149 m_audio.call<void>("appendChild", audioSourceElement);
150
151 m_audio.call<void>("setAttribute", emscripten::val("srcObject"), contentUrl);
152
153 } else {
154 m_source = url.toString();
155 m_audio.set("src", m_source.toStdString());
156 }
157 m_audio.set("id", device.id().toStdString());
158
159 body.call<void>("appendChild", m_audio);
160 qCDebug(qWasmMediaAudioOutput) << Q_FUNC_INFO << device.id();
161
162 doElementCallbacks();
163}
164
165void QWasmAudioOutput::setSource(QIODevice *stream)
166{
167 m_audioIODevice = stream;
168}
169
170void QWasmAudioOutput::start()
171{
172 if (m_audio.isNull() || m_audio.isUndefined()) {
173 qCDebug(qWasmMediaAudioOutput) << "audio failed to start";
174 Q_EMIT errorOccured(QMediaPlayer::ResourceError,
175 QStringLiteral("Audio element resource error"));
176 return;
177 }
178
179 m_audio.call<void>("play");
180}
181
182void QWasmAudioOutput::stop()
183{
184 if (m_audio.isNull() || m_audio.isUndefined()) {
185 qCDebug(qWasmMediaAudioOutput) << "audio failed to start";
186 Q_EMIT errorOccured(QMediaPlayer::ResourceError,
187 QStringLiteral("Audio element resource error"));
188 return;
189 }
190 if (!m_source.isEmpty()) {
191 pause();
192 m_audio.set("currentTime", emscripten::val(0));
193 }
194 if (m_audioIODevice) {
195 m_audioIODevice->close();
196 delete m_audioIODevice;
197 m_audioIODevice = 0;
198 }
199}
200
201void QWasmAudioOutput::pause()
202{
203 if (m_audio.isNull() || m_audio.isUndefined()) {
204 qCDebug(qWasmMediaAudioOutput) << "audio failed to start";
205 Q_EMIT errorOccured(QMediaPlayer::ResourceError,
206 QStringLiteral("Audio element resource error"));
207 return;
208 }
209 m_audio.call<emscripten::val>("pause");
210}
211
212void QWasmAudioOutput::createAudioElement(const std::string &id)
213{
214 emscripten::val document = emscripten::val::global("document");
215 m_audio = document.call<emscripten::val>("createElement", std::string("audio"));
216
217 if (id == "System output") { // defaul output, no perms needed
218 emscripten::val audioContext = emscripten::val::global("window")["AudioContext"].new_();
219 emscripten::val sourceNode = audioContext.call<emscripten::val>("createMediaElementSource", m_audio);
220
221 sourceNode.call<void>("connect", audioContext["destination"]);
222// or control volume // is this needed?
223 // emscripten::val volumeNode = audioContext.call<emscripten::val>("createGain");
224 // sourceNode.call<void>("connect", volumeNode);
225 // volumeNode.call<void>("connect", audioContext["destination"]);
226 }
227
228 // only works in chrome and firefox.
229 // Firefox this feature is behind media.setsinkid.enabled preferences
230 // allows user to choose audio output device
231
232 if (!m_audio.hasOwnProperty("sinkId") || m_audio["sinkId"].isUndefined()) {
233 return;
234 }
235
236 std::string usableId = id;
237 if (usableId.empty() || usableId == "System output")
238 usableId = QMediaDevices::defaultAudioOutput().id();
239
240 qstdweb::PromiseCallbacks sinkIdCallbacks{
241 .thenFunc = [](emscripten::val) { qCWarning(qWasmMediaAudioOutput) << "setSinkId ok"; },
242 .catchFunc =
243 [](emscripten::val) {
244 qCWarning(qWasmMediaAudioOutput) << "Error while trying to setSinkId";
245 },
246 .finallyFunc = nullptr
247 };
248 qstdweb::Promise::make(m_audio, u"setSinkId"_s, std::move(sinkIdCallbacks), std::move(usableId));
249
250 m_audio.set("id", usableId.c_str());
251}
252
253void QWasmAudioOutput::doElementCallbacks()
254{
255 // error
256 auto errorCallback = [&](emscripten::val event) {
257 qCDebug(qWasmMediaAudioOutput) << "error";
258 if (event.isUndefined() || event.isNull())
259 return;
260 Q_EMIT errorOccured(m_audio["error"]["code"].as<int>(),
261 QString::fromStdString(m_audio["error"]["message"].as<std::string>()));
262
263 QString errorMessage =
264 QString::fromStdString(m_audio["error"]["message"].as<std::string>());
265 if (errorMessage.isEmpty()) {
266 switch (m_audio["error"]["code"].as<int>()) {
267 case AudioElementError::MEDIA_ERR_ABORTED:
268 errorMessage = QStringLiteral("aborted by the user agent at the user's request.");
269 break;
270 case AudioElementError::MEDIA_ERR_NETWORK:
271 errorMessage = QStringLiteral("network error.");
272 break;
273 case AudioElementError::MEDIA_ERR_DECODE:
274 errorMessage = QStringLiteral("decoding error.");
275 break;
276 case AudioElementError::MEDIA_ERR_SRC_NOT_SUPPORTED:
277 errorMessage = QStringLiteral("src attribute not suitable.");
278 break;
279 };
280 }
281 qCDebug(qWasmMediaAudioOutput) << m_audio["error"]["code"].as<int>() << errorMessage;
282
283 Q_EMIT errorOccured(m_audio["error"]["code"].as<int>(), errorMessage);
284 };
285 m_errorChangeEvent.reset(new qstdweb::EventCallback(m_audio, "error", errorCallback));
286
287 // loadeddata
288 auto loadedDataCallback = [&](emscripten::val event) {
289 Q_UNUSED(event)
290 qCDebug(qWasmMediaAudioOutput) << "loaded data";
291 qstdweb::window()["URL"].call<emscripten::val>("revokeObjectURL", m_audio["src"]);
292 };
293 m_loadedDataEvent.reset(new qstdweb::EventCallback(m_audio, "loadeddata", loadedDataCallback));
294
295 // canplay
296 auto canPlayCallback = [&](emscripten::val event) {
297 if (event.isUndefined() || event.isNull())
298 return;
299 qCDebug(qWasmMediaAudioOutput) << "can play";
300 Q_EMIT readyChanged(true);
301 Q_EMIT stateChanged(QWasmMediaPlayer::Preparing);
302 };
303 m_canPlayChangeEvent.reset(new qstdweb::EventCallback(m_audio, "canplay", canPlayCallback));
304
305 // canplaythrough
306 auto canPlayThroughCallback = [&](emscripten::val event) {
307 Q_UNUSED(event)
308 Q_EMIT stateChanged(QWasmMediaPlayer::Prepared);
309 };
310 m_canPlayThroughChangeEvent.reset(
311 new qstdweb::EventCallback(m_audio, "canplaythrough", canPlayThroughCallback));
312
313 // play
314 auto playCallback = [&](emscripten::val event) {
315 Q_UNUSED(event)
316 qCDebug(qWasmMediaAudioOutput) << "play";
317 Q_EMIT stateChanged(QWasmMediaPlayer::Started);
318 };
319 m_playEvent.reset(new qstdweb::EventCallback(m_audio, "play", playCallback));
320
321 // durationchange
322 auto durationChangeCallback = [&](emscripten::val event) {
323 qCDebug(qWasmMediaAudioOutput) << "durationChange";
324
325 // duration in ms
326 Q_EMIT durationChanged(event["target"]["duration"].as<double>() * 1000);
327 };
328 m_durationChangeEvent.reset(
329 new qstdweb::EventCallback(m_audio, "durationchange", durationChangeCallback));
330
331 // ended
332 auto endedCallback = [&](emscripten::val event) {
333 Q_UNUSED(event)
334 qCDebug(qWasmMediaAudioOutput) << "ended";
335 m_currentMediaStatus = QMediaPlayer::EndOfMedia;
336 Q_EMIT statusChanged(m_currentMediaStatus);
337 };
338 m_endedEvent.reset(new qstdweb::EventCallback(m_audio, "ended", endedCallback));
339
340 // progress (buffering progress)
341 auto progesssCallback = [&](emscripten::val event) {
342 if (event.isUndefined() || event.isNull())
343 return;
344 qCDebug(qWasmMediaAudioOutput) << "progress";
345 float duration = event["target"]["duration"].as<int>();
346 if (duration < 0) // track not exactly ready yet
347 return;
348
349 emscripten::val timeRanges = event["target"]["buffered"];
350
351 if ((!timeRanges.isNull() || !timeRanges.isUndefined())
352 && timeRanges["length"].as<int>() == 1) {
353 emscripten::val dVal = timeRanges.call<emscripten::val>("end", 0);
354
355 if (!dVal.isNull() || !dVal.isUndefined()) {
356 double bufferedEnd = dVal.as<double>();
357
358 if (duration > 0 && bufferedEnd > 0) {
359 float bufferedValue = (bufferedEnd / duration * 100);
360 qCDebug(qWasmMediaAudioOutput) << "progress buffered" << bufferedValue;
361
362 Q_EMIT bufferingChanged(m_currentBufferedValue);
363 if (bufferedEnd == duration)
364 m_currentMediaStatus = QMediaPlayer::BufferedMedia;
365 else
366 m_currentMediaStatus = QMediaPlayer::BufferingMedia;
367
368 Q_EMIT statusChanged(m_currentMediaStatus);
369 }
370 }
371 }
372 };
373 m_progressChangeEvent.reset(new qstdweb::EventCallback(m_audio, "progress", progesssCallback));
374
375 // timupdate
376 auto timeUpdateCallback = [&](emscripten::val event) {
377 qCDebug(qWasmMediaAudioOutput)
378 << "timeupdate" << (event["target"]["currentTime"].as<double>() * 1000);
379
380 // qt progress is ms
381 Q_EMIT progressChanged(event["target"]["currentTime"].as<double>() * 1000);
382 };
383 m_timeUpdateEvent.reset(new qstdweb::EventCallback(m_audio, "timeupdate", timeUpdateCallback));
384
385 // pause
386 auto pauseCallback = [&](emscripten::val event) {
387 Q_UNUSED(event)
388 qCDebug(qWasmMediaAudioOutput) << "pause";
389
390 int currentTime = m_audio["currentTime"].as<int>(); // in seconds
391 int duration = m_audio["duration"].as<int>(); // in seconds
392 if ((currentTime > 0 && currentTime < duration)) {
393 Q_EMIT stateChanged(QWasmMediaPlayer::Paused);
394 } else {
395 Q_EMIT stateChanged(QWasmMediaPlayer::Stopped);
396 }
397 };
398 m_pauseChangeEvent.reset(new qstdweb::EventCallback(m_audio, "pause", pauseCallback));
399}
400
401QT_END_NAMESPACE
Combined button and popup list for selecting options.
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)