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