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 &signal)
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 &signal)
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 if (isFirefox())
141 setupAudioOutputSelector();
142 else
143 getMediaDevices(); // asynchronous
144}
145
147{
148 return m_cameraDevices.values();
149}
150
152{
153 return m_audioInputs.values();
154}
155
157{
158 return m_audioOutputs.values();
159}
160
161void QWasmMediaDevices::parseDevices(emscripten::val devices)
162{
163 if (devices.isNull() || devices.isUndefined()) {
164 qWarning() << "Something went wrong enumerating devices";
165 return;
166 }
167
168 QList<std::string> cameraDevicesToRemove = m_cameraDevices.keys();
169 QList<std::string> audioOutputsToRemove;
170 QList<std::string> audioInputsToRemove;
171
172 audioOutputsToRemove = m_audioOutputs.keys();
173 audioInputsToRemove = m_audioInputs.keys();
174 m_audioInputsAdded = false;
175 m_audioOutputsAdded = false;
176 m_videoInputsAdded = false;
177
178 bool m_videoInputsRemoved = false;
179 bool m_audioInputsRemoved = false;
180 bool m_audioOutputsRemoved = false;
181
182 for (int i = 0; i < devices["length"].as<int>(); i++) {
183
184 emscripten::val mediaDevice = devices[i];
185
186 const std::string deviceKind = mediaDevice["kind"].as<std::string>();
187 std::string label = mediaDevice["label"].as<std::string>();
188 std::string deviceId = mediaDevice["deviceId"].as<std::string>();
189
190 qCDebug(qWasmMediaDevices) << QString::fromStdString(deviceKind)
191 << QString::fromStdString(deviceId)
192 << QString::fromStdString(label);
193
194 if (deviceId.empty()) { // no permissions we'll use System;
195 label = "System " + deviceKind;
196 deviceId = label;
197 }
198 if (deviceKind.empty())
199 continue;
200 bool isDefault = false;
201
202 if (deviceKind == std::string("videoinput")) {
203 if (!m_cameraDevices.contains(deviceId)) {
204 QCameraDevicePrivate *camera = new QCameraDevicePrivate; // QSharedData
205 camera->id = QString::fromStdString(deviceId).toUtf8();
206 camera->description = QString::fromUtf8(label.c_str());
207 // no camera defaults, first in wins!
208 camera->isDefault = !m_videoInputsAdded;
209 m_cameraDevices.insert(deviceId, camera->create());
210 m_videoInputsAdded = true;
211 }
212 cameraDevicesToRemove.removeOne(deviceId);
213 } else if (deviceKind == std::string("audioinput")) {
214 if (!m_audioInputs.contains(deviceId)) {
215 isDefault = !m_audioInputsAdded;
216 m_audioInputs.insert(
217 deviceId,
218 QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
219 deviceId.c_str(), label.c_str(), isDefault, QAudioDevice::Input)));
220
221 m_audioInputsAdded = true;
222 }
223 audioInputsToRemove.removeOne(deviceId);
224 } else if (deviceKind == std::string("audiooutput")) {
225 if (!m_audioOutputs.contains(deviceId)) {
226 isDefault = !m_audioOutputsAdded;
227 m_audioOutputs.insert(
228 deviceId,
229 QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
230 deviceId.c_str(), label.c_str(), isDefault, QAudioDevice::Output)));
231
232 m_audioOutputsAdded = true;
233 }
234 audioOutputsToRemove.removeOne(deviceId);
235 }
236 // if permissions are given label will hold the actual
237 // camera name, such as "Live! Cam Sync 1080p (041e:409d)"
238 }
239 // any left here were removed
240 int j = 0;
241 for (; j < cameraDevicesToRemove.count(); j++) {
242 m_cameraDevices.remove(cameraDevicesToRemove.at(j));
243 }
244 m_videoInputsRemoved = !cameraDevicesToRemove.isEmpty();
245
246 for (j = 0; j < audioInputsToRemove.count(); j++) {
247 m_audioInputs.remove(audioInputsToRemove.at(j));
248 }
249 m_audioInputsRemoved = !audioInputsToRemove.isEmpty();
250
251 for (j = 0; j < audioOutputsToRemove.count(); j++) {
252 m_audioOutputs.remove(audioOutputsToRemove.at(j));
253 }
254 m_audioOutputsRemoved = !audioOutputsToRemove.isEmpty();
255
256 if (m_videoInputsAdded || m_videoInputsRemoved) {
257 auto videoDevices = static_cast<QWasmCameraDevices*>(QPlatformMediaIntegration::instance()->videoDevices());
258 QMetaObject::invokeMethod(videoDevices, &QWasmCameraDevices::onVideoInputsChanged,
259 Qt::QueuedConnection);
260 }
261 if (m_audioInputsAdded || m_audioInputsRemoved) {
262 auto audioDevices = static_cast<QWasmAudioDevices*>(QPlatformMediaIntegration::instance()->audioDevices());
263 QMetaObject::invokeMethod(audioDevices, &QWasmAudioDevices::onAudioInputsChanged,
264 Qt::QueuedConnection);
265 }
266 if (!m_audioOutputsAdded) {
267 // Firefox and Safari require mic or camera permissions
268 // (or selectAudioOutput for Firefox)
269 // to enumerate output devices, so we just fake one.
270 // The device actually does not require perms to play.
271 m_audioOutputs.insert(
272 "",
273 QAudioDevicePrivate::createQAudioDevice(std::make_unique<QWasmAudioDevice>(
274 "", "System output", true, QAudioDevice::Output)));
275 m_audioOutputsAdded = true;
276 }
277 if (m_audioOutputsAdded || m_audioOutputsRemoved) {
278 auto audioDevices = static_cast<QWasmAudioDevices*>(QPlatformMediaIntegration::instance()->audioDevices());
279 QMetaObject::invokeMethod(audioDevices, &QWasmAudioDevices::onAudioOutputsChanged,
280 Qt::QueuedConnection);
281 }
282
283}
284
286{
287 emscripten::val navigator = emscripten::val::global("navigator");
288 m_jsMediaDevicesInterface = navigator["mediaDevices"];
289
290 if (m_jsMediaDevicesInterface.isNull() || m_jsMediaDevicesInterface.isUndefined()) {
291 qWarning() << "No media devices found";
292 return;
293 }
294
295 if (qstdweb::haveAsyncify()) {
296
297#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY
298 auto asyncEnumerate = [](void *arg){
299 QWasmMediaDevices *mediaDevices = static_cast<QWasmMediaDevices *>(arg);
300 mediaDevices->devicesList = mediaDevices->m_jsMediaDevicesInterface.call<emscripten::val>("enumerateDevices").await();
301 if (mediaDevices->devicesList.isNull() || mediaDevices->devicesList.isUndefined()) {
302 qWarning() << "devices list error";
303 return;
304 }
305 mediaDevices->parseDevices(mediaDevices->devicesList);
306 };
307
308 asyncEnumerate(this);
309
310 m_deviceChangedCallback = std::make_unique<qstdweb::EventCallback>(
311 m_jsMediaDevicesInterface, "devicechange",
312 [this, asyncEnumerate](emscripten::val) {
313 asyncEnumerate(this);
314 });
315#endif
316
317 } else {
318
319 qstdweb::PromiseCallbacks enumerateDevicesCallback{
320 .thenFunc =
321 [&](emscripten::val devices) {
322 parseDevices(devices);
323 },
324 .catchFunc =
325 [this](emscripten::val error) {
326 qWarning() << "mediadevices enumerateDevices fail"
327 << QString::fromStdString(error["name"].as<std::string>())
328 << QString::fromStdString(error["message"].as<std::string>());
329 m_initDone = false;
330 }
331 };
332
333 qstdweb::Promise::make(m_jsMediaDevicesInterface,
334 QStringLiteral("enumerateDevices"),
335 std::move(enumerateDevicesCallback));
336
337 // setup devicechange monitor
338 m_deviceChangedCallback = std::make_unique<qstdweb::EventCallback>(
339 m_jsMediaDevicesInterface, "devicechange",
340 [this, enumerateDevicesCallback](emscripten::val) {
341 qstdweb::Promise::make(m_jsMediaDevicesInterface,
342 QStringLiteral("enumerateDevices"),
343 std::move(enumerateDevicesCallback));
344 });
345 }
346
347}
348
349QT_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()