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.exists()) {
113 qCDebug(qWasmMediaAudioOutput) << "Error"
114 << "Media file does not exist";
115 QMetaObject::invokeMethod(this, &QWasmAudioOutput::errorOccured, Qt::QueuedConnection,
116 QMediaPlayer::ResourceError,
117 QStringLiteral("Media file does not exist"));
118
119 return;
120 }
121
122 if (!mediaFile.open(QIODevice::ReadOnly)) {
123 qCDebug(qWasmMediaAudioOutput) << "Error"
124 << "Media file could not be opened";
125 emit errorOccured(QMediaPlayer::ResourceError,
126 QStringLiteral("Media file could not be opened"));
127 return;
128 }
129
130 // local files are relatively small due to browser filesystem being restricted
131 QByteArray content = mediaFile.readAll();
132
133 QMimeDatabase db;
134 qCDebug(qWasmMediaAudioOutput) << db.mimeTypeForData(content).name();
135
136 qstdweb::Blob contentBlob = qstdweb::Blob::copyFrom(content.constData(), content.size());
137 emscripten::val contentUrl =
138 qstdweb::window()["URL"].call<emscripten::val>("createObjectURL", contentBlob.val());
139
140 emscripten::val audioSourceElement =
141 document.call<emscripten::val>("createElement", std::string("source"));
142
143 audioSourceElement.set("src", contentUrl);
144
145 // work around Safari not being able to read audio from blob URLs.
146 QFileInfo info(m_source);
147 QMimeType mimeType = db.mimeTypeForFile(info);
148
149 audioSourceElement.set("type", mimeType.name().toStdString());
150 m_audio.call<void>("appendChild", audioSourceElement);
151
152 m_audio.call<void>("setAttribute", emscripten::val("srcObject"), contentUrl);
153
154 } else {
155 m_source = url.toString();
156 m_audio.set("src", m_source.toStdString());
157 }
158 m_audio.set("id", device.id().toStdString());
159
160 body.call<void>("appendChild", m_audio);
161 qCDebug(qWasmMediaAudioOutput) << Q_FUNC_INFO << device.id();
162
163 doElementCallbacks();
164}
165
166void QWasmAudioOutput::setSource(QIODevice *stream)
167{
168 m_audioIODevice = stream;
169}
170
171void QWasmAudioOutput::start()
172{
173 if (m_audio.isNull() || m_audio.isUndefined()) {
174 qCDebug(qWasmMediaAudioOutput) << "audio failed to start";
175 emit errorOccured(QMediaPlayer::ResourceError,
176 QStringLiteral("Audio element resource error"));
177 return;
178 }
179
180 m_audio.call<void>("play");
181}
182
183void QWasmAudioOutput::stop()
184{
185 if (m_audio.isNull() || m_audio.isUndefined()) {
186 qCDebug(qWasmMediaAudioOutput) << "audio failed to start";
187 emit errorOccured(QMediaPlayer::ResourceError,
188 QStringLiteral("Audio element resource error"));
189 return;
190 }
191 if (!m_source.isEmpty()) {
192 pause();
193 m_audio.set("currentTime", emscripten::val(0));
194 }
195 if (m_audioIODevice) {
196 m_audioIODevice->close();
197 delete m_audioIODevice;
198 m_audioIODevice = 0;
199 }
200}
201
202void QWasmAudioOutput::pause()
203{
204 if (m_audio.isNull() || m_audio.isUndefined()) {
205 qCDebug(qWasmMediaAudioOutput) << "audio failed to start";
206 emit errorOccured(QMediaPlayer::ResourceError,
207 QStringLiteral("Audio element resource error"));
208 return;
209 }
210 m_audio.call<emscripten::val>("pause");
211}
212
213void QWasmAudioOutput::createAudioElement(const std::string &id)
214{
215 emscripten::val document = emscripten::val::global("document");
216 m_audio = document.call<emscripten::val>("createElement", std::string("audio"));
217
218 if (id == "System output") { // defaul output, no perms needed
219 emscripten::val audioContext = emscripten::val::global("window")["AudioContext"].new_();
220 emscripten::val sourceNode = audioContext.call<emscripten::val>("createMediaElementSource", m_audio);
221
222 sourceNode.call<void>("connect", audioContext["destination"]);
223// or control volume // is this needed?
224 // emscripten::val volumeNode = audioContext.call<emscripten::val>("createGain");
225 // sourceNode.call<void>("connect", volumeNode);
226 // volumeNode.call<void>("connect", audioContext["destination"]);
227 }
228
229 // only works in chrome and firefox.
230 // Firefox this feature is behind media.setsinkid.enabled preferences
231 // allows user to choose audio output device
232
233 if (!m_audio.hasOwnProperty("sinkId") || m_audio["sinkId"].isUndefined()) {
234 return;
235 }
236
237 std::string usableId = id;
238 if (usableId.empty() || usableId == "System output")
239 usableId = QMediaDevices::defaultAudioOutput().id();
240
241 qstdweb::PromiseCallbacks sinkIdCallbacks{
242 .thenFunc = [](emscripten::val) { qCWarning(qWasmMediaAudioOutput) << "setSinkId ok"; },
243 .catchFunc =
244 [](emscripten::val) {
245 qCWarning(qWasmMediaAudioOutput) << "Error while trying to setSinkId";
246 },
247 .finallyFunc = nullptr
248 };
249 qstdweb::Promise::make(m_audio, u"setSinkId"_s, std::move(sinkIdCallbacks), std::move(usableId));
250
251 m_audio.set("id", usableId.c_str());
252}
253
254void QWasmAudioOutput::doElementCallbacks()
255{
256 // error
257 auto errorCallback = [&](emscripten::val event) {
258 qCDebug(qWasmMediaAudioOutput) << "error";
259 if (event.isUndefined() || event.isNull())
260 return;
261 emit errorOccured(m_audio["error"]["code"].as<int>(),
262 QString::fromStdString(m_audio["error"]["message"].as<std::string>()));
263
264 QString errorMessage =
265 QString::fromStdString(m_audio["error"]["message"].as<std::string>());
266 if (errorMessage.isEmpty()) {
267 switch (m_audio["error"]["code"].as<int>()) {
268 case AudioElementError::MEDIA_ERR_ABORTED:
269 errorMessage = QStringLiteral("aborted by the user agent at the user's request.");
270 break;
271 case AudioElementError::MEDIA_ERR_NETWORK:
272 errorMessage = QStringLiteral("network error.");
273 break;
274 case AudioElementError::MEDIA_ERR_DECODE:
275 errorMessage = QStringLiteral("decoding error.");
276 break;
277 case AudioElementError::MEDIA_ERR_SRC_NOT_SUPPORTED:
278 errorMessage = QStringLiteral("src attribute not suitable.");
279 break;
280 };
281 }
282 qCDebug(qWasmMediaAudioOutput) << m_audio["error"]["code"].as<int>() << errorMessage;
283
284 emit errorOccured(m_audio["error"]["code"].as<int>(), errorMessage);
285 };
286 m_errorChangeEvent.reset(new qstdweb::EventCallback(m_audio, "error", errorCallback));
287
288 // loadeddata
289 auto loadedDataCallback = [&](emscripten::val event) {
290 Q_UNUSED(event)
291 qCDebug(qWasmMediaAudioOutput) << "loaded data";
292 qstdweb::window()["URL"].call<emscripten::val>("revokeObjectURL", m_audio["src"]);
293 };
294 m_loadedDataEvent.reset(new qstdweb::EventCallback(m_audio, "loadeddata", loadedDataCallback));
295
296 // canplay
297 auto canPlayCallback = [&](emscripten::val event) {
298 if (event.isUndefined() || event.isNull())
299 return;
300 qCDebug(qWasmMediaAudioOutput) << "can play";
301 emit readyChanged(true);
302 emit stateChanged(QWasmMediaPlayer::Preparing);
303 };
304 m_canPlayChangeEvent.reset(new qstdweb::EventCallback(m_audio, "canplay", canPlayCallback));
305
306 // canplaythrough
307 auto canPlayThroughCallback = [&](emscripten::val event) {
308 Q_UNUSED(event)
309 emit stateChanged(QWasmMediaPlayer::Prepared);
310 };
311 m_canPlayThroughChangeEvent.reset(
312 new qstdweb::EventCallback(m_audio, "canplaythrough", canPlayThroughCallback));
313
314 // play
315 auto playCallback = [&](emscripten::val event) {
316 Q_UNUSED(event)
317 qCDebug(qWasmMediaAudioOutput) << "play";
318 emit stateChanged(QWasmMediaPlayer::Started);
319 };
320 m_playEvent.reset(new qstdweb::EventCallback(m_audio, "play", playCallback));
321
322 // durationchange
323 auto durationChangeCallback = [&](emscripten::val event) {
324 qCDebug(qWasmMediaAudioOutput) << "durationChange";
325
326 // duration in ms
327 emit durationChanged(event["target"]["duration"].as<double>() * 1000);
328 };
329 m_durationChangeEvent.reset(
330 new qstdweb::EventCallback(m_audio, "durationchange", durationChangeCallback));
331
332 // ended
333 auto endedCallback = [&](emscripten::val event) {
334 Q_UNUSED(event)
335 qCDebug(qWasmMediaAudioOutput) << "ended";
336 m_currentMediaStatus = QMediaPlayer::EndOfMedia;
337 emit statusChanged(m_currentMediaStatus);
338 };
339 m_endedEvent.reset(new qstdweb::EventCallback(m_audio, "ended", endedCallback));
340
341 // progress (buffering progress)
342 auto progesssCallback = [&](emscripten::val event) {
343 if (event.isUndefined() || event.isNull())
344 return;
345 qCDebug(qWasmMediaAudioOutput) << "progress";
346 float duration = event["target"]["duration"].as<int>();
347 if (duration < 0) // track not exactly ready yet
348 return;
349
350 emscripten::val timeRanges = event["target"]["buffered"];
351
352 if ((!timeRanges.isNull() || !timeRanges.isUndefined())
353 && timeRanges["length"].as<int>() == 1) {
354 emscripten::val dVal = timeRanges.call<emscripten::val>("end", 0);
355
356 if (!dVal.isNull() || !dVal.isUndefined()) {
357 double bufferedEnd = dVal.as<double>();
358
359 if (duration > 0 && bufferedEnd > 0) {
360 float bufferedValue = (bufferedEnd / duration * 100);
361 qCDebug(qWasmMediaAudioOutput) << "progress buffered" << bufferedValue;
362
363 emit bufferingChanged(m_currentBufferedValue);
364 if (bufferedEnd == duration)
365 m_currentMediaStatus = QMediaPlayer::BufferedMedia;
366 else
367 m_currentMediaStatus = QMediaPlayer::BufferingMedia;
368
369 emit statusChanged(m_currentMediaStatus);
370 }
371 }
372 }
373 };
374 m_progressChangeEvent.reset(new qstdweb::EventCallback(m_audio, "progress", progesssCallback));
375
376 // timupdate
377 auto timeUpdateCallback = [&](emscripten::val event) {
378 qCDebug(qWasmMediaAudioOutput)
379 << "timeupdate" << (event["target"]["currentTime"].as<double>() * 1000);
380
381 // qt progress is ms
382 emit progressChanged(event["target"]["currentTime"].as<double>() * 1000);
383 };
384 m_timeUpdateEvent.reset(new qstdweb::EventCallback(m_audio, "timeupdate", timeUpdateCallback));
385
386 // pause
387 auto pauseCallback = [&](emscripten::val event) {
388 Q_UNUSED(event)
389 qCDebug(qWasmMediaAudioOutput) << "pause";
390
391 int currentTime = m_audio["currentTime"].as<int>(); // in seconds
392 int duration = m_audio["duration"].as<int>(); // in seconds
393 if ((currentTime > 0 && currentTime < duration)) {
394 emit stateChanged(QWasmMediaPlayer::Paused);
395 } else {
396 emit stateChanged(QWasmMediaPlayer::Stopped);
397 }
398 };
399 m_pauseChangeEvent.reset(new qstdweb::EventCallback(m_audio, "pause", pauseCallback));
400}
401
402QT_END_NAMESPACE
Combined button and popup list for selecting options.
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)