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
qwasmmediadevices.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 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#include "private/qcameradevice_p.h"
6#include "private/qplatformmediaintegration_p.h"
10
11#include <QMap>
12#include <QDebug>
13
14#include <emscripten.h>
15
17
18Q_LOGGING_CATEGORY(qWasmMediaDevices, "qt.multimedia.wasm.mediadevices")
19
20static QWasmMediaDevices *s_mediaDevicesInstance = nullptr;
21static bool s_constructingInstance = false;
22
23bool isFirefox() {
24 return !emscripten::val::global("InstallTrigger").isUndefined();
25}
26
27
28// Firefox only as it limits enumerateDevices to inputs only when no permissions are given
29extern "C" {
35} // extern "C"
36
38 const overlay = document.createElement('div');
39 overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;';
40
41 const dialog = document.createElement('div');
42 dialog.style.cssText = 'background:white;padding:24px;border-radius:8px;text-align:center;font-family:sans-serif;min-width:240px;';
43
44 const message = document.createElement('p');
45 message.textContent = 'Select an audio output device to continue.';
46 message.style.cssText = 'margin:0 0 16px 0;font-size:14px;';
47
48 const button = document.createElement('button');
49 button.textContent = 'Select Audio Output';
50 button.style.cssText = 'padding:8px 16px;font-size:14px;cursor:pointer;';
51
52 button.addEventListener('click', async () => {
54 try {
56 console.log("Selected device: ", deviceInfo.label);
58 } catch (err) {
60 }
61 }, { once: true });
62
67});
68
69QWasmCameraDevices::QWasmCameraDevices(QPlatformMediaIntegration *integration)
70 : QPlatformVideoDevices(integration)
71{
72}
73
78
79void QWasmCameraDevices::connectNotify(const QMetaMethod &)
80{
81 Q_ASSERT(QThread::isMainThread());
83}
84
86
88 const QAudioFormat &fmt,
89 QObject *parent)
90{
91 return new QWasmAudioSource(deviceInfo, fmt, parent);
92}
93
94QPlatformAudioSink *QWasmAudioDevices::createAudioSink(const QAudioDevice &deviceInfo,
95 const QAudioFormat &fmt,
96 QObject *parent)
97{
98 return new QWasmAudioSink(deviceInfo, fmt, parent);
99}
100
105
110
111void QWasmAudioDevices::connectNotify(const QMetaMethod &)
112{
113 Q_ASSERT(QThread::isMainThread());
115}
116
120
122{
123 if (s_mediaDevicesInstance)
124 return s_mediaDevicesInstance;
126 return nullptr;
128 s_mediaDevicesInstance = new QWasmMediaDevices();
130 s_mediaDevicesInstance->initDevices();
131 return s_mediaDevicesInstance;
132}
133
135{
136 if (m_initDone)
137 return;
138
139 m_initDone = true;
140
141 // Seed a synchronous fallback output device immediately: enumerateDevices() below
142 // is asynchronous, so without this, any consumer that needs a device synchronously
143 // right after construction (e.g. QSoundEffect) sees an empty list and fails.
144 insertFallbackAudioOutput();
145
146 if (isFirefox())
147 setupAudioOutputSelector();
148 else
149 getMediaDevices(); // asynchronous
150}
151
153{
154 return m_cameraDevices.values();
155}
156
158{
159 return m_audioInputs.values();
160}
161
163{
164 return m_audioOutputs.values();
165}
166
167void QWasmMediaDevices::parseDevices(emscripten::val devices)
168{
169 if (devices.isNull() || devices.isUndefined()) {
170 qWarning() << "Something went wrong enumerating devices";
171 return;
172 }
173
174 QList<std::string> cameraDevicesToRemove = m_cameraDevices.keys();
175 QList<std::string> audioOutputsToRemove;
176 QList<std::string> audioInputsToRemove;
177
178 audioOutputsToRemove = m_audioOutputs.keys();
179 audioInputsToRemove = m_audioInputs.keys();
180 m_audioInputsAdded = false;
181 m_audioOutputsAdded = false;
182 m_videoInputsAdded = false;
183
184 bool m_videoInputsRemoved = false;
185 bool m_audioInputsRemoved = false;
186 bool m_audioOutputsRemoved = false;
187
188 for (int i = 0; i < devices["length"].as<int>(); i++) {
189
190 emscripten::val mediaDevice = devices[i];
191
192 const std::string deviceKind = mediaDevice["kind"].as<std::string>();
193 std::string label = mediaDevice["label"].as<std::string>();
194 std::string deviceId = mediaDevice["deviceId"].as<std::string>();
195
196 qCDebug(qWasmMediaDevices) << QString::fromStdString(deviceKind)
197 << QString::fromStdString(deviceId)
198 << QString::fromStdString(label);
199
200 if (deviceId.empty()) { // no permissions we'll use System;
201 label = "System " + deviceKind;
202 deviceId = label;
203 }
204 if (deviceKind.empty())
205 continue;
206 bool isDefault = false;
207
208 if (deviceKind == std::string("videoinput")) {
209 if (!m_cameraDevices.contains(deviceId)) {
210 QCameraDevicePrivate *camera = new QCameraDevicePrivate; // QSharedData
211 camera->id = QString::fromStdString(deviceId).toUtf8();
212 camera->description = QString::fromUtf8(label.c_str());
213 // no camera defaults, first in wins!
214 camera->isDefault = !m_videoInputsAdded;
215 m_cameraDevices.insert(deviceId, camera->create());
216 m_videoInputsAdded = true;
217 }
218 cameraDevicesToRemove.removeOne(deviceId);
219 } else if (deviceKind == std::string("audioinput")) {
220 if (!m_audioInputs.contains(deviceId)) {
221 isDefault = !m_audioInputsAdded;
222 m_audioInputs.insert(
223 deviceId,
224 QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
225 deviceId.c_str(), label.c_str(), isDefault, QAudioDevice::Input)));
226
227 m_audioInputsAdded = true;
228 }
229 audioInputsToRemove.removeOne(deviceId);
230 } else if (deviceKind == std::string("audiooutput")) {
231 if (!m_audioOutputs.contains(deviceId)) {
232 isDefault = !m_audioOutputsAdded;
233 m_audioOutputs.insert(
234 deviceId,
235 QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
236 deviceId.c_str(), label.c_str(), isDefault, QAudioDevice::Output)));
237
238 m_audioOutputsAdded = true;
239 }
240 audioOutputsToRemove.removeOne(deviceId);
241 }
242 // if permissions are given label will hold the actual
243 // camera name, such as "Live! Cam Sync 1080p (041e:409d)"
244 }
245 // any left here were removed
246 int j = 0;
247 for (; j < cameraDevicesToRemove.count(); j++) {
248 m_cameraDevices.remove(cameraDevicesToRemove.at(j));
249 }
250 m_videoInputsRemoved = !cameraDevicesToRemove.isEmpty();
251
252 for (j = 0; j < audioInputsToRemove.count(); j++) {
253 m_audioInputs.remove(audioInputsToRemove.at(j));
254 }
255 m_audioInputsRemoved = !audioInputsToRemove.isEmpty();
256
257 for (j = 0; j < audioOutputsToRemove.count(); j++) {
258 m_audioOutputs.remove(audioOutputsToRemove.at(j));
259 }
260 m_audioOutputsRemoved = !audioOutputsToRemove.isEmpty();
261
262 if (m_videoInputsAdded || m_videoInputsRemoved) {
263 auto videoDevices = static_cast<QWasmCameraDevices*>(QPlatformMediaIntegration::instance()->videoDevices());
264 QMetaObject::invokeMethod(videoDevices, &QWasmCameraDevices::onVideoInputsChanged,
265 Qt::QueuedConnection);
266 }
267 if (m_audioInputsAdded || m_audioInputsRemoved) {
268 auto audioDevices = static_cast<QWasmAudioDevices*>(QPlatformMediaIntegration::instance()->audioDevices());
269 QMetaObject::invokeMethod(audioDevices, &QWasmAudioDevices::onAudioInputsChanged,
270 Qt::QueuedConnection);
271 }
272 if (!m_audioOutputsAdded) {
273 // Firefox and Safari require mic or camera permissions
274 // (or selectAudioOutput for Firefox)
275 // to enumerate output devices, so we just fake one.
276 // The device actually does not require perms to play.
277 insertFallbackAudioOutput();
278 }
279 if (m_audioOutputsAdded || m_audioOutputsRemoved) {
280 auto audioDevices = static_cast<QWasmAudioDevices*>(QPlatformMediaIntegration::instance()->audioDevices());
281 QMetaObject::invokeMethod(audioDevices, &QWasmAudioDevices::onAudioOutputsChanged,
282 Qt::QueuedConnection);
283 }
284
285}
286
287void QWasmMediaDevices::insertFallbackAudioOutput()
288{
289 m_audioOutputs.insert(
290 "",
291 QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
292 "", "System output", true, QAudioDevice::Output)));
293 m_audioOutputsAdded = true;
294}
295
297{
298 emscripten::val navigator = emscripten::val::global("navigator");
299 m_jsMediaDevicesInterface = navigator["mediaDevices"];
300
301 if (m_jsMediaDevicesInterface.isNull() || m_jsMediaDevicesInterface.isUndefined()) {
302 qWarning() << "No media devices found";
303 return;
304 }
305
306 if (qstdweb::haveAsyncify()) {
307
308#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY
309 auto asyncEnumerate = [](void *arg){
310 QWasmMediaDevices *mediaDevices = static_cast<QWasmMediaDevices *>(arg);
311 mediaDevices->devicesList = mediaDevices->m_jsMediaDevicesInterface.call<emscripten::val>("enumerateDevices").await();
312 if (mediaDevices->devicesList.isNull() || mediaDevices->devicesList.isUndefined()) {
313 qWarning() << "devices list error";
314 return;
315 }
316 mediaDevices->parseDevices(mediaDevices->devicesList);
317 };
318
319 asyncEnumerate(this);
320
321 m_deviceChangedCallback = std::make_unique<qstdweb::EventCallback>(
322 m_jsMediaDevicesInterface, "devicechange",
323 [this, asyncEnumerate](emscripten::val) {
324 asyncEnumerate(this);
325 });
326#endif
327
328 } else {
329
330 qstdweb::PromiseCallbacks enumerateDevicesCallback{
331 .thenFunc =
332 [&](emscripten::val devices) {
333 parseDevices(devices);
334 },
335 .catchFunc =
336 [this](emscripten::val error) {
337 qWarning() << "mediadevices enumerateDevices fail"
338 << QString::fromStdString(error["name"].as<std::string>())
339 << QString::fromStdString(error["message"].as<std::string>());
340 m_initDone = false;
341 },
342 .finallyFunc = {}
343 };
344
345 qstdweb::Promise::make(m_jsMediaDevicesInterface,
346 QStringLiteral("enumerateDevices"),
347 std::move(enumerateDevicesCallback));
348
349 // setup devicechange monitor
350 m_deviceChangedCallback = std::make_unique<qstdweb::EventCallback>(
351 m_jsMediaDevicesInterface, "devicechange",
352 [this, enumerateDevicesCallback](emscripten::val) {
353 qstdweb::Promise::make(m_jsMediaDevicesInterface,
354 QStringLiteral("enumerateDevices"),
355 std::move(enumerateDevicesCallback));
356 });
357 }
358
359}
360
361QT_END_NAMESPACE
The QAudioDevice class provides an information about audio devices and their functionality.
The QCameraDevice class provides general information about camera devices.
QPlatformAudioSink * createAudioSink(const QAudioDevice &, const QAudioFormat &, QObject *parent) override
QList< QAudioDevice > findAudioOutputs() const override
QList< QAudioDevice > findAudioInputs() const override
QPlatformAudioSource * createAudioSource(const QAudioDevice &, const QAudioFormat &, QObject *parent) override
void connectNotify(const QMetaMethod &signal) override
void connectNotify(const QMetaMethod &signal) override
QList< QCameraDevice > findVideoInputs() const override
QList< QCameraDevice > videoInputs() const
static QWasmMediaDevices * instance()
QList< QAudioDevice > audioOutputs() const
QList< QAudioDevice > audioInputs() const
Combined button and popup list for selecting options.
EM_JS(void, setupAudioOutputSelector,(), { const overlay=document.createElement('div');overlay.style.cssText='position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.5);z-index:9999;display:flex;align-items:center;justify-content:center;';const dialog=document.createElement('div');dialog.style.cssText='background:white;padding:24px;border-radius:8px;text-align:center;font-family:sans-serif;min-width:240px;';const message=document.createElement('p');message.textContent='Select an audio output device to continue.';message.style.cssText='margin:0 0 16px 0;font-size:14px;';const button=document.createElement('button');button.textContent='Select Audio Output';button.style.cssText='padding:8px 16px;font-size:14px;cursor:pointer;';button.addEventListener('click', async()=> { document.body.removeChild(overlay);try { const deviceInfo=await navigator.mediaDevices.selectAudioOutput();console.log("Selected device: ", deviceInfo.label);Module._qtMediaDevicesOnAudioOutputSelected();} catch(err) { console.error(err);} }, { once:true });dialog.appendChild(message);dialog.appendChild(button);overlay.appendChild(dialog);document.body.appendChild(overlay);})
static bool s_constructingInstance
EMSCRIPTEN_KEEPALIVE void qtMediaDevicesOnAudioOutputSelected()
bool isFirefox()