diff --git a/interface/resources/html/createGlobalEventBridge.js b/interface/resources/html/createGlobalEventBridge.js index 4a0de464c3..bccbdfaf7c 100644 --- a/interface/resources/html/createGlobalEventBridge.js +++ b/interface/resources/html/createGlobalEventBridge.js @@ -33,6 +33,54 @@ var EventBridge; // replace the TempEventBridge with the real one. var tempEventBridge = EventBridge; EventBridge = channel.objects.eventBridge; + EventBridge.audioOutputDeviceChanged.connect(function(deviceName) { + navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function(mediaStream) { + navigator.mediaDevices.enumerateDevices().then(function(devices) { + devices.forEach(function(device) { + if (device.kind == "audiooutput") { + if (device.label == deviceName){ + console.log("Changing HTML audio output to device " + device.label); + var deviceId = device.deviceId; + var videos = document.getElementsByTagName("video"); + for (var i = 0; i < videos.length; i++){ + videos[i].setSinkId(deviceId); + } + var audios = document.getElementsByTagName("audio"); + for (var i = 0; i < audios.length; i++){ + audios[i].setSinkId(deviceId); + } + } + } + }); + + }).catch(function(err) { + console.log("Error getting media devices"+ err.name + ": " + err.message); + }); + }).catch(function(err) { + console.log("Error getting user media"+ err.name + ": " + err.message); + }); + }); + + // To be able to update the state of the output device selection for every element added to the DOM + // we need to listen to events that might precede the addition of this elements. + // A more robust hack will be to add a setInterval that look for DOM changes every 100-300 ms (low performance?) + + window.onload = function(){ + setTimeout(function() { + EventBridge.forceHtmlAudioOutputDeviceUpdate(); + }, 1200); + }; + document.onclick = function(){ + setTimeout(function() { + EventBridge.forceHtmlAudioOutputDeviceUpdate(); + }, 1200); + }; + document.onchange = function(){ + setTimeout(function() { + EventBridge.forceHtmlAudioOutputDeviceUpdate(); + }, 1200); + }; + tempEventBridge._callbacks.forEach(function (callback) { EventBridge.scriptEventReceived.connect(callback); }); diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 55927fda24..8a6674bc14 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -212,7 +212,7 @@ ScrollingWindow { WebEngineScript { id: createGlobalEventBridge sourceCode: eventBridgeJavaScriptToInject - injectionPoint: WebEngineScript.DocumentCreation + injectionPoint: WebEngineScript.Deferred worldId: WebEngineScript.MainWorld } @@ -233,9 +233,13 @@ ScrollingWindow { anchors.right: parent.right onFeaturePermissionRequested: { - permissionsBar.securityOrigin = securityOrigin; - permissionsBar.feature = feature; - root.showPermissionsBar(); + if (feature == 2) { // QWebEnginePage::MediaAudioCapture + grantFeaturePermission(securityOrigin, feature, true); + } else { + permissionsBar.securityOrigin = securityOrigin; + permissionsBar.feature = feature; + root.showPermissionsBar(); + } } onLoadingChanged: { diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index f28157ff97..ae682a11ea 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME ui) -setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebEngine WebSockets XmlPatterns) -link_hifi_libraries(shared networking gl audio) +setup_hifi_library(OpenGL Multimedia Network Qml Quick Script WebChannel WebEngine WebSockets XmlPatterns) +link_hifi_libraries(shared networking gl audio audio-client plugins) # Required for some low level GL interaction in the OffscreenQMLSurface target_opengl() diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index ecd07a5874..6632b669e3 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -24,7 +24,11 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -595,6 +599,14 @@ void OffscreenQmlSurface::create() { // Find a way to flag older scripts using this mechanism and wanr that this is deprecated _qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(this, _qmlContext)); _renderControl->initialize(_canvas->getContext()); + + // Connect with the audio client and listen for audio device changes + auto audioIO = DependencyManager::get(); + connect(audioIO.data(), &AudioClient::deviceChanged, this, [&](QAudio::Mode mode, const QAudioDeviceInfo& device) { + if (mode == QAudio::Mode::AudioOutput) { + QMetaObject::invokeMethod(this, "changeAudioOutputDevice", Qt::DirectConnection, Q_ARG(QString, device.deviceName())); + } + }); // When Quick says there is a need to render, we will not render immediately. Instead, // a timer with a small interval is used to get better performance. @@ -605,6 +617,68 @@ void OffscreenQmlSurface::create() { _updateTimer.start(); } +void OffscreenQmlSurface::changeAudioOutputDevice(const QString& deviceName, bool isHtmlUpdate) { + if (_rootItem != nullptr && !isHtmlUpdate) { + QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::DirectConnection); + } + emit audioOutputDeviceChanged(deviceName); +} + +void OffscreenQmlSurface::forceHtmlAudioOutputDeviceUpdate() { + auto audioIO = DependencyManager::get(); + QString deviceName = audioIO->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); + QMetaObject::invokeMethod(this, "changeAudioOutputDevice", Qt::DirectConnection, + Q_ARG(QString, deviceName), Q_ARG(bool, true)); +} + +void OffscreenQmlSurface::forceQmlAudioOutputDeviceUpdate() { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); + } + else { + int waitForAudioQmlMs = 500; + QTimer::singleShot(waitForAudioQmlMs, this, SLOT(updateQmlAudio())); + } +} + +void OffscreenQmlSurface::updateQmlAudio() { + auto audioIO = DependencyManager::get(); + QString deviceName = audioIO->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); + for (auto player : _rootItem->findChildren()) { + auto mediaState = player->state(); + QMediaService *svc = player->service(); + if (nullptr == svc) { + return; + } + QAudioOutputSelectorControl *out = qobject_cast + (svc->requestControl(QAudioOutputSelectorControl_iid)); + if (nullptr == out) { + return; + } + QString deviceOuput; + auto outputs = out->availableOutputs(); + for (int i = 0; i < outputs.size(); i++) { + QString output = outputs[i]; + QString description = out->outputDescription(output); + if (description == deviceName) { + deviceOuput = output; + break; + } + } + out->setActiveOutput(deviceOuput); + svc->releaseControl(out); + // if multimedia was paused, it will start playing automatically after changing audio device + // this will reset it back to a paused state + if (mediaState == QMediaPlayer::State::PausedState) { + player->pause(); + } + else if (mediaState == QMediaPlayer::State::StoppedState) { + player->stop(); + } + } + qDebug() << "QML Audio changed to " << deviceName; +} + static uvec2 clampSize(const uvec2& size, uint32_t maxDimension) { return glm::clamp(size, glm::uvec2(1), glm::uvec2(maxDimension)); } @@ -798,6 +872,7 @@ void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext if (newItem) { newItem->setParentItem(_rootItem); } + QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); return; } @@ -817,6 +892,7 @@ void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext for (const auto& callback : callbacks) { callback(qmlContext, newObject); } + QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); } void OffscreenQmlSurface::updateQuick() { diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index 12ee9e59a1..0d053b373d 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -103,6 +103,15 @@ public slots: void onAboutToQuit(); void focusDestroyed(QObject *obj); + // audio output device +public slots: + void changeAudioOutputDevice(const QString& deviceName, bool isHtmlUpdate = false); + void forceHtmlAudioOutputDeviceUpdate(); + void forceQmlAudioOutputDeviceUpdate(); + void updateQmlAudio(); +signals: + void audioOutputDeviceChanged(const QString& deviceName); + // event bridge public slots: void emitScriptEvent(const QVariant& scriptMessage); diff --git a/plugins/hifiSixense/CMakeLists.txt b/plugins/hifiSixense/CMakeLists.txt index 55880584a8..a503fc5710 100644 --- a/plugins/hifiSixense/CMakeLists.txt +++ b/plugins/hifiSixense/CMakeLists.txt @@ -15,4 +15,7 @@ if (NOT ANDROID) setup_hifi_plugin(Script Qml Widgets) link_hifi_libraries(shared controllers ui plugins ui-plugins input-plugins) target_sixense() + if (WIN32) + target_link_libraries(${TARGET_NAME} Winmm.lib) + endif() endif () diff --git a/tests/controllers/CMakeLists.txt b/tests/controllers/CMakeLists.txt index 3221070837..623ee7d20e 100644 --- a/tests/controllers/CMakeLists.txt +++ b/tests/controllers/CMakeLists.txt @@ -17,5 +17,8 @@ if (WIN32) target_include_directories(${TARGET_NAME} PRIVATE ${OPENVR_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${OPENVR_LIBRARIES}) endif() +if (WIN32) + target_link_libraries(${TARGET_NAME} Winmm.lib) +endif() package_libraries_for_deployment() diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index 5b83ff313b..b7f13c88c5 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -14,6 +14,10 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries link_hifi_libraries(shared networking model fbx ktx image octree gl gpu gpu-gl render model-networking networking render-utils entities entities-renderer animation audio avatars script-engine physics procedural midi ui) +if (WIN32) + target_link_libraries(${TARGET_NAME} Winmm.lib) +endif() + package_libraries_for_deployment()