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 2809f91923..f1dd3c8350 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 01f8f4580a..26a190e105 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 @@ -113,6 +117,69 @@ uint64_t uvec2ToUint64(const uvec2& v) { return result; } +// Class to handle changing QML audio output device using another thread +class AudioHandler : public QObject, QRunnable { + Q_OBJECT +public: + AudioHandler(QObject* container, const QString& deviceName, int runDelayMs = 0, QObject* parent = nullptr) : QObject(parent) { + _container = container; + _newTargetDevice = deviceName; + _runDelayMs = runDelayMs; + setAutoDelete(true); + QThreadPool::globalInstance()->start(this); + } + virtual ~AudioHandler() { + qDebug() << "Audio Handler Destroyed"; + } + void run() override { + if (_newTargetDevice.isEmpty()) { + return; + } + if (_runDelayMs > 0) { + QThread::msleep(_runDelayMs); + } + auto audioIO = DependencyManager::get(); + QString deviceName = audioIO->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); + for (auto player : _container->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; + } + +private: + QString _newTargetDevice; + QObject* _container; + int _runDelayMs; +}; + class OffscreenTextures { public: GLuint getNextTexture(const uvec2& size) { @@ -595,6 +662,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::QueuedConnection, 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 +680,32 @@ void OffscreenQmlSurface::create() { _updateTimer.start(); } +void OffscreenQmlSurface::changeAudioOutputDevice(const QString& deviceName, bool isHtmlUpdate) { + if (_rootItem != nullptr && !isHtmlUpdate) { + QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); + } + emit audioOutputDeviceChanged(deviceName); +} + +void OffscreenQmlSurface::forceHtmlAudioOutputDeviceUpdate() { + auto audioIO = DependencyManager::get(); + QString deviceName = audioIO->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); + QMetaObject::invokeMethod(this, "changeAudioOutputDevice", Qt::QueuedConnection, + Q_ARG(QString, deviceName), Q_ARG(bool, true)); +} + +void OffscreenQmlSurface::forceQmlAudioOutputDeviceUpdate() { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); + } else { + auto audioIO = DependencyManager::get(); + QString deviceName = audioIO->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); + int waitForAudioQmlMs = 500; + // The audio device need to be change using oth + new AudioHandler(_rootItem, deviceName, waitForAudioQmlMs); + } +} + static uvec2 clampSize(const uvec2& size, uint32_t maxDimension) { return glm::clamp(size, glm::uvec2(1), glm::uvec2(maxDimension)); } @@ -798,6 +899,7 @@ void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext if (newItem) { newItem->setParentItem(_rootItem); } + QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); return; } @@ -817,6 +919,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 5acdeb4f40..eaff2bc3d1 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -104,6 +104,14 @@ 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(); +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..6e642fce29 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..c3d25cfe2e 100644 --- a/tests/controllers/CMakeLists.txt +++ b/tests/controllers/CMakeLists.txt @@ -17,5 +17,9 @@ 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) + add_dependency_external_projects(wasapi) +endif() package_libraries_for_deployment() diff --git a/tests/entities/CMakeLists.txt b/tests/entities/CMakeLists.txt index 080ae7cdd9..0c33eb8555 100644 --- a/tests/entities/CMakeLists.txt +++ b/tests/entities/CMakeLists.txt @@ -9,4 +9,8 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries link_hifi_libraries(entities avatars shared octree gpu model fbx networking animation audio gl) +if (WIN32) + add_dependency_external_projects(wasapi) +endif () + package_libraries_for_deployment() diff --git a/tests/gpu-test/CMakeLists.txt b/tests/gpu-test/CMakeLists.txt index d73d7a111d..8e49d523b8 100644 --- a/tests/gpu-test/CMakeLists.txt +++ b/tests/gpu-test/CMakeLists.txt @@ -5,6 +5,11 @@ setup_hifi_project(Quick Gui OpenGL Script Widgets) setup_memory_debugger() set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries(networking gl gpu gpu-gl procedural shared fbx model model-networking animation script-engine render render-utils octree image ktx) + +if (WIN32) + add_dependency_external_projects(wasapi) +endif () + package_libraries_for_deployment() target_nsight() diff --git a/tests/qt59/CMakeLists.txt b/tests/qt59/CMakeLists.txt index e0e8138a1e..e3450ae069 100644 --- a/tests/qt59/CMakeLists.txt +++ b/tests/qt59/CMakeLists.txt @@ -11,7 +11,12 @@ setup_memory_debugger() setup_hifi_project(Gui) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") + # link in the shared libraries link_hifi_libraries(shared networking) +if (WIN32) + add_dependency_external_projects(wasapi) +endif() + package_libraries_for_deployment() diff --git a/tests/recording/CMakeLists.txt b/tests/recording/CMakeLists.txt index b5b1e6a54e..dbb942a27a 100644 --- a/tests/recording/CMakeLists.txt +++ b/tests/recording/CMakeLists.txt @@ -4,6 +4,10 @@ setup_hifi_project(Test) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") setup_memory_debugger() link_hifi_libraries(shared recording) +if (WIN32) + target_link_libraries(${TARGET_NAME} Winmm.lib) + add_dependency_external_projects(wasapi) +endif() package_libraries_for_deployment() # FIXME convert to unit tests @@ -14,4 +18,4 @@ package_libraries_for_deployment() # # package_libraries_for_deployment() #endmacro () -#setup_hifi_testcase() +#setup_hifi_testcase() \ No newline at end of file diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index 5b83ff313b..09b2dc6a50 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -14,6 +14,11 @@ 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) + add_dependency_external_projects(wasapi) +endif() + package_libraries_for_deployment() diff --git a/tests/render-texture-load/CMakeLists.txt b/tests/render-texture-load/CMakeLists.txt index 30030914ab..432a1f00d6 100644 --- a/tests/render-texture-load/CMakeLists.txt +++ b/tests/render-texture-load/CMakeLists.txt @@ -26,7 +26,8 @@ target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${QUAZIP_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) if (WIN32) -add_paths_to_fixup_libs(${QUAZIP_DLL_PATH}) + add_paths_to_fixup_libs(${QUAZIP_DLL_PATH}) + add_dependency_external_projects(wasapi) endif () diff --git a/tests/render-utils/CMakeLists.txt b/tests/render-utils/CMakeLists.txt index 4944ad2cbc..4e67aef3be 100644 --- a/tests/render-utils/CMakeLists.txt +++ b/tests/render-utils/CMakeLists.txt @@ -11,4 +11,8 @@ setup_memory_debugger() link_hifi_libraries(render-utils gl gpu gpu-gl shared) target_link_libraries(${TARGET_NAME} ${CMAKE_THREAD_LIBS_INIT}) +if (WIN32) + add_dependency_external_projects(wasapi) +endif () + package_libraries_for_deployment() diff --git a/tests/shaders/CMakeLists.txt b/tests/shaders/CMakeLists.txt index bab1e0dcdc..ba4ca88127 100644 --- a/tests/shaders/CMakeLists.txt +++ b/tests/shaders/CMakeLists.txt @@ -17,4 +17,8 @@ include_directories("${PROJECT_BINARY_DIR}/../../libraries/render-utils/") include_directories("${PROJECT_BINARY_DIR}/../../libraries/entities-renderer/") include_directories("${PROJECT_BINARY_DIR}/../../libraries/model/") +if (WIN32) + add_dependency_external_projects(wasapi) +endif () + package_libraries_for_deployment()