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
20Q_GLOBAL_STATIC(QWasmMediaDevices, s_wasmMediaDevicesInstance);
21
22bool isFirefox() {
23 return !emscripten::val::global("InstallTrigger").isUndefined();
24}
25
26
27// Firefox only as it limits enumerateDevices to inputs only when no permissions are given
28extern "C" {
34} // extern "C"
35
37 const overlay = document.createElement('div');
38 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;';
39
40 const dialog = document.createElement('div');
41 dialog.style.cssText = 'background:white;padding:24px;border-radius:8px;text-align:center;font-family:sans-serif;min-width:240px;';
42
43 const message = document.createElement('p');
44 message.textContent = 'Select an audio output device to continue.';
45 message.style.cssText = 'margin:0 0 16px 0;font-size:14px;';
46
47 const button = document.createElement('button');
48 button.textContent = 'Select Audio Output';
49 button.style.cssText = 'padding:8px 16px;font-size:14px;cursor:pointer;';
50
51 button.addEventListener('click', async () => {
53 try {
55 console.log("Selected device: ", deviceInfo.label);
57 } catch (err) {
59 }
60 }, { once: true });
61
66});
67
68QWasmCameraDevices::QWasmCameraDevices(QPlatformMediaIntegration *integration)
69 : QPlatformVideoDevices(integration)
70{
71}
72
77
78void QWasmCameraDevices::connectNotify(const QMetaMethod &signal)
79{
80 Q_ASSERT(QThread::isMainThread());
82}
83
85
87 const QAudioFormat &fmt,
88 QObject *parent)
89{
90 return new QWasmAudioSource(deviceInfo, fmt, parent);
91}
92
93QPlatformAudioSink *QWasmAudioDevices::createAudioSink(const QAudioDevice &deviceInfo,
94 const QAudioFormat &fmt,
95 QObject *parent)
96{
97 return new QWasmAudioSink(deviceInfo, fmt, parent);
98}
99
104
109
110void QWasmAudioDevices::connectNotify(const QMetaMethod &signal)
111{
112 Q_ASSERT(QThread::isMainThread());
114}
115
120
122{
123 return s_wasmMediaDevicesInstance();
124}
125
127{
128 if (m_initDone)
129 return;
130
131 m_initDone = true;
132 if (isFirefox())
133 setupAudioOutputSelector();
134 else
135 getMediaDevices(); // asynchronous
136}
137
139{
140 return m_cameraDevices.values();
141}
142
144{
145 return m_audioInputs.values();
146}
147
149{
150 return m_audioOutputs.values();
151}
152
153void QWasmMediaDevices::parseDevices(emscripten::val devices)
154{
155 if (devices.isNull() || devices.isUndefined()) {
156 qWarning() << "Something went wrong enumerating devices";
157 return;
158 }
159
160 QList<std::string> cameraDevicesToRemove = m_cameraDevices.keys();
161 QList<std::string> audioOutputsToRemove;
162 QList<std::string> audioInputsToRemove;
163
164 audioOutputsToRemove = m_audioOutputs.keys();
165 audioInputsToRemove = m_audioInputs.keys();
166 m_audioInputsAdded = false;
167 m_audioOutputsAdded = false;
168 m_videoInputsAdded = false;
169
170 bool m_videoInputsRemoved = false;
171 bool m_audioInputsRemoved = false;
172 bool m_audioOutputsRemoved = false;
173
174 for (int i = 0; i < devices["length"].as<int>(); i++) {
175
176 emscripten::val mediaDevice = devices[i];
177
178 const std::string deviceKind = mediaDevice["kind"].as<std::string>();
179 std::string label = mediaDevice["label"].as<std::string>();
180 std::string deviceId = mediaDevice["deviceId"].as<std::string>();
181
182 qCDebug(qWasmMediaDevices) << QString::fromStdString(deviceKind)
183 << QString::fromStdString(deviceId)
184 << QString::fromStdString(label);
185
186 if (deviceId.empty()) { // no permissions we'll use System;
187 label = "System " + deviceKind;
188 deviceId = label;
189 }
190 if (deviceKind.empty())
191 continue;
192 bool isDefault = false;
193
194 if (deviceKind == std::string("videoinput")) {
195 if (!m_cameraDevices.contains(deviceId)) {
196 QCameraDevicePrivate *camera = new QCameraDevicePrivate; // QSharedData
197 camera->id = QString::fromStdString(deviceId).toUtf8();
198 camera->description = QString::fromUtf8(label.c_str());
199 // no camera defaults, first in wins!
200 camera->isDefault = !m_videoInputsAdded;
201 m_cameraDevices.insert(deviceId, camera->create());
202 m_videoInputsAdded = true;
203 }
204 cameraDevicesToRemove.removeOne(deviceId);
205 } else if (deviceKind == std::string("audioinput")) {
206 if (!m_audioInputs.contains(deviceId)) {
207 isDefault = !m_audioInputsAdded;
208 m_audioInputs.insert(
209 deviceId,
210 QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
211 deviceId.c_str(), label.c_str(), isDefault, QAudioDevice::Input)));
212
213 m_audioInputsAdded = true;
214 }
215 audioInputsToRemove.removeOne(deviceId);
216 } else if (deviceKind == std::string("audiooutput")) {
217 if (!m_audioOutputs.contains(deviceId)) {
218 isDefault = !m_audioOutputsAdded;
219 m_audioOutputs.insert(
220 deviceId,
221 QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
222 deviceId.c_str(), label.c_str(), isDefault, QAudioDevice::Output)));
223
224 m_audioOutputsAdded = true;
225 }
226 audioOutputsToRemove.removeOne(deviceId);
227 }
228 // if permissions are given label will hold the actual
229 // camera name, such as "Live! Cam Sync 1080p (041e:409d)"
230 }
231 // any left here were removed
232 int j = 0;
233 for (; j < cameraDevicesToRemove.count(); j++) {
234 m_cameraDevices.remove(cameraDevicesToRemove.at(j));
235 }
236 m_videoInputsRemoved = !cameraDevicesToRemove.isEmpty();
237
238 for (j = 0; j < audioInputsToRemove.count(); j++) {
239 m_audioInputs.remove(audioInputsToRemove.at(j));
240 }
241 m_audioInputsRemoved = !audioInputsToRemove.isEmpty();
242
243 for (j = 0; j < audioOutputsToRemove.count(); j++) {
244 m_audioOutputs.remove(audioOutputsToRemove.at(j));
245 }
246 m_audioOutputsRemoved = !audioOutputsToRemove.isEmpty();
247
248 if (m_videoInputsAdded || m_videoInputsRemoved) {
249 auto videoDevices = static_cast<QWasmCameraDevices*>(QPlatformMediaIntegration::instance()->videoDevices());
250 videoDevices->onVideoInputsChanged();
251 }
252 if (m_audioInputsAdded || m_audioInputsRemoved) {
253 auto audioDevices = static_cast<QWasmAudioDevices*>(QPlatformMediaIntegration::instance()->audioDevices());
254 audioDevices->onAudioInputsChanged();
255 }
256 if (!m_audioOutputsAdded) {
257 // Firefox and Safari require mic or camera permissions
258 // (or selectAudioOutput for Firefox)
259 // to enumerate output devices, so we just fake one.
260 // The device actually does not require perms to play.
261 m_audioOutputs.insert(
262 "",
263 QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
264 "", "System output", true, QAudioDevice::Output)));
265 m_audioOutputsAdded = true;
266 }
267 if (m_audioOutputsAdded || m_audioOutputsRemoved) {
268 auto audioDevices = static_cast<QWasmAudioDevices*>(QPlatformMediaIntegration::instance()->audioDevices());
269 audioDevices->onAudioOutputsChanged();
270 }
271
272}
273
275{
276 emscripten::val navigator = emscripten::val::global("navigator");
277 m_jsMediaDevicesInterface = navigator["mediaDevices"];
278
279 if (m_jsMediaDevicesInterface.isNull() || m_jsMediaDevicesInterface.isUndefined()) {
280 qWarning() << "No media devices found";
281 return;
282 }
283
284 if (qstdweb::haveAsyncify()) {
285
286#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY
287 auto asyncEnumerate = [](void *arg){
288 QWasmMediaDevices *mediaDevices = static_cast<QWasmMediaDevices *>(arg);
289 mediaDevices->devicesList = mediaDevices->m_jsMediaDevicesInterface.call<emscripten::val>("enumerateDevices").await();
290 if (mediaDevices->devicesList.isNull() || mediaDevices->devicesList.isUndefined()) {
291 qWarning() << "devices list error";
292 return;
293 }
294 mediaDevices->parseDevices(mediaDevices->devicesList);
295 };
296
297 asyncEnumerate(this);
298
299 m_deviceChangedCallback = std::make_unique<qstdweb::EventCallback>(
300 m_jsMediaDevicesInterface, "devicechange",
301 [this, asyncEnumerate](emscripten::val) {
302 asyncEnumerate(this);
303 });
304#endif
305
306 } else {
307
308 qstdweb::PromiseCallbacks enumerateDevicesCallback{
309 .thenFunc =
310 [&](emscripten::val devices) {
311 parseDevices(devices);
312 },
313 .catchFunc =
314 [this](emscripten::val error) {
315 qWarning() << "mediadevices enumerateDevices fail"
316 << QString::fromStdString(error["name"].as<std::string>())
317 << QString::fromStdString(error["message"].as<std::string>());
318 m_initDone = false;
319 }
320 };
321
322 qstdweb::Promise::make(m_jsMediaDevicesInterface,
323 QStringLiteral("enumerateDevices"),
324 std::move(enumerateDevicesCallback));
325
326 // setup devicechange monitor
327 m_deviceChangedCallback = std::make_unique<qstdweb::EventCallback>(
328 m_jsMediaDevicesInterface, "devicechange",
329 [this, enumerateDevicesCallback](emscripten::val) {
330 qstdweb::Promise::make(m_jsMediaDevicesInterface,
331 QStringLiteral("enumerateDevices"),
332 std::move(enumerateDevicesCallback));
333 });
334 }
335
336}
337
338QT_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);})
EMSCRIPTEN_KEEPALIVE void qtMediaDevicesOnAudioOutputSelected()
bool isFirefox()