// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qwasmmediadevices_p.h" #include "private/qcameradevice_p.h" #include "private/qplatformmediaintegration_p.h" #include "qwasmaudiosource_p.h" #include "qwasmaudiosink_p.h" #include "qwasmaudiodevice_p.h" #include #include #include #include QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(qWasmMediaDevices, "qt.multimedia.wasm.mediadevices") Q_GLOBAL_STATIC(QWasmMediaDevices, s_wasmMediaDevicesInstance); QWasmCameraDevices::QWasmCameraDevices(QPlatformMediaIntegration *integration) : QPlatformVideoDevices(integration) { } QList QWasmCameraDevices::findVideoInputs() const { return QWasmMediaDevices::instance() ? QWasmMediaDevices::instance()->videoInputs() : QList(); } void QWasmCameraDevices::connectNotify(const QMetaMethod &signal) { Q_ASSERT(QThread::isMainThread()); QWasmMediaDevices::instance(); } QWasmAudioDevices::QWasmAudioDevices() = default; QPlatformAudioSource *QWasmAudioDevices::createAudioSource(const QAudioDevice &deviceInfo, const QAudioFormat &fmt, QObject *parent) { return new QWasmAudioSource(deviceInfo, fmt, parent); } QPlatformAudioSink *QWasmAudioDevices::createAudioSink(const QAudioDevice &deviceInfo, const QAudioFormat &fmt, QObject *parent) { return new QWasmAudioSink(deviceInfo, fmt, parent); } QList QWasmAudioDevices::findAudioInputs() const { return QWasmMediaDevices::instance() ? QWasmMediaDevices::instance()->audioInputs() : QList(); } QList QWasmAudioDevices::findAudioOutputs() const { return QWasmMediaDevices::instance() ? QWasmMediaDevices::instance()->audioOutputs() : QList(); } void QWasmAudioDevices::connectNotify(const QMetaMethod &signal) { Q_ASSERT(QThread::isMainThread()); QWasmMediaDevices::instance(); } QWasmMediaDevices::QWasmMediaDevices() { initDevices(); } QWasmMediaDevices *QWasmMediaDevices::instance() { return s_wasmMediaDevicesInstance(); } void QWasmMediaDevices::initDevices() { if (m_initDone) return; m_initDone = true; getOpenALAudioDevices(); getMediaDevices(); // asynchronous } QList QWasmMediaDevices::videoInputs() const { return m_cameraDevices.values(); } QList QWasmMediaDevices::audioInputs() const { return m_audioInputs.values(); } QList QWasmMediaDevices::audioOutputs() const { return m_audioOutputs.values(); } void QWasmMediaDevices::parseDevices(emscripten::val devices) { if (devices.isNull() || devices.isUndefined()) { qWarning() << "Something went wrong enumerating devices"; return; } QList cameraDevicesToRemove = m_cameraDevices.keys(); QList audioOutputsToRemove; QList audioInputsToRemove; if (m_firstInit) { m_firstInit = false; qWarning() << "m_audioInputs count" << m_audioInputs.count(); } else { audioOutputsToRemove = m_audioOutputs.keys(); audioInputsToRemove = m_audioInputs.keys(); m_audioInputsAdded = false; m_audioOutputsAdded = false; } m_videoInputsAdded = false; bool m_videoInputsRemoved = false; bool m_audioInputsRemoved = false; bool m_audioOutputsRemoved = false; for (int i = 0; i < devices["length"].as(); i++) { emscripten::val mediaDevice = devices[i]; const std::string deviceKind = mediaDevice["kind"].as(); const std::string label = mediaDevice["label"].as(); const std::string deviceId = mediaDevice["deviceId"].as(); qCDebug(qWasmMediaDevices) << QString::fromStdString(deviceKind) << QString::fromStdString(deviceId) << QString::fromStdString(label); if (deviceKind.empty()) continue; bool isDefault = false; if (deviceKind == std::string("videoinput")) { if (!m_cameraDevices.contains(deviceId)) { QCameraDevicePrivate *camera = new QCameraDevicePrivate; // QSharedData camera->id = QString::fromStdString(deviceId).toUtf8(); camera->description = QString::fromUtf8(label.c_str()); // no camera defaults, first in wins! camera->isDefault = !m_videoInputsAdded; m_cameraDevices.insert(deviceId, camera->create()); m_videoInputsAdded = true; } cameraDevicesToRemove.removeOne(deviceId); } else if (deviceKind == std::string("audioinput")) { if (!m_audioInputs.contains(deviceId)) { isDefault = !m_audioInputsAdded; m_audioInputs.insert( deviceId, QAudioDevicePrivate::createQAudioDevice(std::make_unique( deviceId.c_str(), label.c_str(), isDefault, QAudioDevice::Input))); m_audioInputsAdded = true; } audioInputsToRemove.removeOne(deviceId); } else if (deviceKind == std::string("audiooutput")) { if (!m_audioOutputs.contains(deviceId)) { isDefault = !m_audioOutputsAdded; m_audioOutputs.insert( deviceId, QAudioDevicePrivate::createQAudioDevice(std::make_unique( deviceId.c_str(), label.c_str(), isDefault, QAudioDevice::Output))); ; m_audioOutputsAdded = true; } audioOutputsToRemove.removeOne(deviceId); } // if permissions are given label will hold the actual // camera name, such as "Live! Cam Sync 1080p (041e:409d)" } if (!m_firstInit) getOpenALAudioDevices(); // any left here were removed int j = 0; for (; j < cameraDevicesToRemove.count(); j++) { m_cameraDevices.remove(cameraDevicesToRemove.at(j)); } m_videoInputsRemoved = !cameraDevicesToRemove.isEmpty(); for (j = 0; j < audioInputsToRemove.count(); j++) { m_audioInputs.remove(audioInputsToRemove.at(j)); } m_audioInputsRemoved = !audioInputsToRemove.isEmpty(); for (j = 0; j < audioOutputsToRemove.count(); j++) { m_audioOutputs.remove(audioOutputsToRemove.at(j)); } m_audioOutputsRemoved = !audioOutputsToRemove.isEmpty(); if (m_videoInputsAdded || m_videoInputsRemoved) { auto videoDevices = static_cast(QPlatformMediaIntegration::instance()->videoDevices()); videoDevices->onVideoInputsChanged(); } if (m_audioInputsAdded || m_audioInputsRemoved) { auto audioDevices = static_cast(QPlatformMediaIntegration::instance()->audioDevices()); audioDevices->onAudioInputsChanged(); } if (m_audioOutputsAdded || m_audioOutputsRemoved) { auto audioDevices = static_cast(QPlatformMediaIntegration::instance()->audioDevices()); audioDevices->onAudioOutputsChanged(); } m_firstInit = false; } void QWasmMediaDevices::getMediaDevices() { emscripten::val navigator = emscripten::val::global("navigator"); m_jsMediaDevicesInterface = navigator["mediaDevices"]; if (m_jsMediaDevicesInterface.isNull() || m_jsMediaDevicesInterface.isUndefined()) { qWarning() << "No media devices found"; return; } if (qstdweb::haveAsyncify()) { #ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY emscripten::val devicesList = m_jsMediaDevicesInterface.call("enumerateDevices").await(); if (devicesList.isNull() || devicesList.isUndefined()) { qWarning() << "devices list error"; return; } parseDevices(devicesList); #endif } else { qstdweb::PromiseCallbacks enumerateDevicesCallback{ .thenFunc = [&](emscripten::val devices) { parseDevices(devices); }, .catchFunc = [this](emscripten::val error) { qWarning() << "mediadevices enumerateDevices fail" << QString::fromStdString(error["name"].as()) << QString::fromStdString(error["message"].as()); m_initDone = false; } }; qstdweb::Promise::make(m_jsMediaDevicesInterface, QStringLiteral("enumerateDevices"), std::move(enumerateDevicesCallback)); // setup devicechange monitor m_deviceChangedCallback = std::make_unique( m_jsMediaDevicesInterface, "devicechange", [this, enumerateDevicesCallback](emscripten::val) { qstdweb::Promise::make(m_jsMediaDevicesInterface, QStringLiteral("enumerateDevices"), std::move(enumerateDevicesCallback)); }); } } void QWasmMediaDevices::getOpenALAudioDevices() { // VM3959:4 The AudioContext was not allowed to start. // It must be resumed (or created) after a user gesture on the page. https://goo.gl/7K7WLu auto capture = alcGetString(nullptr, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER); // present even if there is no capture device if (capture && !m_audioOutputs.contains(capture)) { m_audioInputs.insert( capture, QAudioDevicePrivate::createQAudioDevice(std::make_unique( capture, "WebAssembly audio capture device", true, QAudioDevice::Input))); m_audioInputsAdded = true; auto audioDevices = static_cast(QPlatformMediaIntegration::instance()->audioDevices()); audioDevices->onAudioInputsChanged(); } auto playback = alcGetString(nullptr, ALC_DEFAULT_DEVICE_SPECIFIER); // present even if there is no playback device if (playback && !m_audioOutputs.contains(capture)) { m_audioOutputs.insert( playback, QAudioDevicePrivate::createQAudioDevice(std::make_unique( playback, "WebAssembly audio playback device", true, QAudioDevice::Output))); auto audioDevices = static_cast(QPlatformMediaIntegration::instance()->audioDevices()); audioDevices->onAudioOutputsChanged(); } m_firstInit = true; } QT_END_NAMESPACE