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()