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 };
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 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 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 emit readyChanged(true);
301 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 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 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 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 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 emit bufferingChanged(m_currentBufferedValue);
363 if (bufferedEnd == duration)
364 m_currentMediaStatus = QMediaPlayer::BufferedMedia;
365 else
366 m_currentMediaStatus = QMediaPlayer::BufferingMedia;
367
368 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 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 emit stateChanged(QWasmMediaPlayer::Paused);
394 } else {
395 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)