From: IOhannes m zmölnig (Debian/GNU) Date: Sun, 22 Sep 2024 20:27:49 +0000 (+0200) Subject: New upstream version 2.4.0+ds X-Git-Tag: archive/raspbian/2.5.1+ds-1+rpi1^2~7^2~3 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=32b064c271461194e5b9ebb8600b1e39cd1b63b5;p=jacktrip.git New upstream version 2.4.0+ds --- diff --git a/CMakeLists.txt b/CMakeLists.txt index a4e80a8..e4db93d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,11 +31,11 @@ if (psi) set(novs TRUE) endif () +set(QRC_FILE "src/images/images.qrc") if (novs) add_compile_definitions(NO_VS) - set(QRC_FILE "src/gui/qjacktrip_novs.qrc") else () - set(QRC_FILE "src/gui/qjacktrip.qrc") + set(QRC_FILE "src/vs/vs.qrc") endif () if (vsftux) @@ -180,37 +180,39 @@ if (NOT nogui) src/gui/textbuf.cpp src/gui/vuMeter.cpp src/Meter.cpp + src/UserInterface.cpp ) if (NOT novs) set (qjacktrip_SRC ${qjacktrip_SRC} - src/gui/virtualstudio.cpp - src/gui/vsApi.cpp - src/gui/vsAuth.cpp - src/gui/vsDeviceCodeFlow.cpp - src/gui/vsDeeplink.cpp - src/gui/vsQuickView.cpp - src/gui/vsServerInfo.cpp - src/gui/vsPing.cpp - src/gui/vsPinger.cpp - src/gui/vsDevice.cpp - src/gui/vsAudio.cpp - src/gui/vsWebSocket.cpp - src/gui/vsPermissions.cpp - src/gui/qjacktrip.qrc + src/vs/virtualstudio.cpp + src/vs/vsApi.cpp + src/vs/vsAuth.cpp + src/vs/vsDeviceCodeFlow.cpp + src/vs/vsDeeplink.cpp + src/vs/vsQuickView.cpp + src/vs/vsServerInfo.cpp + src/vs/vsPing.cpp + src/vs/vsPinger.cpp + src/vs/vsDevice.cpp + src/vs/vsAudio.cpp + src/vs/vsWebSocket.cpp + src/vs/vsPermissions.cpp + src/vs/vs.qrc + src/images/images.qrc src/Analyzer.cpp src/Monitor.cpp src/Volume.cpp src/Tone.cpp # Need to include this for AUTOMOC to do its thing - src/JTApplication.h - src/gui/vsQmlClipboard.h + src/vs/JTApplication.h + src/vs/vsQmlClipboard.h ) if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/vsMacPermissions.mm) + set (qjacktrip_SRC ${qjacktrip_SRC} src/vs/vsMacPermissions.mm) endif () else () - set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/qjacktrip_novs.qrc) + set (qjacktrip_SRC ${qjacktrip_SRC} src/images/images.qrc) endif () if (NOT noupdater) @@ -232,7 +234,7 @@ if (NOT nogui) file(WRITE "win/qjacktrip.rc" "${RC_CONTENTS}") set (qjacktrip_SRC ${qjacktrip_SRC} win/qjacktrip.rc) elseif (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/NoNap.mm) + set (qjacktrip_SRC ${qjacktrip_SRC} src/NoNap.mm) set (CMAKE_C_FLAGS "-x objective-c") set (CMAKE_EXE_LINKER_FLAGS "-framework Foundation") if (NOT novs) diff --git a/LICENSE.md b/LICENSE.md index 0169495..3723a4f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,20 +1,28 @@ # JackTrip License -Copyright © 2008-2020 Juan-Pablo Caceres, Chris Chafe. +Copyright © 2008-2024 Juan-Pablo Caceres, Chris Chafe, et al. SoundWIRE group at CCRMA, Stanford University. +Virtual Studio interface and integration, +Copyright © 2022-2024 JackTrip Labs, Inc. + Classic mode graphical user interface originally released as QJackTrip, Copyright © 2020 Aaron Wyatt -Virtual Studio interface and integration -Copyright © 2022-2023 JackTrip Labs, Inc. +The JackTrip project including Virtual Studio interface and integration +is open source distributed under the MIT license. The Classic mode +graphical interface is open source distributed under a GPL license. + +JackTrip uses the Qt library. Qt's source code can be downloaded from +[https://download.qt.io/official_releases/qt/](https://download.qt.io/official_releases/qt/). -JackTrip project consists of files under MIT and GPL licenses, indicated in the -header of individual files. Early versions of JackTrip were licensed under MIT. +Unsigned builds provided on GitHub's Releases page include the Classic +mode graphical interface and use an open source distribution of Qt. +These are distributed under a GPL license. -JackTrip uses Qt library throughout the project so the resulting binaries are -also subject to Qt's license. The builds provided on GitHub's Releases page use -open source distribution of Qt, licensed under LGPL. +Signed builds for Windows and Mac provided by JackTrip Labs do not +include the Classic mode graphical interface and use a commercial +Qt license. These are distributed under a MIT license. Windows builds of JackTrip may include support for ASIO. ASIO is a trademark and software of Steinberg Media Technologies GmbH. @@ -22,6 +30,5 @@ ASIO is a trademark and software of Steinberg Media Technologies GmbH. Using JackTrip to join Virtual Studios on Windows computers may use AVC (h264) video encoders and decoders subject to the AVC Patent Portfolio License. -The text of individual licenses is provided in the `LICENSES/` folder. Qt's -source code can be downloaded from -[https://download.qt.io/official_releases/qt/](https://download.qt.io/official_releases/qt/). +The text of individual licenses is provided in the `LICENSES/` folder. + diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt index a843d97..adb686a 100644 --- a/LICENSES/MIT.txt +++ b/LICENSES/MIT.txt @@ -1,6 +1,9 @@ - Copyright (c) 2020 Juan-Pablo Caceres, Chris Chafe. + Copyright (c) 2008-2024 Juan-Pablo Caceres, Chris Chafe, et al. SoundWIRE group at CCRMA, Stanford University. + Virtual Studio interface and integration + Copyright (c) 2022-2024 JackTrip Labs, Inc. + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without diff --git a/docs/changelog.yml b/docs/changelog.yml index 4f4be6d..105c429 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -1,3 +1,26 @@ +- Version: "2.4.0" + Date: 2024-09-13 + Description: + - (addded) qWave Quality of Service for Windows users + - (addded) VS Mode warning when speakers selected for output + - (addded) VS Mode dialog when connecting with audio warning + - (updated) PLC auto headroom tweaks to improve audio quality + - (updated) Increased UDP timeout from 30 to 50 milliseconds + - (updated) MIT license for VS mode interface and integration + - (updated) Removed classic mode from JackTrip Labs builds + - (updated) ASIO is now only enabled for JackTrip Labs builds + - (updated) VS Mode now only supports buffer strategy 3 (PLC) + - (updated) VS Mode removed button click to create first studio + - (updated) VS Mode updated to use new studio creation interface + - (updated) VS Mode updated to use new user profile interface + - (updated) VS Mode changed red text color for buttons to black + - (updated) VS Mode improved error handling for login screen + - (updated) VS Mode improved messaging while loading studios + - (fixed) VS Mode possible failures when loading studios + - (fixed) VS Mode studio refresh updated to avoid jumpiness + - (fixed) VS Mode blacklisted iPhone microphone device + - (fixed) Race condition in automatic patching for JACK + - (fixed) Missing files in Linux binary zip file - Version: "2.3.1" Date: 2024-07-26 Description: diff --git a/jacktrip.pro b/jacktrip.pro index c3f9fce..60c2198 100644 --- a/jacktrip.pro +++ b/jacktrip.pro @@ -258,21 +258,21 @@ HEADERS += src/DataProtocol.h \ src/gui/textbuf.h \ src/gui/vuMeter.h !novs { - HEADERS += src/gui/virtualstudio.h \ - src/gui/vsApi.h \ - src/gui/vsAuth.h \ - src/gui/vsDeviceCodeFlow.h \ - src/gui/vsDeeplink.h \ - src/gui/vsDevice.h \ - src/gui/vsAudio.h \ - src/gui/vsServerInfo.h \ - src/gui/vsQuickView.h \ - src/gui/vsWebSocket.h \ - src/gui/vsPermissions.h \ - src/gui/vsPinger.h \ - src/gui/vsPing.h \ - src/gui/vsQmlClipboard.h \ - src/JTApplication.h + HEADERS += src/vs/virtualstudio.h \ + src/vs/vsApi.h \ + src/vs/vsAuth.h \ + src/vs/vsDeviceCodeFlow.h \ + src/vs/vsDeeplink.h \ + src/vs/vsDevice.h \ + src/vs/vsAudio.h \ + src/vs/vsServerInfo.h \ + src/vs/vsQuickView.h \ + src/vs/vsWebSocket.h \ + src/vs/vsPermissions.h \ + src/vs/vsPinger.h \ + src/vs/vsPing.h \ + src/vs/vsQmlClipboard.h \ + src/vs/JTApplication.h } !noupdater:!linux-g++:!linux-g++-64 { HEADERS += src/dblsqd/feed.h \ @@ -325,21 +325,22 @@ SOURCES += src/DataProtocol.cpp \ src/gui/qjacktrip.cpp \ src/gui/about.cpp \ src/gui/textbuf.cpp \ - src/gui/vuMeter.cpp + src/gui/vuMeter.cpp \ + src/UserInterface.cpp !novs { - SOURCES += src/gui/virtualstudio.cpp \ - src/gui/vsApi.cpp \ - src/gui/vsAuth.cpp \ - src/gui/vsDeviceCodeFlow.cpp \ - src/gui/vsDeeplink.cpp \ - src/gui/vsDevice.cpp \ - src/gui/vsAudio.cpp \ - src/gui/vsServerInfo.cpp \ - src/gui/vsQuickView.cpp \ - src/gui/vsWebSocket.cpp \ - src/gui/vsPermissions.cpp \ - src/gui/vsPinger.cpp \ - src/gui/vsPing.cpp + SOURCES += src/vs/virtualstudio.cpp \ + src/vs/vsApi.cpp \ + src/vs/vsAuth.cpp \ + src/vs/vsDeviceCodeFlow.cpp \ + src/vs/vsDeeplink.cpp \ + src/vs/vsDevice.cpp \ + src/vs/vsAudio.cpp \ + src/vs/vsServerInfo.cpp \ + src/vs/vsQuickView.cpp \ + src/vs/vsWebSocket.cpp \ + src/vs/vsPermissions.cpp \ + src/vs/vsPinger.cpp \ + src/vs/vsPing.cpp } !noupdater:!linux-g++:!linux-g++-64 { SOURCES += src/dblsqd/feed.cpp \ @@ -351,20 +352,19 @@ SOURCES += src/DataProtocol.cpp \ !nogui { macx { - HEADERS += src/gui/NoNap.h - OBJECTIVE_SOURCES += src/gui/NoNap.mm + HEADERS += src/NoNap.h + OBJECTIVE_SOURCES += src/NoNap.mm !novs { - HEADERS += src/gui/vsMacPermissions.h - OBJECTIVE_SOURCES += src/gui/vsMacPermissions.mm + HEADERS += src/vs/vsMacPermissions.h + OBJECTIVE_SOURCES += src/vs/vsMacPermissions.mm } } FORMS += src/gui/qjacktrip.ui \ src/gui/about.ui \ src/gui/messageDialog.ui - novs { - RESOURCES += src/gui/qjacktrip_novs.qrc - } else { - RESOURCES += src/gui/qjacktrip.qrc + RESOURCES += src/images/images.qrc + !novs { + RESOURCES += src/vs/vs.qrc } !noupdater:!linux-g++:!linux-g++-64 { FORMS += src/dblsqd/update_dialog.ui diff --git a/macos/assemble_app.sh b/macos/assemble_app.sh index 37c3d4c..22475f3 100755 --- a/macos/assemble_app.sh +++ b/macos/assemble_app.sh @@ -155,7 +155,7 @@ if [ -n "$DYNAMIC_QT" ]; then fi DEPLOY_OPTS="-executable=$APPNAME.app/Contents/MacOS/jacktrip -libpath=$QT_PATH/lib" if [ -n "$DYNAMIC_VS" ]; then - DEPLOY_OPTS="$DEPLOY_OPTS -qmldir=../src/gui" + DEPLOY_OPTS="$DEPLOY_OPTS -qmldir=../src/vs" fi $DEPLOY_CMD "$APPNAME.app" $DEPLOY_OPTS diff --git a/meson.build b/meson.build index 74ea661..0685415 100644 --- a/meson.build +++ b/meson.build @@ -128,88 +128,98 @@ else endif endif -if get_option('nogui') == true +if qt_version == '5' + deps += dependency('qt5', modules: ['Core', 'Network'], include_type: 'system') +else + deps += dependency('qt6', modules: ['Core', 'Network'], include_type: 'system') +endif + +if get_option('nogui') == true or (get_option('noclassic') == true and get_option('novs') == true) + # command line only defines += '-DNO_GUI' +else + # include vs and/or classic gui if qt_version == '5' - deps += dependency('qt5', modules: ['Core', 'Network'], include_type: 'system') + deps += dependency('qt5', modules: ['Gui', 'Widgets'], include_type: 'system') else - deps += dependency('qt6', modules: ['Core', 'Network'], include_type: 'system') + deps += dependency('qt6', modules: ['Gui', 'Widgets'], include_type: 'system') + endif + qres = ['src/images/images.qrc'] + src += 'src/UserInterface.cpp' + + if get_option('noclassic') == true + defines += '-DNO_CLASSIC' + else + # support classic mode + src += [ + 'src/gui/qjacktrip.cpp', + 'src/gui/about.cpp', + 'src/gui/messageDialog.cpp', + 'src/gui/textbuf.cpp', + 'src/gui/vuMeter.cpp' + ] + moc_h += [ + 'src/gui/about.h', + 'src/gui/qjacktrip.h', + 'src/gui/messageDialog.h', + 'src/gui/textbuf.h', + 'src/gui/vuMeter.h' + ] + ui_h += [ + 'src/gui/qjacktrip.ui', + 'src/gui/messageDialog.ui', + 'src/gui/about.ui' + ] endif -else - src += [ - 'src/gui/qjacktrip.cpp', - 'src/gui/about.cpp', - 'src/gui/messageDialog.cpp', - 'src/gui/textbuf.cpp', - 'src/gui/vuMeter.cpp' - ] - moc_h += [ - 'src/gui/about.h', - 'src/gui/qjacktrip.h', - 'src/gui/messageDialog.h', - 'src/gui/textbuf.h', - 'src/gui/vuMeter.h' - ] - ui_h += [ - 'src/gui/qjacktrip.ui', - 'src/gui/messageDialog.ui', - 'src/gui/about.ui' - ] if get_option('novs') == true defines += '-DNO_VS' - if qt_version == '5' - deps += dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system') - else - deps += dependency('qt6', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system') - endif - qres = ['src/gui/qjacktrip_novs.qrc'] else src += [ - 'src/gui/virtualstudio.cpp', - 'src/gui/vsAuth.cpp', - 'src/gui/vsApi.cpp', - 'src/gui/vsDeviceCodeFlow.cpp', - 'src/gui/vsDeeplink.cpp', - 'src/gui/vsDevice.cpp', - 'src/gui/vsAudio.cpp', - 'src/gui/vsServerInfo.cpp', - 'src/gui/vsQuickView.cpp', - 'src/gui/vsWebSocket.cpp', - 'src/gui/vsPermissions.cpp', - 'src/gui/vsPinger.cpp', - 'src/gui/vsPing.cpp', - 'src/gui/WebSocketTransport.cpp' + 'src/vs/virtualstudio.cpp', + 'src/vs/vsAuth.cpp', + 'src/vs/vsApi.cpp', + 'src/vs/vsDeviceCodeFlow.cpp', + 'src/vs/vsDeeplink.cpp', + 'src/vs/vsDevice.cpp', + 'src/vs/vsAudio.cpp', + 'src/vs/vsServerInfo.cpp', + 'src/vs/vsQuickView.cpp', + 'src/vs/vsWebSocket.cpp', + 'src/vs/vsPermissions.cpp', + 'src/vs/vsPinger.cpp', + 'src/vs/vsPing.cpp', + 'src/vs/WebSocketTransport.cpp' ] moc_h += [ - 'src/gui/virtualstudio.h', - 'src/gui/vsApi.h', - 'src/gui/vsAuth.h', - 'src/gui/vsDeviceCodeFlow.h', - 'src/gui/vsDeeplink.h', - 'src/gui/vsDevice.h', - 'src/gui/vsAudio.h', - 'src/gui/vsServerInfo.h', - 'src/gui/vsQuickView.h', - 'src/gui/vsWebSocket.h', - 'src/gui/vsPermissions.h', - 'src/gui/vsPinger.h', - 'src/gui/vsPing.h', - 'src/gui/vsQmlClipboard.h', - 'src/JTApplication.h', - 'src/gui/WebSocketTransport.h' + 'src/vs/virtualstudio.h', + 'src/vs/vsApi.h', + 'src/vs/vsAuth.h', + 'src/vs/vsDeviceCodeFlow.h', + 'src/vs/vsDeeplink.h', + 'src/vs/vsDevice.h', + 'src/vs/vsAudio.h', + 'src/vs/vsServerInfo.h', + 'src/vs/vsQuickView.h', + 'src/vs/vsWebSocket.h', + 'src/vs/vsPermissions.h', + 'src/vs/vsPinger.h', + 'src/vs/vsPing.h', + 'src/vs/vsQmlClipboard.h', + 'src/vs/JTApplication.h', + 'src/vs/WebSocketTransport.h' ] if host_machine.system() == 'darwin' - moc_h += ['src/gui/vsMacPermissions.h'] + moc_h += ['src/vs/vsMacPermissions.h'] endif - if get_option('vsftux') == true + if get_option('vsftux') == true or get_option('noclassic') == true defines += '-DVS_FTUX' endif - deps += dependency('qt6', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Core5Compat', 'Quick', 'QuickControls2', 'Qml', 'ShaderTools', 'Svg', 'WebSockets', 'WebEngineCore', 'WebEngineQuick', 'WebChannel'], include_type: 'system') - qres = ['src/gui/qjacktrip.qrc'] + deps += dependency('qt6', modules: ['Core5Compat', 'Quick', 'QuickControls2', 'Qml', 'ShaderTools', 'Svg', 'WebSockets', 'WebEngineCore', 'WebEngineQuick', 'WebChannel'], include_type: 'system') + qres += ['src/vs/vs.qrc'] endif if get_option('noupdater') == true or host_machine.system() == 'linux' @@ -223,8 +233,6 @@ else ] moc_h += [ 'src/dblsqd/feed.h', - 'src/dblsqd/release.h', - 'src/dblsqd/semver.h', 'src/dblsqd/update_dialog.h' ] ui_h += ['src/dblsqd/update_dialog.ui'] @@ -286,9 +294,10 @@ if get_option('default_library') == 'static' endif endif -# TODO: QT_OPENSOURCE should only be defined for open source Qt distribution -# in QMake this can be checked with QT_EDITION == 'OpenSource' -defines += '-DQT_OPENSOURCE' +# QT_OPENSOURCE should only be defined for open source Qt distribution +if get_option('qtedition') != 'commercial' + defines += '-DQT_OPENSOURCE' +endif rtaudio_dep = dependency('rtaudio', required: get_option('rtaudio')) if rtaudio_dep.found() == true @@ -305,7 +314,7 @@ if rtaudio_dep.found() == false and jack_dep.found() == false endif if host_machine.system() == 'darwin' - src += ['src/gui/NoNap.mm'] + src += ['src/NoNap.mm'] # Adding CoreAudio here is a workaround and should be removed # when https://github.com/thestk/rtaudio/issues/302 is fixed # and arrived in all common package managers @@ -315,8 +324,8 @@ if host_machine.system() == 'darwin' add_languages('objcpp') endif -if host_machine.system() == 'darwin' and get_option('novs') == false - src += ['src/gui/vsMacPermissions.mm'] +if host_machine.system() == 'darwin' and get_option('novs') == false and get_option('nogui') == false + src += ['src/vs/vsMacPermissions.mm'] apple_av_dep = dependency('appleframeworks', modules : ['avfoundation', 'webkit']) deps += apple_av_dep endif diff --git a/meson_options.txt b/meson_options.txt index e7daaf0..ec66535 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,9 +4,11 @@ option('jack', type : 'feature', value : 'auto', description: 'Build with JACK B option('weakjack', type : 'boolean', value : false, description: 'Weak link JACK library') option('nogui', type : 'boolean', value : false, description: 'Build without graphical user interface') option('novs', type : 'boolean', value : false, description: 'Build without Virtual Studio support') +option('noclassic', type : 'boolean', value : false, description: 'Build without classic mode support') option('vsftux', type : 'boolean', value : false, description: 'Build with Virtual Studio first launch experience') option('noupdater', type : 'boolean', value : false, description: 'Build without auto-update support') option('nofeedback', type : 'boolean', value : false, description: 'Build without feedback detection') option('profile', type: 'combo', choices: ['default', 'development'], value: 'default', description: 'Choose build profile / Sets desktop id accordingly') option('qtversion', type : 'combo', choices: ['', '5', '6'], description: 'Choose to build with either Qt5 or Qt6') +option('qtedition', type : 'combo', choices: ['opensource', 'commercial'], description: 'Choose license edition for Qt') option('buildinfo', type : 'string', value : '', yield : true, description: 'Additional info used to describe the build') \ No newline at end of file diff --git a/src/Analyzer.cpp b/src/Analyzer.cpp index a87dff3..824eba6 100644 --- a/src/Analyzer.cpp +++ b/src/Analyzer.cpp @@ -3,8 +3,7 @@ JackTrip: A System for High-Quality Audio Network Performance over the Internet - Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. + Copyright (c) 2022-2024 JackTrip Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/src/Analyzer.h b/src/Analyzer.h index 8c4e6fe..ac698bf 100644 --- a/src/Analyzer.h +++ b/src/Analyzer.h @@ -3,8 +3,7 @@ JackTrip: A System for High-Quality Audio Network Performance over the Internet - Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. + Copyright (c) 2022-2024 JackTrip Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/src/AudioInterface.cpp b/src/AudioInterface.cpp index 918f696..97cb28d 100644 --- a/src/AudioInterface.cpp +++ b/src/AudioInterface.cpp @@ -819,7 +819,7 @@ void AudioInterface::setDevicesWarningMsg(warningMessageT msg) mWarningMsg = "The buffer size setting for your audio device will cause high latency " "or audio delay. Use an audio device that supports small buffer sizes " - "to reduce audio delays. Click for more info."; + "to reduce audio delays."; mWarningHelpUrl = "https://support.jacktrip.com/recommended-audio-interfaces"; mHighLatencyFlag = true; break; @@ -827,7 +827,7 @@ void AudioInterface::setDevicesWarningMsg(warningMessageT msg) mWarningMsg = "You audio device drivers may cause high latency or audio delay. Install " "and use ASIO drivers provided by your device's manufacturer to reduce " - "audio delays. Click for more info."; + "audio delays."; mWarningHelpUrl = "https://support.jacktrip.com/troubleshooting-windows-drivers-and-asio"; mHighLatencyFlag = true; @@ -839,6 +839,13 @@ void AudioInterface::setDevicesWarningMsg(warningMessageT msg) mWarningHelpUrl = ""; mHighLatencyFlag = true; break; + case DEVICE_WARN_SPEAKERS: + mWarningMsg = + "You appear to have selected speakers for audio output. " + "Using speakers with microphones will likely cause a loud feedback " + "loop. We strongly recommend that you use wired headphones instead."; + mWarningHelpUrl = "https://support.jacktrip.com/recommended-audio-interfaces"; + break; default: mWarningMsg = ""; mWarningHelpUrl = ""; diff --git a/src/AudioInterface.h b/src/AudioInterface.h index 6ed62be..d752222 100644 --- a/src/AudioInterface.h +++ b/src/AudioInterface.h @@ -84,7 +84,8 @@ class AudioInterface DEVICE_WARN_NONE, DEVICE_WARN_BUFFER_LATENCY, DEVICE_WARN_ASIO_LATENCY, - DEVICE_WARN_ALSA_LATENCY + DEVICE_WARN_ALSA_LATENCY, + DEVICE_WARN_SPEAKERS }; enum errorMessageT { diff --git a/src/JTApplication.h b/src/JTApplication.h deleted file mode 100644 index 7d27b52..0000000 --- a/src/JTApplication.h +++ /dev/null @@ -1,66 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file JTApplication.h - * \author Matt Hortoon - * \date July 2022 - */ - -#ifndef JTAPPLICATION_H -#define JTAPPLICATION_H - -#include -#include -#include -#include -#include -#include - -class JTApplication : public QApplication -{ - Q_OBJECT - - public: - JTApplication(int& argc, char** argv) : QApplication(argc, argv) {} - - bool event(QEvent* event) override - { - if (event->type() == QEvent::FileOpen) { - QFileOpenEvent* openEvent = static_cast(event); - - QDesktopServices::openUrl(openEvent->url()); - } - return QApplication::event(event); - } -}; - -#endif // JTAPPLICATION_H diff --git a/src/Meter.cpp b/src/Meter.cpp index 9130f15..a641e8b 100644 --- a/src/Meter.cpp +++ b/src/Meter.cpp @@ -3,8 +3,7 @@ JackTrip: A System for High-Quality Audio Network Performance over the Internet - Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. + Copyright (c) 2022-2024 JackTrip Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/src/Meter.h b/src/Meter.h index 7fac7ea..05b2fbf 100644 --- a/src/Meter.h +++ b/src/Meter.h @@ -3,8 +3,7 @@ JackTrip: A System for High-Quality Audio Network Performance over the Internet - Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. + Copyright (c) 2022-2024 JackTrip Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/src/Monitor.cpp b/src/Monitor.cpp index fb17e16..2f9a7d2 100644 --- a/src/Monitor.cpp +++ b/src/Monitor.cpp @@ -3,8 +3,7 @@ JackTrip: A System for High-Quality Audio Network Performance over the Internet - Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. + Copyright (c) 2022-2024 JackTrip Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/src/Monitor.h b/src/Monitor.h index 3a41763..6169b9d 100644 --- a/src/Monitor.h +++ b/src/Monitor.h @@ -3,8 +3,7 @@ JackTrip: A System for High-Quality Audio Network Performance over the Internet - Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. + Copyright (c) 2022-2024 JackTrip Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/src/NoNap.h b/src/NoNap.h new file mode 100644 index 0000000..0eb69e8 --- /dev/null +++ b/src/NoNap.h @@ -0,0 +1,50 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2020 Aaron Wyatt. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +#ifndef __NONAP_H__ +#define __NONAP_H__ + +#include + +class NoNap +{ + public: + NoNap(); + ~NoNap(); + + void disableNap(); + void enableNap(); + + private: + id m_activity; + bool m_preventNap; +}; + +#endif // __NONAP_H__ diff --git a/src/NoNap.mm b/src/NoNap.mm new file mode 100644 index 0000000..5f0c48a --- /dev/null +++ b/src/NoNap.mm @@ -0,0 +1,63 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2020 Aaron Wyatt. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +#include "NoNap.h" +#include + +NoNap::NoNap() : + m_preventNap(false) +{} + +void NoNap::disableNap() +{ + if (m_preventNap) { + return; + } + m_preventNap = true; + m_activity = [[NSProcessInfo processInfo] beginActivityWithOptions:NSActivityLatencyCritical | NSActivityUserInitiated reason:@"Disable App Nap"]; + [m_activity retain]; +} + +void NoNap::enableNap() +{ + if (!m_preventNap) { + return; + } + m_preventNap = false; + [[NSProcessInfo processInfo] endActivity:m_activity]; + [m_activity release]; +} + +NoNap::~NoNap() +{ + if (m_preventNap) { + enableNap(); + } +} diff --git a/src/Patcher.cpp b/src/Patcher.cpp index f4aa065..1a4f796 100644 --- a/src/Patcher.cpp +++ b/src/Patcher.cpp @@ -60,6 +60,13 @@ void Patcher::registerClient(const QString& clientName) { QMutexLocker locker(&m_connectionMutex); + // this works around a JACK timing bug found under pipewire + // if registerClient is called for a second (or subsequent) hub client + // jack_client won't have properly updated its ports + // the workaround is to sleep here and let JACK update + if (m_jackClient) + QThread::msleep(100); + // If our jack client isn't running, start it. if (!m_jackClient) { m_jackClient = jack_client_open("jthubpatcher", JackNoStartServer, &m_status); diff --git a/src/Regulator.cpp b/src/Regulator.cpp index d73bc96..f847a0d 100644 --- a/src/Regulator.cpp +++ b/src/Regulator.cpp @@ -104,7 +104,7 @@ constexpr double AutoInitValFactor = // tweak constexpr int WindowDivisor = 8; // for faster auto tracking constexpr double AutoHeadroomGlitchTolerance = - 0.02; // Acceptable rate of skips before auto headroom is increased (2.0%) + 0.01; // Acceptable rate of skips before auto headroom is increased (1.0%) constexpr double AutoHistoryWindow = 60; // rolling window of time (in seconds) over which auto tolerance roughly adjusts constexpr double AutoSmoothingFactor = @@ -477,7 +477,7 @@ void Regulator::updateTolerance(int glitches, int skipped) if (mAutoHeadroom < 0) { // variable headroom: automatically increase to minimize glitch counts // only increase headroom if doing so would have reduced the number of - // glitches that occured over the past two seconds by 2% or more. + // glitches that occured over the past second by 1% or more. // prevent headroom from growing beyond rolling average of max. const int skipsAllowed = static_cast(AutoHeadroomGlitchTolerance * mSampleRate / mPeerFPP); @@ -486,6 +486,7 @@ void Regulator::updateTolerance(int glitches, int skipped) if (mSkipAutoHeadroom) { mSkipAutoHeadroom = false; } else { + // don't increase headroom two intervals in a row mSkipAutoHeadroom = true; ++mCurrentHeadroom; cout << "PLC glitches=" << glitches << " skipped=" << skipped << ">" @@ -493,7 +494,8 @@ void Regulator::updateTolerance(int glitches, int skipped) << " (max=" << pushStat->longTermMax << ")" << endl; } } else { - mSkipAutoHeadroom = true; + // require 2 seconds in a row if headroom >= two packet intervals + mSkipAutoHeadroom = mMsecTolerance >= (mPeerFPPdurMsec * 2); } } else { // fixed headroom @@ -591,7 +593,8 @@ bool Regulator::pullPacket() if (skipped < 0) skipped += NumSlots; } - if (mIncomingTiming[next] + mMsecTolerance >= now) { + // check if packet's age matches tolerance, or is the best candidate we have + if (mIncomingTiming[next] + mMsecTolerance >= now || i == 0) { // next is the best candidate memcpy(mXfrBuffer, mSlots[next], mPeerBytes); mLastSeqNumOut = next; diff --git a/src/RtAudioInterface.cpp b/src/RtAudioInterface.cpp index fc17538..3fa35cd 100644 --- a/src/RtAudioInterface.cpp +++ b/src/RtAudioInterface.cpp @@ -258,6 +258,14 @@ void RtAudioInterface::setup(bool verbose) throw std::runtime_error(errorMsg.toStdString()); } + // provide warnings for common known failure cases + const QString out_device_lower_name = + QString::fromStdString(out_device.name).toLower(); + if (out_device_lower_name.contains("speakers") + || out_device_lower_name.contains("lautsprecher")) { + AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_SPEAKERS); + } + if (in_device.api == out_device.api) { #ifdef _WIN32 if (in_device.api != RtAudio::WINDOWS_ASIO) { diff --git a/src/UdpDataProtocol.cpp b/src/UdpDataProtocol.cpp index 7c9eb37..9ae8709 100644 --- a/src/UdpDataProtocol.cpp +++ b/src/UdpDataProtocol.cpp @@ -50,6 +50,7 @@ #include "jacktrip_globals.h" #ifdef _WIN32 // #include +#include #include #include //cc need SD_SEND #pragma comment(lib, "ws2_32.lib") @@ -195,11 +196,7 @@ void UdpDataProtocol::setPeerAddress(const char* peerHostOrIP) } } -#if defined(_WIN32) -void UdpDataProtocol::setSocket(SOCKET& socket) -#else -void UdpDataProtocol::setSocket(int& socket) -#endif +void UdpDataProtocol::setSocket(socket_type& socket) { // If we haven't been passed a valid socket, then we should bind one. #if defined(_WIN32) @@ -221,11 +218,7 @@ void UdpDataProtocol::setSocket(int& socket) } //******************************************************************************* -#if defined(_WIN32) -SOCKET UdpDataProtocol::bindSocket() -#else -int UdpDataProtocol::bindSocket() -#endif +socket_type UdpDataProtocol::bindSocket() { QMutexLocker locker(&sUdpMutex); @@ -301,45 +294,104 @@ int UdpDataProtocol::bindSocket() ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); #endif + // set qos for windows after flow is established (requires peer address/port) + if (setSocketQos(sock_fd)) { + std::cout << "Set QoS for network socket" << std::endl; + } else { + std::cerr << "Failed to set QoS for network socket" << std::endl; + } + + // Bind the Socket + if (mIPv6) { + if ((::bind(sock_fd, (struct sockaddr*)&local_addr6, sizeof(local_addr6))) < 0) { + throw std::runtime_error("ERROR: UDP Socket Bind Error"); + } + } else { + if ((::bind(sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr))) < 0) { + throw std::runtime_error("ERROR: UDP Socket Bind Error"); + } + } + + // Return our file descriptor so the socket can be shared for a + // full duplex connection. + return sock_fd; +} + +bool UdpDataProtocol::setSocketQos(socket_type& sock_fd) +{ #if defined(_WIN32) - // TODO: these don't seem to work on windows. we likely need to use qWAVE or qos2 + // Windows QoS (qWave) for audio traffic flows + // https://learn.microsoft.com/en-us/windows/win32/api/_qos/ + // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/qos/qwave-api-reference + + // Initialize the QoS version parameter. + QOS_VERSION Version; + Version.MajorVersion = 1; + Version.MinorVersion = 0; + + // Get a handle to the QoS subsystem. + HANDLE QoSHandle = NULL; + BOOL QoSResult = QOSCreateHandle(&Version, &QoSHandle); + if (QoSResult != TRUE) { + std::cerr << "QOSCreateHandle failed. Error: " << WSAGetLastError() << std::endl; + return false; + } + + // Add socket to flow. + QOS_FLOWID QoSFlowId = 0; // Flow Id must be 0. + PSOCKADDR pSockAddr; + if (mIPv6) { + pSockAddr = reinterpret_cast(&mPeerAddr6); + } else { + pSockAddr = reinterpret_cast(&mPeerAddr); + } + // Note: QOSTrafficTypeVoice sets DSCP to 56 (high VO for WMM) + // without having to call QOSSetFlow(). This is best for voice. + QoSResult = QOSAddSocketToFlow(QoSHandle, sock_fd, pSockAddr, QOSTrafficTypeVoice, + QOS_NON_ADAPTIVE_FLOW, &QoSFlowId); + if (QoSResult != TRUE) { + std::cerr << "QOSAddSocketToFlow failed. Error: "; + std::cerr << WSAGetLastError() << std::endl; + return false; + } #elif defined(__APPLE__) // set service type "Interactive Voice" // TODO: this is supposed to be the right thing to do on OSX, but doesn't seem to do // anything const int val = NET_SERVICE_TYPE_VO; - ::setsockopt(sock_fd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &val, sizeof(val)); + int result = + ::setsockopt(sock_fd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &val, sizeof(val)); + if (result != 0) { + std::cerr << "setsockopt failed. Error: " << errno << std::endl; + return false; + } #else - // Set ToS to DSCP Expedited Forwarding (EF), recommended for Audio + // Set ToS to DSCP 56 (high VO for WMM), recommended for Audio // See RFC2474 https://datatracker.ietf.org/doc/html/rfc2474 // See also // https://www.slashroot.in/understanding-differentiated-services-tos-field-internet-protocol-header - const char tos = 0xB8; // 10111000 + const char tos = 0xE0; // 11100000 (56 << 2) + int result; if (mIPv6) { - ::setsockopt(sock_fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); + result = ::setsockopt(sock_fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)); } else { - ::setsockopt(sock_fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); + result = ::setsockopt(sock_fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); + } + if (result != 0) { + std::cerr << "setsockopt failed. Error: " << errno << std::endl; + return false; } // Set 802.1q QoS priority int priority = 6; - ::setsockopt(sock_fd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)); -#endif - - // Bind the Socket - if (mIPv6) { - if ((::bind(sock_fd, (struct sockaddr*)&local_addr6, sizeof(local_addr6))) < 0) { - throw std::runtime_error("ERROR: UDP Socket Bind Error"); - } - } else { - if ((::bind(sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr))) < 0) { - throw std::runtime_error("ERROR: UDP Socket Bind Error"); - } + result = ::setsockopt(sock_fd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)); + if (result != 0) { + std::cerr << "setsockopt failed. Error: " << errno << std::endl; + return false; } +#endif - // Return our file descriptor so the socket can be shared for a - // full duplex connection. - return sock_fd; + return true; } void UdpDataProtocol::processControlPacket(const char* buf) diff --git a/src/UdpDataProtocol.h b/src/UdpDataProtocol.h index f49ffae..498c2be 100644 --- a/src/UdpDataProtocol.h +++ b/src/UdpDataProtocol.h @@ -50,6 +50,12 @@ #include "jacktrip_globals.h" #include "jacktrip_types.h" +#if defined(_WIN32) +typedef SOCKET socket_type; +#else +typedef int socket_type; +#endif + /** \brief UDP implementation of DataProtocol class * * The class has a bind port and a peer port. The meaning of these @@ -93,11 +99,7 @@ class UdpDataProtocol : public DataProtocol */ void setPeerAddress(const char* peerHostOrIP); -#if defined(_WIN32) - void setSocket(SOCKET& socket); -#else - void setSocket(int& socket); -#endif + void setSocket(socket_type& socket); void processControlPacket(const char* buf); @@ -168,11 +170,11 @@ class UdpDataProtocol : public DataProtocol protected: /** \brief Binds the UDP socket to the available address and specified port */ -#if defined(_WIN32) - SOCKET bindSocket(); -#else - int bindSocket(); -#endif + socket_type bindSocket(); + + /** \brief Setup QoS for the network socket/flow + */ + bool setSocketQos(socket_type& sock_fd); /** \brief This function blocks until data is available for reading in the * socket. The function will timeout after timeout_msec microseconds. diff --git a/src/UserInterface.cpp b/src/UserInterface.cpp new file mode 100644 index 0000000..e2a6ca2 --- /dev/null +++ b/src/UserInterface.cpp @@ -0,0 +1,294 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2024 Michael Dickey, Aaron Wyatt. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file UserInterface.cpp + * \author Michael Dickey, Aaron Wyatt + * \date August 2024 + */ + +#include "UserInterface.h" + +#include + +#ifndef NO_VS +#include "vs/virtualstudio.h" +#endif // NO_VS + +#ifndef NO_CLASSIC +#include "gui/qjacktrip.h" +#endif // NO_CLASSIC + +#if !defined(NO_UPDATER) && !defined(__unix__) +#include "dblsqd/feed.h" +#include "dblsqd/update_dialog.h" +#endif // !defined(NO_UPDATER) && !defined(__unix__) + +#ifdef _WIN32 +#include +#include +#include + +bool isRunFromCmd() +{ + // Get our parent process pid + HANDLE h = NULL; + PROCESSENTRY32 pe; + ZeroMemory(&pe, sizeof(PROCESSENTRY32)); + DWORD pid = GetCurrentProcessId(); + DWORD ppid = 0; + pe.dwSize = sizeof(PROCESSENTRY32); + h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (Process32First(h, &pe)) { + do { + // Loop through the list of processes until we find ours. + if (pe.th32ProcessID == pid) { + ppid = pe.th32ParentProcessID; + break; + } + } while (Process32Next(h, &pe)); + } + CloseHandle(h); + + // Get the name of our parent process; + char pname[MAX_PATH] = {0}; + DWORD size = MAX_PATH; + h = NULL; + h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, ppid); + if (h) { + if (QueryFullProcessImageNameA(h, 0, pname, &size)) { + CloseHandle(h); + + // Check if our parent process is a command line. + if (size >= 14 && strncmp(pname + size - 14, "powershell.exe", 14) == 0) { + return true; + } + if (size >= 7 && strncmp(pname + size - 7, "cmd.exe", 7) == 0) { + return true; + } + if (size >= 6 && strncmp(pname + size - 6, "wt.exe", 6) == 0) { + return true; + } + // a few extras for msys/cygwin/etc + if (size >= 8 && strncmp(pname + size - 8, "bash.exe", 8) == 0) { + return true; + } + if (size >= 6 && strncmp(pname + size - 6, "sh.exe", 6) == 0) { + return true; + } + if (size >= 7 && strncmp(pname + size - 7, "zsh.exe", 7) == 0) { + return true; + } + } else { + CloseHandle(h); + } + } + + return false; +} +#endif // _WIN32 + +UserInterface::UserInterface(QSharedPointer& settings) : m_cliSettings(settings) +{ +} + +UserInterface::~UserInterface() +{ +#ifndef NO_VS + m_vs_ui.clear(); +#endif +#ifndef NO_CLASSIC + m_classic_ui.clear(); +#endif +} + +QCoreApplication* UserInterface::createApplication(int& argc, char* argv[]) +{ +#if defined(__unix__) + // Check if X or Wayland environment variables are set. + if (std::getenv("WAYLAND_DISPLAY") == nullptr && std::getenv("DISPLAY") == nullptr) { + std::cout << "ERROR: Display not found. Make sure X or Wayland is running or " + "try running jacktrip in command line mode." + << std::endl; + std::cout << "(To display a list of command line options run \"jacktrip -h\")" + << std::endl; + std::exit(1); + } +#endif + + QCoreApplication* app; +#ifdef NO_VS + app = QJackTrip::createApplication(argc, argv); +#else + app = VirtualStudio::createApplication(argc, argv); +#endif + app->setOrganizationName(QStringLiteral("jacktrip")); + app->setOrganizationDomain(QStringLiteral("jacktrip.org")); + app->setApplicationName(QStringLiteral("JackTrip")); + app->setApplicationVersion(gVersion); + + return app; +} + +void UserInterface::start(QApplication* app) +{ +#ifdef _WIN32 + // Remove the console that appears if we're on windows and not running from a + // command line. + if (!isRunFromCmd()) { + std::cout << "This extra window is caused by a bug in Microsoft Windows. " + << "It can safely be ignored or closed." << std::endl + << std::endl + << "To fix this bug, please upgrade to the latest version of " + << "Windows Terminal available in the Microsoft App Store:" << std::endl + << "https://aka.ms/terminal" << std::endl; + + FreeConsole(); + } +#endif // _WIN32 + +#ifndef NO_CLASSIC + m_classic_ui.reset(new QJackTrip(*this)); + QObject::connect(m_classic_ui.data(), &QJackTrip::signalExit, app, + &QCoreApplication::quit, Qt::QueuedConnection); +#ifdef NO_VS + m_classic_ui->show(); +#endif // NO_VS +#endif // NO_CLASSIC + + QSettings settings; + +#ifndef NO_VS + m_vs_ui.reset(new VirtualStudio(*this)); + QObject::connect(m_vs_ui.data(), &VirtualStudio::signalExit, app, + &QCoreApplication::quit, Qt::QueuedConnection); + // Check which mode we are running in + uiModeT uiMode = UserInterface::MODE_UNSET; + if (!m_cliSettings->getDeeplink().isEmpty()) { + uiMode = MODE_VS; + } else if (m_cliSettings->guiForceClassicMode()) { + uiMode = MODE_CLASSIC; + // force settings change; otherwise, virtual studio + // window will still be displayed + settings.setValue(QStringLiteral("UiMode"), uiMode); + } else { + uiMode = static_cast( + settings.value(QStringLiteral("UiMode"), MODE_UNSET).toInt()); + } + setMode(uiMode); +#endif // NO_VS + +#if !defined(NO_UPDATER) && !defined(__unix__) +#ifndef PSI + QString updateChannel = + settings.value(QStringLiteral("UpdateChannel"), "stable").toString().toLower(); + QString baseUrl = + QStringLiteral("https://files.jacktrip.org/app-releases/%1").arg(updateChannel); +#else + QString baseUrl = QStringLiteral("https://nuages.psi-borg.org/jacktrip"); +#endif // PSI + // Setup auto-update feed + dblsqd::Feed* feed = new dblsqd::Feed(); +#ifdef _WIN32 + feed->setUrl(QUrl(QString("%1/%2-manifests.json").arg(baseUrl, "win"))); +#endif +#ifdef Q_OS_MACOS + feed->setUrl(QUrl(QString("%1/%2-manifests.json").arg(baseUrl, "mac"))); +#endif + if (feed) { + dblsqd::UpdateDialog* updateDialog = new dblsqd::UpdateDialog(feed); + updateDialog->setIcon(":/qjacktrip/icon.png"); + } +#endif // !defined(NO_UPDATER) && !defined(__unix__) +} + +void UserInterface::setMode(uiModeT m) +{ +#ifdef NO_VS + if (m == MODE_VS) { + std::cerr << "JackTrip was not built with support for Virtual Studio mode." + << std::endl; + } + m = MODE_CLASSIC; +#endif + +#ifdef NO_CLASSIC + if (m == MODE_CLASSIC) { + std::cerr << "JackTrip was not built with support for Classic mode." << std::endl; + } + m = MODE_VS; +#endif + + switch (m) { + case MODE_UNSET: + case MODE_VS: +#ifndef NO_VS + m_vs_ui->show(); + if (m == MODE_VS || (m == MODE_UNSET && m_vs_ui->vsFtux())) { + m_vs_ui->setWindowState(QStringLiteral("login")); + } else if (m == MODE_UNSET && !m_vs_ui->vsFtux()) { + m_vs_ui->setWindowState(QStringLiteral("start")); + } + if (m_vs_ui->windowState() == "login") + m_vs_ui->login(); +#ifndef NO_CLASSIC + if (m_uiMode == MODE_CLASSIC) + m_classic_ui->hide(); +#endif // NO_CLASSIC +#endif // NO_VS + m_uiMode = MODE_VS; + break; + case MODE_CLASSIC: +#ifndef NO_CLASSIC + m_classic_ui->show(); +#ifndef NO_VS + m_vs_ui->hide(); +#endif // NO_VS +#endif // NO_CLASSIC + m_uiMode = MODE_CLASSIC; + break; + default: + return; + } +} + +void UserInterface::enableNap() +{ +#ifdef __APPLE__ + m_noNap.enableNap(); +#endif +} + +void UserInterface::disableNap() +{ +#ifdef __APPLE__ + m_noNap.disableNap(); +#endif +} diff --git a/src/UserInterface.h b/src/UserInterface.h new file mode 100644 index 0000000..7fd9e49 --- /dev/null +++ b/src/UserInterface.h @@ -0,0 +1,111 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2024 Michael Dickey, Aaron Wyatt. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file UserInterface.h + * \author Michael Dickey + * \date August 2024 + */ + +#ifndef __USERINTERFACE_H__ +#define __USERINTERFACE_H__ + +#include +#include +#include + +#include "Settings.h" + +#ifdef __APPLE__ +#include "NoNap.h" +#endif + +// forward class declarations +class VirtualStudio; +class QJackTrip; + +/// UserInterface manages graphical user interfaces for JackTrip +class UserInterface +{ + public: + /** + * @brief which GUI mode is in use + * + * MODE_UNSET: none selected yet + * MODE_VS: Virtual Studio mode (QML/Quick interface) + * MODE_CLASSIC: Classic mode (QJackTrip interface) + */ + enum uiModeT { MODE_UNSET, MODE_VS, MODE_CLASSIC }; + + /// construction requires command line settings + explicit UserInterface(QSharedPointer& settings); + + /// @brief simple destructor + ~UserInterface(); + + /// @return current GUI mode + inline uiModeT getMode() const { return m_uiMode; } + + /// @return command line settings + inline Settings& getSettings() { return *m_cliSettings.data(); } + + /// @brief creates new application using command line arguments + static QCoreApplication* createApplication(int& argc, char* argv[]); + + /// @brief starts graphical user interface + void start(QApplication* app); + + /// @brief sets GUI mode + void setMode(uiModeT m); + + /// @brief enables napping for OSX + void enableNap(); + + /// @brief disables napping for OSX + void disableNap(); + + private: + uiModeT m_uiMode = MODE_UNSET; + QSharedPointer m_cliSettings; + +#ifndef NO_VS + QSharedPointer m_vs_ui; +#endif + +#ifndef NO_CLASSIC + QSharedPointer m_classic_ui; +#endif + +#ifdef __APPLE__ + NoNap m_noNap; +#endif +}; + +#endif diff --git a/src/Volume.cpp b/src/Volume.cpp index f15f9aa..1690b07 100644 --- a/src/Volume.cpp +++ b/src/Volume.cpp @@ -3,8 +3,7 @@ JackTrip: A System for High-Quality Audio Network Performance over the Internet - Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. + Copyright (c) 2022-2024 JackTrip Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/src/Volume.h b/src/Volume.h index 8e8e12f..c4fa440 100644 --- a/src/Volume.h +++ b/src/Volume.h @@ -3,8 +3,7 @@ JackTrip: A System for High-Quality Audio Network Performance over the Internet - Copyright (c) 2020 Julius Smith, Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. + Copyright (c) 2022-2024 JackTrip Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/src/WaitFreeFrameBuffer.h b/src/WaitFreeFrameBuffer.h index 41887fa..740fb6b 100644 --- a/src/WaitFreeFrameBuffer.h +++ b/src/WaitFreeFrameBuffer.h @@ -3,9 +3,7 @@ JackTrip: A System for High-Quality Audio Network Performance over the Internet - Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - JackTrip Labs, Inc. + Copyright (c) 2022-2024 JackTrip Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/src/WaitFreeRingBuffer.h b/src/WaitFreeRingBuffer.h index 1b415d4..83ac360 100644 --- a/src/WaitFreeRingBuffer.h +++ b/src/WaitFreeRingBuffer.h @@ -3,9 +3,7 @@ JackTrip: A System for High-Quality Audio Network Performance over the Internet - Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - JackTrip Labs, Inc. + Copyright (c) 2022-2024 JackTrip Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/src/gui/AppIcon.qml b/src/gui/AppIcon.qml deleted file mode 100644 index 32eb8aa..0000000 --- a/src/gui/AppIcon.qml +++ /dev/null @@ -1,29 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - id: appIcon - - property alias icon: btn.icon - property string color: "" - property string defaultColor: virtualstudio.darkMode ? "#CCCCCC" : "#333333" - signal clicked() - - Button { - id: btn - anchors.fill: parent - anchors.centerIn: parent - topInset: 0 - leftInset: 0 - rightInset: 0 - bottomInset: 0 - padding: 0 - - background: Rectangle { color: "transparent" } - icon.color: color ? color : defaultColor - icon.width: parent.width - icon.height: parent.height - display: AbstractButton.IconOnly - onClicked: appIcon.clicked() - } -} diff --git a/src/gui/AudioInterfaceMode.h b/src/gui/AudioInterfaceMode.h deleted file mode 100644 index 3416b87..0000000 --- a/src/gui/AudioInterfaceMode.h +++ /dev/null @@ -1,88 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file AudioInterfaceMode.h - * \author Matt Horton - * \date December 2022 - */ - -#ifndef AUDIOINTERFACEMODE_H -#define AUDIOINTERFACEMODE_H - -enum class AudioInterfaceMode { - JACK, ///< Jack Mode - RTAUDIO, ///< RtAudio Mode - ALL, - NONE -}; - -#ifdef RT_AUDIO -#ifndef NO_JACK -constexpr AudioInterfaceMode mode = AudioInterfaceMode::ALL; -#else -constexpr AudioInterfaceMode mode = AudioInterfaceMode::RTAUDIO; -#endif -#else -#ifndef NO_JACK -constexpr AudioInterfaceMode mode = AudioInterfaceMode::JACK; -#else -constexpr AudioInterfaceMode mode = AudioInterfaceMode::NONE; -#endif -#endif - -template -constexpr auto isBackendAvailable() -{ - if constexpr (backend == AudioInterfaceMode::RTAUDIO) { - if (mode == AudioInterfaceMode::RTAUDIO || mode == AudioInterfaceMode::ALL) { - return true; - } else { - return false; - } - } else if constexpr (backend == AudioInterfaceMode::JACK) { - if (mode == AudioInterfaceMode::JACK || mode == AudioInterfaceMode::ALL) { - return true; - } else { - return false; - } - } else if constexpr (backend == AudioInterfaceMode::ALL) { - if (mode == AudioInterfaceMode::ALL) { - return true; - } else { - return false; - } - } else { - return false; - } -} - -#endif // AUDIOINTERFACEMODE_H diff --git a/src/gui/AudioSettings.qml b/src/gui/AudioSettings.qml deleted file mode 100644 index 0b56065..0000000 --- a/src/gui/AudioSettings.qml +++ /dev/null @@ -1,803 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Rectangle { - width: parent.width - height: parent.height - color: backgroundColour - - property bool connected: false - property bool showMeters: true - property bool showTestAudio: true - - property int fontBig: 20 - property int fontMedium: 13 - property int fontSmall: 11 - property int fontExtraSmall: 8 - - property int leftMargin: 48 - property int rightMargin: 16 - property int bottomToolTipMargin: 8 - property int rightToolTipMargin: 4 - property int buttonWidth: 103 - property int buttonHeight: 25 - - property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" - property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" - property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" - property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" - property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" - property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" - property string toolTipTextColour: textColour - - property string errorFlagColour: "#DB0A0A" - property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - - function getCurrentInputDeviceIndex () { - if (audio.inputDevice === "") { - return audio.inputComboModel.findIndex(elem => elem.type === "element"); - } - - let idx = audio.inputComboModel.findIndex(elem => elem.type === "element" && elem.text === audio.inputDevice); - if (idx < 0) { - idx = audio.inputComboModel.findIndex(elem => elem.type === "element"); - } - return idx; - } - - function getCurrentOutputDeviceIndex() { - if (audio.outputDevice === "") { - return audio.outputComboModel.findIndex(elem => elem.type === "element"); - } - - let idx = audio.outputComboModel.findIndex(elem => elem.type === "element" && elem.text === audio.outputDevice); - if (idx < 0) { - idx = audio.outputComboModel.findIndex(elem => elem.type === "element"); - } - return idx; - } - - function getCurrentInputChannelsIndex() { - let idx = audio.inputChannelsComboModel.findIndex(elem => elem.baseChannel === audio.baseInputChannel - && elem.numChannels === audio.numInputChannels); - if (idx < 0) { - idx = 0; - } - return idx; - } - - function getCurrentOutputChannelsIndex() { - let idx = audio.outputChannelsComboModel.findIndex(elem => elem.baseChannel === audio.baseOutputChannel - && elem.numChannels === audio.numOutputChannels); - if (idx < 0) { - idx = 0; - } - return idx; - } - - function getCurrentMixModeIndex() { - let idx = audio.inputMixModeComboModel.findIndex(elem => elem.value === audio.inputMixMode); - if (idx < 0) { - idx = 0; - } - return idx; - } - - Loader { - anchors.fill: parent - sourceComponent: audio.audioBackend == "JACK" ? usingJACK : ((!audio.deviceModelsInitialized || audio.scanningDevices) ? scanningDevices : usingRtAudio); - } - - Component { - id: usingRtAudio - - Item { - anchors.top: parent.top - anchors.topMargin: 24 * virtualstudio.uiScale - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.leftMargin: 24 * virtualstudio.uiScale - anchors.right: parent.right - - Rectangle { - id: leftSpacer - x: 0; y: 0 - width: 144 * virtualstudio.uiScale - height: 0 - color: "transparent" - } - - Text { - id: outputLabel - x: 0; y: 0 - text: "Output Device" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - bottomPadding: 10 * virtualstudio.uiScale - color: textColour - } - - InfoTooltip { - id: outputHelpIcon - anchors.left: outputLabel.right - anchors.bottom: outputLabel.top - anchors.bottomMargin: -8 * virtualstudio.uiScale - size: 16 * virtualstudio.uiScale - content: qsTr("How you'll hear the studio audio") - } - - AppIcon { - id: headphonesIcon - anchors.left: outputLabel.left - anchors.top: outputLabel.bottom - width: 28 * virtualstudio.uiScale - height: 28 * virtualstudio.uiScale - icon.source: "headphones.svg" - } - - ComboBox { - id: outputCombo - anchors.left: leftSpacer.right - anchors.verticalCenter: outputLabel.verticalCenter - anchors.rightMargin: rightMargin * virtualstudio.uiScale - width: parent.width - leftSpacer.width - rightMargin * virtualstudio.uiScale - model: audio.outputComboModel - currentIndex: getCurrentOutputDeviceIndex() - delegate: ItemDelegate { - required property var modelData - required property int index - - leftPadding: 0 - - width: parent.width - contentItem: Text { - leftPadding: modelData.type === "element" && outputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12 - text: modelData.text - font.bold: modelData.type === "header" - } - highlighted: outputCombo.highlightedIndex === index - MouseArea { - anchors.fill: parent - onClicked: { - if (modelData.type == "element") { - outputCombo.currentIndex = index - outputCombo.popup.close() - audio.outputDevice = modelData.text - if (modelData.category.startsWith("Low-Latency")) { - let inputComboIdx = inputCombo.model.findIndex(it => it.category.startsWith("Low-Latency") && it.text === modelData.text); - if (inputComboIdx !== null && inputComboIdx !== undefined) { - inputCombo.currentIndex = inputComboIdx; - audio.inputDevice = modelData.text - } - } - if (connected) { - virtualstudio.triggerReconnect(false); - } else { - audio.validateDevices() - audio.restartAudio() - } - } - } - } - } - contentItem: Text { - leftPadding: 12 - font: outputCombo.font - horizontalAlignment: Text.AlignHLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: outputCombo.model[outputCombo.currentIndex]!=undefined && outputCombo.model[outputCombo.currentIndex].text ? outputCombo.model[outputCombo.currentIndex].text : "" - } - } - - Meter { - id: outputDeviceMeters - anchors.left: outputCombo.left - anchors.right: outputCombo.right - anchors.top: outputCombo.bottom - anchors.topMargin: 16 * virtualstudio.uiScale - height: 24 * virtualstudio.uiScale - model: showMeters ? audio.outputMeterLevels : [0, 0] - clipped: audio.outputClipped - visible: showMeters - enabled: audio.audioReady && !Boolean(audio.devicesError) - } - - VolumeSlider { - id: outputSlider - anchors.left: outputCombo.left - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.top: outputDeviceMeters.bottom - anchors.topMargin: 16 * virtualstudio.uiScale - height: 30 * virtualstudio.uiScale - labelText: "Studio" - tooltipText: "How loudly you hear other participants" - showLabel: false - sliderEnabled: true - visible: showMeters - } - - Text { - id: outputChannelsLabel - anchors.left: outputCombo.left - anchors.right: outputCombo.horizontalCenter - anchors.top: showMeters ? outputSlider.bottom : outputCombo.bottom - anchors.topMargin: 12 * virtualstudio.uiScale - textFormat: Text.RichText - text: "Output Channel(s)" - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - ComboBox { - id: outputChannelsCombo - anchors.left: outputCombo.left - anchors.right: outputCombo.horizontalCenter - anchors.rightMargin: 8 * virtualstudio.uiScale - anchors.top: outputChannelsLabel.bottom - anchors.topMargin: 4 * virtualstudio.uiScale - model: audio.outputChannelsComboModel - enabled: audio.outputChannelsComboModel.length > 1 - currentIndex: getCurrentOutputChannelsIndex() - delegate: ItemDelegate { - required property var modelData - required property int index - width: parent.width - contentItem: Text { - text: modelData.label - } - highlighted: outputChannelsCombo.highlightedIndex === index - MouseArea { - anchors.fill: parent - onClicked: { - outputChannelsCombo.currentIndex = index - outputChannelsCombo.popup.close() - audio.baseOutputChannel = modelData.baseChannel - audio.numOutputChannels = modelData.numChannels - if (connected) { - virtualstudio.triggerReconnect(false); - } else { - audio.validateDevices() - audio.restartAudio() - } - } - } - } - contentItem: Text { - leftPadding: 12 - font: inputCombo.font - horizontalAlignment: Text.AlignHLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: outputChannelsCombo.model[outputChannelsCombo.currentIndex].label || "" - } - } - - Button { - id: testOutputAudioButton - visible: showTestAudio - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: testOutputAudioButton.down ? buttonPressedColour : (testOutputAudioButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: testOutputAudioButton.down || testOutputAudioButton.hovered ? buttonPressedStroke : (testOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { audio.playOutputAudio() } - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.verticalCenter: outputChannelsCombo.verticalCenter - width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Play Test Tone" - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - Rectangle { - id: divider1 - anchors.top: showTestAudio ? testOutputAudioButton.bottom : outputChannelsCombo.bottom - anchors.topMargin: 24 * virtualstudio.uiScale - width: parent.width - x - (16 * virtualstudio.uiScale); height: 2 * virtualstudio.uiScale - color: "#E0E0E0" - } - - Text { - id: inputLabel - anchors.left: outputLabel.left - anchors.top: divider1.bottom - anchors.topMargin: 24 * virtualstudio.uiScale - text: "Input Device" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - bottomPadding: 10 * virtualstudio.uiScale - color: textColour - } - - InfoTooltip { - id: inputHelpIcon - anchors.left: inputLabel.right - anchors.bottom: inputLabel.top - anchors.bottomMargin: -8 * virtualstudio.uiScale - size: 16 * virtualstudio.uiScale - content: qsTr("Audio sent to the studio (microphone, instrument, mixer, etc.)") - } - - AppIcon { - id: microphoneIcon - anchors.left: inputLabel.left - anchors.top: inputLabel.bottom - width: 32 * virtualstudio.uiScale - height: 32 * virtualstudio.uiScale - icon.source: "mic.svg" - } - - ComboBox { - id: inputCombo - model: audio.inputComboModel - currentIndex: getCurrentInputDeviceIndex() - anchors.left: outputCombo.left - anchors.right: outputCombo.right - anchors.verticalCenter: inputLabel.verticalCenter - delegate: ItemDelegate { - required property var modelData - required property int index - - leftPadding: 0 - - width: parent.width - contentItem: Text { - leftPadding: modelData.type === "element" && inputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12 - text: modelData.text - font.bold: modelData.type === "header" - } - highlighted: inputCombo.highlightedIndex === index - MouseArea { - anchors.fill: parent - onClicked: { - if (modelData.type == "element") { - inputCombo.currentIndex = index - inputCombo.popup.close() - audio.inputDevice = modelData.text - if (modelData.category.startsWith("Low-Latency")) { - let outputComboIdx = outputCombo.model.findIndex(it => it.category.startsWith("Low-Latency") && it.text === modelData.text); - if (outputComboIdx !== null && outputComboIdx !== undefined) { - outputCombo.currentIndex = outputComboIdx; - audio.outputDevice = modelData.text - } - } - if (connected) { - virtualstudio.triggerReconnect(false); - } else { - audio.validateDevices() - audio.restartAudio() - } - } - } - } - } - contentItem: Text { - leftPadding: 12 - font: inputCombo.font - horizontalAlignment: Text.AlignHLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: inputCombo.model[inputCombo.currentIndex] != undefined && inputCombo.model[inputCombo.currentIndex].text ? inputCombo.model[inputCombo.currentIndex].text : "" - } - } - - Meter { - id: inputDeviceMeters - anchors.left: inputCombo.left - anchors.right: inputCombo.right - anchors.top: inputCombo.bottom - anchors.topMargin: 16 * virtualstudio.uiScale - height: 24 * virtualstudio.uiScale - model: showMeters ? audio.inputMeterLevels : [0, 0] - clipped: audio.inputClipped - visible: showMeters - enabled: audio.audioReady && !Boolean(audio.devicesError) - } - - VolumeSlider { - id: inputSlider - anchors.left: inputCombo.left - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.top: inputDeviceMeters.bottom - anchors.topMargin: 16 * virtualstudio.uiScale - height: 30 * virtualstudio.uiScale - labelText: "Send" - tooltipText: "How loudly other participants hear you" - showLabel: false - sliderEnabled: true - visible: showMeters - } - - Button { - id: hiddenInputButton - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.verticalCenter: inputSlider.verticalCenter - width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - visible: false - } - - Text { - id: inputChannelsLabel - anchors.left: inputCombo.left - anchors.right: inputCombo.horizontalCenter - anchors.top: showMeters ? inputSlider.bottom : inputCombo.bottom - anchors.topMargin: 12 * virtualstudio.uiScale - textFormat: Text.RichText - text: "Input Channel(s)" - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - ComboBox { - id: inputChannelsCombo - anchors.left: inputCombo.left - anchors.right: inputCombo.horizontalCenter - anchors.rightMargin: 8 * virtualstudio.uiScale - anchors.top: inputChannelsLabel.bottom - anchors.topMargin: 4 * virtualstudio.uiScale - model: audio.inputChannelsComboModel - enabled: audio.inputChannelsComboModel.length > 1 - currentIndex: getCurrentInputChannelsIndex() - delegate: ItemDelegate { - required property var modelData - required property int index - width: parent.width - contentItem: Text { - text: modelData.label - } - highlighted: inputChannelsCombo.highlightedIndex === index - MouseArea { - anchors.fill: parent - onClicked: { - inputChannelsCombo.currentIndex = index - inputChannelsCombo.popup.close() - audio.baseInputChannel = modelData.baseChannel - audio.numInputChannels = modelData.numChannels - if (connected) { - virtualstudio.triggerReconnect(false); - } else { - audio.validateDevices() - audio.restartAudio() - } - } - } - } - contentItem: Text { - leftPadding: 12 - font: inputCombo.font - horizontalAlignment: Text.AlignHLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: inputChannelsCombo.model[inputChannelsCombo.currentIndex].label || "" - } - } - - Text { - id: inputMixModeLabel - anchors.left: inputCombo.horizontalCenter - anchors.right: inputCombo.right - anchors.rightMargin: 8 * virtualstudio.uiScale - anchors.top: showMeters ? inputSlider.bottom : inputCombo.bottom - anchors.topMargin: 12 * virtualstudio.uiScale - textFormat: Text.RichText - text: "Mono / Stereo" - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - ComboBox { - id: inputMixModeCombo - anchors.left: inputCombo.horizontalCenter - anchors.right: inputCombo.right - anchors.rightMargin: 8 * virtualstudio.uiScale - anchors.top: inputMixModeLabel.bottom - anchors.topMargin: 4 * virtualstudio.uiScale - model: audio.inputMixModeComboModel - enabled: audio.inputMixModeComboModel.length > 1 - currentIndex: getCurrentMixModeIndex() - delegate: ItemDelegate { - required property var modelData - required property int index - width: parent.width - contentItem: Text { - text: modelData.label - } - highlighted: inputMixModeCombo.highlightedIndex === index - MouseArea { - anchors.fill: parent - onClicked: { - inputMixModeCombo.currentIndex = index - inputMixModeCombo.popup.close() - audio.inputMixMode = audio.inputMixModeComboModel[index].value - if (connected) { - virtualstudio.triggerReconnect(false); - } else { - audio.validateDevices() - audio.restartAudio() - } - } - } - } - contentItem: Text { - leftPadding: 12 - font: inputCombo.font - horizontalAlignment: Text.AlignHLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: inputMixModeCombo.model[inputMixModeCombo.currentIndex].label || "" - } - } - - Text { - id: inputChannelHelpMessage - anchors.left: inputChannelsCombo.left - anchors.leftMargin: 2 * virtualstudio.uiScale - anchors.right: inputChannelsCombo.right - anchors.top: inputChannelsCombo.bottom - anchors.topMargin: 8 * virtualstudio.uiScale - textFormat: Text.RichText - wrapMode: Text.WordWrap - text: audio.inputChannelsComboModel.length > 1 ? "Choose up to 2 channels" : "Only 1 channel available" - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - Text { - id: inputMixModeHelpMessage - anchors.left: inputMixModeCombo.left - anchors.leftMargin: 2 * virtualstudio.uiScale - anchors.right: inputMixModeCombo.right - anchors.top: inputMixModeCombo.bottom - anchors.topMargin: 8 * virtualstudio.uiScale - textFormat: Text.RichText - wrapMode: Text.WordWrap - text: (() => { - if (audio.inputMixMode === 2) { - return "Treat the channels as Left and Right signals, coming through each speaker separately."; - } else if (audio.inputMixMode === 3) { - return "Combine the channels into one central channel coming through both speakers."; - } else if (audio.inputMixMode === 1) { - return "Send a single channel of audio"; - } else { - return ""; - } - })() - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - Connections { - target: audio - // anything that sets currentIndex to the value of a function needs - // to be manually updated whenever there is a change to any vars it uses - function onInputDeviceChanged() { - inputCombo.currentIndex = getCurrentInputDeviceIndex(); - } - function onOutputDeviceChanged() { - outputCombo.currentIndex = getCurrentOutputDeviceIndex(); - } - function onNumInputChannelsChanged() { - inputChannelsCombo.currentIndex = getCurrentInputChannelsIndex(); - } - function onBaseInputChannelChanged() { - inputChannelsCombo.currentIndex = getCurrentInputChannelsIndex(); - } - function onNumOutputChannelsChanged() { - outputChannelsCombo.currentIndex = getCurrentOutputChannelsIndex(); - } - function onBaseOutputChannelChanged() { - outputChannelsCombo.currentIndex = getCurrentOutputChannelsIndex(); - } - function onInputMixModeChanged() { - inputMixModeCombo.currentIndex = getCurrentMixModeIndex(); - } - } - } - } - - Component { - id: usingJACK - - Item { - anchors.top: parent.top - anchors.topMargin: 24 * virtualstudio.uiScale - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.leftMargin: leftMargin * virtualstudio.uiScale - anchors.right: parent.right - - Text { - id: jackLabel - x: 0; y: 0 - width: parent.width - rightMargin * virtualstudio.uiScale - text: "Using JACK for audio input and output. Use QjackCtl to adjust your sample rate, buffer, and device settings." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - wrapMode: Text.WordWrap - color: textColour - } - - Text { - id: jackOutputLabel - anchors.left: jackLabel.left - anchors.top: jackLabel.bottom - anchors.topMargin: 48 * virtualstudio.uiScale - width: 144 * virtualstudio.uiScale - text: "Output Volume" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - wrapMode: Text.WordWrap - bottomPadding: 10 * virtualstudio.uiScale - color: textColour - } - - AppIcon { - id: jackHeadphonesIcon - anchors.left: jackOutputLabel.left - anchors.top: jackOutputLabel.bottom - width: 28 * virtualstudio.uiScale - height: 28 * virtualstudio.uiScale - icon.source: "headphones.svg" - } - - Meter { - id: jackOutputMeters - anchors.left: jackOutputLabel.right - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.verticalCenter: jackOutputLabel.verticalCenter - height: 24 * virtualstudio.uiScale - model: showMeters ? audio.outputMeterLevels : [0, 0] - clipped: audio.outputClipped - enabled: audio.audioReady && !Boolean(audio.devicesError) - } - - Button { - id: jackTestOutputAudioButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: jackTestOutputAudioButton.down ? buttonPressedColour : (jackTestOutputAudioButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: jackTestOutputAudioButton.down ? buttonPressedStroke : (jackTestOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { audio.playOutputAudio() } - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.verticalCenter: jackOutputVolumeSlider.verticalCenter - width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Play Test Tone" - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - VolumeSlider { - id: jackOutputVolumeSlider - anchors.left: jackOutputMeters.left - anchors.right: jackTestOutputAudioButton.left - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.top: jackOutputMeters.bottom - anchors.topMargin: 16 * virtualstudio.uiScale - height: 30 * virtualstudio.uiScale - labelText: "Studio" - tooltipText: "How loudly you hear other participants" - showLabel: false - sliderEnabled: true - } - - Text { - id: jackInputLabel - anchors.left: jackLabel.left - anchors.top: jackOutputVolumeSlider.bottom - anchors.topMargin: 48 * virtualstudio.uiScale - width: 144 * virtualstudio.uiScale - text: "Input Volume" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - wrapMode: Text.WordWrap - bottomPadding: 10 * virtualstudio.uiScale - color: textColour - } - - AppIcon { - id: jackMicrophoneIcon - anchors.left: jackInputLabel.left - anchors.top: jackInputLabel.bottom - width: 32 * virtualstudio.uiScale - height: 32 * virtualstudio.uiScale - icon.source: "mic.svg" - } - - Meter { - id: jackInputMeters - anchors.left: jackInputLabel.right - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.verticalCenter: jackInputLabel.verticalCenter - height: 24 * virtualstudio.uiScale - model: showMeters ? audio.inputMeterLevels : [0, 0] - clipped: audio.inputClipped - enabled: audio.audioReady && !Boolean(audio.devicesError) - } - - VolumeSlider { - id: jackInputVolumeSlider - anchors.left: jackInputMeters.left - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.top: jackInputMeters.bottom - anchors.topMargin: 16 * virtualstudio.uiScale - height: 30 * virtualstudio.uiScale - labelText: "Send" - tooltipText: "How loudly other participants hear you" - showLabel: false - sliderEnabled: true - } - - Button { - id: jackHiddenInputButton - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.verticalCenter: jackInputVolumeSlider.verticalCenter - width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - visible: false - } - } - } - - Component { - id: noBackend - - Item { - anchors.top: parent.top - anchors.topMargin: 24 * virtualstudio.uiScale - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.leftMargin: leftMargin * virtualstudio.uiScale - anchors.right: parent.right - - Text { - id: noBackendLabel - x: 0; y: 0 - width: parent.width - (16 * virtualstudio.uiScale) - text: "JackTrip has been compiled without an audio backend. Please rebuild with the rtaudio flag or without the nojack flag." - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - wrapMode: Text.WordWrap - color: textColour - } - } - } - - Component { - id: scanningDevices - - Item { - anchors.top: parent.top - anchors.topMargin: 24 * virtualstudio.uiScale - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.leftMargin: leftMargin * virtualstudio.uiScale - anchors.right: parent.right - - Text { - id: scanningDevicesLabel - x: 0; y: 0 - width: parent.width - (16 * virtualstudio.uiScale) - text: "Scanning audio devices..." - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - wrapMode: Text.WordWrap - color: textColour - } - } - } -} diff --git a/src/gui/Browse.qml b/src/gui/Browse.qml deleted file mode 100644 index d632152..0000000 --- a/src/gui/Browse.qml +++ /dev/null @@ -1,328 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - width: parent.width; height: parent.height - clip: true - - Rectangle { - width: parent.width; height: parent.height - color: backgroundColour - } - - property bool refreshing: false - - property int buttonHeight: 25 - property int buttonWidth: 103 - property int extraSettingsButtonWidth: 16 - property int emptyListMessageWidth: 450 - property int createMessageTopMargin: 16 - property int createButtonTopMargin: 24 - property int fontBig: 28 - property int fontMedium: 11 - - property int scrollY: 0 - - property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" - property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" - property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" - property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" - property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - property string createButtonStroke: virtualstudio.darkMode ? "#AB0F0F" : "#0F0D0D" - - function refresh() { - scrollY = studioListView.contentY; - var currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY); - if (currentIndex == -1) { - currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY + (16 * virtualstudio.uiScale)); - } - virtualstudio.refreshStudios(currentIndex, true) - } - - Rectangle { - z: 1 - width: parent.width; height: parent.height - color: "#40000000" - visible: refreshing - MouseArea { - anchors.fill: parent - propagateComposedEvents: false - hoverEnabled: true - preventStealing: true - } - } - - Component { - id: footer - Rectangle { - height: 16 * virtualstudio.uiScale - x: 16 * virtualstudio.uiScale - width: parent.width - (2 * x) - color: backgroundColour - } - } - - ListView { - id: studioListView - x:0; y: 0; width: parent.width - (2 * x); height: parent.height - 36 * virtualstudio.uiScale - spacing: 16 * virtualstudio.uiScale - header: footer - footer: footer - model: virtualstudio.serverModel - clip: true - boundsBehavior: Flickable.StopAtBounds - delegate: Studio { - x: 16 * virtualstudio.uiScale - width: studioListView.width - (2 * x) - serverLocation: virtualstudio.regions[modelData.location] ? "in " + virtualstudio.regions[modelData.location].label : "" - flagImage: modelData.bannerURL ? modelData.bannerURL : modelData.flag - studioName: modelData.name - publicStudio: modelData.isPublic - admin: modelData.isAdmin - available: modelData.canConnect - connected: false - studioId: modelData.id ? modelData.id : "" - streamId: modelData.streamId ? modelData.streamId : "" - inviteKeyString: modelData.inviteKey ? modelData.inviteKey : "" - sampleRate: modelData.sampleRate - } - - section { property: "modelData.type"; criteria: ViewSection.FullString; delegate: SectionHeading {} } - - // Show sectionHeading if there are no Studios in list - SectionHeading { - id: emptyListSectionHeading - listIsEmpty: true - visible: parent.count == 0 && !virtualstudio.showCreateStudio - } - - Text { - id: emptyListMessage - visible: parent.count == 0 && !virtualstudio.showCreateStudio - text: "No studios found that match your filter criteria." - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: emptyListMessageWidth - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: emptyListSectionHeading.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - - Button { - id: resetFiltersButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: resetFiltersButton.down ? buttonPressedColour : (resetFiltersButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: resetFiltersButton.down ? buttonPressedStroke : (resetFiltersButton.hovered ? buttonHoverStroke : buttonStroke) - } - visible: parent.count == 0 && !virtualstudio.showCreateStudio - onClicked: { - virtualstudio.showSelfHosted = false; - virtualstudio.showInactive = true; - refreshing = true; - refresh(); - } - anchors.top: emptyListMessage.bottom - anchors.topMargin: createButtonTopMargin - anchors.horizontalCenter: emptyListMessage.horizontalCenter - width: 120 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale - Text { - text: "Reset Filters" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - Rectangle { - id: newUserEmptyList - anchors.fill: parent - color: "transparent" - visible: parent.count == 0 && virtualstudio.showCreateStudio - - Rectangle { - color: "transparent" - width: emptyListMessageWidth - height: createButton.height + createStudioMessage.height + welcomeMessage.height + createButtonTopMargin + createMessageTopMargin - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - - Text { - id: welcomeMessage - text: "Welcome" - font { family: "Poppins"; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale; weight: Font.Bold } - color: textColour - width: emptyListMessageWidth - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - } - - Text { - id: createStudioMessage - text: "Looks like you're not a member of any studios!\nHave the studio owner send you an invite link, or create your own studio to invite others." - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: emptyListMessageWidth - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: welcomeMessage.bottom - anchors.topMargin: createMessageTopMargin - } - - Button { - id: createButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: createButton.down ? "#E7E8E8" : "#F2F3F3" - border.width: 1 - border.color: createButton.down ? "#B0B5B5" : createButtonStroke - layer.enabled: createButton.hovered && !createButton.down - } - onClicked: { virtualstudio.createStudio(); } - anchors.top: createStudioMessage.bottom - anchors.topMargin: createButtonTopMargin - anchors.horizontalCenter: createStudioMessage.horizontalCenter - width: 210 * virtualstudio.uiScale; height: 45 * virtualstudio.uiScale - Text { - text: "Create a Studio" - font.family: "Poppins" - font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: "#DB0A0A" - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - } - } - - // Disable momentum scroll - MouseArea { - z: -1 - anchors.fill: parent - onWheel: function (wheel) { - // trackpad - studioListView.contentY -= wheel.pixelDelta.y; - // mouse wheel - studioListView.contentY -= wheel.angleDelta.y; - studioListView.returnToBounds(); - } - } - - Component.onCompleted: { - // Customize scroll properties on different platforms - if (Qt.platform.os == "linux" || Qt.platform.os == "osx" || - Qt.platform.os == "unix" || Qt.platform.os == "windows") { - var scrollBar = Qt.createQmlObject('import QtQuick.Controls; ScrollBar{}', - studioListView, - "dynamicSnippet1"); - scrollBar.policy = ScrollBar.AlwaysOn; - ScrollBar.vertical = scrollBar; - } - } - } - - Rectangle { - x: 0; y: parent.height - 36 * virtualstudio.uiScale; width: parent.width; height: 36 * virtualstudio.uiScale - border.color: "#33979797" - color: backgroundColour - - Button { - id: refreshButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { refreshing = true; refresh() } - anchors.verticalCenter: parent.verticalCenter - x: 16 * virtualstudio.uiScale - width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale - Text { - text: "Refresh List" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - Button { - id: aboutButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: aboutButton.down ? buttonPressedColour : (aboutButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: aboutButton.down ? buttonPressedStroke : (aboutButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { virtualstudio.showAbout() } - anchors.verticalCenter: parent.verticalCenter - x: parent.width - ((230 + extraSettingsButtonWidth) * virtualstudio.uiScale) - width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale - Text { - text: "About" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - Button { - id: settingsButton - text: "Settings" - palette.buttonText: textColour - icon { - source: "cog.svg"; - color: textColour; - } - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: settingsButton.down ? buttonPressedColour : (settingsButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: settingsButton.down ? buttonPressedStroke : (settingsButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { virtualstudio.windowState = "settings"; audio.startAudio(); } - display: AbstractButton.TextBesideIcon - font { - family: "Poppins"; - pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; - } - leftPadding: 0 - rightPadding: 4 - spacing: 0 - anchors.verticalCenter: parent.verticalCenter - x: parent.width - ((119 + extraSettingsButtonWidth) * virtualstudio.uiScale) - width: (buttonWidth + extraSettingsButtonWidth) * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale - } - } - - FeedbackSurvey { - } - - Connections { - target: virtualstudio - // Need to do this to avoid layout issues with our section header. - function onNewScale() { - studioListView.positionViewAtEnd(); - studioListView.positionViewAtBeginning(); - scrollY = studioListView.contentY; - } - function onRefreshFinished(index) { - refreshing = false; - if (index == -1) { - studioListView.contentY = scrollY - } else { - studioListView.positionViewAtIndex(index, ListView.Beginning); - } - } - function onPeriodicRefresh() { refresh() } - } -} diff --git a/src/gui/ChangeDevices.qml b/src/gui/ChangeDevices.qml deleted file mode 100644 index 7937ec2..0000000 --- a/src/gui/ChangeDevices.qml +++ /dev/null @@ -1,115 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Rectangle { - width: parent.width; height: parent.height - color: backgroundColour - clip: true - - property int fontBig: 28 - property int fontMedium: 12 - property int fontSmall: 10 - property int fontTiny: 8 - - property int rightMargin: 16 - property int bottomToolTipMargin: 8 - property int rightToolTipMargin: 4 - - property string saveButtonText: "#DB0A0A" - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0" - property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0 - property real muteButtonMutedLightnessValue: 0.24 - property real muteButtonMutedSaturationValue: 0.73 - property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1" - property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" - property string toolTipTextColour: textColour - - property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" - property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" - property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" - property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" - property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - - property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" - - MouseArea { - anchors.fill: parent - propagateComposedEvents: false - } - - Rectangle { - id: audioSettingsView - width: parent.width; - height: parent.height; - color: backgroundColour - radius: 6 * virtualstudio.uiScale - - DeviceRefreshButton { - id: refreshButton - anchors.top: parent.top; - anchors.topMargin: 16 * virtualstudio.uiScale; - anchors.right: parent.right; - anchors.rightMargin: 16 * virtualstudio.uiScale; - enabled: !audio.scanningDevices - onDeviceRefresh: function () { - virtualstudio.triggerReconnect(true); - } - } - - Text { - text: "Restarting Audio" - anchors.verticalCenter: refreshButton.verticalCenter - anchors.right: refreshButton.left; - anchors.rightMargin: 16 * virtualstudio.uiScale; - font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - visible: audio.scanningDevices - } - - AudioSettings { - id: audioSettings - showMeters: false - showTestAudio: false - connected: true - height: 300 * virtualstudio.uiScale - anchors.top: refreshButton.bottom; - anchors.topMargin: 16 * virtualstudio.uiScale; - } - } - - Button { - id: backButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour) - } - onClicked: { - virtualstudio.saveSettings(); - virtualstudio.windowState = "connected"; - } - anchors.bottom: parent.bottom - anchors.bottomMargin: 16 * virtualstudio.uiScale; - anchors.left: parent.left - anchors.leftMargin: 16 * virtualstudio.uiScale; - width: 150 * virtualstudio.uiScale; height: 36 * virtualstudio.uiScale - - Text { - text: "Back" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - DeviceWarning { - id: deviceWarning - anchors.left: backButton.right - anchors.leftMargin: 24 * virtualstudio.uiScale - anchors.bottom: parent.bottom - anchors.bottomMargin: 16 * virtualstudio.uiScale; - visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) - } -} diff --git a/src/gui/Connected.qml b/src/gui/Connected.qml deleted file mode 100644 index 00d6798..0000000 --- a/src/gui/Connected.qml +++ /dev/null @@ -1,85 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import org.jacktrip.jacktrip 1.0 - -Item { - width: parent.width; height: parent.height - clip: true - - property bool connecting: false - - property int leftHeaderMargin: 16 - property int fontBig: 28 - property int fontMedium: 12 - property int fontSmall: 10 - property int fontTiny: 8 - - property int bodyMargin: 60 - property int rightMargin: 16 - property int bottomToolTipMargin: 8 - property int rightToolTipMargin: 4 - - property string studioStatus: (virtualstudio.currentStudio.id === "" ? "" : virtualstudio.currentStudio.status) - property bool showReadyScreen: studioStatus === "Ready" - property bool showStartingScreen: studioStatus === "Starting" - property bool showStoppingScreen: (virtualstudio.currentStudio.id === "" ? false : (virtualstudio.currentStudio.isAdmin && !virtualstudio.currentStudio.enabled && virtualstudio.currentStudio.cloudId !== "")) - property bool showWaitingScreen: !showStoppingScreen && !showStartingScreen && !showReadyScreen - - property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - - property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" - property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" - property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" - property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" - property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - property string saveButtonBackgroundColour: "#F2F3F3" - property string saveButtonPressedColour: "#E7E8E8" - property string saveButtonStroke: "#EAEBEB" - property string saveButtonPressedStroke: "#B0B5B5" - property string saveButtonText: "#DB0A0A" - - property string muteButtonMutedColor: "#FCB6B6" - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0" - property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0 - property real muteButtonMutedLightnessValue: 0.24 - property real muteButtonMutedSaturationValue: 0.73 - property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1" - property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" - property string toolTipTextColour: textColour - property string warningTextColour: "#DB0A0A" - property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" - - property string meterGreen: "#61C554" - property string meterYellow: "#F5BF4F" - property string meterRed: "#F21B1B" - - property bool isUsingRtAudio: audio.audioBackend == "RtAudio" - - Loader { - id: studioWebLoader - anchors.top: parent.top - anchors.right: parent.right - anchors.left: parent.left - anchors.bottom: deviceControlsGroup.top - - property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" - property string studioId: virtualstudio.currentStudio.id - - source: accessToken && studioId ? "Web.qml" : "WebNull.qml" - } - - DeviceControlsGroup { - id: deviceControlsGroup - anchors.bottom: footer.top - } - - Footer { - id: footer - anchors.bottom: parent.bottom - } -} diff --git a/src/gui/CreateStudio.qml b/src/gui/CreateStudio.qml deleted file mode 100644 index 4228f51..0000000 --- a/src/gui/CreateStudio.qml +++ /dev/null @@ -1,124 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtWebEngine -import org.jacktrip.jacktrip 1.0 - -Item { - width: parent.width; height: parent.height - clip: true - - property int fontMedium: 12 - property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" - property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" - property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - - Loader { - id: webLoader - anchors.top: parent.top - anchors.right: parent.right - anchors.left: parent.left - anchors.bottom: footer.top - property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" - sourceComponent: virtualstudio.windowState === "create_studio" && accessToken ? createStudioWeb : createStudioNull - } - - Component { - id: createStudioNull - Rectangle { - anchors.fill: parent - color: backgroundColour - } - } - - Component { - id: createStudioWeb - WebEngineView { - id: webEngineView - anchors.fill: parent - settings.javascriptCanAccessClipboard: true - settings.javascriptCanPaste: true - settings.screenCaptureEnabled: true - profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}` - url: `https://${virtualstudio.apiHost}/qt/create?accessToken=${accessToken}` - - onContextMenuRequested: function(request) { - // this disables the default context menu: https://doc.qt.io/qt-6.2/qml-qtwebengine-contextmenurequest.html#accepted-prop - request.accepted = true; - } - - onNewWindowRequested: function(request) { - Qt.openUrlExternally(request.requestedUrl); - } - - onFeaturePermissionRequested: function(securityOrigin, feature) { - webEngineView.grantFeaturePermission(securityOrigin, feature, true); - } - - onRenderProcessTerminated: function(terminationStatus, exitCode) { - var status = ""; - switch (terminationStatus) { - case WebEngineView.NormalTerminationStatus: - status = "(normal exit)"; - break; - case WebEngineView.AbnormalTerminationStatus: - status = "(abnormal exit)"; - break; - case WebEngineView.CrashedTerminationStatus: - status = "(crashed)"; - break; - case WebEngineView.KilledTerminationStatus: - status = "(killed)"; - break; - } - console.log("Render process exited with code " + exitCode + " " + status); - } - } - } - - Rectangle { - id: footer - anchors.bottom: parent.bottom - width: parent.width - height: 48 - color: backgroundColour - - RowLayout { - id: layout - anchors.fill: parent - - Item { - Layout.fillHeight: true - Layout.fillWidth: true - - Button { - id: backButton - anchors.centerIn: parent - width: 180 * virtualstudio.uiScale - height: 36 * virtualstudio.uiScale - background: Rectangle { - radius: 8 * virtualstudio.uiScale - color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour) - } - onClicked: virtualstudio.windowState = "browse" - - Text { - text: "Back to Studios" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - } - } - - Rectangle { - id: backgroundBorder - width: parent.width - height: 1 - y: parent.height - footer.height - color: buttonStroke - } - } -} diff --git a/src/gui/DeviceControls.qml b/src/gui/DeviceControls.qml deleted file mode 100644 index 38e1745..0000000 --- a/src/gui/DeviceControls.qml +++ /dev/null @@ -1,197 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects - -Item { - width: parent.width - height: parent.height - clip: true - - required property bool isInput - property string muteButtonMutedColor: "#FCB6B6" - property string buttonColour: virtualstudio.darkMode ? "#494646" : "#D5D9D9" - - Component { - id: controlIndicator - - Button { - id: iconButton - width: 24 * virtualstudio.uiScale - height: 24 * virtualstudio.uiScale - - anchors.left: parent.left - anchors.leftMargin: 8 * virtualstudio.uiScale - - background: Rectangle { - color: isInput ? (audio.inputMuted ? muteButtonMutedColor : buttonColour) : "transparent" - width: 24 * virtualstudio.uiScale - radius: 4 * virtualstudio.uiScale - } - - onClicked: isInput ? audio.inputMuted = !audio.inputMuted : console.log() - - AppIcon { - id: iconImage - anchors.centerIn: parent - width: 24 * virtualstudio.uiScale - height: 24 * virtualstudio.uiScale - icon.source: isInput ? (audio.inputMuted ? "micoff.svg" : "mic.svg") : "headphones.svg" - color: isInput ? (audio.inputMuted ? "red" : ( virtualstudio.darkMode ? "#CCCCCC" : "#333333" )) : (virtualstudio.darkMode ? "#CCCCCC" : "#333333") - onClicked: isInput ? audio.inputMuted = !audio.inputMuted : console.log() - } - - ToolTip { - visible: isInput && iconButton.hovered - x: iconButton.x + iconButton.width - y: iconButton.y + iconButton.height - - contentItem: Text { - text: audio.inputMuted ? qsTr("Click to unmute yourself") : qsTr("Click to mute yourself") - font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - background: Rectangle { - color: toolTipBackgroundColour - radius: 4 - layer.enabled: true - layer.effect: Glow { - color: "#66000000" - transparentBorder: true - } - } - } - } - } - - Component { - id: inputControls - - ColumnLayout { - anchors.fill: parent - spacing: 2 * virtualstudio.uiScale - - VolumeSlider { - Layout.fillWidth: true - Layout.fillHeight: true - labelText: "Send" - tooltipText: "How loudly other participants hear you" - sliderEnabled: !audio.inputMuted - } - - DeviceWarning { - id: deviceWarning - visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) - } - } - } - - Component { - id: outputControls - - ColumnLayout { - anchors.fill: parent - spacing: 4 * virtualstudio.uiScale - - VolumeSlider { - Layout.fillWidth: true - Layout.fillHeight: true - labelText: "Studio" - tooltipText: "How loudly you hear other participants" - sliderEnabled: true - } - - VolumeSlider { - Layout.fillWidth: true - Layout.fillHeight: true - labelText: "Monitor" - tooltipText: "How loudly you hear yourself" - sliderEnabled: true - } - } - } - - ColumnLayout { - anchors.fill: parent - spacing: 5 * virtualstudio.uiScale - - Item { - Layout.topMargin: 5 * virtualstudio.uiScale - Layout.preferredHeight: 30 * virtualstudio.uiScale - Layout.fillWidth: true - - RowLayout { - anchors.fill: parent - spacing: 8 * virtualstudio.uiScale - - Item { - Layout.fillHeight: true - Layout.preferredWidth: 100 * virtualstudio.uiScale - - Loader { - id: typeIconIndicator - anchors.left: parent.left - sourceComponent: controlIndicator - } - - Text { - id: label - anchors.left: parent.left - anchors.leftMargin: 36 * virtualstudio.uiScale - - text: isInput ? "Input" : "Output" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - InfoTooltip { - content: isInput ? qsTr("Audio sent to the studio (microphone, instrument, mixer, etc.)") : qsTr("How you'll hear the studio audio") - size: 16 - anchors.left: label.right - anchors.leftMargin: 2 * virtualstudio.uiScale - anchors.verticalCenter: label.verticalCenter - } - } - - Item { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.preferredWidth: 200 * virtualstudio.uiScale - - Meter { - anchors.fill: parent - anchors.rightMargin: 8 * virtualstudio.uiScale - model: isInput ? audio.inputMeterLevels : audio.outputMeterLevels - clipped: isInput ? audio.inputClipped : audio.outputClipped - enabled: true - } - } - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.bottomMargin: 5 * virtualstudio.uiScale - - RowLayout { - anchors.fill: parent - spacing: 8 * virtualstudio.uiScale - - Item { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.leftMargin: 8 * virtualstudio.uiScale - Layout.rightMargin: 8 * virtualstudio.uiScale - - Loader { - anchors.fill: parent - anchors.top: parent.top - sourceComponent: isInput ? inputControls : outputControls - } - } - } - } - } -} diff --git a/src/gui/DeviceControlsGroup.qml b/src/gui/DeviceControlsGroup.qml deleted file mode 100644 index 43ca5fd..0000000 --- a/src/gui/DeviceControlsGroup.qml +++ /dev/null @@ -1,383 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -Rectangle { - property string disabledButtonText: "#D3D4D4" - property string saveButtonText: "#DB0A0A" - property int fullHeight: 88 * virtualstudio.uiScale - property int minimumHeight: 48 * virtualstudio.uiScale - - property bool isUsingRtAudio: audio.audioBackend == "RtAudio" - property bool isReady: virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready" - property bool showDeviceControls: getShowDeviceControls() - - id: deviceControlsGroup - width: parent.width - height: isReady ? (showDeviceControls ? fullHeight : (feedbackDetectedModal.visible ? minimumHeight : 0)) : minimumHeight; - color: backgroundColour - - function getShowDeviceControls () { - // self-managed servers do not support minified controls so keep it full size - return (!virtualstudio.currentStudio.isManaged && virtualstudio.currentStudio.sessionId === "") || (!virtualstudio.collapseDeviceControls && isReady); - } - - MouseArea { - anchors.fill: parent - propagateComposedEvents: false - } - - RowLayout { - id: layout - anchors.fill: parent - spacing: 2 - visible: !feedbackDetectedModal.visible - - Item { - Layout.fillHeight: true - Layout.fillWidth: true - visible: !isReady - - Button { - id: backButton - anchors.centerIn: parent - width: 180 * virtualstudio.uiScale - height: 36 * virtualstudio.uiScale - background: Rectangle { - radius: 8 * virtualstudio.uiScale - color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour) - } - onClicked: virtualstudio.disconnect() - - Text { - text: "Back to Studios" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - } - - Item { - Layout.fillHeight: true - Layout.fillWidth: true - visible: showDeviceControls - - DeviceControls { - isInput: true - } - } - - Item { - Layout.fillHeight: true - Layout.fillWidth: true - visible: showDeviceControls - - DeviceControls { - isInput: false - } - } - - Item { - Layout.fillHeight: true - Layout.preferredWidth: 48 * virtualstudio.uiScale - visible: showDeviceControls - - ColumnLayout { - anchors.fill: parent - spacing: 2 - - Item { - Layout.preferredHeight: 24 * virtualstudio.uiScale - Layout.preferredWidth: 24 * virtualstudio.uiScale - Layout.topMargin: 2 * virtualstudio.uiScale - Layout.rightMargin: 2 * virtualstudio.uiScale - Layout.alignment: Qt.AlignRight | Qt.AlignTop - - Button { - id: closeDeviceControlsButton - visible: virtualstudio.currentStudio.isManaged || virtualstudio.currentStudio.sessionId !== "" - width: 24 * virtualstudio.uiScale - height: 24 * virtualstudio.uiScale - background: Rectangle { - color: backgroundColour - } - anchors.top: parent.top - anchors.right: parent.right - onClicked: { - virtualstudio.collapseDeviceControls = true; - } - - AppIcon { - id: closeDeviceControlsIcon - anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } - width: 24 * virtualstudio.uiScale - height: 24 * virtualstudio.uiScale - color: closeDeviceControlsButton.hovered ? textColour : browserButtonHoverColour - icon.source: "close.svg" - onClicked: { - virtualstudio.collapseDeviceControls = true; - } - } - } - } - - Item { - visible: isUsingRtAudio - Layout.preferredWidth: 40 * virtualstudio.uiScale - Layout.preferredHeight: 64 * virtualstudio.uiScale - Layout.bottomMargin: 5 * virtualstudio.uiScale - Layout.topMargin: 2 * virtualstudio.uiScale - Layout.rightMargin: 2 * virtualstudio.uiScale - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - - Button { - id: changeDevicesButton - width: 36 * virtualstudio.uiScale - height: 36 * virtualstudio.uiScale - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - background: Rectangle { - radius: 8 * virtualstudio.uiScale - color: changeDevicesButton.down ? browserButtonPressedColour : (changeDevicesButton.hovered ? browserButtonHoverColour : browserButtonColour) - } - onClicked: { - virtualstudio.windowState = "change_devices" - if (!audio.deviceModelsInitialized) { - audio.refreshDevices(); - } - } - - AppIcon { - id: changeDevicesIcon - anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } - width: 20 * virtualstudio.uiScale - height: 20 * virtualstudio.uiScale - icon.source: "cog.svg" - onClicked: { - virtualstudio.windowState = "change_devices" - if (!audio.deviceModelsInitialized) { - audio.refreshDevices(); - } - } - } - } - - Text { - anchors.top: changeDevicesButton.bottom - text: "Devices" - font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale} - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - } - } - } - - Rectangle { - id: backgroundBorder - width: parent.width - height: 1 - anchors.top: layout.top - color: strokeColor - } - - Popup { - id: feedbackDetectedModal - padding: 1 - width: parent.width - height: parent.height - anchors.centerIn: parent - dim: false - modal: false - focus: true - closePolicy: Popup.NoAutoClose - - background: Rectangle { - anchors.fill: parent - color: "transparent" - border.width: 1 - border.color: buttonStroke - clip: true - } - - contentItem: Rectangle { - width: parent.width - height: 232 * virtualstudio.uiScale - color: backgroundColour - - Item { - id: feedbackDetectedContent - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.leftMargin: 16 * virtualstudio.uiScale - anchors.right: parent.right - - AppIcon { - id: feedbackWarningIcon - anchors.left: parent.left - anchors.top: parent.top - anchors.topMargin: 10 * virtualstudio.uiScale - width: 32 * virtualstudio.uiScale - height: 32 * virtualstudio.uiScale - icon.source: "warning.svg" - color: "#F21B1B" - visible: showDeviceControls - } - - AppIcon { - id: feedbackWarningIconMinified - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - height: 24 * virtualstudio.uiScale - width: 24 * virtualstudio.uiScale - icon.source: "warning.svg" - color: "#F21B1B" - visible: !showDeviceControls - } - - Text { - id: feedbackDetectedHeader - anchors.top: parent.top - anchors.topMargin: 10 * virtualstudio.uiScale - anchors.left: feedbackWarningIcon.right - anchors.leftMargin: 16 * virtualstudio.uiScale - width: parent.width - text: "Audio feedback detected!" - font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale; bold: true } - color: textColour - elide: Text.ElideRight - wrapMode: Text.WordWrap - visible: showDeviceControls - } - - Text { - id: feedbackDetectedText - anchors.top: feedbackDetectedHeader.bottom - anchors.topMargin: 4 * virtualstudio.uiScale - anchors.left: feedbackWarningIcon.right - anchors.leftMargin: 16 * virtualstudio.uiScale - width: parent.width - text: "JackTrip detected a feedback loop. Your monitor and input volume have automatically been disabled." - font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - elide: Text.ElideRight - wrapMode: Text.WordWrap - visible: showDeviceControls - } - - Text { - id: feedbackDetectedTextMinified - anchors.verticalCenter: parent.verticalCenter - anchors.left: feedbackWarningIcon.right - anchors.leftMargin: 16 * virtualstudio.uiScale - width: parent.width - text: "JackTrip detected a feedback loop. Your monitor and input volume have automatically been disabled." - font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - elide: Text.ElideRight - wrapMode: Text.WordWrap - visible: !showDeviceControls - } - - Text { - id: feedbackDetectedText2 - anchors.top: feedbackDetectedText.bottom - anchors.topMargin: 2 * virtualstudio.uiScale - anchors.left: feedbackWarningIcon.right - anchors.leftMargin: 16 * virtualstudio.uiScale - width: parent.width - text: "You can disable this behavior under Settings > Advanced" - textFormat: Text.RichText - font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - elide: Text.ElideRight - wrapMode: Text.WordWrap - visible: showDeviceControls - } - - Button { - id: closeFeedbackDetectedModalButton - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - width: 128 * virtualstudio.uiScale; - height: 30 * virtualstudio.uiScale - onClicked: feedbackDetectedModal.close() - - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour) - border.width: 1 - border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke) - } - - Text { - text: "Ok" - font.family: "Poppins" - font.pixelSize: showDeviceControls ? fontSmall * virtualstudio.fontScale * virtualstudio.uiScale : fontTiny * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - visible: showDeviceControls - } - - Button { - id: closeFeedbackDetectedModalButtonMinified - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - width: 80 * virtualstudio.uiScale - height: 24 * virtualstudio.uiScale - onClicked: feedbackDetectedModal.close() - - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour) - border.width: 1 - border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke) - } - - Text { - text: "Ok" - font.family: "Poppins" - font.pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - visible: !showDeviceControls - } - } - } - } - - Connections { - target: virtualstudio - - function onFeedbackDetected() { - if (virtualstudio.windowState === "connected") { - feedbackDetectedModal.visible = true; - } - } - - function onCollapseDeviceControlsChanged(collapseDeviceControls) { - showDeviceControls = getShowDeviceControls() - } - - function onCurrentStudioChanged(currentStudio) { - isReady = virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready" - showDeviceControls = getShowDeviceControls() - } - - function onConnectionStateChanged(connectionState) { - isReady = virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready" - showDeviceControls = getShowDeviceControls() - } - } -} \ No newline at end of file diff --git a/src/gui/DeviceRefreshButton.qml b/src/gui/DeviceRefreshButton.qml deleted file mode 100644 index 1c3ed65..0000000 --- a/src/gui/DeviceRefreshButton.qml +++ /dev/null @@ -1,38 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Button { - id: refreshButton - text: "Refresh Devices" - - property int fontExtraSmall: 8 - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" - property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" - property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" - property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - property var onDeviceRefresh: function () { audio.refreshDevices(); }; - - width: 144 * virtualstudio.uiScale; - height: 30 * virtualstudio.uiScale - palette.buttonText: textColour - display: AbstractButton.TextBesideIcon - - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke) - } - icon { - source: "refresh.svg"; - color: textColour; - } - onClicked: { onDeviceRefresh(); } - font { - family: "Poppins" - pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale - } -} diff --git a/src/gui/DeviceWarning.qml b/src/gui/DeviceWarning.qml deleted file mode 100644 index 4b8302a..0000000 --- a/src/gui/DeviceWarning.qml +++ /dev/null @@ -1,58 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - height: 28 * virtualstudio.uiScale - property string devicesWarningColour: "#F21B1B" - - AppIcon { - id: devicesWarningIcon - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: parent.height - height: parent.height - icon.source: "warning.svg" - color: devicesWarningColour - visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) - } - - Text { - id: warningOrErrorText - text: Boolean(audio.devicesError) ? "Audio Configuration Error" : "Audio Configuration Warning" - anchors.left: devicesWarningIcon.right - anchors.leftMargin: 4 * virtualstudio.uiScale - anchors.verticalCenter: devicesWarningIcon.verticalCenter - visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) - font { family: "Poppins"; pixelSize: 9 * virtualstudio.fontScale * virtualstudio.uiScale } - color: devicesWarningColour - } - - InfoTooltip { - id: devicesWarningTooltip - anchors.left: warningOrErrorText.right - anchors.leftMargin: 2 * virtualstudio.uiScale - anchors.top: devicesWarningIcon.top - content: qsTr(audio.devicesError || audio.devicesWarning) - iconColor: devicesWarningColour - size: 16 * virtualstudio.uiScale - visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) - } - - MouseArea { - id: devicesWarningToolTipArea - anchors.top: devicesWarningIcon.top - anchors.bottom: devicesWarningIcon.bottom - anchors.left: devicesWarningIcon.left - anchors.right: devicesWarningTooltip.right - hoverEnabled: true - onEntered: devicesWarningTooltip.showToolTip = true - onExited: devicesWarningTooltip.showToolTip = false - onClicked: { - if (Boolean(audio.devicesError) && audio.devicesErrorHelpUrl !== "") { - virtualstudio.openLink(audio.devicesErrorHelpUrl); - } else if (Boolean(audio.devicesWarning) && audio.devicesWarningHelpUrl !== "") { - virtualstudio.openLink(audio.devicesWarningHelpUrl); - } - } - } -} diff --git a/src/gui/Failed.qml b/src/gui/Failed.qml deleted file mode 100644 index 956406d..0000000 --- a/src/gui/Failed.qml +++ /dev/null @@ -1,79 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - width: parent.width; height: parent.height - clip: true - - property int leftMargin: 16 - property int fontBig: 28 - property int fontMedium: 18 - property int fontSmall: 11 - - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1" - property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5" - property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5" - property string buttonStroke: virtualstudio.darkMode ? "#9C9C9C" : "#A4A7A7" - property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A" - property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A" - property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A" - - AppIcon { - id: ohnoImage - y: 60 - anchors.horizontalCenter: parent.horizontalCenter - width: 180 - height: 180 - icon.source: "sentiment_very_dissatisfied.svg" - } - - Text { - id: ohnoHeader - text: "Oh no!" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: ohnoImage.bottom - anchors.topMargin: 16 * virtualstudio.uiScale - } - - Text { - id: ohnoMessage - text: virtualstudio.failedMessage || "Unable to process request - please try again later." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 400 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: ohnoHeader.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Button { - id: backButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour) - border.width: backButton.down ? 1 : 0 - border.color: buttonStroke - layer.enabled: !backButton.down - } - onClicked: { virtualstudio.windowState = "browse" } - width: 256 * virtualstudio.uiScale - height: 42 * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: ohnoMessage.bottom - anchors.topMargin: 60 * virtualstudio.uiScale - Text { - text: "Back" - font.family: "Poppins" - font.pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - color: backButton.down ? buttonTextPressed : (backButton.hovered ? buttonTextHover : buttonTextColour) - } - visible: true - } -} diff --git a/src/gui/FeedbackSurvey.qml b/src/gui/FeedbackSurvey.qml deleted file mode 100644 index 6c1528d..0000000 --- a/src/gui/FeedbackSurvey.qml +++ /dev/null @@ -1,421 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - id: userFeedbackSurvey - - anchors.centerIn: parent - width: 480 * virtualstudio.uiScale - height: 232 * virtualstudio.uiScale - - property int leftHeaderMargin: 16 - property int fontBig: 28 - property int fontMedium: 12 - property int fontSmall: 10 - property int fontTiny: 8 - property int bodyMargin: 60 - property int rightMargin: 16 - property int bottomToolTipMargin: 8 - property int rightToolTipMargin: 4 - - property string buttonColour: "#F2F3F3" - property string buttonHoverColour: "#E7E8E8" - property string buttonPressedColour: "#E7E8E8" - property string buttonStroke: "#EAEBEB" - property string buttonHoverStroke: "#B0B5B5" - property string buttonPressedStroke: "#B0B5B5" - - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string textAreaTextColour: virtualstudio.darkMode ? "#A6A6A6" : "#757575" - property string textAreaColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - - property string serverId: "" - - property int rating: 0 - property int hover: star1MouseArea.containsMouse ? 1 : star2MouseArea.containsMouse ? 2 : star3MouseArea.containsMouse ? 3 : star4MouseArea.containsMouse ? 4 : star5MouseArea.containsMouse ? 5 : 0 - property int currentView: (hover > 0 ? hover : rating) - property bool submitted: false; - - property string message: "" - - Popup { - id: userFeedbackModal - padding: 1 - width: parent.width - height: 300 * virtualstudio.uiScale - anchors.centerIn: parent - modal: true - focus: true - closePolicy: Popup.NoAutoClose - - background: Rectangle { - anchors.fill: parent - color: "transparent" - radius: 6 * virtualstudio.uiScale - border.width: 1 - border.color: buttonStroke - clip: true - } - - contentItem: Rectangle { - width: parent.width - height: parent.height - color: backgroundColour - radius: 6 * virtualstudio.uiScale - - Item { - id: userFeedbackSurveyContent - anchors.top: parent.top - anchors.topMargin: 24 * virtualstudio.uiScale - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - visible: !submitted - - Text { - id: userFeedbackSurveyHeader - anchors.top: parent.top - anchors.topMargin: 16 * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - text: "How did your session go?" - font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true } - horizontalAlignment: Text.AlignHCenter - color: textColour - elide: Text.ElideRight - wrapMode: Text.WordWrap - } - - Text { - id: ratingItemInstructions - anchors.top: userFeedbackSurveyHeader.bottom - anchors.topMargin: 12 * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - text: "Rate your session on a scale of 1 to 5" - font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - horizontalAlignment: Text.AlignHCenter - color: textColour - elide: Text.ElideRight - wrapMode: Text.WordWrap - } - - Item { - id: ratingItem - anchors.top: ratingItemInstructions.bottom - anchors.topMargin: 4 * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - height: 40 * virtualstudio.uiScale - width: 200 * virtualstudio.uiScale - - Item { - id: star1Container - anchors.verticalCenter: parent.verticalCenter - anchors.right: star2Container.left - width: parent.width / 5 - height: parent.height - - AppIcon { - id: star1Icon - anchors.centerIn: parent - width: currentView >= 1 ? parent.width : 20 * virtualstudio.uiScale - height: currentView >= 1 ? parent.height : 20 * virtualstudio.uiScale - icon.source: "star.svg" - color: currentView >= 1 ? "#faaf00" : "#606060" - } - - MouseArea { - id: star1MouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: () => { - if (rating === 1) { - rating = 0; - } else { - rating = 1; - } - } - } - } - - Item { - id: star2Container - anchors.verticalCenter: parent.verticalCenter - anchors.right: star3Container.left - width: parent.width / 5 - height: parent.height - - AppIcon { - id: star2Icon - anchors.centerIn: parent - width: currentView >= 2 ? parent.width : 20 * virtualstudio.uiScale - height: currentView >= 2 ? parent.height : 20 * virtualstudio.uiScale - icon.source: "star.svg" - color: currentView >= 2 ? "#faaf00" : "#606060" - } - - MouseArea { - id: star2MouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: () => { - if (rating === 2) { - rating = 0; - } else { - rating = 2; - } - } - } - } - - Item { - id: star3Container - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width / 5 - height: parent.height - - AppIcon { - id: star3Icon - anchors.centerIn: parent - width: currentView >= 3 ? parent.width : 20 * virtualstudio.uiScale - height: currentView >= 3 ? parent.height : 20 * virtualstudio.uiScale - icon.source: "star.svg" - color: currentView >= 3 ? "#faaf00" : "#606060" - } - - MouseArea { - id: star3MouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: () => { - if (rating === 3) { - rating = 0; - } else { - rating = 3; - } - } - } - } - - Item { - id: star4Container - anchors.verticalCenter: parent.verticalCenter - anchors.left: star3Container.right - width: parent.width / 5 - height: parent.height - - AppIcon { - id: star4Icon - anchors.centerIn: parent - width: currentView >= 4 ? parent.width : 20 * virtualstudio.uiScale - height: currentView >= 4 ? parent.height : 20 * virtualstudio.uiScale - icon.source: "star.svg" - color: currentView >= 4 ? "#faaf00" : "#606060" - } - - MouseArea { - id: star4MouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: () => { - if (rating === 4) { - rating = 0; - } else { - rating = 4; - } - } - } - } - - Item { - id: star5Container - anchors.verticalCenter: parent.verticalCenter - anchors.left: star4Container.right - width: parent.width / 5 - height: parent.height - - AppIcon { - id: star5Icon - anchors.centerIn: parent - width: currentView >= 5 ? parent.width : 20 * virtualstudio.uiScale - height: currentView >= 5 ? parent.height : 20 * virtualstudio.uiScale - icon.source: "star.svg" - color: currentView >= 5 ? "#faaf00" : "#606060" - } - - MouseArea { - id: star5MouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: () => { - if (rating === 5) { - rating = 0; - } else { - rating = 5; - } - } - } - } - } - - ScrollView { - id: messageBoxScrollArea - anchors.left: parent.left - anchors.leftMargin: 32 * virtualstudio.uiScale - anchors.right: parent.right - anchors.rightMargin: 32 * virtualstudio.uiScale - anchors.top: ratingItem.bottom - anchors.topMargin: 12 * virtualstudio.uiScale - height: 64 * virtualstudio.uiScale - - TextArea { - id: messageBox - placeholderText: qsTr("(Optional) Let us know how we can improve your experience.") - placeholderTextColor: textAreaTextColour - color: textColour - background: Rectangle { - color: textAreaColour - radius: 6 * virtualstudio.uiScale - border.width: 1 - border.color: buttonStroke - } - } - } - - Item { - id: buttonsArea - height: 32 * virtualstudio.uiScale - width: 324 * virtualstudio.uiScale - anchors.horizontalCenter: messageBoxScrollArea.horizontalCenter - anchors.top: messageBoxScrollArea.bottom - anchors.topMargin: 24 * virtualstudio.uiScale - - Button { - id: userFeedbackButton - anchors.right: buttonsArea.right - anchors.horizontalCenter: buttonsArea.horizontalCenter - anchors.verticalCenter: parent.buttonsArea - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - onClicked: () => { - if (rating === 0 && messageBox.text === "") { - userFeedbackModal.close(); - serverId = ""; - messageBox.clear(); - return; - } - virtualstudio.collectFeedbackSurvey(serverId, rating, messageBox.text); - submitted = true; - rating = 0; - serverId = ""; - messageBox.clear(); - userFeedbackModal.height = 150 * virtualstudio.uiScale - submittedFeedbackTimer.start(); - } - - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: userFeedbackButton.down ? buttonPressedColour : (userFeedbackButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: userFeedbackButton.down ? buttonPressedStroke : (userFeedbackButton.hovered ? buttonHoverStroke : buttonStroke) - } - - Text { - text: (rating === 0 && messageBox.text === "") ? "Dismiss" : "Submit" - font.family: "Poppins" - font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: "#DB0A0A" - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - - Timer { - id: submittedFeedbackTimer - interval: 5000; running: false; repeat: false - onTriggered: () => { - submitted = false; - userFeedbackModal.height = 300 * virtualstudio.uiScale - userFeedbackModal.close(); - } - } - } - } - - Item { - id: submittedFeedbackContent - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - visible: submitted - - Text { - id: submittedFeedbackHeader - anchors.top: submittedFeedbackContent.top - anchors.topMargin: 24 * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - text: "Thank you!" - font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true } - horizontalAlignment: Text.AlignHCenter - color: textColour - elide: Text.ElideRight - wrapMode: Text.WordWrap - } - - Text { - id: submittedFeedbackText - anchors.top: submittedFeedbackHeader.bottom - anchors.topMargin: 16 * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - text: "Your feedback has been recorded." - font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - horizontalAlignment: Text.AlignHCenter - color: textColour - elide: Text.ElideRight - wrapMode: Text.WordWrap - } - - Button { - id: closeButtonFeedback - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: submittedFeedbackText.bottom - anchors.topMargin: 16 * virtualstudio.uiScale - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - onClicked: () => { - submitted = false; - userFeedbackModal.height = 300 * virtualstudio.uiScale - userFeedbackModal.close(); - } - - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: closeButtonFeedback.down ? buttonPressedColour : (closeButtonFeedback.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: closeButtonFeedback.down ? buttonPressedStroke : (closeButtonFeedback.hovered ? buttonHoverStroke : buttonStroke) - } - - Text { - text: "Close" - font.family: "Poppins" - font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - } - } - } - - Connections { - target: virtualstudio - - function onOpenFeedbackSurveyModal(serverId) { - userFeedbackSurvey.serverId = serverId; - userFeedbackModal.open(); - } - } -} diff --git a/src/gui/FirstLaunch.qml b/src/gui/FirstLaunch.qml deleted file mode 100644 index 70d8f1c..0000000 --- a/src/gui/FirstLaunch.qml +++ /dev/null @@ -1,128 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - width: parent.width; height: parent.height - clip: true - - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1" - property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1" - property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5" - property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5" - property string buttonStroke: virtualstudio.darkMode ? "#636060" : "#DEDFDF" - property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" - property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" - - Image { - id: jtlogo - source: "logo.svg" - anchors.horizontalCenter: parent.horizontalCenter - y: 35 * virtualstudio.uiScale - width: 50 * virtualstudio.uiScale; height: 92 * virtualstudio.uiScale - sourceSize: Qt.size(jtlogo.width,jtlogo.height) - fillMode: Image.PreserveAspectFit - smooth: true - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - y: 168 * virtualstudio.uiScale - text: "Sign in with a Virtual Studio account?" - font.family: "Poppins" - font.pixelSize: 17 * virtualstudio.fontScale * virtualstudio.uiScale - color: textColour - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - y: 219 * virtualstudio.uiScale - text: "You'll be able to change your mind later" - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - color: textColour - } - - Button { - id: vsButton - background: Rectangle { - radius: 10 * virtualstudio.uiScale - color: vsButton.down ? buttonPressedColour : (vsButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: vsButton.down ? buttonPressedStroke : (vsButton.hovered ? buttonHoverStroke : buttonStroke) - layer.enabled: vsButton.hovered && !vsButton.down - } - onClicked: { virtualstudio.showFirstRun = false; virtualstudio.windowState = "login"; virtualstudio.toVirtualStudio(); } - x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale - width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale - Text { - text: "Yes" - font.family: "Poppins" - font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: "#DB0A0A" - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - Text { - text: "• Seamless Audio & Video
• Recording & Livestreaming
• No Servers Required" - textFormat: Text.StyledText - font.family: "Poppins" - font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale - x: parent.width / 2 - (265 * virtualstudio.uiScale); - y: 355 * virtualstudio.uiScale; - width: 230 * virtualstudio.uiScale - padding: 0 - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - color: textColour - } - Image { - source: "JTVS.png" - x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale - width: 234 * virtualstudio.uiScale; height: 195 * virtualstudio.uiScale; - } - - Button { - id: standardButton - background: Rectangle { - radius: 10 * virtualstudio.uiScale - color: standardButton.down ? buttonPressedColour : (standardButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: standardButton.down ? buttonPressedStroke : (standardButton.hovered ? buttonHoverStroke : buttonStroke) - layer.enabled: standardButton.hovered && !standardButton.down - } - onClicked: { virtualstudio.toStandard(); } - x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale - width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale - Text { - text: "No" - font.family: "Poppins" - font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: "#DB0A0A" - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - Image { - source: "JTOriginal.png" - x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale - width: 234 * virtualstudio.uiScale; height: 337.37 * virtualstudio.uiScale; - } - Text { - text: virtualstudio.psiBuild ? "• Connect via IP address
• Run a local hub server
• The Standard JackTrip experience" : - "• Connect via IP address
• Run a local hub server
• The Classic JackTrip experience" - textFormat: Text.StyledText - font.family: "Poppins" - font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale - x: parent.width / 2 + (32 * virtualstudio.uiScale); - y: 355 * virtualstudio.uiScale; - width: 230 * virtualstudio.uiScale - padding: 0 - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - color: textColour - } -} diff --git a/src/gui/Footer.qml b/src/gui/Footer.qml deleted file mode 100644 index 18f8292..0000000 --- a/src/gui/Footer.qml +++ /dev/null @@ -1,180 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -Rectangle { - id: footer - width: parent.width - height: 24 - anchors.bottom: parent.bottom - color: backgroundColour - clip: true - - property string statsOrange: "#b26a00" - property string connectionStateColor: getConnectionStateColor() - property variant networkStatsText: getNetworkStatsText() - - function getConnectionStateColor() { - if (virtualstudio.connectionState == "Connected") { - return meterGreen - } - if (virtualstudio.connectionState.includes("Disconnected") || virtualstudio.connectionState.includes("Error")) { - return meterRed - } - if (studioStatus === "Starting" || virtualstudio.connectionState == "Connecting..." || virtualstudio.connectionState == "Reconnecting...") { - return meterYellow - } - return "grey" - } - - function getNetworkStatsText() { - let minRtt = virtualstudio.networkStats.minRtt; - let maxRtt = virtualstudio.networkStats.maxRtt; - let avgRtt = virtualstudio.networkStats.avgRtt; - - let texts = ["Unstable", "Please plug into Ethernet & turn off WIFI.", meterRed]; - if (virtualstudio.networkOutage) { - return texts; - } - - texts = ["Measuring...", "", "grey"]; - if (!minRtt || !maxRtt) { - return texts; - } - - texts[1] = "" + minRtt + " ms - " + maxRtt + " ms, avg " + avgRtt + " ms"; - let quality = "Poor"; - let color = meterRed; - if (avgRtt < 10 && maxRtt < 15) { - quality = "Excellent"; - color = meterGreen; - } else if (avgRtt < 20 && maxRtt < 30) { - quality = "Good"; - color = meterYellow; - } else if (avgRtt < 30 && maxRtt < 40) { - quality = "Fair"; - color = statsOrange; - } - - texts[0] = quality - texts[2] = color; - return texts; - } - - MouseArea { - anchors.fill: parent - propagateComposedEvents: false - } - - RowLayout { - id: layout - anchors.fill: parent - spacing: 4 - - Rectangle { - color: backgroundColour - Layout.minimumWidth: 256 - Layout.preferredWidth: 512 - Layout.maximumWidth: 640 - Layout.fillHeight: true - Layout.fillWidth: true - visible: studioStatus === "Ready" - - AppIcon { - id: connectionQualityIcon - anchors.left: parent.left - anchors.leftMargin: 8 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - width: 20 * virtualstudio.uiScale - height: 20 * virtualstudio.uiScale - icon.source: "speed.svg" - } - - Text { - id: connectionQualityText - anchors.left: connectionQualityIcon.right - anchors.leftMargin: 4 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - text: "Connection:" - font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - Text { - id: connectionQualityName - anchors.left: connectionQualityText.right - anchors.leftMargin: 2 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - text: networkStatsText[0] - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } - color: networkStatsText[2] - } - - Text { - id: connectionQualityTime - anchors.left: connectionQualityName.right - anchors.leftMargin: 8 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - text: networkStatsText[1] - font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - } - - Rectangle { - color: backgroundColour - Layout.minimumWidth: 96 - Layout.preferredWidth: 128 - Layout.maximumWidth: 160 - Layout.fillHeight: true - - Rectangle { - id: connectionStatusDot - anchors.right: connectionStatusText.left - anchors.rightMargin: 4 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - width: 12 - height: connectionStatusDot.width - radius: connectionStatusDot.height / 2 - color: connectionStateColor - } - - Text { - id: connectionStatusText - anchors.right: parent.right - anchors.rightMargin: 8 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - text: studioStatus === "Starting" ? "Starting..." : virtualstudio.connectionState - font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - } - } - - Rectangle { - id: backgroundBorder - width: parent.width - height: 1 - y: parent.height - footer.height - color: buttonStroke - } - - Connections { - target: virtualstudio - - function onConnectionStateChanged() { - connectionStatusDot.color = getConnectionStateColor() - } - function onNetworkStatsChanged() { - networkStatsText = getNetworkStatsText(); - } - function onUpdatedNetworkOutage() { - networkStatsText = getNetworkStatsText(); - } - } -} diff --git a/src/gui/InfoTooltip.qml b/src/gui/InfoTooltip.qml deleted file mode 100644 index 5d5c95e..0000000 --- a/src/gui/InfoTooltip.qml +++ /dev/null @@ -1,63 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects - -Item { - required property string content - required property int size - - width: size * virtualstudio.uiScale - height: size * virtualstudio.uiScale - - property string iconSource: "help.svg" - property string iconColor: "" - property string backgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" - property bool showToolTip: false - - Item { - anchors.fill: parent - - AppIcon { - id: tooltipIcon - anchors.centerIn: parent - width: parent.width - height: parent.height - icon.source: iconSource - color: iconColor - } - - MouseArea { - id: mouseArea - anchors.fill: tooltipIcon - hoverEnabled: true - onEntered: showToolTip = true - onExited: showToolTip = false - } - - ToolTip { - visible: showToolTip - x: tooltipIcon.x + tooltipIcon.width - y: tooltipIcon.y + tooltipIcon.height - - contentItem: Text { - text: content - font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - elide: Text.ElideRight - wrapMode: Text.WordWrap - } - - background: Rectangle { - color: backgroundColour - radius: 4 - layer.enabled: true - layer.effect: Glow { - radius: 8 - color: "#66000000" - transparentBorder: true - } - } - } - } -} diff --git a/src/gui/JTOriginal.png b/src/gui/JTOriginal.png deleted file mode 100644 index a6c92cd..0000000 Binary files a/src/gui/JTOriginal.png and /dev/null differ diff --git a/src/gui/JTVS.png b/src/gui/JTVS.png deleted file mode 100644 index 05573f2..0000000 Binary files a/src/gui/JTVS.png and /dev/null differ diff --git a/src/gui/LearnMoreButton.qml b/src/gui/LearnMoreButton.qml deleted file mode 100644 index aa26177..0000000 --- a/src/gui/LearnMoreButton.qml +++ /dev/null @@ -1,30 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Button { - property string url - property string buttonText: "Learn more" - - width: 150 * virtualstudio.uiScale; - height: 30 * virtualstudio.uiScale - - onClicked: { - virtualstudio.openLink(url); - } - - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: parent.down ? buttonPressedColour : (parent.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: parent.down ? buttonPressedStroke : (parent.hovered ? buttonHoverStroke : buttonStroke) - } - - Text { - text: buttonText - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } -} diff --git a/src/gui/Login.qml b/src/gui/Login.qml deleted file mode 100644 index d52400b..0000000 --- a/src/gui/Login.qml +++ /dev/null @@ -1,328 +0,0 @@ -import QtQuick -import QtQuick.Controls -import VS 1.0 - -Item { - width: parent.width; height: parent.height - clip: true - - state: auth.authenticationStage - states: [ - State { - name: "unauthenticated" - }, - State { - name: "refreshing" - }, - State { - name: "polling" - }, - State { - name: "success" - }, - State { - name: "failed" - } - ] - - Rectangle { - width: parent.width; height: parent.height - color: backgroundColour - } - - property bool codeCopied: false - property int numFailures: 0; - - property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1" - property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5" - property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5" - property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" - property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" - property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A" - property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A" - property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A" - property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1" - property string linkTextColour: virtualstudio.darkMode ? "#8B8D8D" : "#272525" - property string toolTipTextColour: codeCopied ? "#FAFBFB" : textColour - property string toolTipBackgroundColour: codeCopied ? "#57B147" : (virtualstudio.darkMode ? "#323232" : "#F3F3F3") - property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - property string disabledButtonText: "#D3D4D4" - property string errorTextColour: "#DB0A0A" - - property bool showCodeFlow: (loginScreen.state === "unauthenticated" && !auth.attemptingRefreshToken) || (loginScreen.state === "polling" || loginScreen.state === "failed" || (loginScreen.state === "success" && auth.authenticationMethod === "code flow")) - property bool showLoading: (loginScreen.state === "unauthenticated" ** auth.attemptingRefreshToken) || loginScreen.state === "refreshing" || (loginScreen.state === "success" && auth.authenticationMethod === "refresh token") - - Clipboard { - id: clipboard - } - - Item { - id: loginScreenHeader - anchors.horizontalCenter: parent.horizontalCenter - y: showCodeFlow ? 48 * virtualstudio.uiScale : 144 * virtualstudio.uiScale - - Image { - id: loginLogo - source: "logo.svg" - x: parent.width / 2 - (150 * virtualstudio.uiScale); - width: 42 * virtualstudio.uiScale; height: 76 * virtualstudio.uiScale - sourceSize: Qt.size(loginLogo.width,loginLogo.height) - fillMode: Image.PreserveAspectFit - smooth: true - } - - Image { - source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png" - anchors.bottom: loginLogo.bottom - x: parent.width / 2 - (88 * virtualstudio.uiScale) - width: 238 * virtualstudio.uiScale; height: 56 * virtualstudio.uiScale - } - - Text { - text: "Virtual Studio" - font.family: "Poppins" - font.pixelSize: 24 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - y: 80 * virtualstudio.uiScale - color: textColour - } - } - - Item { - id: codeFlow - anchors.horizontalCenter: parent.horizontalCenter - y: 68 * virtualstudio.uiScale - height: parent.height - codeFlow.y - visible: showCodeFlow - width: parent.width - - Text { - id: deviceVerificationExplanation - text: `To get started, please sign in and confirm the following code using your web browser. Return here when you are done.` - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - y: 128 * virtualstudio.uiScale - width: 500 * virtualstudio.uiScale; - visible: true - color: textColour - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - textFormat: Text.RichText - onLinkActivated: link => { - if (!Boolean(auth.verificationCode)) { - return; - } - virtualstudio.openLink(link) - } - } - - AppIcon { - id: successIcon - y: 224 * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - width: 96 * virtualstudio.uiScale - height: 96 * virtualstudio.uiScale - icon.source: "check.svg" - color: "green" - visible: loginScreen.state === "success" - } - - Text { - id: deviceVerificationCode - text: auth.verificationCode || ((numFailures >= 5) ? "Error" : "Loading..."); - font.family: "Poppins" - font.pixelSize: 20 * virtualstudio.fontScale * virtualstudio.uiScale - font.letterSpacing: Boolean(auth.verificationCode) ? 8 : 1 - anchors.horizontalCenter: parent.horizontalCenter - y: 196 * virtualstudio.uiScale - width: 360 * virtualstudio.uiScale; - visible: !auth.isAuthenticated - color: Boolean(auth.verificationCode) ? textColour : disabledButtonText - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - - Timer { - id: copiedResetTimer - interval: 2000; running: false; repeat: false - onTriggered: codeCopied = false; - } - - MouseArea { - id: deviceVerificationCodeMouseArea - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - enabled: Boolean(auth.verificationCode) - hoverEnabled: true - onClicked: () => { - codeCopied = true; - clipboard.setText(auth.verificationCode); - copiedResetTimer.restart() - } - } - - ToolTip { - parent: deviceVerificationCode - visible: loginScreen.state === "polling" && deviceVerificationCodeMouseArea.containsMouse - delay: 100 - contentItem: Rectangle { - color: toolTipBackgroundColour - radius: 3 - anchors.fill: parent - layer.enabled: true - border.width: 1 - border.color: tooltipStroke - - Text { - anchors.centerIn: parent - font { family: "Poppins"; pixelSize: 8 * virtualstudio.fontScale * virtualstudio.uiScale} - text: codeCopied ? qsTr("📋 Copied code to clipboard") : qsTr("📋 Copy code to Clipboard") - color: toolTipTextColour - } - } - background: Rectangle { - color: "transparent" - } - } - } - - Button { - id: loginButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: loginButton.down ? buttonPressedColour : (loginButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: loginButton.down ? buttonPressedStroke : (loginButton.hovered ? buttonHoverStroke : buttonStroke) - layer.enabled: !loginButton.down - } - onClicked: { - if (auth.verificationCode && auth.verificationUrl) { - virtualstudio.openLink(auth.verificationUrl); - } - } - anchors.horizontalCenter: parent.horizontalCenter - y: 260 * virtualstudio.uiScale - width: 263 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale - Text { - text: "Sign In" - font.family: "Poppins" - font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - color: loginButton.down ? buttonTextPressed : (loginButton.hovered ? buttonTextHover : buttonTextColour) - } - visible: !auth.isAuthenticated - } - - Text { - id: authFailedText - text: "There was an error trying to sign in. Please try again." - font.family: "Poppins" - font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: loginScreenFooter.top - anchors.bottomMargin: 16 * virtualstudio.uiScale - visible: (loginScreen.state === "failed" || numFailures > 0) && loginScreen.state !== "success" - color: errorTextColour - } - - Item { - id: loginScreenFooter - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: 24 * virtualstudio.uiScale - width: parent.width - height: 48 * virtualstudio.uiScale - - property bool showBackButton: !virtualstudio.vsFtux - - Item { - id: backButton - visible: parent.showBackButton - anchors.verticalCenter: parent.verticalCenter - x: (parent.x + parent.width / 2) - backButton.width - 8 * virtualstudio.uiScale - width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale - Text { - text: "Back" - font.family: "Poppins" - font.underline: true - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - color: textColour - } - MouseArea { - anchors.fill: parent - onClicked: () => { if (!auth.isAuthenticated) { virtualstudio.windowState = "start"; } } - cursorShape: Qt.PointingHandCursor - } - } - - Item { - id: resetCodeButton - visible: true - x: parent.showBackButton ? (parent.x + parent.width / 2) + 8 * virtualstudio.uiScale : (parent.x + parent.width / 2) - resetCodeButton.width / 2 - anchors.verticalCenter: parent.verticalCenter - width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale - Text { - text: "Reset Code" - font.family: "Poppins" - font.underline: true - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - color: textColour - } - MouseArea { - anchors.fill: parent - onClicked: () => { - if (auth.verificationCode && auth.verificationUrl) { - auth.resetCode(); - } - } - cursorShape: Qt.PointingHandCursor - } - } - } - } - - Item { - id: refreshToken - anchors.horizontalCenter: parent.horizontalCenter - y: 108 * virtualstudio.uiScale - visible: showLoading - - Text { - id: loadingViaRefreshToken - text: "Logging In..."; - font.family: "Poppins" - font.pixelSize: 20 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - y: 208 * virtualstudio.uiScale - width: 360 * virtualstudio.uiScale; - color: textColour - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - } - } - - Connections { - target: auth - function onUpdatedAuthenticationStage (stage) { - loginScreen.state = stage; - if (stage === "failed") { - numFailures = numFailures + 1; - if (numFailures < 5 && !virtualstudio.hasRefreshToken) { - virtualstudio.login(); - } - } - if (stage === "success") { - numFailures = 0; - } - } - } -} diff --git a/src/gui/Meter.qml b/src/gui/Meter.qml deleted file mode 100644 index 292cef9..0000000 --- a/src/gui/Meter.qml +++ /dev/null @@ -1,53 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - required property var model - property int bins: Math.max(15, width/20) - property int innerMargin: 2 * virtualstudio.uiScale - property int boxRadius: 3 * virtualstudio.uiScale - property int boxThickness: 12 - required property bool clipped - property bool enabled: true - property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : (virtualstudio.darkMode ? "#7b7979" : "#EAECEC") - property string meterRed: "#F21B1B" - - Item { - id: meters - width: parent.width - boxThickness - innerMargin - height: parent.height - - MeterBars { - id: leftchannel - x: 0 - y: 0 - width: parent.width - height: boxThickness - level: parent.parent.model[0] - enabled: parent.parent.enabled - } - - MeterBars { - id: rightchannel - x: 0; - anchors.top: leftchannel.bottom - anchors.topMargin: innerMargin - width: parent.width - height: boxThickness - level: parent.parent.model[1] - enabled: parent.parent.enabled - } - } - - Rectangle { - id: clipIndicator - y: 0 - anchors.left: meters.right - anchors.leftMargin: innerMargin - - width: boxThickness - height: leftchannel.height + rightchannel.height + innerMargin - radius: boxRadius - color: clipped ? meterRed : meterColor - } -} \ No newline at end of file diff --git a/src/gui/MeterBars.qml b/src/gui/MeterBars.qml deleted file mode 100644 index cbdbf0f..0000000 --- a/src/gui/MeterBars.qml +++ /dev/null @@ -1,49 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -Item { - required property var level - required property var enabled - property int boxHeight: height - property int boxWidth: (width / bins) - innerMargin - property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : (virtualstudio.darkMode ? "#7b7979" : "#EAECEC") - property string meterGreen: "#61C554" - property string meterYellow: "#F5BF4F" - property string meterRed: "#F21B1B" - - function getBoxColor (idx) { - // Case where the meter should not be filled - if (!enabled || level <= (idx / bins)) { - return meterColor; - } - // Case where the meter should be filled - let fillColor = meterGreen; - if (idx > 0.5*bins && idx <= 0.8*bins) { - fillColor = meterYellow; - } else if (idx > 0.8*bins) { - fillColor = meterRed; - } - return fillColor; - } - - RowLayout { - anchors.fill: parent - spacing: innerMargin - - Repeater { - model: bins - Rectangle { - Layout.fillHeight: true - Layout.fillWidth: true - x: (boxWidth) * index + innerMargin * index; - y: 0 - z: 1 - width: boxWidth - height: boxHeight - color: getBoxColor(index) - radius: boxRadius - } - } - } -} diff --git a/src/gui/NoNap.h b/src/gui/NoNap.h deleted file mode 100644 index 0fe3eda..0000000 --- a/src/gui/NoNap.h +++ /dev/null @@ -1,45 +0,0 @@ -//***************************************************************** -/* - QJackTrip: Bringing a graphical user interface to JackTrip, a - system for high quality audio network performance over the - internet. - - Copyright (c) 2020 Aaron Wyatt. - - This file is part of QJackTrip. - - QJackTrip is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - QJackTrip is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with QJackTrip. If not, see . -*/ -//***************************************************************** - -#ifndef __NONAP_H__ -#define __NONAP_H__ - -#include - -class NoNap -{ - public: - NoNap(); - ~NoNap(); - - void disableNap(); - void enableNap(); - - private: - id m_activity; - bool m_preventNap; -}; - -#endif // __NONAP_H__ diff --git a/src/gui/NoNap.mm b/src/gui/NoNap.mm deleted file mode 100644 index cb2edb2..0000000 --- a/src/gui/NoNap.mm +++ /dev/null @@ -1,58 +0,0 @@ -//***************************************************************** -/* - QJackTrip: Bringing a graphical user interface to JackTrip, a - system for high quality audio network performance over the - internet. - - Copyright (c) 2020 Aaron Wyatt. - - This file is part of QJackTrip. - - QJackTrip is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - QJackTrip is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with QJackTrip. If not, see . -*/ -//***************************************************************** - -#include "NoNap.h" -#include - -NoNap::NoNap() : - m_preventNap(false) -{} - -void NoNap::disableNap() -{ - if (m_preventNap) { - return; - } - m_preventNap = true; - m_activity = [[NSProcessInfo processInfo] beginActivityWithOptions:NSActivityLatencyCritical | NSActivityUserInitiated reason:@"Disable App Nap"]; - [m_activity retain]; -} - -void NoNap::enableNap() -{ - if (!m_preventNap) { - return; - } - m_preventNap = false; - [[NSProcessInfo processInfo] endActivity:m_activity]; - [m_activity release]; -} - -NoNap::~NoNap() -{ - if (m_preventNap) { - enableNap(); - } -} diff --git a/src/gui/Permissions.qml b/src/gui/Permissions.qml deleted file mode 100644 index da538c6..0000000 --- a/src/gui/Permissions.qml +++ /dev/null @@ -1,202 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - width: parent.width; height: parent.height - clip: true - - property int fontBig: 20 - property int fontMedium: 13 - property int fontSmall: 11 - property int fontExtraSmall: 8 - - property string saveButtonBackgroundColour: "#F2F3F3" - property string saveButtonStroke: "#EAEBEB" - property string saveButtonText: "#DB0A0A" - - Item { - id: requestMicPermissionsItem - width: parent.width; height: parent.height - visible: permissions.micPermission == "unknown" - - AppIcon { - id: microphonePrompt - y: 60 - anchors.horizontalCenter: parent.horizontalCenter - width: 260 - height: 250 - icon.source: "Prompt.svg" - } - - Image { - id: micLogo - source: "logo.svg" - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: microphonePrompt.top - anchors.topMargin: 18 * virtualstudio.uiScale - width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale - sourceSize: Qt.size(micLogo.width,micLogo.height) - fillMode: Image.PreserveAspectFit - smooth: true - } - - Button { - id: showPromptButton - width: 112 * virtualstudio.uiScale - height: 30 * virtualstudio.uiScale - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: showPromptButton.down ? saveButtonPressedColour : saveButtonBackgroundColour - border.width: 2 - border.color: showPromptButton.down || showPromptButton.hovered ? saveButtonPressedStroke : saveButtonStroke - layer.enabled: showPromptButton.hovered && !showPromptButton.down - } - onClicked: { - permissions.getMicPermission(); - } - anchors.right: microphonePrompt.right - anchors.rightMargin: 13.5 * virtualstudio.uiScale - anchors.bottomMargin: 17 * virtualstudio.uiScale - anchors.bottom: microphonePrompt.bottom - Text { - text: "OK" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: saveButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - - Text { - id: micPermissionsHeader - text: "JackTrip needs your sounds!" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: microphonePrompt.bottom - anchors.topMargin: 48 * virtualstudio.uiScale - } - - Text { - id: micPermissionsSubheader1 - text: "JackTrip requires permission to use your microphone." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 400 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: micPermissionsHeader.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Text { - id: micPermissionsSubheader2 - text: "Click ‘OK’ to give JackTrip access to your microphone, instrument, or other audio device." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 400 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: micPermissionsSubheader1.bottom - anchors.topMargin: 24 * virtualstudio.uiScale - } - } - - Item { - id: noMicItem - width: parent.width; height: parent.height - visible: permissions.micPermission == "denied" - - AppIcon { - id: noMic - y: 60 - anchors.horizontalCenter: parent.horizontalCenter - width: 109.27 - height: 170 - icon.source: "micoff.svg" - } - - Button { - id: openSettingsButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: openSettingsButton.down ? saveButtonPressedColour : saveButtonBackgroundColour - border.width: 1 - border.color: openSettingsButton.down || openSettingsButton.hovered ? saveButtonPressedStroke : saveButtonStroke - layer.enabled: openSettingsButton.hovered && !openSettingsButton.down - } - onClicked: { - permissions.openSystemPrivacy(); - } - anchors.right: parent.right - anchors.rightMargin: 16 * virtualstudio.uiScale - anchors.bottomMargin: 16 * virtualstudio.uiScale - anchors.bottom: parent.bottom - width: 200 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Open Privacy Settings" - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: saveButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - - Text { - id: noMicHeader - text: "JackTrip can't hear you!" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: noMic.bottom - anchors.topMargin: 48 * virtualstudio.uiScale - } - - Text { - id: noMicSubheader1 - text: "JackTrip requires permission to use your microphone." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 400 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: noMicHeader.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Text { - id: noMicSubheader2 - text: "Click 'Open Privacy Settings' to give JackTrip permission to access your microphone, instrument, or other audio device." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 400 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: noMicSubheader1.bottom - anchors.topMargin: 24 * virtualstudio.uiScale - } - } - - Connections { - target: permissions - - function onMicPermissionUpdated() { - if (permissions.micPermission === "granted") { - if (virtualstudio.studioToJoin === "") { - virtualstudio.windowState = "browse"; - } else { - virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected"; - virtualstudio.joinStudio(); - } - } - } - } - -} diff --git a/src/gui/Poppins-Bold.ttf b/src/gui/Poppins-Bold.ttf deleted file mode 100644 index 00559ee..0000000 Binary files a/src/gui/Poppins-Bold.ttf and /dev/null differ diff --git a/src/gui/Poppins-Regular.ttf b/src/gui/Poppins-Regular.ttf deleted file mode 100644 index 9f0c71b..0000000 Binary files a/src/gui/Poppins-Regular.ttf and /dev/null differ diff --git a/src/gui/Prompt.svg b/src/gui/Prompt.svg deleted file mode 100644 index 110d116..0000000 --- a/src/gui/Prompt.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/gui/Recommendations.qml b/src/gui/Recommendations.qml deleted file mode 100644 index 5e5fd57..0000000 --- a/src/gui/Recommendations.qml +++ /dev/null @@ -1,564 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - width: parent.width; height: parent.height - clip: true - - property int fontBig: 20 - property int fontMedium: 13 - property int fontSmall: 11 - property int fontExtraSmall: 8 - - property int buttonWidth: 103 - property int buttonHeight: 25 - - property int leftMargin: 48 - property int rightMargin: 16 - - property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" - property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" - property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" - property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - property string saveButtonBackgroundColour: "#F2F3F3" - property string saveButtonPressedColour: "#E7E8E8" - property string saveButtonStroke: "#EAEBEB" - property string saveButtonPressedStroke: "#B0B5B5" - property string recommendationText: "#DB0A0A" - property string saveButtonText: "#DB0A0A" - property string checkboxStroke: "#0062cc" - property string checkboxPressedStroke: "#007AFF" - property string disabledButtonText: "#D3D4D4" - property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" - - property bool currShowRecommendations: virtualstudio.showWarnings - property string recommendationScreen: virtualstudio.showWarnings ? "ethernet" : ( permissions.micPermission == "unknown" ? "microphone" : "acknowledged") - property bool onWindows: Qt.platform.os === "windows" - - Rectangle { - id: recommendationsHeader - x: -1 - y: 0 - - width: parent.width + 2 - height: 64 - - color: backgroundColour - border.color: "#33979797" - - Image { - source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png" - anchors.left: parent.left - anchors.leftMargin: 32 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - width: 119 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale - } - - Text { - id: gettingStartedText1 - visible: recommendationScreen === "ethernet" - text: "Getting Started with JackTrip (1/5)" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.right: parent.right - anchors.rightMargin: 32 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - } - - Text { - id: gettingStartedText2 - visible: recommendationScreen === "fiber" - text: "Getting Started with JackTrip (2/5)" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.right: parent.right - anchors.rightMargin: 32 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - } - - Text { - id: gettingStartedText3 - visible: recommendationScreen === "audiointerface" - text: "Getting Started with JackTrip (3/5)" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.right: parent.right - anchors.rightMargin: 32 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - } - - Text { - id: gettingStartedText4 - visible: recommendationScreen === "headphones" - text: "Getting Started with JackTrip (4/5)" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.right: parent.right - anchors.rightMargin: 32 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - } - - Text { - id: gettingStartedText5 - visible: recommendationScreen === "acknowledged" - text: "Getting Started with JackTrip (5/5)" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.right: parent.right - anchors.rightMargin: 32 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - } - } - - Item { - id: ethernetRecommendationItem - width: parent.width; height: parent.height - visible: recommendationScreen == "ethernet" - - AppIcon { - id: ethernetRecommendationLogo - y: 90 - anchors.horizontalCenter: parent.horizontalCenter - width: 179 - height: 128 - icon.source: "ethernet.svg" - } - - Text { - id: ethernetRecommendationHeader1 - text: "Wired Ethernet Recommended" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: ethernetRecommendationLogo.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Text { - id: ethernetRecommendationSubheader1 - text: "JackTrip works best when you connect your computer directly to your Internet router via a wired ethernet cable." - + "

" - + "WiFi works OK for some people, but generates significantly more latency and audio glitches." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 600 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: ethernetRecommendationHeader1.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - LearnMoreButton { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: ethernetRecommendationSubheader1.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - url: "https://support.jacktrip.com/wired-internet-versus-wi-fi" - } - - Button { - id: okButtonEthernet - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: okButtonEthernet.down ? saveButtonPressedColour : saveButtonBackgroundColour - border.width: 1 - border.color: okButtonEthernet.down || okButtonEthernet.hovered ? saveButtonPressedStroke : saveButtonStroke - layer.enabled: okButtonEthernet.hovered && !okButtonEthernet.down - } - onClicked: { recommendationScreen = "fiber" } - anchors.right: parent.right - anchors.rightMargin: 16 * virtualstudio.uiScale - anchors.bottomMargin: 16 * virtualstudio.uiScale - anchors.bottom: parent.bottom - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Continue" - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: saveButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - } - - Item { - id: fiberRecommendationItem - width: parent.width; height: parent.height - visible: recommendationScreen == "fiber" - - AppIcon { - id: fiberRecommendationLogo - y: 90 - anchors.horizontalCenter: parent.horizontalCenter - width: 179 - height: 128 - icon.source: "networkCheck.svg" - } - - Text { - id: fiberRecommendationHeader - text: "Fiber Internet Recommended" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: fiberRecommendationLogo.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Text { - id: fiberRecommendationSubheader - text: "A Fiber Internet connection from your Internet Service Provider (ISP) will give you the best experience while using JackTrip." - + "

" - + "It's OK to use JackTrip with Cable and DSL, but these types of Internet connections introduce significantly more latency." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 600 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: fiberRecommendationHeader.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - LearnMoreButton { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: fiberRecommendationSubheader.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - url: "https://support.jacktrip.com/how-to-optimize-latency-when-using-jacktrip" - } - - Button { - id: okButtonFiber - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: okButtonFiber.down ? saveButtonPressedColour : saveButtonBackgroundColour - border.width: 1 - border.color: okButtonFiber.down || okButtonFiber.hovered ? saveButtonPressedStroke : saveButtonStroke - layer.enabled: okButtonFiber.hovered && !okButtonFiber.down - } - onClicked: { recommendationScreen = "audiointerface" } - anchors.right: parent.right - anchors.rightMargin: 16 * virtualstudio.uiScale - anchors.bottomMargin: 16 * virtualstudio.uiScale - anchors.bottom: parent.bottom - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Continue" - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: saveButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - } - - Item { - id: headphoneRecommendationItem - width: parent.width; height: parent.height - visible: recommendationScreen == "headphones" - - AppIcon { - id: headphoneWarningLogo - y: 90 - anchors.horizontalCenter: parent.horizontalCenter - width: 118 - height: 128 - icon.source: "headphones.svg" - } - - Text { - id: headphoneRecommendationHeader1 - text: "Wired Headphones Required" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: headphoneWarningLogo.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Text { - id: headphoneRecommendationSubheader1 - text: "JackTrip requires the use of wired headphones." - + "

" - + "Using speakers will generate echos and loud feedback loops." - + "

" - + "Wireless and bluetooth headphones introduce higher latency." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 600 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: headphoneRecommendationHeader1.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Button { - id: okButtonHeadphones - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: okButtonHeadphones.down ? saveButtonPressedColour : saveButtonBackgroundColour - border.width: 1 - border.color: okButtonHeadphones.down || okButtonHeadphones.hovered ? saveButtonPressedStroke : saveButtonStroke - layer.enabled: okButtonHeadphones.hovered && !okButtonHeadphones.down - } - onClicked: { - recommendationScreen = "acknowledged"; - } - anchors.right: parent.right - anchors.rightMargin: 16 * virtualstudio.uiScale - anchors.bottomMargin: 16 * virtualstudio.uiScale - anchors.bottom: parent.bottom - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Continue" - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: saveButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - } - - Item { - id: audioInterfaceRecommendationItem - width: parent.width; height: parent.height - visible: recommendationScreen == "audiointerface" - - AppIcon { - id: audioInterfaceRecommendationLogo - y: 90 - anchors.horizontalCenter: parent.horizontalCenter - width: 118 - height: 128 - icon.source: "externalMic.svg" - } - - Text { - id: audioInterfaceRecommendationHeaderNonWindows - visible: !onWindows - text: "Use Recommended Audio Devices" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: audioInterfaceRecommendationLogo.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Text { - id: audioInterfaceRecommendationSubheaderNonWindows - visible: !onWindows - text: "Many audio devices are too slow to work well with JackTrip." - + "

" - + "We recommend external USB or Thunderbolt interfaces released within the past " - + "few years for the best quality, low latency and glitch-free sound." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 600 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: audioInterfaceRecommendationHeaderNonWindows.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Text { - id: audioInterfaceRecommendationHeaderWindows - visible: onWindows - text: "Use Recommended Audio Devices" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: audioInterfaceRecommendationLogo.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Text { - id: audioInterfaceRecommendationSubheaderWindows - visible: onWindows - text: "Many audio devices are too slow to work well with JackTrip." - + "
" - + "ASIO drivers are required for low latency on Windows. " - + "

" - + "We recommend external USB or Thunderbolt interfaces released within the past " - + "few years for the best quality, low latency and glitch-free sound." - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 600 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: audioInterfaceRecommendationHeaderWindows.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - LearnMoreButton { - width: 250 * virtualstudio.uiScale; - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: onWindows ? audioInterfaceRecommendationSubheaderWindows.bottom : audioInterfaceRecommendationSubheaderNonWindows.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - buttonText: "See recommended devices" - url: "https://support.jacktrip.com/recommended-audio-interfaces" - } - - Button { - id: okButtonAudioInterface - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: okButtonAudioInterface.down ? saveButtonPressedColour : saveButtonBackgroundColour - border.width: 1 - border.color: okButtonAudioInterface.down || okButtonAudioInterface.hovered ? saveButtonPressedStroke : saveButtonStroke - layer.enabled: okButtonAudioInterface.hovered && !okButtonAudioInterface.down - } - onClicked: { - recommendationScreen = "headphones"; - } - anchors.right: parent.right - anchors.rightMargin: 16 * virtualstudio.uiScale - anchors.bottomMargin: 16 * virtualstudio.uiScale - anchors.bottom: parent.bottom - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Continue" - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: saveButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - } - - Item { - id: acknowledgedRecommendationItem - width: parent.width; height: parent.height - visible: recommendationScreen == "acknowledged" - - Text { - id: acknowledgedHeader - text: "Remind Me Again Next Time?" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: 176 * virtualstudio.uiScale - } - - Text { - id: acknowledgedSubheader - text: "Would you like to review the getting started recommendations again the next time you start JackTrip?" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 600 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: acknowledgedHeader.bottom - anchors.topMargin: 32 * virtualstudio.uiScale - } - - Item { - id: acknowledgedButtonsContainer - width: 320 * virtualstudio.uiScale - - anchors.top: acknowledgedSubheader.bottom - anchors.topMargin: 64 * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - - Button { - id: acknowledgedYesButton - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: acknowledgedYesButton.down ? saveButtonPressedColour : saveButtonBackgroundColour - border.width: 1 - border.color: acknowledgedYesButton.down || acknowledgedYesButton.hovered ? saveButtonPressedStroke : saveButtonStroke - layer.enabled: acknowledgedYesButton.hovered && !acknowledgedYesButton.down - } - onClicked: { - virtualstudio.showWarnings = true; - virtualstudio.saveSettings(); - if (permissions.micPermission !== "granted") { - virtualstudio.windowState = "permissions"; - } else if (virtualstudio.studioToJoin === "") { - virtualstudio.windowState = "browse"; - } else { - virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected"; - virtualstudio.joinStudio(); - } - } - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Yes" - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: saveButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - - Button { - id: acknowledgedNoButton - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: acknowledgedNoButton.down ? saveButtonPressedColour : saveButtonBackgroundColour - border.width: 1 - border.color: acknowledgedNoButton.down || acknowledgedNoButton.hovered ? saveButtonPressedStroke : saveButtonStroke - layer.enabled: acknowledgedNoButton.hovered && !acknowledgedNoButton.down - } - onClicked: { - virtualstudio.showWarnings = false; - virtualstudio.saveSettings(); - if (permissions.micPermission !== "granted") { - virtualstudio.windowState = "permissions"; - } else if (virtualstudio.studioToJoin === "") { - virtualstudio.windowState = "browse"; - } else { - virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected"; - virtualstudio.joinStudio(); - } - } - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "No" - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: saveButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - } - - - Text { - id: acknowledgedSettingsInfo - text: "You can change this setting at any time under Settings > Advanced" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - width: 600 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: acknowledgedButtonsContainer.bottom - anchors.topMargin: 64 * virtualstudio.uiScale - } - } -} diff --git a/src/gui/SectionHeading.qml b/src/gui/SectionHeading.qml deleted file mode 100644 index 84568df..0000000 --- a/src/gui/SectionHeading.qml +++ /dev/null @@ -1,163 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Rectangle { - property string filterStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - - property bool listIsEmpty: false - // required property string section: section (for 5.15) - color: "transparent" - height: 72 * virtualstudio.uiScale - x: 16 * virtualstudio.uiScale - y: listIsEmpty ? 16 * virtualstudio.uiScale : 0 - width: listIsEmpty ? parent.width - (2 * x) : ListView.view.width - (2 * x) - Text { - id: sectionText - //anchors.bottom: parent.bottom - y: 12 * virtualstudio.uiScale - // text: parent.section (for 5.15) - width: parent.width - 332 * virtualstudio.uiScale - fontSizeMode: Text.HorizontalFit - text: listIsEmpty ? "No Studios" : section - font { family: "Poppins"; pixelSize: 28 * virtualstudio.fontScale * virtualstudio.uiScale; weight: Font.Bold } - color: textColour - verticalAlignment: Text.AlignBottom - } - Button { - id: createButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: createButton.down ? "#E7E8E8" : "#F2F3F3" - border.width: 1 - border.color: createButton.down ? "#B0B5B5" : "#EAEBEB" - layer.enabled: createButton.hovered && !createButton.down - } - onClicked: { virtualstudio.createStudio(); } - anchors.right: filterButton.left - anchors.rightMargin: 16 - anchors.verticalCenter: sectionText.verticalCenter - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Create a Studio" - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: "#DB0A0A" - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - visible: listIsEmpty ? true : (section == virtualstudio.logoSection ? true : false) - } - Button { - id: filterButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: filterButton.down ? "#E7E8E8" : "#F2F3F3" - border.width: 1 - border.color: filterButton.down ? "#B0B5B5" : "#EAEBEB" - layer.enabled: filterButton.hovered && !filterButton.down - } - onClicked: { filterMenu.open(); } - anchors.right: parent.right - anchors.verticalCenter: sectionText.verticalCenter - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Filter Studios" - font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - visible: listIsEmpty ? true : (section == virtualstudio.logoSection ? true : false) - - Popup { - id: filterMenu - y: Math.round(parent.height + 8) - rightMargin: 16 * virtualstudio.uiScale - width: 210 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale - modal: false - focus: false - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: "#F6F8F8" - border.width: 1 - border.color: filterStroke - layer.enabled: true - } - contentItem: Column { - anchors.fill: parent - CheckBox { - id: inactiveCheckbox - text: qsTr("Show my inactive Studios") - checkState: virtualstudio.showInactive ? Qt.Checked : Qt.Unchecked - onClicked: { virtualstudio.showInactive = inactiveCheckbox.checkState == Qt.Checked; - refreshing = true; - refresh(); - } - indicator: Rectangle { - implicitWidth: 16 * virtualstudio.uiScale - implicitHeight: 16 * virtualstudio.uiScale - x: inactiveCheckbox.leftPadding - y: parent.height / 2 - height / 2 - radius: 3 * virtualstudio.uiScale - border.color: inactiveCheckbox.down ? "#007AFF" : "#0062cc" - - Rectangle { - width: 10 * virtualstudio.uiScale - height: 10 * virtualstudio.uiScale - x: 3 * virtualstudio.uiScale - y: 3 * virtualstudio.uiScale - radius: 2 * virtualstudio.uiScale - color: inactiveCheckbox.down ? "#007AFF" : "#0062cc" - visible: inactiveCheckbox.checked - } - } - contentItem: Text { - text: inactiveCheckbox.text - font.family: "Poppins" - font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - leftPadding: inactiveCheckbox.indicator.width + inactiveCheckbox.spacing - } - } - CheckBox { - id: selfHostedCheckbox - text: qsTr("Show self-hosted Studios") - checkState: virtualstudio.showSelfHosted ? Qt.Checked : Qt.Unchecked - onClicked: { virtualstudio.showSelfHosted = selfHostedCheckbox.checkState == Qt.Checked; - refreshing = true; - refresh(); - } - indicator: Rectangle { - implicitWidth: 16 * virtualstudio.uiScale - implicitHeight: 16 * virtualstudio.uiScale - x: selfHostedCheckbox.leftPadding - y: parent.height / 2 - height / 2 - radius: 3 * virtualstudio.uiScale - border.color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC" - - Rectangle { - width: 10 * virtualstudio.uiScale - height: 10 * virtualstudio.uiScale - x: 3 * virtualstudio.uiScale - y: 3 * virtualstudio.uiScale - radius: 2 * virtualstudio.uiScale - color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC" - visible: selfHostedCheckbox.checked - } - } - contentItem: Text { - text: selfHostedCheckbox.text - font.family: "Poppins" - font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - leftPadding: selfHostedCheckbox.indicator.width + selfHostedCheckbox.spacing - } - } - } - } - } -} \ No newline at end of file diff --git a/src/gui/Settings.qml b/src/gui/Settings.qml deleted file mode 100644 index b970e91..0000000 --- a/src/gui/Settings.qml +++ /dev/null @@ -1,781 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - width: parent.width; height: parent.height - clip: true - - Rectangle { - width: parent.width; height: parent.height - color: backgroundColour - } - - property int fontBig: 20 - property int fontMedium: 13 - property int fontSmall: 11 - property int fontExtraSmall: 8 - - property int leftMargin: 48 - property int rightMargin: 16 - property int buttonWidth: 103 - property int buttonHeight: 25 - - property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" - property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" - property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" - property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" - property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - property string sliderColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC" - property string sliderPressedColour: virtualstudio.darkMode ? "#ACAFAF" : "#DEE0E0" - property string sliderTrackColour: virtualstudio.darkMode ? "#5B5858" : "light gray" - property string sliderActiveTrackColour: virtualstudio.darkMode ? "light gray" : "black" - property string warningTextColour: "#DB0A0A" - property string checkboxStroke: "#0062cc" - property string checkboxPressedStroke: "#007AFF" - property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" - - property string errorFlagColour: "#DB0A0A" - property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - - property string settingsGroupView: "Audio" - - function getCurrentBufferSizeIndex () { - let bufferSize = audio.bufferSize; - let idx = audio.bufferSizeComboModel.findIndex(elem => parseInt(elem) === bufferSize); - if (idx < 0) { - idx = 0; - } - return idx; - } - - function getCurrentAudioBackendIndex () { - let idx = audio.audioBackendComboModel.findIndex(elem => elem === audio.audioBackend); - if (idx < 0) { - idx = 0; - } - return idx; - } - - Rectangle { - id: audioSettingsView - width: 0.8 * parent.width - height: parent.height - header.height - x: 0.2 * window.width - y: header.height - visible: settingsGroupView == "Audio" - - AudioSettings { - id: audioSettings - } - } - - ToolBar { - id: header - width: parent.width - height: 64 * virtualstudio.uiScale - - background: Rectangle { - border.color: "#33979797" - color: backgroundColour - width: parent.width - } - - contentItem: Item { - id: headerContent - width: header.width - height: header.height - - Label { - id: pageTitle - text: "Settings" - height: headerContent.height; - anchors.left: headerContent.left; - anchors.leftMargin: 32 * virtualstudio.uiScale - elide: Label.ElideRight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - DeviceRefreshButton { - id: refreshButton - anchors.verticalCenter: pageTitle.verticalCenter; - anchors.right: headerContent.right; - anchors.rightMargin: 16 * virtualstudio.uiScale; - visible: audio.audioBackend == "RtAudio" && settingsGroupView == "Audio" - enabled: audio.audioReady && !audio.scanningDevices - } - - Text { - text: "Restarting Audio" - anchors.verticalCenter: pageTitle.verticalCenter; - anchors.right: refreshButton.left; - anchors.rightMargin: 16 * virtualstudio.uiScale; - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - visible: !audio.audioReady - } - } - } - - Drawer { - id: drawer - width: 0.2 * parent.width - height: parent.height - header.height - y: header.height-1 - modal: false - interactive: false - visible: virtualstudio.windowState == "settings" - - background: Rectangle { - border.color: "#33979797" - color: backgroundColour - } - - ButtonGroup { - buttons: viewControls.children - onClicked: function(button) { - settingsGroupView = button.text - } - } - - Column { - id: viewControls - width: parent.width - spacing: 24 * virtualstudio.uiScale - anchors.centerIn: parent - Button { - id: audioBtn - text: "Audio" - width: parent.width - contentItem: Item { - implicitWidth: audioButtonText.implicitWidth - implicitHeight: audioButtonText.implicitHeight - - Label { - id: audioButtonText - text: audioBtn.text - width: Boolean(audio.devicesError) ? parent.width - 16 * virtualstudio.uiScale : parent.width - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: textColour - } - - Rectangle { - id: audioDevicesErrorFlag - anchors.left: audioButtonText.right - anchors.verticalCenter: audioButtonText.verticalCenter - anchors.rightMargin: 16 * virtualstudio.uiScale - width: 8 * virtualstudio.uiScale - height: 8 * virtualstudio.uiScale - color: errorFlagColour - radius: 4 * virtualstudio.uiScale - visible: Boolean(audio.devicesError) - } - } - background: Rectangle { - width: parent.width - color: audioBtn.down ? buttonPressedColour : (audioBtn.hovered || settingsGroupView == "Audio" ? buttonHoverColour : backgroundColour) - } - } - Button { - id: appearanceBtn - text: "Appearance" - width: parent.width - contentItem: Item { - implicitWidth: appearanceButtonText.implicitWidth - implicitHeight: appearanceButtonText.implicitHeight - - Label { - id: appearanceButtonText - text: appearanceBtn.text - width: parent.width - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: textColour - } - } - - - background: Rectangle { - width: parent.width - color: appearanceBtn.down ? buttonPressedColour : (appearanceBtn.hovered || settingsGroupView == "Appearance" ? buttonHoverColour : backgroundColour) - } - } - Button { - id: advancedBtn - text: "Advanced" - width: parent.width - contentItem: Item { - implicitWidth: advancedButtonText.implicitWidth - implicitHeight: advancedButtonText.implicitHeight - - Label { - id: advancedButtonText - text: advancedBtn.text - width: parent.width - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: textColour - } - } - background: Rectangle { - width: parent.width - color: advancedBtn.down ? buttonPressedColour : (advancedBtn.hovered || settingsGroupView == "Advanced" ? buttonHoverColour : backgroundColour) - } - } - Button { - id: profileBtn - text: "Profile" - width: parent.width - contentItem: Item { - - implicitWidth: profileButtonText.implicitWidth - implicitHeight: profileButtonText.implicitHeight - - Label { - id: profileButtonText - text: profileBtn.text - width: parent.width - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: textColour - } - } - background: Rectangle { - width: parent.width - color: profileBtn.down ? buttonPressedColour : (profileBtn.hovered || settingsGroupView == "Profile" ? buttonHoverColour : backgroundColour) - } - } - } - - Column { - id: appVersion - width: parent.width - spacing: 24 * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - - Text { - text: "Version " + virtualstudio.versionString - font { family: "Poppins"; pixelSize: 9 * virtualstudio.fontScale * virtualstudio.uiScale} - color: textColour - opacity: 0.8 - width: parent.width - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - bottomPadding: 5 * virtualstudio.uiScale - } - } - } - - Rectangle { - id: appearanceSettingsView - width: 0.8 * parent.width - height: parent.height - header.height - x: 0.2 * window.width - y: header.height - color: backgroundColour - visible: settingsGroupView == "Appearance" - - Slider { - id: scaleSlider - x: 220 * virtualstudio.uiScale; - y: 100 * virtualstudio.uiScale - width: backendCombo.width - from: 1; to: 1.25; value: virtualstudio.uiScale - onMoved: { virtualstudio.uiScale = value } - - background: Rectangle { - x: scaleSlider.leftPadding - y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2 - implicitWidth: parent.width - implicitHeight: 6 - width: scaleSlider.availableWidth - height: implicitHeight - radius: 4 - color: sliderTrackColour - - Rectangle { - width: scaleSlider.visualPosition * parent.width - height: parent.height - color: sliderActiveTrackColour - radius: 4 - } - } - - handle: Rectangle { - x: scaleSlider.leftPadding + scaleSlider.visualPosition * (scaleSlider.availableWidth - width) - y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2 - implicitWidth: 26 * virtualstudio.uiScale - implicitHeight: 26 * virtualstudio.uiScale - radius: 13 * virtualstudio.uiScale - color: scaleSlider.pressed ? sliderPressedColour : sliderColour - border.color: buttonStroke - } - } - - Text { - anchors.verticalCenter: scaleSlider.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Scale Interface" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - Button { - id: darkButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: darkButton.down ? buttonPressedColour : (darkButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: darkButton.down ? buttonPressedStroke : (darkButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { virtualstudio.darkMode = !virtualstudio.darkMode; } - x: parent.width - (232 * virtualstudio.uiScale); y: modeButton.y + (56 * virtualstudio.uiScale) - width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: virtualstudio.darkMode ? "Switch to Light Mode" : "Switch to Dark Mode" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - Text { - anchors.verticalCenter: darkButton.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Color Theme" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - } - - Rectangle { - id: advancedSettingsView - width: 0.8 * parent.width - height: parent.height - header.height - x: 0.2 * window.width - y: header.height - color: backgroundColour - visible: settingsGroupView == "Advanced" - - Button { - id: modeButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: modeButton.down ? buttonPressedColour : (modeButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { - // essentially the same here as clicking the cancel button - audio.stopAudio(); - virtualstudio.windowState = "browse"; - virtualstudio.loadSettings(); - audio.validateDevices(); - - // switch mode - virtualstudio.toStandard(); - } - x: 220 * virtualstudio.uiScale; - y: 100 * virtualstudio.uiScale - width: 216 * virtualstudio.uiScale; - height: 30 * virtualstudio.uiScale - Text { - text: virtualstudio.psiBuild ? "Switch to Standard Mode" : "Switch to Classic Mode" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - Text { - anchors.verticalCenter: modeButton.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Display Mode" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - ComboBox { - id: updateChannelCombo - x: 220 * virtualstudio.uiScale; y: modeButton.y + (48 * virtualstudio.uiScale) - width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale - model: virtualstudio.updateChannelComboModel - currentIndex: virtualstudio.updateChannel == "stable" ? 0 : 1 - onActivated: { virtualstudio.updateChannel = currentIndex == 0 ? "stable": "edge" } - font.family: "Poppins" - enabled: !virtualstudio.noUpdater - } - - Text { - anchors.verticalCenter: updateChannelCombo.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Update Channel" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - ComboBox { - id: backendCombo - model: audio.audioBackendComboModel - enabled: audio.audioBackendComboModel.length > 1 - currentIndex: getCurrentAudioBackendIndex() - onActivated: { - audio.audioBackend = currentText - audio.restartAudio(); - } - x: 220 * virtualstudio.uiScale; y: updateChannelCombo.y + (48 * virtualstudio.uiScale) - width: updateChannelCombo.width; height: updateChannelCombo.height - } - - Text { - id: backendLabel - anchors.verticalCenter: backendCombo.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Audio Backend" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - ComboBox { - id: bufferCombo - x: 220 * virtualstudio.uiScale; y: backendCombo.y + (48 * virtualstudio.uiScale) - width: backendCombo.width; height: updateChannelCombo.height - model: audio.bufferSizeComboModel - currentIndex: getCurrentBufferSizeIndex() - onActivated: { - audio.bufferSize = parseInt(currentText, 10); - audio.restartAudio(); - } - font.family: "Poppins" - } - - Text { - anchors.verticalCenter: bufferCombo.verticalCenter - x: 48 * virtualstudio.uiScale - text: "Buffer Size" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - ComboBox { - id: bufferStrategyCombo - x: updateChannelCombo.x; y: bufferCombo.y + (48 * virtualstudio.uiScale) - width: updateChannelCombo.width; height: updateChannelCombo.height - model: audio.bufferStrategyComboModel - currentIndex: audio.bufferStrategy - onActivated: { audio.bufferStrategy = currentIndex } - font.family: "Poppins" - } - - Text { - anchors.verticalCenter: bufferStrategyCombo.verticalCenter - x: 48 * virtualstudio.uiScale - text: "Buffer Strategy" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - ComboBox { - id: feedbackDetectionCombo - x: updateChannelCombo.x; y: bufferStrategyCombo.y + (48 * virtualstudio.uiScale) - width: updateChannelCombo.width; height: updateChannelCombo.height - model: audio.feedbackDetectionComboModel - currentIndex: audio.feedbackDetectionEnabled ? 0 : 1 - onActivated: { - if (currentIndex === 1) { - audio.feedbackDetectionEnabled = false; - } else { - audio.feedbackDetectionEnabled = true; - } - } - font.family: "Poppins" - } - - Text { - anchors.verticalCenter: feedbackDetectionCombo.verticalCenter - x: 48 * virtualstudio.uiScale - text: "Feedback Detection" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - CheckBox { - id: showStartupSetup - checked: virtualstudio.showDeviceSetup - text: qsTr("Show device setup screen before connecting to a studio") - x: updateChannelCombo.x; y: feedbackDetectionCombo.y + (48 * virtualstudio.uiScale) - onClicked: { virtualstudio.showDeviceSetup = showStartupSetup.checkState == Qt.Checked; } - indicator: Rectangle { - implicitWidth: 16 * virtualstudio.uiScale - implicitHeight: 16 * virtualstudio.uiScale - x: showStartupSetup.leftPadding - y: parent.height / 2 - height / 2 - radius: 3 * virtualstudio.uiScale - border.color: showStartupSetup.down || showStartupSetup.hovered ? checkboxPressedStroke : checkboxStroke - - Rectangle { - width: 10 * virtualstudio.uiScale - height: 10 * virtualstudio.uiScale - x: 3 * virtualstudio.uiScale - y: 3 * virtualstudio.uiScale - radius: 2 * virtualstudio.uiScale - color: showStartupSetup.down || showStartupSetup.hovered ? checkboxPressedStroke : checkboxStroke - visible: showStartupSetup.checked - } - } - contentItem: Text { - text: showStartupSetup.text - font.family: "Poppins" - font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - leftPadding: showStartupSetup.indicator.width + showStartupSetup.spacing - color: textColour - } - } - - Text { - anchors.verticalCenter: showStartupSetup.verticalCenter - x: 48 * virtualstudio.uiScale - text: "Device Setup" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - CheckBox { - id: showStartupWarnings - checked: virtualstudio.showWarnings - text: qsTr("Show recommendations on startup again next time") - x: updateChannelCombo.x; y: showStartupSetup.y + (48 * virtualstudio.uiScale) - onClicked: { virtualstudio.showWarnings = showStartupWarnings.checkState == Qt.Checked; } - indicator: Rectangle { - implicitWidth: 16 * virtualstudio.uiScale - implicitHeight: 16 * virtualstudio.uiScale - x: showStartupWarnings.leftPadding - y: parent.height / 2 - height / 2 - radius: 3 * virtualstudio.uiScale - border.color: showStartupWarnings.down || showStartupWarnings.hovered ? checkboxPressedStroke : checkboxStroke - - Rectangle { - width: 10 * virtualstudio.uiScale - height: 10 * virtualstudio.uiScale - x: 3 * virtualstudio.uiScale - y: 3 * virtualstudio.uiScale - radius: 2 * virtualstudio.uiScale - color: showStartupWarnings.down || showStartupWarnings.hovered ? checkboxPressedStroke : checkboxStroke - visible: showStartupWarnings.checked - } - } - contentItem: Text { - text: showStartupWarnings.text - font.family: "Poppins" - font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - leftPadding: showStartupWarnings.indicator.width + showStartupWarnings.spacing - color: textColour - } - } - - Text { - anchors.verticalCenter: showStartupWarnings.verticalCenter - x: 48 * virtualstudio.uiScale - text: "Recommendations" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - } - - Rectangle { - id: profileSettingsView - width: 0.8 * parent.width - height: parent.height - header.height - x: 0.2 * window.width - y: header.height - color: backgroundColour - visible: settingsGroupView == "Profile" - - Image { - id: profilePicture - width: 96; height: 96 - y: 60 * virtualstudio.uiScale - source: virtualstudio.userMetadata.picture ? virtualstudio.userMetadata.picture : "" - anchors.horizontalCenter: parent.horizontalCenter - fillMode: Image.PreserveAspectCrop - } - - Text { - id: displayName - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: profilePicture.bottom - text: virtualstudio.userMetadata.user_metadata ? ( virtualstudio.userMetadata.user_metadata.display_name ? virtualstudio.userMetadata.user_metadata.display_name : virtualstudio.userMetadata.nickname ) : virtualstudio.userMetadata.name || "" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - Text { - id: email - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: displayName.bottom - text: virtualstudio.userMetadata.email ? virtualstudio.userMetadata.email : "" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - Button { - id: editButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: editButton.down ? buttonPressedColour : (editButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: editButton.down ? buttonPressedStroke : (editButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { virtualstudio.editProfile(); } - anchors.horizontalCenter: parent.horizontalCenter - y: email.y + (56 * virtualstudio.uiScale) - width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Edit Profile" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - Button { - id: logoutButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: logoutButton.down ? buttonPressedColour : (logoutButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: logoutButton.down ? buttonPressedStroke : (logoutButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { virtualstudio.showFirstRun = false; virtualstudio.logout(); } - anchors.horizontalCenter: parent.horizontalCenter - y: editButton.y + (48 * virtualstudio.uiScale) - width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Log Out" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - Button { - id: testModeButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: testModeButton.down ? buttonPressedColour : (testModeButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: testModeButton.down ? buttonPressedStroke : (testModeButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { - virtualstudio.testMode = !virtualstudio.testMode; - - // behave like "Cancel" and switch back to browse mode - audio.stopAudio(); - virtualstudio.windowState = "browse"; - virtualstudio.loadSettings(); - audio.validateDevices(); - } - anchors.horizontalCenter: parent.horizontalCenter - y: logoutButton.y + (48 * virtualstudio.uiScale) - width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - visible: virtualstudio.userMetadata.email ? ( virtualstudio.userMetadata.email.endsWith("@jacktrip.org") ? true : false ) : false - Text { - text: virtualstudio.testMode ? "Switch to Prod Mode" : "Switch to Test Mode" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - } - - Rectangle { - x: -1; y: parent.height - (36 * virtualstudio.uiScale) - width: parent.width; height: (36 * virtualstudio.uiScale) - border.color: "#33979797" - color: backgroundColour - - Button { - id: cancelButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: cancelButton.down ? buttonPressedColour : (cancelButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: cancelButton.down ? buttonPressedStroke : (cancelButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { - audio.stopAudio(); - virtualstudio.windowState = "browse"; - virtualstudio.loadSettings(); - audio.validateDevices(); - } - anchors.verticalCenter: parent.verticalCenter - x: parent.width - (230 * virtualstudio.uiScale) - width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale - Text { - text: "Cancel" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour - } - } - - Button { - id: saveButton - enabled: !Boolean(audio.devicesError) - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: saveButton.down ? buttonPressedColour : (saveButton.hovered ? buttonHoverColour : buttonColour) - border.width: 1 - border.color: saveButton.down ? buttonPressedStroke : (saveButton.hovered ? buttonHoverStroke : buttonStroke) - } - onClicked: { - audio.stopAudio(); - virtualstudio.windowState = "browse"; - virtualstudio.saveSettings(); - } - anchors.verticalCenter: parent.verticalCenter - x: parent.width - (119 * virtualstudio.uiScale) - width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale - Text { - text: "Save" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: Boolean(audio.devicesError) ? disabledButtonTextColour : textColour - } - } - - DeviceWarning { - id: deviceWarning - x: (0.2 * window.width) + 16 * virtualstudio.uiScale - anchors.verticalCenter: parent.verticalCenter - visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) - } - } - - Connections { - target: audio - // anything that sets currentIndex to the value of a function needs - // to be manually updated whenever there is a change to any vars it uses - function onBufferSizeChanged() { - bufferCombo.currentIndex = getCurrentBufferSizeIndex(); - } - function onAudioBackendChanged() { - backendCombo.currentIndex = getCurrentAudioBackendIndex(); - } - } -} diff --git a/src/gui/Setup.qml b/src/gui/Setup.qml deleted file mode 100644 index 969325d..0000000 --- a/src/gui/Setup.qml +++ /dev/null @@ -1,201 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - width: parent.width; height: parent.height - clip: true - - property int fontBig: 20 - property int fontMedium: 13 - property int fontSmall: 11 - property int fontExtraSmall: 8 - - property int leftMargin: 48 - property int rightMargin: 16 - - property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" - property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" - property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" - property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" - property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - property string saveButtonBackgroundColour: "#F2F3F3" - property string saveButtonPressedColour: "#E7E8E8" - property string saveButtonStroke: "#EAEBEB" - property string saveButtonPressedStroke: "#B0B5B5" - property string saveButtonText: "#DB0A0A" - property string checkboxStroke: "#0062cc" - property string checkboxPressedStroke: "#007AFF" - property string disabledButtonText: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - - Item { - id: setupItem - width: parent.width; height: parent.height - - property bool isUsingRtAudio: audio.audioBackend == "RtAudio" - - Text { - id: pageTitle - x: 16 * virtualstudio.uiScale; - y: 16 * virtualstudio.uiScale - text: "Choose your audio devices" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - DeviceRefreshButton { - id: refreshButton - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.verticalCenter: pageTitle.verticalCenter - visible: parent.isUsingRtAudio - enabled: audio.audioReady && !audio.scanningDevices - } - - Text { - text: "Restarting Audio" - anchors.verticalCenter: pageTitle.verticalCenter; - anchors.right: refreshButton.left; - anchors.rightMargin: 16 * virtualstudio.uiScale; - font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - visible: !audio.audioReady - } - - AudioSettings { - id: audioSettings - width: parent.width - anchors.top: pageTitle.bottom - anchors.topMargin: 16 * virtualstudio.uiScale - } - - Rectangle { - id: headerBorder - width: parent.width - height: 1 - anchors.top: audioSettings.top - color: strokeColor - } - - Rectangle { - id: footerBorder - width: parent.width - height: 1 - anchors.top: audioSettings.bottom - color: strokeColor - } - - Rectangle { - property int footerHeight: (30 + (rightMargin * 2)) * virtualstudio.uiScale; - x: -1; y: parent.height - footerHeight; - width: parent.width; height: footerHeight; - border.color: "#33979797" - color: backgroundColour - - Button { - id: backButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: backButton.down ? buttonPressedColour : buttonColour - border.width: 1 - border.color: backButton.down || backButton.hovered ? buttonPressedStroke : buttonStroke - } - onClicked: { virtualstudio.windowState = "browse"; virtualstudio.studioToJoin = ""; audio.stopAudio(); } - anchors.left: parent.left - anchors.leftMargin: 16 * virtualstudio.uiScale - anchors.bottomMargin: rightMargin * virtualstudio.uiScale - anchors.bottom: parent.bottom - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Back" - font.family: "Poppins" - font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale - color: textColour - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - - DeviceWarning { - id: deviceWarning - anchors.left: backButton.right - anchors.leftMargin: 16 * virtualstudio.uiScale - anchors.verticalCenter: backButton.verticalCenter - visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) - } - - Button { - id: saveButton - background: Rectangle { - radius: 6 * virtualstudio.uiScale - color: saveButton.down ? saveButtonPressedColour : saveButtonBackgroundColour - border.width: 1 - border.color: saveButton.down || saveButton.hovered ? saveButtonPressedStroke : saveButtonStroke - } - enabled: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady - onClicked: { - audio.stopAudio(true); - virtualstudio.studioToJoin = virtualstudio.currentStudio.id; - virtualstudio.windowState = "connected"; - virtualstudio.saveSettings(); - virtualstudio.joinStudio(); - } - anchors.right: parent.right - anchors.rightMargin: rightMargin * virtualstudio.uiScale - anchors.bottomMargin: rightMargin * virtualstudio.uiScale - anchors.bottom: parent.bottom - width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - Text { - text: "Connect to Studio" - font.family: "Poppins" - font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale - font.weight: Font.Bold - color: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady ? saveButtonText : disabledButtonText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - } - } - - CheckBox { - id: showAgainCheckbox - checked: virtualstudio.showDeviceSetup - visible: audio.backendAvailable - text: qsTr("Ask again next time") - anchors.right: saveButton.left - anchors.rightMargin: 16 * virtualstudio.uiScale - anchors.verticalCenter: saveButton.verticalCenter - onClicked: { virtualstudio.showDeviceSetup = showAgainCheckbox.checkState == Qt.Checked } - indicator: Rectangle { - implicitWidth: 16 * virtualstudio.uiScale - implicitHeight: 16 * virtualstudio.uiScale - x: showAgainCheckbox.leftPadding - y: parent.height / 2 - height / 2 - radius: 3 * virtualstudio.uiScale - border.color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke - - Rectangle { - width: 10 * virtualstudio.uiScale - height: 10 * virtualstudio.uiScale - x: 3 * virtualstudio.uiScale - y: 3 * virtualstudio.uiScale - radius: 2 * virtualstudio.uiScale - color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke - visible: showAgainCheckbox.checked - } - } - - contentItem: Text { - text: showAgainCheckbox.text - font.family: "Poppins" - font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - leftPadding: showAgainCheckbox.indicator.width + showAgainCheckbox.spacing - color: textColour - } - } - } - } -} diff --git a/src/gui/Studio.qml b/src/gui/Studio.qml deleted file mode 100644 index 726ac52..0000000 --- a/src/gui/Studio.qml +++ /dev/null @@ -1,402 +0,0 @@ -import QtQuick -import QtQuick.Controls -import Qt5Compat.GraphicalEffects -import VS 1.0 - -Rectangle { - width: 664; height: 83 * virtualstudio.uiScale - radius: 6 * virtualstudio.uiScale - color: backgroundColour - - property string serverLocation: "Germany - Berlin" - property string flagImage: "flags/DE.svg" - property string hostname: "app.jacktrip.com" - property string studioName: "Test Studio" - property string studioId: "" - property string streamId: "" - property string inviteKeyString: "" - property int sampleRate: 48000 - property bool publicStudio: false - property bool admin: false - property bool available: true - property bool connected: false - property bool inviteCopied: false - - property int leftMargin: 81 - property int topMargin: 13 - property int bottomToolTipMargin: 8 - property int rightToolTipMargin: 4 - - property real fontBig: 18 - property real fontMedium: 11 - property real fontSmall: 8 - - property string backgroundColour: virtualstudio.darkMode ? "#494646" : "#F4F6F6" - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1" - property string inviteToolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" - property string inviteToolTipTextColour: textColour - property string inviteCopiedBackgroundColour: "#57B147" - property string inviteCopiedTextColour: "#FAFBFB" - property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - - property string baseButtonColour: virtualstudio.darkMode ? "#F0F1F1" : "#EAEBEB" - property string baseButtonHoverColour: virtualstudio.darkMode ? "#CCCDCD" : "#D3D3D3" - property string baseButtonPressedColour: virtualstudio.darkMode ? "#E4E5E5" : "#EAEBEB" - property string baseButtonStroke: virtualstudio.darkMode ? "#8B8D8D" : "#949494" - - property string joinAvailableColour: virtualstudio.darkMode ? "#E2EBE0" : "#C4F4BE" - property string joinAvailableHoverColour: virtualstudio.darkMode ? "#BAC7B8" : "#B0DCAB" - property string joinAvailablePressedColour: virtualstudio.darkMode ? "#D8E2D6" : "#BAE8B5" - property string joinAvailableStroke: virtualstudio.darkMode ? "#748F70" : "#5DB752" - - property string joinUnavailableColour: baseButtonColour - property string joinUnavailableHoverColour: baseButtonHoverColour - property string joinUnavailablePressedColour: baseButtonPressedColour - property string joinUnavailableStroke: baseButtonStroke - - property string startColour: virtualstudio.darkMode ? "#E2EBE0" : "#C4F4BE" - property string startHoverColour: virtualstudio.darkMode ? "#BAC7B8" : "#B0DCAB" - property string startPressedColour: virtualstudio.darkMode ? "#D8E2D6" : "#BAE8B5" - property string startStroke: virtualstudio.darkMode ? "#748F70" : "#5DB752" - - property string manageColour: baseButtonColour - property string manageHoverColour: baseButtonHoverColour - property string managePressedColour: baseButtonPressedColour - property string manageStroke: baseButtonStroke - - property string leaveColour: virtualstudio.darkMode ? "#FCB6B6" : "#FCB6B6" - property string leaveHoverColour: virtualstudio.darkMode ? "#D49696" : "#E3A4A4" - property string leavePressedColour: virtualstudio.darkMode ? "#F2AEAE" : "#EFADAD" - property string leaveStroke: virtualstudio.darkMode ? "#A65959" : "#C95E5E" - - property string studioStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" - - border.width: 1 - border.color: studioStroke - - Clipboard { - id: clipboard - } - - Rectangle { - id: shadow - anchors.fill: parent - color: "transparent" - radius: 6 - } - - Rectangle { - width: 12 * virtualstudio.uiScale; height: parent.height - radius: width / 2 - color: available ? "#0C1424" : "#B3B3B3" - } - - Image { - id: wedge - source: available ? "wedge.svg" : "wedge_inactive.svg" - x: 6; y: 0; width: 52 * virtualstudio.uiScale; height: 83 * virtualstudio.uiScale - sourceSize: Qt.size(wedge.width,wedge.height) - fillMode: Image.PreserveAspectFit - smooth: true - } - - Image { - id: studioLogo - source: "logo.svg" - x: 8; y: 11; width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale - sourceSize: Qt.size(studioLogo.width,studioLogo.height) - fillMode: Image.PreserveAspectFit - smooth: true - } - - Rectangle { - x: 33 * virtualstudio.uiScale; y: 8 * virtualstudio.uiScale - width: 32 * virtualstudio.uiScale; height: width - radius: width / 2 - color: available ? "#0C1424" : "#B3B3B3" - } - - Image { - id: flag - source: flagImage - x: 30 * virtualstudio.uiScale; y: 9 * virtualstudio.uiScale - width: 40 * virtualstudio.uiScale; height: width / 4 * 3 - fillMode: Image.PreserveAspectCrop - layer.enabled: true - layer.effect: OpacityMask { - maskSource: mask - } - - AppIcon { - id: defaultFlag - anchors.fill: parent - width: 32 * virtualstudio.uiScale - height: 32 * virtualstudio.uiScale - icon.source: "language.svg" - color: "white" - visible: flag.status != Image.Ready - } - } - - Rectangle { - id: mask - x: 0 ; y: 0 ; width: flag.width; height: flag.height - visible: false - color: "#00000000" - Rectangle { - x: 7 * virtualstudio.uiScale; y: 3 * virtualstudio.uiScale - width:24 * virtualstudio.uiScale; height: width - radius: width / 2 - } - } - - Text { - x: leftMargin * virtualstudio.uiScale; y: 11 * virtualstudio.uiScale; - width: (admin || connected) ? parent.width - (310 * virtualstudio.uiScale) : parent.width - (233 * virtualstudio.uiScale) - text: studioName - fontSizeMode: Text.HorizontalFit - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - color: textColour - } - - Rectangle { - id: publicRect - x: leftMargin * virtualstudio.uiScale; y: 52 * virtualstudio.uiScale - width: 14 * virtualstudio.uiScale; height: width - radius: 2 * virtualstudio.uiScale - color: publicStudio ? "#0095FF" : "#FF9800" - Image { - id: pubPriv - source: publicStudio ? "public.svg" : "private.svg" - x: 1 * virtualstudio.uiScale; y: x; width: 12 * virtualstudio.uiScale; height: width - sourceSize: Qt.size(pubPriv.width,pubPriv.height) - fillMode: Image.PreserveAspectFit - smooth: true - } - } - - Text { - anchors.verticalCenter: publicRect.verticalCenter - x: (leftMargin + 22) * virtualstudio.uiScale - width: (admin || connected) ? parent.width - (255 * virtualstudio.uiScale) : parent.width - (178 * virtualstudio.uiScale) - text: publicStudio ? "Public hub studio " + serverLocation : "Private hub studio " + serverLocation - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - elide: Text.ElideRight - color: textColour - } - - Button { - id: joinButton - x: (admin || connected) ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale) - y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width - background: Rectangle { - radius: width / 2 - color: available ? (joinButton.down ? joinAvailablePressedColour : (joinButton.hovered ? joinAvailableHoverColour : joinAvailableColour)) - : (joinButton.down ? joinUnavailablePressedColour : (joinButton.hovered ? joinUnavailableHoverColour : joinUnavailableColour)) - border.width: joinButton.down ? 1 : 0 - border.color: available ? joinAvailableStroke : joinUnavailableStroke - } - visible: !connected - onClicked: { - virtualstudio.studioToJoin = studioId; - virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected"; - virtualstudio.joinStudio(); - } - Image { - id: join - width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale - anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } - source: "join.svg" - sourceSize: Qt.size(join.width,join.height) - fillMode: Image.PreserveAspectFit - smooth: true - } - } - - Button { - id: leaveButton - x: (admin || connected) ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale) - y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width - background: Rectangle { - radius: width / 2 - color: leaveButton.down ? leavePressedColour : (leaveButton.hovered ? leaveHoverColour : leaveColour) - border.width: leaveButton.down ? 1 : 0 - border.color: leaveStroke - } - visible: connected - onClicked: { - virtualstudio.disconnect(); - } - Image { - id: leave - width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale - anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } - source: "leave.svg" - sourceSize: Qt.size(leave.width,leave.height) - fillMode: Image.PreserveAspectFit - smooth: true - } - } - - Text { - anchors.horizontalCenter: joinButton.horizontalCenter - y: 56 * virtualstudio.uiScale - text: connected ? "Leave" : "Join" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} - visible: true - color: textColour - } - - Button { - id: inviteButton - x: (admin || connected) ? parent.width - (142 * virtualstudio.uiScale) : parent.width - (65 * virtualstudio.uiScale) - y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width - background: Rectangle { - radius: width / 2 - color: inviteButton.down ? managePressedColour : (inviteButton.hovered ? manageHoverColour : manageColour) - border.width: inviteButton.down ? 1 : 0 - border.color: manageStroke - } - Timer { - id: copiedResetTimer - interval: 3000; running: false; repeat: false - onTriggered: inviteCopied = false; - } - onClicked: { - inviteCopied = true; - if (virtualstudio.testMode) { - hostname = "test.jacktrip.com"; - } - if (!inviteKeyString) { - clipboard.setText(qsTr("https://" + hostname + "/studios/" + studioId + "?invited=true")); - } else { - clipboard.setText(qsTr("https://" + hostname + "/studios/" + studioId + "?invited=" + inviteKeyString)); - } - copiedResetTimer.restart() - } - visible: true - Image { - id: shareImg - width: 24 * virtualstudio.uiScale; height: width - anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } - source: "share.svg" - sourceSize: Qt.size(shareImg.width,shareImg.height) - fillMode: Image.PreserveAspectFit - smooth: true - } - ToolTip { - parent: inviteButton - visible: !inviteCopied && inviteButton.hovered - bottomPadding: bottomToolTipMargin * virtualstudio.uiScale - rightPadding: rightToolTipMargin * virtualstudio.uiScale - delay: 100 - contentItem: Rectangle { - color: inviteToolTipBackgroundColour - radius: 3 - anchors.fill: parent - anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale - anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale - layer.enabled: true - border.width: 1 - border.color: tooltipStroke - - Text { - anchors.centerIn: parent - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale} - text: qsTr("Copy invite link for Studio") - color: inviteToolTipTextColour - } - } - background: Rectangle { - color: "transparent" - } - } - ToolTip { - parent: inviteButton - visible: inviteCopied - bottomPadding: bottomToolTipMargin * virtualstudio.uiScale - rightPadding: rightToolTipMargin * virtualstudio.uiScale - delay: 100 - contentItem: Rectangle { - color: inviteCopiedBackgroundColour - radius: 3 - anchors.fill: parent - anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale - anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale - layer.enabled: true - border.width: 1 - border.color: tooltipStroke - - Text { - anchors.centerIn: parent - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale} - text: qsTr("📋 Copied invitation link to Clipboard") - color: inviteCopiedTextColour - } - } - background: Rectangle { - color: "transparent" - } - } - } - - Text { - anchors.horizontalCenter: inviteButton.horizontalCenter - y: 56 * virtualstudio.uiScale - text: "Invite" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - visible: true - color: textColour - } - - Button { - id: manageButton - x: parent.width - (65 * virtualstudio.uiScale); y: topMargin * virtualstudio.uiScale - width: 40 * virtualstudio.uiScale; height: width - background: Rectangle { - radius: width / 2 - color: manageButton.down ? managePressedColour : (manageButton.hovered ? manageHoverColour : manageColour) - border.width: manageButton.down ? 1 : 0 - border.color: manageStroke - } - onClicked: { - var url = ""; - if (streamId === "") { - if (virtualstudio.testMode) { - url = "https://test.jacktrip.com/studios/" + studioId; - } else { - url = "https://app.jacktrip.com/studios/" + studioId; - } - } else { - if (virtualstudio.testMode) { - url = "https://next-test.jacktrip.com/@" + streamId + "/dashboard"; - } else { - url = "https://www.jacktrip.com/@" + streamId + "/dashboard"; - } - } - virtualstudio.openLink(qsTr(url)); - } - visible: admin || connected - Image { - id: manageImg - width: 20 * virtualstudio.uiScale; height: width - anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } - source: "manage.svg" - sourceSize: Qt.size(manageImg.width,manageImg.height) - fillMode: Image.PreserveAspectFit - smooth: true - } - } - - Text { - anchors.horizontalCenter: manageButton.horizontalCenter - y: 56 * virtualstudio.uiScale - text: "Manage" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - visible: admin || connected - color: textColour - } -} diff --git a/src/gui/VolumeSlider.qml b/src/gui/VolumeSlider.qml deleted file mode 100644 index 1f2a467..0000000 --- a/src/gui/VolumeSlider.qml +++ /dev/null @@ -1,122 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -Item { - width: parent.width - height: parent.height - - required property string labelText - required property string tooltipText - required property bool sliderEnabled - property bool showLabel: true - property int fontSize: 8 - - property string iconColor: virtualstudio.darkMode ? "#ACAFAF" : "gray" - property string sliderPressedColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC" - - Item { - anchors.fill: parent - anchors.verticalCenter: parent.verticalCenter - - Text { - id: label - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: 40 * virtualstudio.uiScale - horizontalAlignment: Text.AlignRight - text: labelText - font {family: "Poppins"; weight: Font.Medium; pixelSize: fontSize * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - visible: showLabel - } - - InfoTooltip { - id: tooltip - content: tooltipText - size: 16 - anchors.left: label.right - anchors.leftMargin: 2 * virtualstudio.uiScale - anchors.verticalCenter: label.verticalCenter - visible: showLabel - } - - AppIcon { - id: quieterIcon - anchors.left: showLabel ? tooltip.right : parent.left - anchors.leftMargin: showLabel ? 8 * virtualstudio.uiScale : 0 - anchors.verticalCenter: label.verticalCenter - width: 16 - height: 16 - icon.source: "quiet.svg" - color: iconColor - } - - AppIcon { - id: louderIcon - anchors.right: parent.right - anchors.verticalCenter: label.verticalCenter - width: 18 - height: 18 - icon.source: "loud.svg" - color: iconColor - } - - Slider { - id: slider - value: labelText == "Monitor" ? audio.monitorVolume : (labelText == "Studio" ? audio.outputVolume : audio.inputVolume ) - onMoved: { - if (labelText == "Monitor") { - audio.monitorVolume = value; - } else if (labelText == "Studio") { - audio.outputVolume = value; - } else { - audio.inputVolume = value; - } - } - enabled: sliderEnabled - from: 0.0 - to: 1.0 - stepSize: 0.01 - padding: 0 - anchors.left: quieterIcon.right - anchors.leftMargin: 4 * virtualstudio.uiScale - anchors.right: louderIcon.left - anchors.rightMargin: 4 * virtualstudio.uiScale - anchors.verticalCenter: label.verticalCenter - - background: Rectangle { - x: slider.leftPadding - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - implicitWidth: parent.width - implicitHeight: 8 * virtualstudio.uiScale - width: slider.availableWidth - height: implicitHeight - radius: 4 - color: "gray" - - Rectangle { - width: slider.visualPosition * parent.width - height: parent.height - radius: 4 - gradient: Gradient { - orientation: Gradient.Horizontal - GradientStop { position: 0.0; color: sliderEnabled ? "#67C6F3" : "light gray" } - GradientStop { position: 1.0; color: sliderEnabled ? "#00897b" : "light gray" } - } - } - } - - handle: Rectangle { - x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - implicitWidth: 18 * virtualstudio.uiScale - implicitHeight: 18 * virtualstudio.uiScale - radius: implicitWidth / 2 - color: slider.pressed ? sliderPressedColour : "white" - border.width: 3 - border.color: sliderEnabled ? "#00897b" : "light gray" - } - } - } -} diff --git a/src/gui/Web.qml b/src/gui/Web.qml deleted file mode 100644 index 13927f8..0000000 --- a/src/gui/Web.qml +++ /dev/null @@ -1,10 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Loader { - anchors.fill: parent - source: "WebEngine.qml" - - // TODO: Add support for QtWebView - // source: useWebEngine ? "WebEngine.qml" : "WebView.qml" -} diff --git a/src/gui/WebEngine.qml b/src/gui/WebEngine.qml deleted file mode 100644 index a45b459..0000000 --- a/src/gui/WebEngine.qml +++ /dev/null @@ -1,121 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtWebEngine - -Item { - width: parent.width; height: parent.height - clip: true - - function contentScriptFactory (port) { - return ` - // add script tag for qwebchannel - document.head.addEventListener("initqwebchannel", () => { - - var script = document.createElement("script"); - script.onload = function () { - var url = "ws://localhost:${port}"; - var socket = new WebSocket(url); - - socket.onclose = function() { - console.error("[QT] web channel closed"); - }; - socket.onerror = function(event) { - console.error("[QT] web channel error: " + event.type); - }; - socket.onopen = function() { - new QWebChannel(socket, function(channel) { - console.log("[QT] Socket opened"); - - // make core object accessible globally - window.virtualstudio = channel.objects.virtualstudio; - window.auth = channel.objects.auth; - window.clipboard = channel.objects.clipboard; - - const event = new CustomEvent("qwebchannelinitialized"); - document.head.dispatchEvent(event); - console.log("[QT] Dispatched qwebchannelinitialized event"); - console.log("[QT] Connected to WebChannel, ready to send/receive messages!"); - }); - } - } - script.setAttribute("src", "qrc:///qtwebchannel/qwebchannel.js"); - script.setAttribute("type", "text/javascript"); - document.head.appendChild(script); - console.log("[QT] Added qwebchannel initialization script to DOM."); - }); - console.log("[QT] Added initqwebchannel event listener"); - ` - } - - Rectangle { - id: web - anchors.fill: parent - color: backgroundColour - - property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" - property string studioId: virtualstudio.currentStudio.id - - WebEngineView { - id: webEngineView - anchors.fill: parent - settings.javascriptCanAccessClipboard: true - settings.javascriptCanPaste: true - settings.screenCaptureEnabled: true - profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}` - url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}` - - // useful for debugging - // onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID) { - // console.log(level, message, lineNumber, sourceID); - // } - - // useful for debugging - // onLoadingChanged: function(loadRequest) { - // console.log("onLoadingChanged", loadRequest.errorCode, loadRequest.errorDomain, loadRequest.errorString, loadRequest.status, loadRequest.url); - // } - - onContextMenuRequested: function(request) { - // this disables the default context menu: https://doc.qt.io/qt-6.2/qml-qtwebengine-contextmenurequest.html#accepted-prop - request.accepted = true; - } - - onNewWindowRequested: function(request) { - Qt.openUrlExternally(request.requestedUrl); - } - - onFeaturePermissionRequested: function(securityOrigin, feature) { - webEngineView.grantFeaturePermission(securityOrigin, feature, true); - } - - onRenderProcessTerminated: function(terminationStatus, exitCode) { - var status = ""; - switch (terminationStatus) { - case WebEngineView.NormalTerminationStatus: - status = "(normal exit)"; - break; - case WebEngineView.AbnormalTerminationStatus: - status = "(abnormal exit)"; - break; - case WebEngineView.CrashedTerminationStatus: - status = "(crashed)"; - break; - case WebEngineView.KilledTerminationStatus: - status = "(killed)"; - break; - } - console.log("Render process exited with code " + exitCode + " " + status); - } - - onNavigationRequested: function(request) { - webEngineView.userScripts.collection = [ - { - name: "script", - sourceCode: contentScriptFactory(virtualstudio.webChannelPort), - injectionPoint: WebEngineScript.DocumentReady, - worldId: WebEngineScript.MainWorld - } - ] - } - } - } -} diff --git a/src/gui/WebNull.qml b/src/gui/WebNull.qml deleted file mode 100644 index 6f1c6de..0000000 --- a/src/gui/WebNull.qml +++ /dev/null @@ -1,12 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - width: parent.width; height: parent.height - clip: true - - Item { - id: webNull - anchors.fill: parent - } -} diff --git a/src/gui/WebSocketTransport.cpp b/src/gui/WebSocketTransport.cpp deleted file mode 100644 index d9c9688..0000000 --- a/src/gui/WebSocketTransport.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, -*author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "WebSocketTransport.h" - -#include -#include -#include -#include - -/*! - \brief QWebChannelAbstractSocket implementation that uses a QWebSocket internally. - The transport delegates all messages received over the QWebSocket over its - textMessageReceived signal. Analogously, all calls to sendTextMessage will - be send over the QWebSocket to the remote client. -*/ - -QT_BEGIN_NAMESPACE - -/*! - Construct the transport object and wrap the given socket. - The socket is also set as the parent of the transport object. -*/ -WebSocketTransport::WebSocketTransport(QWebSocket* socket) - : QWebChannelAbstractTransport(socket), m_socket(socket) -{ - connect(socket, &QWebSocket::textMessageReceived, this, - &WebSocketTransport::textMessageReceived); -} - -/*! - Destroys the WebSocketTransport. -*/ -WebSocketTransport::~WebSocketTransport() {} - -/*! - Serialize the JSON message and send it as a text message via the WebSocket to the - client. -*/ -void WebSocketTransport::sendMessage(const QJsonObject& message) -{ - QJsonDocument doc(message); - m_socket->sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); -} - -/*! - Deserialize the stringified JSON messageData and emit messageReceived. -*/ -void WebSocketTransport::textMessageReceived(const QString& messageData) -{ - QJsonParseError error; - QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error); - if (error.error) { - qWarning() << "Failed to parse text message as JSON object:" << messageData - << "Error is:" << error.errorString(); - return; - } else if (!message.isObject()) { - qWarning() << "Received JSON message that is not an object: " << messageData; - return; - } - emit messageReceived(message.object(), this); -} - -QT_END_NAMESPACE \ No newline at end of file diff --git a/src/gui/WebSocketTransport.h b/src/gui/WebSocketTransport.h deleted file mode 100644 index 4f93b23..0000000 --- a/src/gui/WebSocketTransport.h +++ /dev/null @@ -1,61 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, -*author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef WEBSOCKETTRANSPORT_H -#define WEBSOCKETTRANSPORT_H - -#include - -QT_BEGIN_NAMESPACE - -class QWebSocket; -class WebSocketTransport : public QWebChannelAbstractTransport -{ - Q_OBJECT - public: - explicit WebSocketTransport(QWebSocket* socket); - virtual ~WebSocketTransport(); - - void sendMessage(const QJsonObject& message) Q_DECL_OVERRIDE; - - private Q_SLOTS: - void textMessageReceived(const QString& message); - - private: - QWebSocket* m_socket; -}; - -QT_END_NAMESPACE - -#endif // WEBSOCKETTRANSPORT_H \ No newline at end of file diff --git a/src/gui/WebView.qml b/src/gui/WebView.qml deleted file mode 100644 index 871a1d9..0000000 --- a/src/gui/WebView.qml +++ /dev/null @@ -1,23 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtWebView - -Item { - width: parent.width; height: parent.height - clip: true - - Item { - id: web - anchors.fill: parent - - property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" - property string studioId: virtualstudio.currentStudio.id - - WebView { - id: webEngineView - anchors.fill: parent - httpUserAgent: `JackTrip/${virtualstudio.versionString}` - url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}` - } - } -} diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 5c25979..bfe7afe 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -87,7 +87,7 @@ About::About(QWidget* parent) : QDialog(parent), m_ui(new Ui::About) m_ui->aboutLabel->text().replace(QLatin1String("%BUILD%"), buildString)); #ifdef __APPLE__ - m_ui->aboutImage->setPixmap(QPixmap(":/qjacktrip/about@2x.png")); + m_ui->aboutImage->setPixmap(QPixmap(":/images/icon_256.png")); #endif aboutText.setHtml(m_ui->aboutLabel->text()); diff --git a/src/gui/about.png b/src/gui/about.png deleted file mode 100644 index 0f877b9..0000000 Binary files a/src/gui/about.png and /dev/null differ diff --git a/src/gui/about.ui b/src/gui/about.ui index e644f2e..e30f1a6 100644 --- a/src/gui/about.ui +++ b/src/gui/about.ui @@ -41,7 +41,7 @@ border: 4px solid black; - :/qjacktrip/about.png + :/images/icon_128.png true diff --git a/src/gui/about@2x.png b/src/gui/about@2x.png deleted file mode 100644 index f51508b..0000000 Binary files a/src/gui/about@2x.png and /dev/null differ diff --git a/src/gui/check.svg b/src/gui/check.svg deleted file mode 100644 index 333cf07..0000000 --- a/src/gui/check.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/close.svg b/src/gui/close.svg deleted file mode 100644 index e96a4e7..0000000 --- a/src/gui/close.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/cog.svg b/src/gui/cog.svg deleted file mode 100644 index ba5645a..0000000 --- a/src/gui/cog.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/ethernet.svg b/src/gui/ethernet.svg deleted file mode 100644 index d35645b..0000000 --- a/src/gui/ethernet.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/expand_less.svg b/src/gui/expand_less.svg deleted file mode 100644 index 08039b6..0000000 --- a/src/gui/expand_less.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/expand_more.svg b/src/gui/expand_more.svg deleted file mode 100644 index 71b70fd..0000000 --- a/src/gui/expand_more.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/externalMic.svg b/src/gui/externalMic.svg deleted file mode 100644 index ad8e7dd..0000000 --- a/src/gui/externalMic.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/AE.svg b/src/gui/flags/AE.svg deleted file mode 100644 index 59ddafd..0000000 --- a/src/gui/flags/AE.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/AU.svg b/src/gui/flags/AU.svg deleted file mode 100644 index f91b013..0000000 --- a/src/gui/flags/AU.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/BE.svg b/src/gui/flags/BE.svg deleted file mode 100644 index cc1b013..0000000 --- a/src/gui/flags/BE.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/BR.svg b/src/gui/flags/BR.svg deleted file mode 100644 index f4dbb02..0000000 --- a/src/gui/flags/BR.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/CA.svg b/src/gui/flags/CA.svg deleted file mode 100644 index 457d316..0000000 --- a/src/gui/flags/CA.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/CH.svg b/src/gui/flags/CH.svg deleted file mode 100644 index 498b7d1..0000000 --- a/src/gui/flags/CH.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/DE.svg b/src/gui/flags/DE.svg deleted file mode 100644 index df0775b..0000000 --- a/src/gui/flags/DE.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/FR.svg b/src/gui/flags/FR.svg deleted file mode 100644 index 9f02836..0000000 --- a/src/gui/flags/FR.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/GB.svg b/src/gui/flags/GB.svg deleted file mode 100644 index 4ada58a..0000000 --- a/src/gui/flags/GB.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/HK.svg b/src/gui/flags/HK.svg deleted file mode 100644 index 284a722..0000000 --- a/src/gui/flags/HK.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/ID.svg b/src/gui/flags/ID.svg deleted file mode 100644 index 45d3745..0000000 --- a/src/gui/flags/ID.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/IT.svg b/src/gui/flags/IT.svg deleted file mode 100644 index 17b1314..0000000 --- a/src/gui/flags/IT.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/JP.svg b/src/gui/flags/JP.svg deleted file mode 100644 index 92eb885..0000000 --- a/src/gui/flags/JP.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/RO.svg b/src/gui/flags/RO.svg deleted file mode 100644 index fabf12e..0000000 --- a/src/gui/flags/RO.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/SE.svg b/src/gui/flags/SE.svg deleted file mode 100644 index 7ec1787..0000000 --- a/src/gui/flags/SE.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/SG.svg b/src/gui/flags/SG.svg deleted file mode 100644 index c374c47..0000000 --- a/src/gui/flags/SG.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/TW.svg b/src/gui/flags/TW.svg deleted file mode 100644 index c3660f1..0000000 --- a/src/gui/flags/TW.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/US.svg b/src/gui/flags/US.svg deleted file mode 100644 index dc427e7..0000000 --- a/src/gui/flags/US.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/flags/ZA.svg b/src/gui/flags/ZA.svg deleted file mode 100644 index 1b294c9..0000000 --- a/src/gui/flags/ZA.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/headphones.svg b/src/gui/headphones.svg deleted file mode 100644 index 8041f67..0000000 --- a/src/gui/headphones.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/help.svg b/src/gui/help.svg deleted file mode 100644 index d2a8d02..0000000 --- a/src/gui/help.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/icon.png b/src/gui/icon.png deleted file mode 100644 index 9344dff..0000000 Binary files a/src/gui/icon.png and /dev/null differ diff --git a/src/gui/jacktrip white.png b/src/gui/jacktrip white.png deleted file mode 100644 index 7ba69b0..0000000 Binary files a/src/gui/jacktrip white.png and /dev/null differ diff --git a/src/gui/jacktrip.png b/src/gui/jacktrip.png deleted file mode 100644 index c4b998a..0000000 Binary files a/src/gui/jacktrip.png and /dev/null differ diff --git a/src/gui/join.svg b/src/gui/join.svg deleted file mode 100644 index bc50d97..0000000 --- a/src/gui/join.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/language.svg b/src/gui/language.svg deleted file mode 100644 index 8273aed..0000000 --- a/src/gui/language.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/leave.svg b/src/gui/leave.svg deleted file mode 100644 index c44f7e7..0000000 --- a/src/gui/leave.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/logo.svg b/src/gui/logo.svg deleted file mode 100644 index 508c81b..0000000 --- a/src/gui/logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/loud.svg b/src/gui/loud.svg deleted file mode 100644 index b3aeb16..0000000 --- a/src/gui/loud.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/gui/manage.svg b/src/gui/manage.svg deleted file mode 100644 index 7d9ae31..0000000 --- a/src/gui/manage.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/mic.svg b/src/gui/mic.svg deleted file mode 100644 index 520b7f9..0000000 --- a/src/gui/mic.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/micoff.svg b/src/gui/micoff.svg deleted file mode 100644 index 8816538..0000000 --- a/src/gui/micoff.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/network.svg b/src/gui/network.svg deleted file mode 100644 index 4aa8827..0000000 --- a/src/gui/network.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/gui/networkCheck.svg b/src/gui/networkCheck.svg deleted file mode 100644 index 3b402a6..0000000 --- a/src/gui/networkCheck.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/private.svg b/src/gui/private.svg deleted file mode 100644 index c08eb3d..0000000 --- a/src/gui/private.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/public.svg b/src/gui/public.svg deleted file mode 100644 index 1407d57..0000000 --- a/src/gui/public.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/qjacktrip.cpp b/src/gui/qjacktrip.cpp index 0cfbabe..f7a91ec 100644 --- a/src/gui/qjacktrip.cpp +++ b/src/gui/qjacktrip.cpp @@ -35,9 +35,6 @@ #include #include "about.h" -#ifndef NO_VS -#include "virtualstudio.h" -#endif #include "ui_qjacktrip.h" #ifdef USE_WEAK_JACK #include "weak_libjack.h" @@ -54,9 +51,9 @@ #include "../Meter.h" #include "../Reverb.h" -QJackTrip::QJackTrip(QSharedPointer settings, bool suppressCommandlineWarning, - QWidget* parent) +QJackTrip::QJackTrip(UserInterface& interface, QWidget* parent) : QMainWindow(parent) + , m_interface(interface) , m_ui(new Ui::QJackTrip) , m_netManager(new QNetworkAccessManager(this)) , m_statsDialog(new MessageDialog(this, QStringLiteral("Stats"))) @@ -66,11 +63,9 @@ QJackTrip::QJackTrip(QSharedPointer settings, bool suppressCommandline , m_jackTripRunning(false) , m_isExiting(false) , m_exitSent(false) - , m_suppressCommandlineWarning(suppressCommandlineWarning) , m_hideWarning(false) { m_ui->setupUi(this); - m_cliSettings = settings; // Set up our debug window, and relay everything to our real cout. std::cout.rdbuf(m_debugDialog->getOutputStream()->rdbuf()); @@ -109,9 +104,11 @@ QJackTrip::QJackTrip(QSharedPointer settings, bool suppressCommandline QStringLiteral("(This is for JackTrip's inbuilt authentication system. To easily " "connect to a Virtual Studio server, download a Virtual Studio " "enabled version of JackTrip.)")); + m_ui->vsModeButton->setVisible(false); #else connect(m_ui->vsModeButton, &QPushButton::clicked, this, &QJackTrip::virtualStudioMode); + m_ui->vsModeButton->setVisible(true); #endif connect(m_ui->autoPatchComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [=]() { @@ -249,7 +246,6 @@ QJackTrip::QJackTrip(QSharedPointer settings, bool suppressCommandline m_ui->autoPatchGroupBox->setVisible(false); m_ui->requireAuthGroupBox->setVisible(false); m_ui->backendWarningLabel->setVisible(false); - m_ui->vsModeButton->setVisible(false); m_ui->inputGroupBox->setVisible(false); m_ui->outputGroupBox->setVisible(false); @@ -378,10 +374,10 @@ void QJackTrip::showEvent(QShowEvent* event) QMainWindow::showEvent(event); if (m_firstShow) { QSettings settings; - loadSettings(m_cliSettings.data()); + loadSettings(&m_interface.getSettings()); // Display a warning about any ignored command line options. - if (m_cliSettings->guiIgnoresArguments() && !m_suppressCommandlineWarning) { + if (m_interface.getSettings().guiIgnoresArguments()) { QMessageBox msgBox; msgBox.setText( "You have supplied command line options that the GUI version of JackTrip " @@ -484,14 +480,6 @@ void QJackTrip::showEvent(QShowEvent* event) } } -#ifndef NO_VS -void QJackTrip::setVs(QSharedPointer vs) -{ - m_vs = vs; - m_ui->vsModeButton->setVisible(!m_vs.isNull()); -} -#endif - void QJackTrip::processFinished() { if (!m_jackTripRunning) { @@ -499,9 +487,7 @@ void QJackTrip::processFinished() return; } m_jackTripRunning = false; -#ifdef __APPLE__ - m_noNap.enableNap(); -#endif + m_interface.enableNap(); m_ui->disconnectButton->setEnabled(false); if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) { m_udpHub.reset(); @@ -1058,9 +1044,7 @@ void QJackTrip::start() m_ui->addressComboBox->insertItem(0, serverAddress); m_ui->addressComboBox->setCurrentIndex(0); -#ifdef __APPLE__ - m_noNap.disableNap(); -#endif + m_interface.disableNap(); } void QJackTrip::stop() @@ -1123,9 +1107,9 @@ void QJackTrip::updatedOutputMeasurements(const float* valuesInDb, int numChanne #ifndef NO_VS void QJackTrip::virtualStudioMode() { - this->hide(); - m_vs->show(); - m_vs->toVirtualStudio(); + m_interface.setMode(UserInterface::MODE_VS); + QSettings settings; + settings.setValue(QStringLiteral("UiMode"), UserInterface::MODE_VS); } #endif @@ -2044,3 +2028,24 @@ QJackTrip::~QJackTrip() std::cout.rdbuf(m_realCout.rdbuf()); std::cerr.rdbuf(m_realCerr.rdbuf()); } + +QCoreApplication* QJackTrip::createApplication(int& argc, char* argv[]) +{ + // Turn on high DPI support. +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +#endif + + // Fix for display scaling like 125% or 150% on Windows +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) +#if defined(NO_VS) && defined(_WIN32) + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::RoundPreferFloor); +#else + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif // NO_VS +#endif // QT_VERSION + + return new QApplication(argc, argv); +} diff --git a/src/gui/qjacktrip.h b/src/gui/qjacktrip.h index 8b9a33f..0c61034 100644 --- a/src/gui/qjacktrip.h +++ b/src/gui/qjacktrip.h @@ -41,13 +41,10 @@ #include "../JackTrip.h" #include "../Settings.h" #include "../UdpHubListener.h" +#include "../UserInterface.h" #include "messageDialog.h" #include "vuMeter.h" -#ifdef __APPLE__ -#include "NoNap.h" -#endif - #ifdef RT_AUDIO #include #endif @@ -57,28 +54,18 @@ namespace Ui class QJackTrip; } // namespace Ui -#ifndef NO_VS -class VirtualStudio; -#endif - class QJackTrip : public QMainWindow { Q_OBJECT public: - explicit QJackTrip(QSharedPointer settings, - bool suppressCommandlineWarning = false, - QWidget* parent = nullptr); + explicit QJackTrip(UserInterface& interface, QWidget* parent = nullptr); ~QJackTrip() override; void closeEvent(QCloseEvent* event) override; void resizeEvent(QResizeEvent* event) override; void showEvent(QShowEvent* event) override; - -#ifndef NO_VS - enum uiModeT { UNSET, VIRTUAL_STUDIO, STANDARD }; - void setVs(QSharedPointer vs); -#endif + static QCoreApplication* createApplication(int& argc, char* argv[]); signals: void signalExit(); @@ -129,6 +116,7 @@ class QJackTrip : public QMainWindow JackTrip::hubConnectionModeT hubModeFromPatchType(patchTypeT patchType); + UserInterface& m_interface; QScopedPointer m_ui; QScopedPointer m_udpHub; QScopedPointer m_jackTrip; @@ -144,7 +132,6 @@ class QJackTrip : public QMainWindow bool m_isExiting; bool m_exitSent; - QSharedPointer m_cliSettings; bool m_suppressCommandlineWarning; float m_meterMax = 0.0; @@ -164,13 +151,6 @@ class QJackTrip : public QMainWindow QLabel m_autoQueueIndicator; bool m_hideWarning; bool m_firstShow = true; - -#ifndef NO_VS - QSharedPointer m_vs; -#endif -#ifdef __APPLE__ - NoNap m_noNap; -#endif }; #endif // QJACKTRIP_H diff --git a/src/gui/qjacktrip.qrc b/src/gui/qjacktrip.qrc deleted file mode 100644 index 7f5e0dc..0000000 --- a/src/gui/qjacktrip.qrc +++ /dev/null @@ -1,98 +0,0 @@ - - - about@2x.png - about.png - icon.png - - - vs.qml - FirstLaunch.qml - Login.qml - LearnMoreButton.qml - Recommendations.qml - Permissions.qml - ChangeDevices.qml - Studio.qml - Browse.qml - AudioSettings.qml - Settings.qml - Meter.qml - MeterBars.qml - Connected.qml - CreateStudio.qml - Failed.qml - Setup.qml - SectionHeading.qml - Footer.qml - VolumeSlider.qml - DeviceControls.qml - DeviceControlsGroup.qml - DeviceRefreshButton.qml - DeviceWarning.qml - InfoTooltip.qml - Web.qml - WebView.qml - WebEngine.qml - WebNull.qml - FeedbackSurvey.qml - AppIcon.qml - logo.svg - wedge.svg - wedge_inactive.svg - private.svg - public.svg - join.svg - leave.svg - manage.svg - speed.svg - share.svg - start.svg - star.svg - cog.svg - mic.svg - language.svg - micoff.svg - help.svg - quiet.svg - loud.svg - refresh.svg - ethernet.svg - networkCheck.svg - externalMic.svg - check.svg - warning.svg - expand_less.svg - expand_more.svg - sentiment_very_dissatisfied.svg - headphones.svg - Prompt.svg - network.svg - video.svg - close.svg - jacktrip.png - jacktrip white.png - JTOriginal.png - JTVS.png - flags/AE.svg - flags/AU.svg - flags/BE.svg - flags/BR.svg - flags/CA.svg - flags/CH.svg - flags/DE.svg - flags/FR.svg - flags/GB.svg - flags/HK.svg - flags/ID.svg - flags/IT.svg - flags/JP.svg - flags/RO.svg - flags/SE.svg - flags/SG.svg - flags/TW.svg - flags/US.svg - flags/ZA.svg - Poppins-Bold.ttf - Poppins-Regular.ttf - - diff --git a/src/gui/qjacktrip.ui b/src/gui/qjacktrip.ui index b6d0dd5..0c537cc 100644 --- a/src/gui/qjacktrip.ui +++ b/src/gui/qjacktrip.ui @@ -14,8 +14,8 @@ JackTrip - - :/qjacktrip/icon.png:/qjacktrip/icon.png + + :/images/icon_32.png:/images/icon_32.png diff --git a/src/gui/qjacktrip_novs.qrc b/src/gui/qjacktrip_novs.qrc deleted file mode 100644 index 179c85a..0000000 --- a/src/gui/qjacktrip_novs.qrc +++ /dev/null @@ -1,7 +0,0 @@ - - - about@2x.png - about.png - icon.png - - diff --git a/src/gui/quiet.svg b/src/gui/quiet.svg deleted file mode 100644 index b2ea070..0000000 --- a/src/gui/quiet.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/gui/refresh.svg b/src/gui/refresh.svg deleted file mode 100644 index d2c5a92..0000000 --- a/src/gui/refresh.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/gui/sentiment_very_dissatisfied.svg b/src/gui/sentiment_very_dissatisfied.svg deleted file mode 100644 index 5af1e4b..0000000 --- a/src/gui/sentiment_very_dissatisfied.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/gui/share.svg b/src/gui/share.svg deleted file mode 100644 index 77f6e6c..0000000 --- a/src/gui/share.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/speed.svg b/src/gui/speed.svg deleted file mode 100644 index 7ab86e4..0000000 --- a/src/gui/speed.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/gui/star.svg b/src/gui/star.svg deleted file mode 100644 index 971dd91..0000000 --- a/src/gui/star.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/start.svg b/src/gui/start.svg deleted file mode 100644 index f75a587..0000000 --- a/src/gui/start.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/video.svg b/src/gui/video.svg deleted file mode 100644 index a1b644f..0000000 --- a/src/gui/video.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/virtualstudio.cpp b/src/gui/virtualstudio.cpp deleted file mode 100644 index 7223a99..0000000 --- a/src/gui/virtualstudio.cpp +++ /dev/null @@ -1,1638 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file virtualstudio.cpp - * \author Matt Horton, based on code by Aaron Wyatt - * \date March 2022 - */ - -#include "virtualstudio.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// TODO: remove me; including this to work-around this bug -// https://bugreports.qt.io/browse/QTBUG-55199 -#include - -#include "../JackTrip.h" -#include "../Settings.h" -#include "../jacktrip_globals.h" -#include "WebSocketTransport.h" -#include "about.h" -#include "qjacktrip.h" -#include "vsApi.h" -#include "vsAudio.h" -#include "vsAuth.h" -#include "vsDevice.h" -#include "vsWebSocket.h" - -#ifdef __APPLE__ -#include "vsMacPermissions.h" -#else -#include "vsPermissions.h" -#endif - -#ifdef _WIN32 -#include -#endif - -VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) - : QObject(parent) - , m_audioConfigPtr( - new VsAudio(this)) // this needs to be constructed before loadSettings() - , m_showFirstRun(firstRun) -{ - // load or initialize persisted settings - loadSettings(); - - // TODO: remove me; this is a hack for this bug - // https://bugreports.qt.io/browse/QTBUG-55199 - QSvgGenerator svgImageHack; - - // use a singleton QNetworkAccessManager - // WARNING: using a raw pointer and intentionally leaking this because - // it crashes at shutdown if you try to destruct it directly or try - // calling QObject::deleteLater() - m_networkAccessManagerPtr = new QNetworkAccessManager; - - // instantiate API - m_api.reset(new VsApi(m_networkAccessManagerPtr)); - m_api->setApiHost(PROD_API_HOST); - if (m_testMode) { - m_api->setApiHost(TEST_API_HOST); - } - - // instantiate auth - m_auth.reset(new VsAuth(m_networkAccessManagerPtr, m_api.data())); - connect(m_auth.data(), &VsAuth::authSucceeded, this, - &VirtualStudio::slotAuthSucceeded); - connect(m_auth.data(), &VsAuth::refreshTokenFailed, this, [=]() { - m_auth->authenticate(QStringLiteral("")); // retry without using refresh token - }); - connect(m_auth.data(), &VsAuth::fetchUserInfoFailed, this, [=]() { - m_auth->authenticate(QStringLiteral("")); // retry without using refresh token - }); - connect(m_auth.data(), &VsAuth::deviceCodeExpired, this, [=]() { - m_auth->authenticate(QStringLiteral("")); // retry without using refresh token - }); - - m_webChannelServer.reset(new QWebSocketServer( - QStringLiteral("Qt6 Virtual Studio Server"), QWebSocketServer::NonSecureMode)); - connect(m_webChannelServer.data(), &QWebSocketServer::newConnection, this, [=]() { - m_webChannel->connectTo( - new WebSocketTransport(m_webChannelServer->nextPendingConnection())); - }); - - m_webChannel.reset(new QWebChannel()); - m_webChannel->registerObject(QStringLiteral("virtualstudio"), this); - - // Load our font for our qml interface - QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Regular.ttf")); - QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Bold.ttf")); - - // Set our font scaling to convert points to pixels - m_fontScale = float(4.0 / 3.0); - - // Initialize timer needed for network outage indicator - m_networkOutageTimer.setTimerType(Qt::CoarseTimer); - m_networkOutageTimer.setSingleShot(true); - m_networkOutageTimer.setInterval(5000); - m_networkOutageTimer.callOnTimeout([&]() { - if (m_devicePtr.isNull()) - return; - m_devicePtr->setNetworkOutage(false); - emit updatedNetworkOutage(false); - }); - - if ((m_uiMode == QJackTrip::UNSET && vsFtux()) - || (m_uiMode == QJackTrip::VIRTUAL_STUDIO)) { - m_windowState = QStringLiteral("login"); - } - - // register QML types - qmlRegisterType("org.jacktrip.jacktrip", 1, 0, "VsServerInfo"); - - // setup QML view - m_view.engine()->rootContext()->setContextProperty(QStringLiteral("virtualstudio"), - this); - m_view.engine()->rootContext()->setContextProperty(QStringLiteral("auth"), - m_auth.get()); - m_view.engine()->rootContext()->setContextProperty(QStringLiteral("audio"), - m_audioConfigPtr.get()); - m_view.engine()->rootContext()->setContextProperty( - QStringLiteral("permissions"), - QVariant::fromValue(&m_audioConfigPtr->getPermissions())); - m_view.setSource(QUrl(QStringLiteral("qrc:/vs/vs.qml"))); - m_view.setMinimumSize(QSize(800, 640)); - // m_view.setMaximumSize(QSize(696, 577)); - m_view.setResizeMode(QQuickView::SizeRootObjectToView); - m_view.resize(800 * m_uiScale, 640 * m_uiScale); - - // Connect our timers - connect(&m_refreshTimer, &QTimer::timeout, this, [&]() { - emit periodicRefresh(); - }); - connect(&m_heartbeatTimer, &QTimer::timeout, this, &VirtualStudio::sendHeartbeat, - Qt::QueuedConnection); - - // QueuedConnection since refreshFinished is sometimes signaled from a network reply - // thread - connect(this, &VirtualStudio::refreshFinished, this, &VirtualStudio::joinStudio, - Qt::QueuedConnection); - - // handle audio config errors - connect(&m_audioConfigPtr->getWorker(), &VsAudioWorker::signalError, this, - &VirtualStudio::processError, Qt::QueuedConnection); - - // when connected to server, trigger UI modal when feedback is detected - connect(m_audioConfigPtr.get(), &VsAudio::feedbackDetected, this, - &VirtualStudio::detectedFeedbackLoop, Qt::QueuedConnection); - - // call exit() when the UI window is closed - connect(&m_view, &VsQuickView::windowClose, this, &VirtualStudio::exit, - Qt::QueuedConnection); - - if ((m_uiMode == QJackTrip::UNSET && vsFtux()) - || (m_uiMode == QJackTrip::VIRTUAL_STUDIO)) { - login(); - } -} - -void VirtualStudio::setStandardWindow(QSharedPointer window) -{ - m_standardWindow = window; -} - -void VirtualStudio::setCLISettings(QSharedPointer settings) -{ - m_cliSettings = settings; -} - -void VirtualStudio::show() -{ - if (m_checkSsl) { - // Check our available SSL version - QString sslVersion = QSslSocket::sslLibraryVersionString(); - // Important: this needs to be output with qDebug rather than to std::cout - // otherwise it may get passed to an existing JackTrip instance in place of our - // deeplink. (Need to find the root cause of this.) - qDebug() << "SSL Library: " << sslVersion; - if (sslVersion.isEmpty()) { - QMessageBox msgBox; - msgBox.setText( - QStringLiteral("OpenSSL was not found. You will not be able to connect " - "to the Virtual Studio server.")); - msgBox.setWindowTitle(QStringLiteral("SSL Error")); - msgBox.exec(); - } - m_checkSsl = false; - } - - while (m_view.status() == QQuickView::Loading) { - // I don't think there is any need to load network data, but just in case - // See https://doc.qt.io/qt-6/qquickview.html#Status-enum - qDebug() << "JackTrip is still loading the QML view"; - QThread::sleep(1); - } - - if (m_view.status() != QQuickView::Ready) { - QMessageBox msgBox; - msgBox.setText( - "JackTrip detected that some modules required for the " - "Virtual Studio mode are missing on your system. " - "Click \"OK\" to proceed to classic mode.\n\n" - "Details: JackTrip failed to load the QML view. " - "This is likely caused by missing QML plugins. " - "Please consult support.jacktrip.com for possible solutions."); - msgBox.setWindowTitle(QStringLiteral("JackTrip Is Missing QML Modules")); - connect(&msgBox, &QMessageBox::finished, this, &VirtualStudio::toStandard, - Qt::QueuedConnection); - msgBox.exec(); - return; - } - - raiseToTop(); -} - -void VirtualStudio::raiseToTop() -{ - if (m_view.status() != QQuickView::Ready) - return; - m_view.show(); // Restore from systray - m_view.raise(); // raise to top - m_view.requestActivate(); // focus on window -} - -int VirtualStudio::webChannelPort() -{ - return m_webChannelPort; -} - -bool VirtualStudio::showFirstRun() -{ - return m_showFirstRun; -} - -void VirtualStudio::setShowFirstRun(bool show) -{ - if (m_showFirstRun == show) - return; - m_showFirstRun = show; - emit showFirstRunChanged(); -} - -bool VirtualStudio::hasRefreshToken() -{ - return !m_refreshToken.isEmpty(); -} - -QString VirtualStudio::versionString() -{ - return QLatin1String(gVersion); -} - -QString VirtualStudio::logoSection() -{ - return m_logoSection; -} - -QString VirtualStudio::connectedErrorMsg() -{ - return m_connectedErrorMsg; -} - -void VirtualStudio::setConnectedErrorMsg(const QString& msg) -{ - if (m_connectedErrorMsg == msg) - return; - m_connectedErrorMsg = msg; - emit connectedErrorMsgChanged(); -} - -bool VirtualStudio::networkOutage() -{ - return m_devicePtr.isNull() ? false : m_devicePtr->getNetworkOutage(); -} - -QJsonObject VirtualStudio::regions() -{ - return m_regions; -} - -QJsonObject VirtualStudio::userMetadata() -{ - return m_userMetadata; -} - -QString VirtualStudio::connectionState() -{ - return m_connectionState; -} - -QJsonObject VirtualStudio::networkStats() -{ - return m_networkStats; -} - -QString VirtualStudio::updateChannel() -{ - return m_updateChannel; -} - -void VirtualStudio::setUpdateChannel(const QString& channel) -{ - if (m_updateChannel == channel) - return; - m_updateChannel = channel; - emit updateChannelChanged(); -} - -bool VirtualStudio::showInactive() -{ - return m_showInactive; -} - -void VirtualStudio::setShowInactive(bool inactive) -{ - if (m_showInactive == inactive) - return; - m_showInactive = inactive; - emit showInactiveChanged(); - - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - settings.setValue(QStringLiteral("ShowInactive"), m_showInactive); - settings.endGroup(); -} - -bool VirtualStudio::showSelfHosted() -{ - return m_showSelfHosted; -} - -void VirtualStudio::setShowSelfHosted(bool selfHosted) -{ - if (m_showSelfHosted == selfHosted) - return; - m_showSelfHosted = selfHosted; - emit showSelfHostedChanged(); - - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - settings.setValue(QStringLiteral("ShowSelfHosted"), m_showSelfHosted); - settings.endGroup(); -} - -bool VirtualStudio::showCreateStudio() -{ - return m_showCreateStudio; -} - -void VirtualStudio::setShowCreateStudio(bool createStudio) -{ - if (m_showCreateStudio == createStudio) - return; - m_showCreateStudio = createStudio; - emit showCreateStudioChanged(); -} - -bool VirtualStudio::showDeviceSetup() -{ - return m_showDeviceSetup; -} - -void VirtualStudio::setShowDeviceSetup(bool show) -{ - if (m_showDeviceSetup == show) - return; - m_showDeviceSetup = show; - emit showDeviceSetupChanged(); -} - -QString VirtualStudio::windowState() -{ - return m_windowState; -} - -void VirtualStudio::setWindowState(QString state) -{ - if (m_windowState == state) - return; - m_windowState = state; - emit windowStateUpdated(); -} - -QString VirtualStudio::apiHost() -{ - return m_apiHost; -} - -void VirtualStudio::setApiHost(QString host) -{ - if (m_apiHost == host) - return; - m_apiHost = host; - emit apiHostChanged(); -} - -bool VirtualStudio::vsFtux() -{ - return m_vsFtux; -} - -bool VirtualStudio::isExiting() -{ - return m_isExiting; -} - -void VirtualStudio::collectFeedbackSurvey(QString serverId, int rating, QString message) -{ - QJsonObject feedback; - - QString sysInfo = QString("[platform=%1").arg(QSysInfo::prettyProductName()); -#ifdef RT_AUDIO - QString inputDevice = - QString::fromStdString(m_audioConfigPtr->getInputDevice().toStdString()); - if (!inputDevice.isEmpty()) { - sysInfo.append(QString(",input=%1").arg(inputDevice)); - } - QString outputDevice = - QString::fromStdString(m_audioConfigPtr->getOutputDevice().toStdString()); - if (!outputDevice.isEmpty()) { - sysInfo.append(QString(",output=%1").arg(outputDevice)); - } -#endif - sysInfo.append("]"); - - feedback.insert(QStringLiteral("rating"), rating); - if (message.isEmpty()) { - feedback.insert(QStringLiteral("message"), sysInfo); - } else { - feedback.insert(QStringLiteral("message"), message + " " + sysInfo); - } - - QJsonDocument data = QJsonDocument(feedback); - m_api->submitServerFeedback(serverId, data.toJson()); - return; -} - -bool VirtualStudio::showWarnings() -{ - return m_showWarnings; -} - -void VirtualStudio::setShowWarnings(bool show) -{ - if (m_showWarnings == show) - return; - m_showWarnings = show; - emit showWarningsChanged(); -} - -float VirtualStudio::fontScale() -{ - return m_fontScale; -} - -float VirtualStudio::uiScale() -{ - return m_uiScale; -} - -void VirtualStudio::setUiScale(float scale) -{ - if (scale == m_uiScale) - return; - m_uiScale = scale; - emit uiScaleChanged(); -} - -bool VirtualStudio::darkMode() -{ - return m_darkMode; -} - -void VirtualStudio::setDarkMode(bool dark) -{ - if (dark == m_darkMode) - return; - m_darkMode = dark; - emit darkModeChanged(); -} - -bool VirtualStudio::collapseDeviceControls() -{ - return m_collapseDeviceControls; -} - -void VirtualStudio::setCollapseDeviceControls(bool collapseDeviceControls) -{ - if (m_collapseDeviceControls == collapseDeviceControls) - return; - m_collapseDeviceControls = collapseDeviceControls; - emit collapseDeviceControlsChanged(collapseDeviceControls); -} - -bool VirtualStudio::testMode() -{ - return m_testMode; -} - -void VirtualStudio::setTestMode(bool test) -{ - if (m_testMode == test) - return; - - QString userEmail = m_userMetadata[QStringLiteral("email")].toString(); - if (m_userMetadata.isEmpty() || userEmail == "" - || !userEmail.endsWith("@jacktrip.org")) { - qDebug() << "Not allowed"; - return; - } - - // deregister app - if (!m_devicePtr.isNull()) { - m_devicePtr->removeApp(); - m_devicePtr->disconnect(); - m_devicePtr.reset(); - } - - m_testMode = test; - - // clear existing auth state - m_auth->logout(); - - // Clear existing registers - any existing instance data will be overwritten - // when m_auth->authenticate finishes and slotAuthSucceeded() is called again - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - settings.setValue(QStringLiteral("TestMode"), m_testMode); - settings.remove(QStringLiteral("RefreshToken")); - settings.remove(QStringLiteral("UserId")); - settings.endGroup(); - - // stop timers, clear data, etc. - resetState(); - - // clear user data - m_userMetadata = QJsonObject(); - m_userId.clear(); - - // re-run authentication. This should not require another browser flow since - // we're starting with the existing refresh token - m_auth->authenticate(m_refreshToken); - emit testModeChanged(); -} - -QString VirtualStudio::studioToJoin() -{ - return m_studioToJoin; -} - -void VirtualStudio::setStudioToJoin(const QString& id) -{ - if (m_studioToJoin == id) - return; - m_studioToJoin = id; - emit studioToJoinChanged(); -} - -bool VirtualStudio::noUpdater() -{ -#ifdef NO_UPDATER - return true; -#else - return false; -#endif -} - -bool VirtualStudio::psiBuild() -{ -#ifdef PSI - return true; -#else - return false; -#endif -} - -QString VirtualStudio::failedMessage() -{ - return m_failedMessage; -} - -void VirtualStudio::joinStudio() -{ - // nothing to do unless on setup or connected windows with studio to join - if ((m_windowState != "setup" && m_windowState != "connected") - || !m_auth->isAuthenticated() || m_studioToJoin.isEmpty()) - return; - - // make sure we've retrieved a list of servers - QMutexLocker locker(&m_refreshMutex); - if (m_servers.isEmpty()) { - // No servers yet. Making sure we have them. - // getServerList emits refreshFinished which - // will come back to this function. - locker.unlock(); - getServerList(true); - return; - } - - // pop studioToJoin - const QString targetId = m_studioToJoin; - setStudioToJoin(""); - - // stop audio if already running (settings or setup windows) - m_audioConfigPtr->stopAudio(true); - - // find and populate data for current studio - VsServerInfoPointer sPtr; - for (const VsServerInfoPointer& s : m_servers) { - if (s->id() == targetId) { - sPtr = s; - break; - } - } - locker.unlock(); - - if (sPtr.isNull()) { - m_failedMessage = "Unable to find studio " + targetId; - emit failedMessageChanged(); - emit failed(); - return; - } - - m_currentStudio = *sPtr; - emit currentStudioChanged(); - - if (m_windowState == "setup") { - m_audioConfigPtr->setSampleRate(m_currentStudio.sampleRate()); - m_audioConfigPtr->startAudio(); - return; - } - - // m_windowState == "connected" - connectToStudio(); -} - -void VirtualStudio::toStandard() -{ - if (m_standardWindow.isNull()) - qDebug() << "Unable to switch modes: standard window is missing!"; - - m_view.hide(); - m_standardWindow->show(); - - QSettings settings; - m_uiMode = QJackTrip::STANDARD; - settings.setValue(QStringLiteral("UiMode"), m_uiMode); - - // stop timers, clear data, etc. - resetState(); - setWindowState(QStringLiteral("start")); - m_auth->logout(); - - if (m_showFirstRun) { - m_showFirstRun = false; - emit showFirstRunChanged(); - } -} - -void VirtualStudio::toVirtualStudio() -{ - QSettings settings; - m_uiMode = QJackTrip::VIRTUAL_STUDIO; - settings.setValue(QStringLiteral("UiMode"), m_uiMode); - - if (m_windowState == "start") { - setWindowState(QStringLiteral("login")); - } - if (m_windowState == "login") { - login(); - } -} - -void VirtualStudio::login() -{ - if (m_refreshToken.isEmpty()) { - m_auth->authenticate(QStringLiteral("")); - } else { - m_auth->authenticate(m_refreshToken); - } -} - -void VirtualStudio::logout() -{ - // deregister app - if (!m_devicePtr.isNull()) { - m_devicePtr->removeApp(); - m_devicePtr->disconnect(); - m_devicePtr.reset(); - } - - QUrl logoutURL = QUrl("https://auth.jacktrip.org/v2/logout"); - QUrlQuery query; - query.addQueryItem(QStringLiteral("client_id"), AUTH_CLIENT_ID); - if (m_testMode) { - query.addQueryItem(QStringLiteral("returnTo"), - QStringLiteral("https://next-test.jacktrip.com/")); - } else { - query.addQueryItem(QStringLiteral("returnTo"), - QStringLiteral("https://www.jacktrip.com/")); - } - - logoutURL.setQuery(query); - QDesktopServices::openUrl(logoutURL); - - m_auth->logout(); - - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - settings.remove(QStringLiteral("RefreshToken")); - settings.remove(QStringLiteral("UserId")); - settings.endGroup(); - - // stop timers, clear data, etc. - resetState(); - - // clear user data - m_refreshToken.clear(); - m_userMetadata = QJsonObject(); - m_userId.clear(); - emit hasRefreshTokenChanged(); - - // reset window state - setWindowState(QStringLiteral("login")); - login(); // called to retrieve new code flow token -} - -void VirtualStudio::refreshStudios(int index, bool signalRefresh) -{ - getSubscriptions(); - getServerList(signalRefresh, index); -} - -void VirtualStudio::loadSettings() -{ - QSettings settings; - m_uiMode = static_cast( - settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt()); - setUpdateChannel( - settings.value(QStringLiteral("UpdateChannel"), "stable").toString().toLower()); - - settings.beginGroup(QStringLiteral("VirtualStudio")); - m_refreshToken = settings.value(QStringLiteral("RefreshToken"), "").toString(); - m_userId = settings.value(QStringLiteral("UserId"), "").toString(); - m_testMode = settings.value(QStringLiteral("TestMode"), false).toBool(); - m_showInactive = settings.value(QStringLiteral("ShowInactive"), true).toBool(); - m_showSelfHosted = settings.value(QStringLiteral("ShowSelfHosted"), false).toBool(); - - // use setters to emit signals for these if they change; otherwise, the - // user interface will not revert back after cancelling settings changes - setUiScale(settings.value(QStringLiteral("UiScale"), 1).toFloat()); - setDarkMode(settings.value(QStringLiteral("DarkMode"), false).toBool()); - setShowDeviceSetup(settings.value(QStringLiteral("ShowDeviceSetup"), true).toBool()); - setShowWarnings(settings.value(QStringLiteral("ShowWarnings"), true).toBool()); - settings.endGroup(); - - m_audioConfigPtr->loadSettings(); -} - -void VirtualStudio::saveSettings() -{ - QSettings settings; - settings.setValue(QStringLiteral("UpdateChannel"), m_updateChannel); - settings.beginGroup(QStringLiteral("VirtualStudio")); - settings.setValue(QStringLiteral("UiScale"), m_uiScale); - settings.setValue(QStringLiteral("DarkMode"), m_darkMode); - settings.setValue(QStringLiteral("ShowDeviceSetup"), m_showDeviceSetup); - settings.setValue(QStringLiteral("ShowWarnings"), m_showWarnings); - settings.endGroup(); - - m_audioConfigPtr->saveSettings(); -} - -void VirtualStudio::connectToStudio() -{ - m_refreshTimer.stop(); - - m_networkStats = QJsonObject(); - emit networkStatsChanged(); - - m_onConnectedScreen = true; - - m_studioSocketPtr.reset(new VsWebSocket( - QUrl(QStringLiteral("wss://%1/api/servers/%2?auth_code=%3") - .arg(m_api->getApiHost(), m_currentStudio.id(), m_auth->accessToken())), - m_auth->accessToken(), QString(), QString())); - connect(m_studioSocketPtr.get(), &VsWebSocket::textMessageReceived, this, - &VirtualStudio::handleWebsocketMessage); - connect(m_studioSocketPtr.get(), &VsWebSocket::disconnected, this, - &VirtualStudio::restartStudioSocket); - m_studioSocketPtr->openSocket(); - - // Check if we have an address for our server - if (m_currentStudio.status() != "Ready") { - m_connectionState = QStringLiteral("Waiting..."); - emit connectionStateChanged(); - } else { - completeConnection(); - } - - m_reconnectState = ReconnectState::NOT_RECONNECTING; -} - -void VirtualStudio::completeConnection() -{ - // sanity check - if (m_currentStudio.id() == "" - || m_currentStudio.status() == QStringLiteral("Disabled")) { - processError("Studio session has ended"); - return; - } - - // these shouldn't happen - if (m_currentStudio.status() != "Ready") { - processError("Studio session is not ready"); - return; - } - if (m_currentStudio.host().isEmpty()) { - processError("Studio host is unknown"); - return; - } - - // always connect with audio device controls open - setCollapseDeviceControls(false); - - m_jackTripRunning = true; - m_connectionState = QStringLiteral("Preparing audio..."); - emit connectionStateChanged(); - try { - bool useRtAudio = m_audioConfigPtr->getUseRtAudio(); - std::string input = ""; - std::string output = ""; - int buffer_size = 0; - int inputMixMode = -1; - int baseInputChannel = 0; - int numInputChannels = 2; - int baseOutputChannel = 0; - int numOutputChannels = 2; -#ifdef RT_AUDIO - if (useRtAudio) { - // pre-populate device cache and validate first, if using rtaudio - if (!m_audioConfigPtr->getDeviceModelsInitialized()) - m_audioConfigPtr->refreshDevices(true); - // initialize jacktrip using audio settings - input = m_audioConfigPtr->getInputDevice().toStdString(); - output = m_audioConfigPtr->getOutputDevice().toStdString(); - buffer_size = m_audioConfigPtr->getBufferSize(); - inputMixMode = m_audioConfigPtr->getInputMixMode(); - baseInputChannel = m_audioConfigPtr->getBaseInputChannel(); - numInputChannels = m_audioConfigPtr->getNumInputChannels(); - baseOutputChannel = m_audioConfigPtr->getBaseOutputChannel(); - numOutputChannels = m_audioConfigPtr->getNumOutputChannels(); - } -#endif - - // increment buffer_strategy by 1 for array-index mapping - int buffer_strategy = m_audioConfigPtr->getBufferStrategy() + 1; - // adjust buffer_strategy for PLC "auto" mode menu item - if (buffer_strategy == 4 || buffer_strategy == 5) { - buffer_strategy = 3; - } - - // create a new JackTrip instance - JackTrip* jackTrip = m_devicePtr->initJackTrip( - useRtAudio, input, output, baseInputChannel, numInputChannels, - baseOutputChannel, numOutputChannels, inputMixMode, buffer_size, - buffer_strategy, &m_currentStudio); - if (jackTrip == 0) { - processError("Could not bind port"); - return; - } - jackTrip->setIOStatTimeout(m_cliSettings->getIOStatTimeout()); - m_audioConfigPtr->setSampleRate(jackTrip->getSampleRate()); - - // this passes ownership to JackTrip - jackTrip->setAudioInterface(m_audioConfigPtr->newAudioInterface(jackTrip)); - - QObject::connect(jackTrip, &JackTrip::signalProcessesStopped, this, - &VirtualStudio::connectionFinished, Qt::QueuedConnection); - QObject::connect(jackTrip, &JackTrip::signalError, this, - &VirtualStudio::processError, Qt::QueuedConnection); - QObject::connect(jackTrip, &JackTrip::signalReceivedConnectionFromPeer, this, - &VirtualStudio::receivedConnectionFromPeer, - Qt::QueuedConnection); - QObject::connect(jackTrip, &JackTrip::signalUdpWaitingTooLong, this, - &VirtualStudio::udpWaitingTooLong, Qt::QueuedConnection); - - m_connectionState = QStringLiteral("Connecting..."); - emit connectionStateChanged(); - m_devicePtr->startJackTrip(m_currentStudio); - - // update device error messages and warnings based on latest results - // this is necessary because we may have never loaded audio settings, - // or the state may have changed via the connected change devices screen - m_audioConfigPtr->setDevicesWarningMsg(jackTrip->getDevicesWarningMsg()); - m_audioConfigPtr->setDevicesErrorMsg(jackTrip->getDevicesErrorMsg()); - m_audioConfigPtr->setDevicesWarningHelpUrl(jackTrip->getDevicesWarningHelpUrl()); - m_audioConfigPtr->setDevicesErrorHelpUrl(jackTrip->getDevicesErrorHelpUrl()); - m_audioConfigPtr->setHighLatencyFlag(jackTrip->getHighLatencyFlag()); - - } catch (const std::exception& e) { - // Let the user know what our exception was. - m_connectionState = QStringLiteral("JackTrip Error"); - emit connectionStateChanged(); - - processError(QString::fromUtf8(e.what())); - return; - } - -#ifdef __APPLE__ - m_noNap.disableNap(); -#endif -} - -void VirtualStudio::triggerReconnect(bool refresh) -{ - if (!m_jackTripRunning || m_devicePtr.isNull()) { - if (refresh) - m_audioConfigPtr->refreshDevices(true); - else - m_audioConfigPtr->validateDevices(); - return; - } - - // this needs to be synchronous to avoid both trying - // to use the audio interfaces at the same time - // note that connectionFinished() checks m_reconnectState - // and uses that to update audio, then reconnect - m_reconnectState = refresh ? ReconnectState::RECONNECTING_REFRESH - : ReconnectState::RECONNECTING_VALIDATE; - m_connectionState = QStringLiteral("Reconnecting..."); - emit connectionStateChanged(); - - // keep device enabled while stopping jacktrip - m_devicePtr->stopJackTrip(true); -} - -void VirtualStudio::disconnect() -{ - // stop jackrip if it's running - if (m_jackTripRunning) { - m_devicePtr->stopJackTrip(false); - // persist any volume level or device changes - m_audioConfigPtr->saveSettings(); - } - - m_connectionState = QStringLiteral("Disconnected"); - emit connectionStateChanged(); - setConnectedErrorMsg(""); - - if (m_isExiting) { - emit signalExit(); - return; - } - - // if this occurs on the setup or settings screen (for example, due to an issue with - // devices) then don't emit disconnected, as that would move you back to the "Browse" - // screen - if (m_onConnectedScreen) { - m_onConnectedScreen = false; - emit disconnected(); - // Refresh studios and restart timer - refreshStudios(0, true); - m_refreshTimer.start(); - } - - if (!m_jackTripRunning) { - return; - } - m_jackTripRunning = false; - - if (!m_studioSocketPtr.isNull()) { - m_studioSocketPtr->closeSocket(); - m_studioSocketPtr->disconnect(); - m_studioSocketPtr.reset(); - } - - // reset network statistics - m_networkStats = QJsonObject(); - - if (!m_currentStudio.id().isEmpty()) { - emit openFeedbackSurveyModal(m_currentStudio.id()); - m_currentStudio.setId(""); - emit currentStudioChanged(); - } - -#ifdef __APPLE__ - m_noNap.enableNap(); -#endif -} - -void VirtualStudio::createStudio() -{ - setWindowState(QStringLiteral("create_studio")); -} - -void VirtualStudio::editProfile() -{ - QUrl url = QUrl(QStringLiteral("https://%1/profile").arg(m_api->getApiHost())); - QDesktopServices::openUrl(url); -} - -void VirtualStudio::showAbout() -{ - About about; - about.exec(); -} - -void VirtualStudio::openLink(const QString& link) -{ - QUrl url = QUrl(link); - QDesktopServices::openUrl(url); -} - -void VirtualStudio::handleDeeplinkRequest(const QUrl& link) -{ - // check link is valid - QString studioId; - if (link.scheme() != QLatin1String("jacktrip") || link.path().length() <= 1) { - qDebug() << "Ignoring invalid deeplink to" << link; - return; - } - if (link.host() == QLatin1String("join")) { - studioId = link.path().remove(0, 1); - } else if (link.host().isEmpty() && link.path().startsWith("join/")) { - studioId = link.path().remove(0, 5); - } else { - qDebug() << "Ignoring invalid deeplink to" << link; - return; - } - - // check if already connected (ignore) - if (m_windowState == "connected" || m_windowState == "change_devices") { - qDebug() << "Already connected; ignoring deeplink to" << link; - return; - } - - if (m_windowState == "setup" - && (m_studioToJoin == studioId || m_currentStudio.id() == studioId)) { - qDebug() << "Already preparing to connect; ignoring deeplink to" << link; - return; - } - - qDebug() << "Handling deeplink to" << link; - setStudioToJoin(studioId); - raiseToTop(); - - // Switch to virtual studio mode, if necessary - // Note that this doesn't change the startup preference - if (m_uiMode != QJackTrip::VIRTUAL_STUDIO) { - m_standardWindow->hide(); - if (m_windowState == "start") { - setWindowState(QStringLiteral("login")); - } - if (m_windowState == "login") { - login(); - } - } - - // automatically navigate if on certain window screens - // note that the studio creation happens inside of the web view, - // and the app doesn't really know anything about it. we depend - // on the web app triggering a deep link join event, which is - // handled here. it's unlikely that the new studio has been - // noticed yet, so we don't join right away; otherwise we'd just - // get an unknown studio error. instead, we trigger a refresh and - // rely on it to kick off the join afterwards. - if (m_windowState == "browse" || m_windowState == "create_studio" - || m_windowState == "settings" || m_windowState == "setup" - || m_windowState == "failed") { - if (showDeviceSetup()) { - setWindowState("setup"); - } else { - setWindowState("connected"); - } - refreshStudios(0, true); - } - - // otherwise, assume we are on setup screens and can let the normal flow handle it -} - -void VirtualStudio::exit() -{ - // if multiple close events are received, emit the signalExit to force-quit the app - if (m_isExiting) { - emit signalExit(); - } - - // triggering isExitingChanged will force any WebEngine things to close properly - m_isExiting = true; - emit isExitingChanged(); - - // stop timers, clear data, etc. - resetState(); - - if (m_onConnectedScreen) { - // manually disconnect on self-managed studios - if (!m_currentStudio.id().isEmpty() && !m_currentStudio.isManaged()) { - disconnect(); - } - } else { - emit signalExit(); - } -} - -void VirtualStudio::slotAuthSucceeded() -{ - // Make sure window is on top (instead of browser, during first auth) - raiseToTop(); - - // Determine which API host to use - m_apiHost = PROD_API_HOST; - if (m_testMode) { - m_apiHost = TEST_API_HOST; - } - m_api->setApiHost(m_apiHost); - - // Get refresh token and userId - m_refreshToken = m_auth->refreshToken(); - m_userId = m_auth->userId(); - emit hasRefreshTokenChanged(); - - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - settings.setValue(QStringLiteral("RefreshToken"), m_refreshToken); - settings.setValue(QStringLiteral("UserId"), m_userId); - settings.endGroup(); - - // initialize new VsDevice and wire up signals/slots before registering app - m_devicePtr.reset(new VsDevice(m_auth, m_api, m_audioConfigPtr)); - connect(m_devicePtr.get(), &VsDevice::updateNetworkStats, this, - &VirtualStudio::updatedStats); - connect(m_audioConfigPtr.get(), &VsAudio::updatedInputVolume, m_devicePtr.get(), - &VsDevice::syncDeviceSettings); - connect(m_audioConfigPtr.get(), &VsAudio::updatedInputMuted, m_devicePtr.get(), - &VsDevice::syncDeviceSettings); - connect(m_audioConfigPtr.get(), &VsAudio::updatedOutputVolume, m_devicePtr.get(), - &VsDevice::syncDeviceSettings); - connect(m_audioConfigPtr.get(), &VsAudio::updatedMonitorVolume, m_devicePtr.get(), - &VsDevice::syncDeviceSettings); - - m_devicePtr->registerApp(); - - if (!m_webChannelServer->listen(QHostAddress::LocalHost)) { - // shouldn't happen - std::cout << "ERROR: Failed to start server!" << std::endl; - } - m_webChannelPort = m_webChannelServer->serverPort(); - emit webChannelPortChanged(m_webChannelPort); - std::cout << "QWebChannel listening on port: " << m_webChannelPort << std::endl; - - getRegions(); - getUserMetadata(); - getSubscriptions(); - getServerList(false); -} - -void VirtualStudio::connectionFinished() -{ - if (!m_devicePtr.isNull() - && (m_reconnectState == ReconnectState::RECONNECTING_VALIDATE - || m_reconnectState == ReconnectState::RECONNECTING_REFRESH)) { - if (m_devicePtr->hasTerminated()) { - if (m_reconnectState == ReconnectState::RECONNECTING_REFRESH) { - m_audioConfigPtr->refreshDevices(true); - } else { - m_audioConfigPtr->validateDevices(true); - } - connectToStudio(); - } - return; - } - m_reconnectState = ReconnectState::NOT_RECONNECTING; - - // use disconnect function to handle reset of all internal flags and timers - disconnect(); -} - -void VirtualStudio::processError(const QString& errorMessage) -{ - static const QString RtAudioErrorMsg = QStringLiteral("RtAudio Error"); - static const QString JackAudioErrorMsg = - QStringLiteral("The Jack server was shut down"); - - const bool shouldSwitchToRtAudio = - (errorMessage == QLatin1String("Maybe the JACK server is not running?")); - - QMessageBox msgBox; - if (shouldSwitchToRtAudio) { - // Report the other end quitting as a regular occurance rather than an error. - msgBox.setText("The JACK server is not running. Switching back to RtAudio."); - msgBox.setWindowTitle(QStringLiteral("No JACK server")); - } else if (errorMessage == QLatin1String("Peer Stopped")) { - // Report the other end quitting as a regular occurance rather than an error. - msgBox.setText("The Studio has been stopped."); - msgBox.setWindowTitle(QStringLiteral("Disconnected")); - } else if (errorMessage.startsWith(RtAudioErrorMsg)) { - if (errorMessage.length() > RtAudioErrorMsg.length() + 2) { - const QString details(errorMessage.sliced(RtAudioErrorMsg.length() + 2)); - if (details.contains(QStringLiteral("device was disconnected")) - || details.contains( - QStringLiteral("Unable to retrieve capture buffer"))) { - msgBox.setText(QStringLiteral("Your audio interface was disconnected.")); - } else { - msgBox.setText(details); - } - } else { - msgBox.setText(errorMessage); - } - msgBox.setWindowTitle(QStringLiteral("Audio Interface Error")); - } else if (errorMessage.startsWith(JackAudioErrorMsg)) { - if (errorMessage.length() > JackAudioErrorMsg.length() + 2) { - msgBox.setText(errorMessage.sliced(JackAudioErrorMsg.length() + 2)); - } else { - msgBox.setText(QStringLiteral("The JACK Audio Server was stopped.")); - } - msgBox.setWindowTitle(QStringLiteral("Jack Audio Error")); - } else { - msgBox.setText(QStringLiteral("Error: ").append(errorMessage)); - msgBox.setWindowTitle(QStringLiteral("Doh!")); - } - msgBox.exec(); - - if (m_jackTripRunning) - connectionFinished(); -} - -void VirtualStudio::receivedConnectionFromPeer() -{ - // Connect via API - m_connectionState = QStringLiteral("Connected"); - emit connectionStateChanged(); - std::cout << "Received connection" << std::endl; - emit connected(); -} - -void VirtualStudio::handleWebsocketMessage(const QString& msg) -{ - QJsonObject serverState = QJsonDocument::fromJson(msg.toUtf8()).object(); - QString serverStatus = serverState[QStringLiteral("status")].toString(); - bool serverEnabled = serverState[QStringLiteral("enabled")].toBool(); - QString serverCloudId = serverState[QStringLiteral("cloudId")].toString(); - - // server notifications are also transmitted along this websocket, so ignore data if - // it contains "message" - QString message = serverState[QStringLiteral("message")].toString(); - if (!message.isEmpty()) { - return; - } - if (m_currentStudio.id() == "") { - return; - } - m_currentStudio.setStatus(serverStatus); - m_currentStudio.setEnabled(serverEnabled); - m_currentStudio.setCloudId(serverCloudId); - if (!m_jackTripRunning) { - if (serverStatus == QLatin1String("Ready") && m_onConnectedScreen) { - m_currentStudio.setHost(serverState[QStringLiteral("serverHost")].toString()); - m_currentStudio.setPort(serverState[QStringLiteral("serverPort")].toInt()); - m_currentStudio.setSessionId( - serverState[QStringLiteral("sessionId")].toString()); - completeConnection(); - } - } - - emit currentStudioChanged(); -} - -void VirtualStudio::restartStudioSocket() -{ - if (m_onConnectedScreen) { - if (!m_studioSocketPtr.isNull()) { - m_studioSocketPtr->openSocket(); - } - } -} - -void VirtualStudio::updatedStats(const QJsonObject& stats) -{ - QJsonObject newStats; - for (int i = 0; i < stats.keys().size(); i++) { - QString key = stats.keys().at(i); - newStats.insert(key, stats[key].toDouble()); - } - - m_networkStats = newStats; - emit networkStatsChanged(); - return; -} - -void VirtualStudio::udpWaitingTooLong() -{ - if (m_devicePtr.isNull()) - return; - m_networkOutageTimer.start(); - m_devicePtr->setNetworkOutage(true); - emit updatedNetworkOutage(true); -} - -void VirtualStudio::sendHeartbeat() -{ - if (!m_devicePtr.isNull() && m_connectionState != "Connecting..." - && m_connectionState != "Preparing audio...") { - m_devicePtr->sendHeartbeat(); - } -} - -void VirtualStudio::resetState() -{ - m_webChannelServer->close(); - m_refreshTimer.stop(); - m_heartbeatTimer.stop(); - m_startTimer.stop(); - m_networkOutageTimer.stop(); - m_firstRefresh = true; -} - -void VirtualStudio::getServerList(bool signalRefresh, int index) -{ - // only allow one thread to refresh at a time - QMutexLocker refreshLock(&m_refreshMutex); - if (m_refreshInProgress) - return; - m_refreshInProgress = true; - refreshLock.unlock(); - - // Get the serverId of the server at the top of our screen if we know it - QString topServerId; - if (index >= 0 && index < m_serverModel.count()) { - topServerId = m_serverModel.at(index)->id(); - } - - QNetworkReply* reply = m_api->getServers(); - connect( - reply, &QNetworkReply::finished, this, [&, reply, topServerId, signalRefresh]() { - if (reply->error() != QNetworkReply::NoError) { - if (signalRefresh) { - emit refreshFinished(index); - } - std::cerr << "Error: " << reply->errorString().toStdString() << std::endl; - reply->deleteLater(); - QMutexLocker refreshLock(&m_refreshMutex); - m_refreshInProgress = false; - return; - } - - QByteArray response = reply->readAll(); - QJsonDocument serverList = QJsonDocument::fromJson(response); - reply->deleteLater(); - if (!serverList.isArray()) { - if (signalRefresh) { - emit refreshFinished(index); - } - std::cerr << "Error: Not an array" << std::endl; - QMutexLocker refreshLock(&m_refreshMutex); - m_refreshInProgress = false; - return; - } - - QJsonArray servers = serverList.array(); - // Divide our servers by category initially so that they're easier to sort - QVector yourServers; - QVector subServers; - QVector pubServers; - int skippedStudios = 0; - - QMutexLocker refreshLock(&m_refreshMutex); // protect m_servers - m_servers.clear(); - for (int i = 0; i < servers.count(); i++) { - if (servers.at(i)[QStringLiteral("type")].toString().contains( - QStringLiteral("JackTrip"))) { - QSharedPointer serverInfo(new VsServerInfo(this)); - serverInfo->setIsAdmin( - servers.at(i)[QStringLiteral("admin")].toBool()); - serverInfo->setName(servers.at(i)[QStringLiteral("name")].toString()); - serverInfo->setHost( - servers.at(i)[QStringLiteral("serverHost")].toString()); - serverInfo->setIsManaged( - servers.at(i)[QStringLiteral("managed")].toBool()); - serverInfo->setStatus( - servers.at(i)[QStringLiteral("status")].toString()); - serverInfo->setPort( - servers.at(i)[QStringLiteral("serverPort")].toInt()); - serverInfo->setIsPublic( - servers.at(i)[QStringLiteral("public")].toBool()); - serverInfo->setRegion( - servers.at(i)[QStringLiteral("region")].toString()); - serverInfo->setPeriod( - servers.at(i)[QStringLiteral("period")].toInt()); - serverInfo->setSampleRate( - servers.at(i)[QStringLiteral("sampleRate")].toInt()); - serverInfo->setQueueBuffer( - servers.at(i)[QStringLiteral("queueBuffer")].toInt()); - serverInfo->setBannerURL( - servers.at(i)[QStringLiteral("bannerURL")].toString()); - serverInfo->setId(servers.at(i)[QStringLiteral("id")].toString()); - serverInfo->setSessionId( - servers.at(i)[QStringLiteral("sessionId")].toString()); - serverInfo->setStreamId( - servers.at(i)[QStringLiteral("streamId")].toString()); - serverInfo->setInviteKey( - servers.at(i)[QStringLiteral("inviteKey")].toString()); - serverInfo->setCloudId( - servers.at(i)[QStringLiteral("cloudId")].toString()); - serverInfo->setEnabled( - servers.at(i)[QStringLiteral("enabled")].toBool()); - serverInfo->setIsOwner( - servers.at(i)[QStringLiteral("owner")].toBool()); - - // Always add servers to m_servers - m_servers.append(serverInfo); - - // Only add servers to the model that we want to show - if (serverInfo->isAdmin() || serverInfo->isOwner()) { - if (filterStudio(*serverInfo)) { - ++skippedStudios; - } else { - yourServers.append(serverInfo); - serverInfo->setSection(VsServerInfo::YOUR_STUDIOS); - } - } else if (m_subscribedServers.contains(serverInfo->id())) { - if (filterStudio(*serverInfo)) { - ++skippedStudios; - } else { - subServers.append(serverInfo); - serverInfo->setSection(VsServerInfo::SUBSCRIBED_STUDIOS); - } - } else { - if (!filterStudio(*serverInfo)) { - pubServers.append(serverInfo); - serverInfo->setSection(VsServerInfo::PUBLIC_STUDIOS); - } - // don't count public studios in skipped count - } - } - } - refreshLock.unlock(); - - // sort studios in each section by name - auto serverSorter = [](VsServerInfoPointer first, - VsServerInfoPointer second) { - return *first < *second; - }; - std::sort(yourServers.begin(), yourServers.end(), serverSorter); - std::sort(subServers.begin(), subServers.end(), serverSorter); - std::sort(pubServers.begin(), pubServers.end(), serverSorter); - - // If we don't have any owned servers, move the JackTrip logo to an - // appropriate section header. - if (yourServers.isEmpty()) { - if (subServers.isEmpty()) { - m_logoSection = QStringLiteral("Public Studios"); - - if (skippedStudios == 0) { - // This is a new user - setShowCreateStudio(true); - } else { - // This is not a new user. One or more studios were filtered. - setShowCreateStudio(false); - } - } else { - m_logoSection = QStringLiteral("Subscribed Studios"); - } - } else { - m_logoSection = QStringLiteral("Your Studios"); - } - emit logoSectionChanged(); - - m_serverModel.clear(); - for (const VsServerInfoPointer& s : yourServers) { - m_serverModel.append(s.get()); - } - for (const VsServerInfoPointer& s : subServers) { - m_serverModel.append(s.get()); - } - for (const VsServerInfoPointer& s : pubServers) { - m_serverModel.append(s.get()); - } - emit serverModelChanged(); - int index = -1; - if (!topServerId.isEmpty()) { - for (int i = 0; i < m_serverModel.count(); i++) { - if (m_serverModel.at(i)->id() == topServerId) { - index = i; - break; - } - } - } - if (m_firstRefresh) { - m_refreshTimer.setInterval(5000); - m_refreshTimer.start(); - m_heartbeatTimer.setInterval(5000); - m_heartbeatTimer.start(); - m_firstRefresh = false; - } - m_refreshInProgress = false; - if (signalRefresh) { - emit refreshFinished(index); - } - }); -} - -bool VirtualStudio::filterStudio(const VsServerInfo& serverInfo) const -{ - // Return true if we want to filter the studio out of the display model - bool activeStudio = serverInfo.status() == QLatin1String("Ready"); - bool hostedStudio = serverInfo.isManaged(); - if (!m_showSelfHosted && !hostedStudio) { - return true; - } - if (!m_showInactive && !activeStudio) { - return true; - } - return false; -} - -void VirtualStudio::getSubscriptions() -{ - if (m_userId.isEmpty()) { - qDebug() << "Invalid user ID"; - return; - } - QNetworkReply* reply = m_api->getSubscriptions(m_userId); - connect(reply, &QNetworkReply::finished, this, [&, reply]() { - if (reply->error() != QNetworkReply::NoError) { - std::cout << "Error: " << reply->errorString().toStdString() << std::endl; - reply->deleteLater(); - return; - } - - QByteArray response = reply->readAll(); - QJsonDocument subscriptionList = QJsonDocument::fromJson(response); - if (!subscriptionList.isArray()) { - std::cout << "Error: Not an array" << std::endl; - reply->deleteLater(); - return; - } - m_subscribedServers.clear(); - QJsonArray subscriptions = subscriptionList.array(); - for (int i = 0; i < subscriptions.count(); i++) { - m_subscribedServers.insert( - subscriptions.at(i)[QStringLiteral("serverId")].toString(), true); - } - reply->deleteLater(); - }); -} - -void VirtualStudio::getRegions() -{ - QNetworkReply* reply = m_api->getRegions(m_userId); - connect(reply, &QNetworkReply::finished, this, [&, reply]() { - if (reply->error() != QNetworkReply::NoError) { - std::cout << "Error: " << reply->errorString().toStdString() << std::endl; - reply->deleteLater(); - return; - } - - m_regions = QJsonDocument::fromJson(reply->readAll()).object(); - emit regionsChanged(); - reply->deleteLater(); - }); -} - -void VirtualStudio::getUserMetadata() -{ - QNetworkReply* reply = m_api->getUser(m_userId); - connect(reply, &QNetworkReply::finished, this, [&, reply]() { - if (reply->error() != QNetworkReply::NoError) { - std::cout << "Error: " << reply->errorString().toStdString() << std::endl; - reply->deleteLater(); - return; - } - - m_userMetadata = QJsonDocument::fromJson(reply->readAll()).object(); - emit userMetadataChanged(); - reply->deleteLater(); - }); -} - -bool VirtualStudio::readyToJoin() -{ - // FTUX shows warnings and device setup views - // if any of these enabled, do not immediately join - return m_windowState == "connected" - && (m_connectionState == QStringLiteral("Waiting...") - || m_connectionState == QStringLiteral("Disconnected")); -} - -void VirtualStudio::detectedFeedbackLoop() -{ - emit feedbackDetected(); -} - -VirtualStudio::~VirtualStudio() -{ - QDesktopServices::unsetUrlHandler("jacktrip"); - // stop the audio worker thread before destructing other things - m_audioConfigPtr.reset(); -} diff --git a/src/gui/virtualstudio.h b/src/gui/virtualstudio.h deleted file mode 100644 index fbd02c6..0000000 --- a/src/gui/virtualstudio.h +++ /dev/null @@ -1,344 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file virtualstudio.h - * \author Matt Horton, based on code by Aaron Wyatt - * \date March 2022 - */ - -#ifndef VIRTUALSTUDIO_H -#define VIRTUALSTUDIO_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../Settings.h" -#include "qjacktrip.h" -#include "vsConstants.h" -#include "vsQuickView.h" -#include "vsServerInfo.h" - -#ifdef __APPLE__ -#include "NoNap.h" -#endif - -class JackTrip; -class VsAudio; -class VsApi; -class VsAuth; -class VsDevice; -class VsWebSocket; - -typedef QSharedPointer VsServerInfoPointer; - -class VirtualStudio : public QObject -{ - Q_OBJECT - Q_PROPERTY(int webChannelPort READ webChannelPort NOTIFY webChannelPortChanged) - Q_PROPERTY(bool showFirstRun READ showFirstRun WRITE setShowFirstRun NOTIFY - showFirstRunChanged) - Q_PROPERTY(bool hasRefreshToken READ hasRefreshToken NOTIFY hasRefreshTokenChanged) - Q_PROPERTY(QString versionString READ versionString CONSTANT) - Q_PROPERTY(QString logoSection READ logoSection NOTIFY logoSectionChanged) - Q_PROPERTY( - QString connectedErrorMsg READ connectedErrorMsg NOTIFY connectedErrorMsgChanged) - - Q_PROPERTY( - QVector serverModel READ getServerModel NOTIFY serverModelChanged) - Q_PROPERTY(VsServerInfo* currentStudio READ currentStudio NOTIFY currentStudioChanged) - Q_PROPERTY(QString studioToJoin READ studioToJoin WRITE setStudioToJoin NOTIFY - studioToJoinChanged) - Q_PROPERTY(QJsonObject regions READ regions NOTIFY regionsChanged) - Q_PROPERTY(QJsonObject userMetadata READ userMetadata NOTIFY userMetadataChanged) - Q_PROPERTY(bool showInactive READ showInactive WRITE setShowInactive NOTIFY - showInactiveChanged) - Q_PROPERTY(bool showSelfHosted READ showSelfHosted WRITE setShowSelfHosted NOTIFY - showSelfHostedChanged) - Q_PROPERTY(bool showCreateStudio READ showCreateStudio WRITE setShowCreateStudio - NOTIFY showCreateStudioChanged) - Q_PROPERTY(QString connectionState READ connectionState NOTIFY connectionStateChanged) - Q_PROPERTY(QJsonObject networkStats READ networkStats NOTIFY networkStatsChanged) - Q_PROPERTY(bool networkOutage READ networkOutage NOTIFY updatedNetworkOutage) - - Q_PROPERTY(QString updateChannel READ updateChannel WRITE setUpdateChannel NOTIFY - updateChannelChanged) - Q_PROPERTY(float fontScale READ fontScale CONSTANT) - Q_PROPERTY(float uiScale READ uiScale WRITE setUiScale NOTIFY uiScaleChanged) - Q_PROPERTY(bool darkMode READ darkMode WRITE setDarkMode NOTIFY darkModeChanged) - Q_PROPERTY(bool collapseDeviceControls READ collapseDeviceControls WRITE - setCollapseDeviceControls NOTIFY collapseDeviceControlsChanged) - Q_PROPERTY(bool testMode READ testMode WRITE setTestMode NOTIFY testModeChanged) - Q_PROPERTY(bool showDeviceSetup READ showDeviceSetup WRITE setShowDeviceSetup NOTIFY - showDeviceSetupChanged) - Q_PROPERTY(bool showWarnings READ showWarnings WRITE setShowWarnings NOTIFY - showWarningsChanged) - Q_PROPERTY(bool isExiting READ isExiting NOTIFY isExitingChanged) - Q_PROPERTY(bool noUpdater READ noUpdater CONSTANT) - Q_PROPERTY(bool psiBuild READ psiBuild CONSTANT) - Q_PROPERTY(QString failedMessage READ failedMessage NOTIFY failedMessageChanged) - Q_PROPERTY(QString windowState READ windowState WRITE setWindowState NOTIFY - windowStateUpdated) - Q_PROPERTY(QString apiHost READ apiHost WRITE setApiHost NOTIFY apiHostChanged) - Q_PROPERTY(bool vsFtux READ vsFtux CONSTANT) - Q_PROPERTY( - QStringList updateChannelComboModel READ getUpdateChannelComboModel CONSTANT) - - public: - explicit VirtualStudio(bool firstRun = false, QObject* parent = nullptr); - ~VirtualStudio() override; - - void setStandardWindow(QSharedPointer window); - void setCLISettings(QSharedPointer settings); - void show(); - void raiseToTop(); - - int webChannelPort(); - bool showFirstRun(); - void setShowFirstRun(bool show); - bool hasRefreshToken(); - QString versionString(); - QString logoSection(); - QString connectedErrorMsg(); - void setConnectedErrorMsg(const QString& msg); - const QVector& getServerModel() const { return m_serverModel; } - VsServerInfo* currentStudio() { return &m_currentStudio; } - QJsonObject regions(); - QJsonObject userMetadata(); - QString connectionState(); - QJsonObject networkStats(); - QString updateChannel(); - void setUpdateChannel(const QString& channel); - bool showInactive(); - void setShowInactive(bool inactive); - bool showSelfHosted(); - void setShowSelfHosted(bool selfHosted); - bool showCreateStudio(); - void setShowCreateStudio(bool createStudio); - float fontScale(); - float uiScale(); - void setUiScale(float scale); - bool darkMode(); - void setDarkMode(bool dark); - bool collapseDeviceControls(); - void setCollapseDeviceControls(bool collapseDeviceControls); - bool testMode(); - void setTestMode(bool test); - QString studioToJoin(); - void setStudioToJoin(const QString& id); - bool showDeviceSetup(); - void setShowDeviceSetup(bool show); - bool showWarnings(); - void setShowWarnings(bool show); - bool noUpdater(); - bool psiBuild(); - QString failedMessage(); - bool networkOutage(); - bool backendAvailable(); - QString windowState(); - QString apiHost(); - void setApiHost(QString host); - bool vsFtux(); - bool isExiting(); - const QStringList& getUpdateChannelComboModel() const - { - return m_updateChannelOptions; - } - - public slots: - void toStandard(); - void toVirtualStudio(); - void login(); - void logout(); - void refreshStudios(int index, bool signalRefresh = false); - void loadSettings(); - void saveSettings(); - void triggerReconnect(bool refresh); - void createStudio(); - void editProfile(); - void showAbout(); - void openLink(const QString& url); - void handleDeeplinkRequest(const QUrl& url); - void udpWaitingTooLong(); - void setWindowState(QString state); - void joinStudio(); - void disconnect(); - void collectFeedbackSurvey(QString serverId, int rating, QString message); - - signals: - void failed(); - void connected(); - void disconnected(); - void refreshFinished(int index); - void webChannelPortChanged(int webChannelPort); - void showFirstRunChanged(); - void hasRefreshTokenChanged(); - void logoSectionChanged(); - void connectedErrorMsgChanged(); - void serverModelChanged(); - void currentStudioChanged(); - void regionsChanged(); - void userMetadataChanged(); - void showInactiveChanged(); - void showSelfHostedChanged(); - void showCreateStudioChanged(); - void connectionStateChanged(); - void networkStatsChanged(); - void updateChannelChanged(); - void showDeviceSetupChanged(); - void showWarningsChanged(); - void uiScaleChanged(); - void collapseDeviceControlsChanged(bool collapseDeviceControls); - void newScale(); - void darkModeChanged(); - void testModeChanged(); - void signalExit(); - void periodicRefresh(); - void failedMessageChanged(); - void studioToJoinChanged(); - void updatedNetworkOutage(bool outage); - void windowStateUpdated(); - void isExitingChanged(); - void apiHostChanged(); - void feedbackDetected(); - void openFeedbackSurveyModal(QString serverId); - - private slots: - void slotAuthSucceeded(); - void receivedConnectionFromPeer(); - void handleWebsocketMessage(const QString& msg); - void restartStudioSocket(); - void updatedStats(const QJsonObject& stats); - void processError(const QString& errorMessage); - void detectedFeedbackLoop(); - void sendHeartbeat(); - void connectionFinished(); - void exit(); - - private: - void resetState(); - void getServerList(bool signalRefresh = false, int index = -1); - bool filterStudio(const VsServerInfo& serverInfo) const; - void getSubscriptions(); - void getRegions(); - void getUserMetadata(); - bool readyToJoin(); - void connectToStudio(); - void completeConnection(); - - private: - enum ReconnectState { - NOT_RECONNECTING = 0, - RECONNECTING_VALIDATE, - RECONNECTING_REFRESH - }; - - VsQuickView m_view; - VsServerInfo m_currentStudio; - QNetworkAccessManager* m_networkAccessManagerPtr; - QSharedPointer m_standardWindow; - QSharedPointer m_cliSettings; - QSharedPointer m_auth; - QSharedPointer m_api; - QScopedPointer m_devicePtr; - QScopedPointer m_studioSocketPtr; - QSharedPointer m_audioConfigPtr; - QVector m_servers; - QVector m_serverModel; //< qml doesn't like smart pointers - QScopedPointer m_webChannelServer; - QScopedPointer m_webChannel; - QMap m_subscribedServers; - QJsonObject m_regions; - QJsonObject m_userMetadata; - QJsonObject m_networkStats; - QTimer m_startTimer; - QTimer m_refreshTimer; - QTimer m_heartbeatTimer; - QTimer m_networkOutageTimer; - QMutex m_refreshMutex; - QString m_studioToJoin; - QString m_updateChannel; - QString m_refreshToken; - QString m_userId; - QString m_apiHost = PROD_API_HOST; - ReconnectState m_reconnectState = ReconnectState::NOT_RECONNECTING; - QJackTrip::uiModeT m_uiMode = QJackTrip::UNSET; - - bool m_firstRefresh = true; - bool m_jackTripRunning = false; - bool m_showFirstRun = false; - bool m_checkSsl = true; - bool m_refreshInProgress = false; - bool m_onConnectedScreen = false; - bool m_isExiting = false; - bool m_showInactive = true; - bool m_showSelfHosted = false; - bool m_showCreateStudio = false; - bool m_showDeviceSetup = true; - bool m_showWarnings = true; - bool m_darkMode = false; - bool m_collapseDeviceControls = false; - bool m_testMode = false; - bool m_authenticated = false; - float m_fontScale = 1; - float m_uiScale = 1; - uint32_t m_webChannelPort = 1; - - QString m_failedMessage = QStringLiteral(""); - QString m_windowState = QStringLiteral("start"); - QString m_connectedErrorMsg = QStringLiteral(""); - QString m_logoSection = QStringLiteral("Your Studios"); - QString m_connectionState = QStringLiteral("Waiting..."); - QStringList m_updateChannelOptions = {"Stable", "Edge"}; - -#ifdef __APPLE__ - NoNap m_noNap; -#endif - -#ifdef VS_FTUX - bool m_vsFtux = true; -#else - bool m_vsFtux = false; -#endif -}; - -#endif // VIRTUALSTUDIO_H diff --git a/src/gui/vs.qml b/src/gui/vs.qml deleted file mode 100644 index 9eee794..0000000 --- a/src/gui/vs.qml +++ /dev/null @@ -1,312 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Rectangle { - property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" - property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" - - color: backgroundColour - state: virtualstudio.windowState - anchors.fill: parent - - id: window - states: [ - State { - name: "start" - PropertyChanges { target: startScreen; x: 0 } - PropertyChanges { target: loginScreen; x: window.width; } - PropertyChanges { target: recommendationsScreen; x: window.width } - PropertyChanges { target: permissionsScreen; x: window.width } - PropertyChanges { target: setupScreen; x: window.width } - PropertyChanges { target: browseScreen; x: window.width } - PropertyChanges { target: settingsScreen; x: window.width } - PropertyChanges { target: createStudioScreen; x: window.width } - PropertyChanges { target: connectedScreen; x: window.width } - PropertyChanges { target: changeDevicesScreen; x: 2*window.width } - PropertyChanges { target: failedScreen; x: window.width } - }, - - State { - name: "login" - PropertyChanges { target: startScreen; x: -startScreen.width } - PropertyChanges { target: loginScreen; x: 0; } - PropertyChanges { target: recommendationsScreen; x: window.width } - PropertyChanges { target: permissionsScreen; x: window.width } - PropertyChanges { target: setupScreen; x: window.width } - PropertyChanges { target: browseScreen; x: window.width } - PropertyChanges { target: settingsScreen; x: window.width } - PropertyChanges { target: createStudioScreen; x: window.width } - PropertyChanges { target: connectedScreen; x: window.width } - PropertyChanges { target: changeDevicesScreen; x: 2*window.width } - PropertyChanges { target: failedScreen; x: window.width } - }, - - State { - name: "recommendations" - PropertyChanges { target: loginScreen; x: -loginScreen.width } - PropertyChanges { target: startScreen; x: -startScreen.width } - PropertyChanges { target: recommendationsScreen; x: 0 } - PropertyChanges { target: permissionsScreen; x: window.width } - PropertyChanges { target: setupScreen; x: window.width } - PropertyChanges { target: browseScreen; x: window.width } - PropertyChanges { target: settingsScreen; x: window.width } - PropertyChanges { target: createStudioScreen; x: window.width } - PropertyChanges { target: connectedScreen; x: window.width } - PropertyChanges { target: changeDevicesScreen; x: 2*window.width } - PropertyChanges { target: failedScreen; x: window.width } - }, - - State { - name: "permissions" - PropertyChanges { target: loginScreen; x: -loginScreen.width } - PropertyChanges { target: startScreen; x: -startScreen.width } - PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } - PropertyChanges { target: permissionsScreen; x: 0 } - PropertyChanges { target: setupScreen; x: window.width } - PropertyChanges { target: browseScreen; x: window.width } - PropertyChanges { target: settingsScreen; x: window.width } - PropertyChanges { target: createStudioScreen; x: window.width } - PropertyChanges { target: connectedScreen; x: window.width } - PropertyChanges { target: changeDevicesScreen; x: 2*window.width } - PropertyChanges { target: failedScreen; x: window.width } - }, - - State { - name: "setup" - PropertyChanges { target: loginScreen; x: -loginScreen.width } - PropertyChanges { target: startScreen; x: -startScreen.width } - PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } - PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } - PropertyChanges { target: setupScreen; x: 0 } - PropertyChanges { target: browseScreen; x: -browseScreen.width } - PropertyChanges { target: settingsScreen; x: window.width } - PropertyChanges { target: createStudioScreen; x: window.width } - PropertyChanges { target: connectedScreen; x: window.width } - PropertyChanges { target: changeDevicesScreen; x: 2*window.width } - PropertyChanges { target: failedScreen; x: window.width } - }, - - State { - name: "browse" - PropertyChanges { target: loginScreen; x: -loginScreen.width } - PropertyChanges { target: startScreen; x: -startScreen.width } - PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } - PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } - PropertyChanges { target: setupScreen; x: window.width } - PropertyChanges { target: browseScreen; x: 0 } - PropertyChanges { target: settingsScreen; x: window.width } - PropertyChanges { target: createStudioScreen; x: window.width } - PropertyChanges { target: connectedScreen; x: window.width } - PropertyChanges { target: changeDevicesScreen; x: 2*window.width } - PropertyChanges { target: failedScreen; x: window.width } - }, - - State { - name: "settings" - PropertyChanges { target: loginScreen; x: -loginScreen.width } - PropertyChanges { target: startScreen; x: -startScreen.width } - PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } - PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } - PropertyChanges { target: setupScreen; x: window.width } - PropertyChanges { target: browseScreen; x: -browseScreen.width } - PropertyChanges { target: settingsScreen; x: 0 } - PropertyChanges { target: createStudioScreen; x: window.width } - PropertyChanges { target: connectedScreen; x: window.width } - PropertyChanges { target: changeDevicesScreen; x: 2*window.width } - PropertyChanges { target: failedScreen; x: window.width } - }, - - State { - name: "create_studio" - PropertyChanges { target: loginScreen; x: -loginScreen.width } - PropertyChanges { target: startScreen; x: -startScreen.width } - PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } - PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } - PropertyChanges { target: setupScreen; x: window.width } - PropertyChanges { target: browseScreen; x: -browseScreen.width } - PropertyChanges { target: settingsScreen; x: window.width } - PropertyChanges { target: createStudioScreen; x: 0 } - PropertyChanges { target: connectedScreen; x: window.width } - PropertyChanges { target: changeDevicesScreen; x: window.width } - PropertyChanges { target: failedScreen; x: window.width } - }, - - State { - name: "connected" - PropertyChanges { target: loginScreen; x: -loginScreen.width } - PropertyChanges { target: startScreen; x: -startScreen.width } - PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } - PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } - PropertyChanges { target: setupScreen; x: 0 } - PropertyChanges { target: browseScreen; x: -browseScreen.width } - PropertyChanges { target: settingsScreen; x: window.width } - PropertyChanges { target: createStudioScreen; x: -createStudioScreen.width } - PropertyChanges { target: connectedScreen; x: 0 } - PropertyChanges { target: changeDevicesScreen; x: window.width } - PropertyChanges { target: failedScreen; x: window.width } - }, - - State { - name: "change_devices" - PropertyChanges { target: loginScreen; x: -loginScreen.width } - PropertyChanges { target: startScreen; x: -startScreen.width } - PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } - PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } - PropertyChanges { target: setupScreen; x: 0 } - PropertyChanges { target: browseScreen; x: -browseScreen.width } - PropertyChanges { target: settingsScreen; x: window.width } - PropertyChanges { target: createStudioScreen; x: -createStudioScreen.width } - PropertyChanges { target: connectedScreen; x: 0 } - PropertyChanges { target: changeDevicesScreen; x: 0 } - PropertyChanges { target: failedScreen; x: window.width } - }, - - State { - name: "failed" - PropertyChanges { target: loginScreen; x: -loginScreen.width } - PropertyChanges { target: startScreen; x: -startScreen.width } - PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } - PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } - PropertyChanges { target: setupScreen; x: -setupScreen.width } - PropertyChanges { target: browseScreen; x: -browseScreen.width } - PropertyChanges { target: settingsScreen; x: window.width } - PropertyChanges { target: createStudioScreen; x: window.width } - PropertyChanges { target: connectedScreen; x: window.width } - PropertyChanges { target: changeDevicesScreen; x: 2*window.width } - PropertyChanges { target: failedScreen; x: 0 } - } - ] - - transitions: Transition { - NumberAnimation { properties: "x"; duration: 500; easing.type: Easing.InOutQuad } - } - - FirstLaunch { - id: startScreen - } - - Login { - id: loginScreen - } - - Recommendations { - id: recommendationsScreen - } - - Permissions { - id: permissionsScreen - } - - Browse { - id: browseScreen - } - - Setup { - id: setupScreen - } - - Settings { - id: settingsScreen - } - - Connected { - id: connectedScreen - } - - ChangeDevices { - id: changeDevicesScreen - } - - CreateStudio { - id: createStudioScreen - } - - Failed { - id: failedScreen - } - - onWidthChanged: { - if (virtualstudio.windowState === "start") { - startScreen.x = 0 - } else if (virtualstudio.windowState === "login") { - loginScreen.x = 0 - } else if (virtualstudio.windowState === "recommendations") { - recommendationsScreen.x = 0; - } else if (virtualstudio.windowState === "permissions") { - permissionsScreen.x = 0; - } else if (virtualstudio.windowState === "setup") { - setupScreen.x = 0 - } else if (virtualstudio.windowState === "browse") { - browseScreen.x = 0 - } else if (virtualstudio.windowState === "settings") { - settingsScreen.x = 0 - } else if (virtualstudio.windowState === "create_studio") { - createStudioScreen.x = 0 - } else if (virtualstudio.windowState === "connected") { - connectedScreen.x = 0 - } else if (virtualstudio.windowState === "change_devices") { - changeDevicesScreen.x = 0 - } else if (virtualstudio.windowState === "failed") { - failedScreen.x = 0 - } - } - - onHeightChanged: { - if (virtualstudio.windowState === "start") { - startScreen.x = 0 - } else if (virtualstudio.windowState === "login") { - loginScreen.x = 0 - } else if (virtualstudio.windowState === "recommendations") { - recommendationsScreen.x = 0; - } else if (virtualstudio.windowState === "permissions") { - permissionsScreen.x = 0; - } else if (virtualstudio.windowState === "setup") { - setupScreen.x = 0 - } else if (virtualstudio.windowState === "browse") { - browseScreen.x = 0 - } else if (virtualstudio.windowState === "settings") { - settingsScreen.x = 0 - } else if (virtualstudio.windowState === "create_studio") { - createStudioScreen.x = 0 - } else if (virtualstudio.windowState === "connected") { - connectedScreen.x = 0 - } else if (virtualstudio.windowState === "change_devices") { - changeDevicesScreen.x = 0 - } else if (virtualstudio.windowState === "failed") { - failedScreen.x = 0 - } - } - - Connections { - target: auth - function onAuthSucceeded() { - if (virtualstudio.windowState !== "login") { - // can happen on settings screen when switching between prod and test - return; - } - if (virtualstudio.showWarnings) { - virtualstudio.windowState = "recommendations"; - } else if (virtualstudio.studioToJoin === "") { - virtualstudio.windowState = "browse"; - } else { - virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected"; - virtualstudio.joinStudio(); - } - } - } - Connections { - target: virtualstudio - function onConnected() { - if (virtualstudio.windowState == "change_devices") { - return; - } - virtualstudio.windowState = "connected"; - } - function onFailed() { - virtualstudio.windowState = "failed"; - } - function onDisconnected() { - virtualstudio.windowState = "browse"; - } - } -} diff --git a/src/gui/vsApi.cpp b/src/gui/vsApi.cpp deleted file mode 100644 index 2a50de2..0000000 --- a/src/gui/vsApi.cpp +++ /dev/null @@ -1,163 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsApi.cpp - * \author Dominick Hing - * \date May 2023 - */ - -#include "vsApi.h" - -#include "../jacktrip_globals.h" - -VsApi::VsApi(QNetworkAccessManager* networkAccessManager) -{ - m_networkAccessManager = networkAccessManager; -} - -QNetworkReply* VsApi::getAuth0UserInfo() -{ - return get(QUrl("https://auth.jacktrip.org/userinfo")); -} - -QNetworkReply* VsApi::getUser(const QString& userId) -{ - return get(QUrl(QString("https://%1/api/users/%2").arg(m_apiHost, userId))); -} - -QNetworkReply* VsApi::getServers() -{ - return get(QUrl(QString("https://%1/api/servers").arg(m_apiHost))); -} - -QNetworkReply* VsApi::getSubscriptions(const QString& userId) -{ - return get( - QUrl(QString("https://%1/api/users/%2/subscriptions").arg(m_apiHost, userId))); -} - -QNetworkReply* VsApi::getRegions(const QString& userId) -{ - return get(QUrl(QString("https://%1/api/users/%2/regions").arg(m_apiHost, userId))); -} - -QNetworkReply* VsApi::getDevice(const QString& deviceId) -{ - return get(QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId))); -} - -QNetworkReply* VsApi::postDevice(const QByteArray& data) -{ - return post(QUrl(QString("https://%1/api/devices").arg(m_apiHost)), data); -} - -QNetworkReply* VsApi::postDeviceHeartbeat(const QString& deviceId, const QByteArray& data) -{ - return post( - QUrl(QString("https://%1/api/devices/%2/heartbeat").arg(m_apiHost, deviceId)), - data); -} - -QNetworkReply* VsApi::submitServerFeedback(const QString& serverId, - const QByteArray& data) -{ - return post( - QUrl(QString("https://%1/api/servers/%2/feedback").arg(m_apiHost, serverId)), - data); -} - -QNetworkReply* VsApi::updateServer(const QString& serverId, const QByteArray& data) -{ - return put(QUrl(QString("https://%1/api/servers/%2").arg(m_apiHost, serverId)), data); -} - -QNetworkReply* VsApi::updateDevice(const QString& deviceId, const QByteArray& data) -{ - return put(QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)), data); -} - -QNetworkReply* VsApi::deleteDevice(const QString& deviceId) -{ - return deleteResource( - QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId))); -} - -QNetworkReply* VsApi::get(const QUrl& url) -{ - QNetworkRequest request = QNetworkRequest(url); - request.setRawHeader(QByteArray("User-Agent"), - QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8()); - request.setRawHeader(QByteArray("Authorization"), - QString("Bearer %1").arg(m_accessToken).toUtf8()); - - QNetworkReply* reply = m_networkAccessManager->get(request); - return reply; -} - -QNetworkReply* VsApi::post(const QUrl& url, const QByteArray& data) -{ - QNetworkRequest request = QNetworkRequest(url); - request.setRawHeader(QByteArray("User-Agent"), - QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8()); - request.setRawHeader(QByteArray("Authorization"), - QString("Bearer %1").arg(m_accessToken).toUtf8()); - request.setRawHeader(QByteArray("Content-Type"), - QString("application/json").toUtf8()); - - QNetworkReply* reply = m_networkAccessManager->post(request, data); - return reply; -} - -QNetworkReply* VsApi::put(const QUrl& url, const QByteArray& data) -{ - QNetworkRequest request = QNetworkRequest(url); - request.setRawHeader(QByteArray("User-Agent"), - QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8()); - request.setRawHeader(QByteArray("Authorization"), - QString("Bearer %1").arg(m_accessToken).toUtf8()); - request.setRawHeader(QByteArray("Content-Type"), - QString("application/json").toUtf8()); - QNetworkReply* reply = m_networkAccessManager->put(request, data); - return reply; -} - -QNetworkReply* VsApi::deleteResource(const QUrl& url) -{ - QNetworkRequest request = QNetworkRequest(url); - request.setRawHeader(QByteArray("User-Agent"), - QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8()); - request.setRawHeader(QByteArray("Authorization"), - QString("Bearer %1").arg(m_accessToken).toUtf8()); - - QNetworkReply* reply = m_networkAccessManager->deleteResource(request); - return reply; -} \ No newline at end of file diff --git a/src/gui/vsApi.h b/src/gui/vsApi.h deleted file mode 100644 index 2d64f0d..0000000 --- a/src/gui/vsApi.h +++ /dev/null @@ -1,89 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsApi.h - * \author Dominick Hing - * \date May 2023 - */ - -#ifndef VSAPI_H -#define VSAPI_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class VsApi : public QObject -{ - Q_OBJECT - - public: - VsApi(QNetworkAccessManager* networkAccessManager); - void setAccessToken(QString token) { m_accessToken = token; }; - void setApiHost(QString host) { m_apiHost = host; } - QString getApiHost() { return m_apiHost; } - - QNetworkReply* getAuth0UserInfo(); - QNetworkReply* getUser(const QString& userId); - QNetworkReply* getServers(); - QNetworkReply* getSubscriptions(const QString& userId); - QNetworkReply* getRegions(const QString& userId); - QNetworkReply* getDevice(const QString& deviceId); - - QNetworkReply* postDevice(const QByteArray& data); - QNetworkReply* postDeviceHeartbeat(const QString& deviceId, const QByteArray& data); - QNetworkReply* submitServerFeedback(const QString& serverId, const QByteArray& data); - - QNetworkReply* updateServer(const QString& serverId, const QByteArray& data); - QNetworkReply* updateDevice(const QString& deviceId, const QByteArray& data); - - QNetworkReply* deleteDevice(const QString& deviceId); - - private: - QNetworkReply* get(const QUrl& url); - QNetworkReply* put(const QUrl& url, const QByteArray& data); - QNetworkReply* post(const QUrl& url, const QByteArray& data); - QNetworkReply* deleteResource(const QUrl& url); - - QString m_accessToken; - QString m_apiHost; - QNetworkAccessManager* m_networkAccessManager; -}; - -#endif // VSAPI_H \ No newline at end of file diff --git a/src/gui/vsAudio.cpp b/src/gui/vsAudio.cpp deleted file mode 100644 index a9fda8f..0000000 --- a/src/gui/vsAudio.cpp +++ /dev/null @@ -1,1420 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsAudio.cpp - * \author Matt Horton - * \date September 2022 - */ - -#include "vsAudio.h" - -#include -#include -#include -#include -#include - -#ifdef USE_WEAK_JACK -#include "weak_libjack.h" -#endif - -#ifndef NO_JACK -#include "../JackAudioInterface.h" -#endif - -#ifdef __APPLE__ -#include "vsMacPermissions.h" -#else -#include "vsPermissions.h" -#endif - -#ifndef NO_FEEDBACK -#include "../Analyzer.h" -#endif - -#include "../JackTrip.h" -#include "../Meter.h" -#include "../Monitor.h" -#include "../Tone.h" -#include "../Volume.h" -#include "AudioInterfaceMode.h" - -// generic function to wait for a signal to be emitted -template -static inline void WaitForSignal(SignalSenderPtr sender, SignalFuncPtr signal) -{ - QTimer timer; - timer.setTimerType(Qt::CoarseTimer); - timer.setSingleShot(true); - - QEventLoop loop; - QObject::connect(sender, signal, &loop, &QEventLoop::quit); - QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); - timer.start(10000); // wait for max 10 seconds - loop.exec(); -} - -// Constructor -VsAudio::VsAudio(QObject* parent) - : QObject(parent) - , m_inputMeterLevels(2, 0) - , m_outputMeterLevels(2, 0) - , m_inputComboModel(QJsonArray::fromStringList(QStringList(QLatin1String("")))) - , m_outputComboModel(QJsonArray::fromStringList(QStringList(QLatin1String("")))) - , m_inputChannelsComboModel( - QJsonArray::fromStringList(QStringList(QLatin1String("")))) - , m_outputChannelsComboModel( - QJsonArray::fromStringList(QStringList(QLatin1String("")))) - , m_inputMixModeComboModel(QJsonArray::fromStringList(QStringList(QLatin1String("")))) - , m_audioWorkerPtr(new VsAudioWorker(this)) - , m_workerThreadPtr(nullptr) - , m_inputMeterPluginPtr(nullptr) - , m_outputMeterPluginPtr(nullptr) - , m_inputVolumePluginPtr(nullptr) - , m_outputVolumePluginPtr(nullptr) - , m_monitorPluginPtr(nullptr) - , mHasErrors(false) -{ - loadSettings(); - - QJsonObject element; - element.insert(QString::fromStdString("label"), QString::fromStdString("Mono")); - element.insert(QString::fromStdString("value"), - static_cast(AudioInterface::MONO)); - m_inputMixModeComboModel = QJsonArray(); - m_inputMixModeComboModel.push_back(element); - - element = QJsonObject(); - element.insert(QString::fromStdString("label"), QString::fromStdString("1")); - element.insert(QString::fromStdString("baseChannel"), QVariant(0).toInt()); - element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt()); - m_inputChannelsComboModel = QJsonArray(); - m_inputChannelsComboModel.push_back(element); - - element = QJsonObject(); - element.insert(QString::fromStdString("label"), QString::fromStdString("1 & 2")); - element.insert(QString::fromStdString("baseChannel"), QVariant(0).toInt()); - element.insert(QString::fromStdString("numChannels"), QVariant(2).toInt()); - m_outputChannelsComboModel = QJsonArray(); - m_outputChannelsComboModel.push_back(element); - - // Initialize timers needed for clip indicators - m_inputClipTimer.setTimerType(Qt::CoarseTimer); - m_inputClipTimer.setSingleShot(true); - m_inputClipTimer.setInterval(3000); - m_outputClipTimer.setTimerType(Qt::CoarseTimer); - m_outputClipTimer.setSingleShot(true); - m_outputClipTimer.setInterval(3000); - m_inputClipTimer.callOnTimeout([&]() { - if (m_inputClipped) { - m_inputClipped = false; - emit updatedInputClipped(m_inputClipped); - } - }); - m_outputClipTimer.callOnTimeout([&]() { - if (m_outputClipped) { - m_outputClipped = false; - emit updatedOutputClipped(m_outputClipped); - } - }); - - // move audio worker to its own thread - m_workerThreadPtr = new QThread; - m_workerThreadPtr->setObjectName("VsAudioWorker"); - m_workerThreadPtr->start(); - m_audioWorkerPtr->moveToThread(m_workerThreadPtr); - - // connect worker signals to slots - connect(this, &VsAudio::signalStartAudio, m_audioWorkerPtr.get(), - &VsAudioWorker::openAudioInterface, Qt::QueuedConnection); - connect(this, &VsAudio::signalStopAudio, m_audioWorkerPtr.get(), - &VsAudioWorker::closeAudioInterface, Qt::QueuedConnection); -#ifdef RT_AUDIO - connect(this, &VsAudio::signalRefreshDevices, m_audioWorkerPtr.get(), - &VsAudioWorker::refreshDevices, Qt::QueuedConnection); - connect(this, &VsAudio::signalValidateDevices, m_audioWorkerPtr.get(), - &VsAudioWorker::validateDevices, Qt::QueuedConnection); - connect(m_audioWorkerPtr.get(), &VsAudioWorker::signalUpdatedDeviceModels, this, - &VsAudio::setDeviceModels, Qt::QueuedConnection); -#endif - - // Add permissions for Mac -#ifdef __APPLE__ - m_permissionsPtr.reset(new VsMacPermissions()); - if (m_permissionsPtr->micPermissionChecked() - && m_permissionsPtr->micPermission() == "unknown") { - m_permissionsPtr->getMicPermission(); - } -#else - m_permissionsPtr.reset(new VsPermissions()); -#endif -} - -VsAudio::~VsAudio() -{ - if (m_workerThreadPtr == nullptr) - return; - m_workerThreadPtr->quit(); - WaitForSignal(m_workerThreadPtr, &QThread::finished); - m_workerThreadPtr->deleteLater(); -} - -bool VsAudio::backendAvailable() const -{ - if constexpr ((isBackendAvailable() - || isBackendAvailable())) { - return true; - } else { - return false; - } -} - -bool VsAudio::jackIsAvailable() const -{ - if constexpr (isBackendAvailable()) { -#ifdef USE_WEAK_JACK - // Check if Jack is available - return (have_libjack() == 0); -#else - return true; -#endif - } else { - return false; - } -} - -void VsAudio::setAudioReady(bool ready) -{ - if (ready == m_audioReady) - return; - m_audioReady = ready; - emit signalAudioReadyChanged(); - if (m_audioReady) - emit signalAudioIsReady(); - else - emit signalAudioIsNotReady(); -} - -void VsAudio::setScanningDevices(bool b) -{ - if (b == m_scanningDevices) - return; - m_scanningDevices = b; - emit signalScanningDevicesChanged(); -} - -void VsAudio::setAudioBackend(const QString& backend) -{ - bool useRtAudio = (backend == QStringLiteral("RtAudio")); - if (useRtAudio) { - if (getUseRtAudio()) - return; - m_backend = AudioBackendType::RTAUDIO; - } else { - if (!getUseRtAudio()) - return; - m_backend = AudioBackendType::JACK; - } - emit audioBackendChanged(useRtAudio); -} - -void VsAudio::setFeedbackDetectionEnabled(bool enabled) -{ - if (m_feedbackDetectionEnabled == enabled) - return; - m_feedbackDetectionEnabled = enabled; - emit feedbackDetectionEnabledChanged(); -} - -void VsAudio::setSampleRate(int sampleRate) -{ - if (m_audioSampleRate == sampleRate) - return; - m_audioSampleRate = sampleRate; - emit sampleRateChanged(); -} - -void VsAudio::setBufferSize(int bufSize) -{ - if (m_audioBufferSize == bufSize) - return; - m_audioBufferSize = bufSize; - emit bufferSizeChanged(); -} - -void VsAudio::setBufferStrategy(int bufStrategy) -{ - if (m_bufferStrategy == bufStrategy) - return; - m_bufferStrategy = bufStrategy; - emit bufferStrategyChanged(); -} - -void VsAudio::setNumInputChannels(int numChannels) -{ - if (numChannels == m_numInputChannels) - return; - m_numInputChannels = numChannels; - emit numInputChannelsChanged(numChannels); -} - -void VsAudio::setNumOutputChannels(int numChannels) -{ - if (numChannels == m_numOutputChannels) - return; - m_numOutputChannels = numChannels; - emit numOutputChannelsChanged(numChannels); -} - -void VsAudio::setBaseInputChannel(int baseChannel) -{ - if (baseChannel == m_baseInputChannel) - return; - m_baseInputChannel = baseChannel; - emit baseInputChannelChanged(baseChannel); - return; -} - -void VsAudio::setBaseOutputChannel(int baseChannel) -{ - if (baseChannel == m_baseOutputChannel) - return; - m_baseOutputChannel = baseChannel; - emit baseOutputChannelChanged(baseChannel); - return; -} - -void VsAudio::setInputMixMode(const int mode) -{ - if (mode == m_inputMixMode) - return; - m_inputMixMode = mode; - emit inputMixModeChanged(mode); - return; -} - -void VsAudio::setInputMuted(bool muted) -{ - if (m_inMuted == muted) - return; - m_inMuted = muted; - emit updatedInputMuted(muted); -} - -void VsAudio::setInputVolume(float multiplier) -{ - if (multiplier == m_inMultiplier) - return; - m_inMultiplier = multiplier; - emit updatedInputVolume(multiplier); -} - -void VsAudio::setOutputVolume(float multiplier) -{ - if (multiplier == m_outMultiplier) - return; - m_outMultiplier = multiplier; - emit updatedOutputVolume(multiplier); -} - -void VsAudio::setMonitorVolume(float multiplier) -{ - if (multiplier == m_monMultiplier) - return; - m_monMultiplier = multiplier; - emit updatedMonitorVolume(multiplier); -} - -void VsAudio::setInputDevice([[maybe_unused]] const QString& device) -{ - if (!getUseRtAudio()) - return; -#ifdef RT_AUDIO - if (device == m_inputDevice) - return; - m_inputDevice = device; - emit inputDeviceChanged(m_inputDevice); -#endif -} - -void VsAudio::setOutputDevice([[maybe_unused]] const QString& device) -{ - if (!getUseRtAudio()) - return; -#ifdef RT_AUDIO - if (device == m_outputDevice) - return; - m_outputDevice = device; - emit outputDeviceChanged(m_outputDevice); -#endif -} - -void VsAudio::setDevicesErrorMsg(const QString& msg) -{ - if (m_devicesErrorMsg == msg) - return; - m_devicesErrorMsg = msg; - emit devicesErrorChanged(); -} - -void VsAudio::setDevicesWarningMsg(const QString& msg) -{ - if (m_devicesWarningMsg == msg) - return; - m_devicesWarningMsg = msg; - emit devicesWarningChanged(); -} - -void VsAudio::setDevicesErrorHelpUrl(const QString& url) -{ - if (m_devicesErrorHelpUrl == url) - return; - m_devicesErrorHelpUrl = url; - emit devicesErrorHelpUrlChanged(); -} - -void VsAudio::setDevicesWarningHelpUrl(const QString& url) -{ - if (m_devicesWarningHelpUrl == url) - return; - m_devicesWarningHelpUrl = url; - emit devicesWarningHelpUrlChanged(); -} - -void VsAudio::setHighLatencyFlag(bool highLatencyFlag) -{ - if (m_highLatencyFlag == highLatencyFlag) - return; - m_highLatencyFlag = highLatencyFlag; - emit highLatencyFlagChanged(highLatencyFlag); -} - -void VsAudio::startAudio(bool block) -{ - // note this is also used for restartAudio() - emit signalStartAudio(); - if (!block) - return; - WaitForSignal(this, &VsAudio::signalAudioIsReady); -} - -void VsAudio::stopAudio(bool block) -{ - if (!getAudioReady()) - return; - emit signalStopAudio(); - if (!block) - return; - WaitForSignal(this, &VsAudio::signalAudioIsNotReady); -} - -void VsAudio::refreshDevices(bool block) -{ - if (!getUseRtAudio()) - return; - emit signalRefreshDevices(); - if (!block) - return; - WaitForSignal(m_audioWorkerPtr.get(), &VsAudioWorker::signalDevicesValidated); -} - -void VsAudio::validateDevices(bool block) -{ - if (!getUseRtAudio()) - return; - emit signalValidateDevices(); - if (!block) - return; - WaitForSignal(m_audioWorkerPtr.get(), &VsAudioWorker::signalDevicesValidated); -} - -void VsAudio::loadSettings() -{ - QSettings settings; - settings.beginGroup(QStringLiteral("Audio")); - setInputVolume(settings.value(QStringLiteral("InMultiplier"), 1).toFloat()); - setOutputVolume(settings.value(QStringLiteral("OutMultiplier"), 1).toFloat()); - setMonitorVolume(settings.value(QStringLiteral("MonMultiplier"), 0).toFloat()); - // note: we should always reset input muted to false; otherwise, bad things - setInputMuted(false); - // setInputMuted(settings.value(QStringLiteral("InMuted"), false).toBool()); - - // load audio backend - AudioBackendType audioBackend; - if constexpr (isBackendAvailable()) { - audioBackend = - (settings.value(QStringLiteral("Backend"), AudioBackendType::RTAUDIO).toInt() - == 1) - ? AudioBackendType::RTAUDIO - : AudioBackendType::JACK; - } else if constexpr (isBackendAvailable()) { - audioBackend = AudioBackendType::RTAUDIO; - } else { - audioBackend = AudioBackendType::JACK; - } - if (audioBackend != m_backend) { - setAudioBackend(audioBackend == AudioBackendType::RTAUDIO - ? QStringLiteral("RtAudio") - : QStringLiteral("JACK")); - } - - // load input and output devices - QString inputDevice = settings.value(QStringLiteral("InputDevice"), "").toString(); - QString outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString(); - if (inputDevice == QStringLiteral("(default)")) { - inputDevice = ""; - } - if (outputDevice == QStringLiteral("(default)")) { - outputDevice = ""; - } - setInputDevice(inputDevice); - setOutputDevice(outputDevice); - - // use default base channel 0, if the setting does not exist - setBaseInputChannel(settings.value(QStringLiteral("BaseInputChannel"), 0).toInt()); - setBaseOutputChannel(settings.value(QStringLiteral("BaseOutputChannel"), 0).toInt()); - - // Handle migration scenarios. Assume this is a new user - // if we have m_inputDevice == "" and m_outputDevice == "" - if (m_inputDevice == "" && m_outputDevice == "") { - // for fresh installs, use mono by default - setNumInputChannels( - settings.value(QStringLiteral("NumInputChannels"), 1).toInt()); - setInputMixMode(settings - .value(QStringLiteral("InputMixMode"), - static_cast(AudioInterface::MONO)) - .toInt()); - - // use 2 channels for output - setNumOutputChannels( - settings.value(QStringLiteral("NumOutputChannels"), 2).toInt()); - } else { - // existing installs - keep using stereo - setNumInputChannels( - settings.value(QStringLiteral("NumInputChannels"), 2).toInt()); - setInputMixMode(settings - .value(QStringLiteral("InputMixMode"), - static_cast(AudioInterface::STEREO)) - .toInt()); - - // use 2 channels for output - setNumOutputChannels( - settings.value(QStringLiteral("NumOutputChannels"), 2).toInt()); - } - - setBufferSize(settings.value(QStringLiteral("BufferSize"), 128).toInt()); - int buffer_strategy = settings.value(QStringLiteral("BufferStrategy"), 2).toInt(); - if (buffer_strategy == 3 || buffer_strategy == 4) - buffer_strategy = 2; - setBufferStrategy(buffer_strategy); - setFeedbackDetectionEnabled( - settings.value(QStringLiteral("FeedbackDetectionEnabled"), true).toBool()); - settings.endGroup(); -} - -void VsAudio::saveSettings() -{ - QSettings settings; - settings.beginGroup(QStringLiteral("Audio")); - settings.setValue(QStringLiteral("InMultiplier"), m_inMultiplier); - settings.setValue(QStringLiteral("OutMultiplier"), m_outMultiplier); - settings.setValue(QStringLiteral("MonMultiplier"), m_monMultiplier); - // settings.setValue(QStringLiteral("InMuted"), m_inMuted); - settings.setValue(QStringLiteral("Backend"), getUseRtAudio() ? 1 : 0); - settings.setValue(QStringLiteral("InputDevice"), m_inputDevice); - settings.setValue(QStringLiteral("OutputDevice"), m_outputDevice); - settings.setValue(QStringLiteral("BaseInputChannel"), m_baseInputChannel); - settings.setValue(QStringLiteral("NumInputChannels"), m_numInputChannels); - settings.setValue(QStringLiteral("InputMixMode"), m_inputMixMode); - settings.setValue(QStringLiteral("BaseOutputChannel"), m_baseOutputChannel); - settings.setValue(QStringLiteral("NumOutputChannels"), m_numOutputChannels); - settings.setValue(QStringLiteral("BufferSize"), m_audioBufferSize); - settings.setValue(QStringLiteral("BufferStrategy"), m_bufferStrategy); - settings.setValue(QStringLiteral("FeedbackDetectionEnabled"), - m_feedbackDetectionEnabled); - settings.endGroup(); -} - -void VsAudio::detectedFeedbackLoop() -{ - setInputMuted(true); - setMonitorVolume(0); - emit feedbackDetected(); -} - -void VsAudio::updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels) -{ - bool detectedClip = false; - - // Always output 2 meter readings to the UI - for (int i = 0; i < 2; i++) { - // Determine decibel reading - float dB = m_meterMin; - if (i < numChannels) { - dB = std::max(m_meterMin, valuesInDecibels[i]); - } - - // Produce a normalized value from 0 to 1 - m_inputMeterLevels[i] = (dB - m_meterMin) / (m_meterMax - m_meterMin); - - // Signal a clip if we haven't done so already - if (dB >= -0.05 && !detectedClip) { - m_inputClipTimer.start(); - m_inputClipped = true; - emit updatedInputClipped(m_inputClipped); - detectedClip = true; - } - } - -#ifdef RT_AUDIO - // For certain specific cases, copy the first channel's value into the second - // channel's value - if (getUseRtAudio() - && ((m_inputMixMode == static_cast(AudioInterface::MONO) - && m_numInputChannels == 1) - || (m_inputMixMode == static_cast(AudioInterface::MIXTOMONO) - && m_numInputChannels == 2))) { - m_inputMeterLevels[1] = m_inputMeterLevels[0]; - } -#endif - - emit updatedInputMeterLevels(m_inputMeterLevels); -} - -void VsAudio::updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels) -{ - bool detectedClip = false; - - // Always output 2 meter readings to the UI - for (int i = 0; i < 2; i++) { - // Determine decibel reading - float dB = m_meterMin; - if (i < numChannels) { - dB = std::max(m_meterMin, valuesInDecibels[i]); - } - - // Produce a normalized value from 0 to 1 - m_outputMeterLevels[i] = (dB - m_meterMin) / (m_meterMax - m_meterMin); - - // Signal a clip if we haven't done so already - if (dB >= -0.05 && !detectedClip) { - m_outputClipTimer.start(); - m_outputClipped = true; - emit updatedOutputClipped(m_outputClipped); - detectedClip = true; - } - } -#ifdef RT_AUDIO - if (m_numOutputChannels == 1) { - m_outputMeterLevels[1] = m_outputMeterLevels[0]; - } -#endif - emit updatedOutputMeterLevels(m_outputMeterLevels); -} - -void VsAudio::appendProcessPlugins(AudioInterface& audioInterface, bool forJackTrip, - int numInputChannels, int numOutputChannels) -{ - // Make sure clip timers are stopped - m_inputClipTimer.stop(); - m_outputClipTimer.stop(); - - // Reset meters - m_inputMeterLevels[0] = m_inputMeterLevels[1] = 0; - m_outputMeterLevels[0] = m_outputMeterLevels[1] = 0; - m_inputClipped = m_outputClipped = false; - emit updatedInputMeterLevels(m_inputMeterLevels); - emit updatedOutputMeterLevels(m_outputMeterLevels); - emit updatedInputClipped(m_inputClipped); - emit updatedOutputClipped(m_outputClipped); - setInputMuted(false); - - // Create plugins - m_inputMeterPluginPtr = new Meter(numInputChannels); - m_outputMeterPluginPtr = new Meter(numOutputChannels); - m_inputVolumePluginPtr = new Volume(numInputChannels); - m_outputVolumePluginPtr = new Volume(numOutputChannels); - - // initialize input and output volumes - m_outputVolumePluginPtr->volumeUpdated(m_outMultiplier); - m_inputVolumePluginPtr->volumeUpdated(m_inMultiplier); - m_inputVolumePluginPtr->muteUpdated(m_inMuted); - - // Connect plugins for communication with UI - connect(m_inputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this, - &VsAudio::updatedInputVuMeasurements); - connect(m_outputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this, - &VsAudio::updatedOutputVuMeasurements); - connect(this, &VsAudio::updatedInputVolume, m_inputVolumePluginPtr, - &Volume::volumeUpdated); - connect(this, &VsAudio::updatedOutputVolume, m_outputVolumePluginPtr, - &Volume::volumeUpdated); - connect(this, &VsAudio::updatedInputMuted, m_inputVolumePluginPtr, - &Volume::muteUpdated); - - // Note that plugin ownership is passed to the JackTrip class - // In particular, the AudioInterface that it uses to connect - audioInterface.appendProcessPluginToNetwork(m_inputVolumePluginPtr); - audioInterface.appendProcessPluginToNetwork(m_inputMeterPluginPtr); - - if (forJackTrip) { - // plugins for stream going to audio interface - audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr); - - // Setup monitor - // Note: Constructor determines how many internal monitor buffers to allocate - m_monitorPluginPtr = new Monitor(std::max(numInputChannels, numOutputChannels)); - m_monitorPluginPtr->volumeUpdated(m_monMultiplier); - audioInterface.appendProcessPluginToMonitor(m_monitorPluginPtr); - connect(this, &VsAudio::updatedMonitorVolume, m_monitorPluginPtr, - &Monitor::volumeUpdated); - -#ifndef NO_FEEDBACK - // Setup output analyzer - if (m_feedbackDetectionEnabled) { - m_outputAnalyzerPluginPtr = new Analyzer(numOutputChannels); - m_outputAnalyzerPluginPtr->setIsMonitoringAnalyzer(true); - audioInterface.appendProcessPluginToMonitor(m_outputAnalyzerPluginPtr); - connect(m_outputAnalyzerPluginPtr, &Analyzer::signalFeedbackDetected, this, - &VsAudio::detectedFeedbackLoop); - } -#endif - - // Setup output meter - // Note: Add this to monitor process to include self-volume - m_outputMeterPluginPtr->setIsMonitoringMeter(true); - audioInterface.appendProcessPluginToMonitor(m_outputMeterPluginPtr); - - } else { - // tone plugin is used to test audio output - Tone* outputTonePluginPtr = new Tone(getNumOutputChannels()); - connect(this, &VsAudio::signalPlayOutputAudio, outputTonePluginPtr, - &Tone::triggerPlayback); - audioInterface.appendProcessPluginFromNetwork(outputTonePluginPtr); - - // plugins for stream going to audio interface - audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr); - audioInterface.appendProcessPluginFromNetwork(m_outputMeterPluginPtr); - } -} - -void VsAudio::setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel) -{ - m_inputComboModel = inputComboModel; - m_outputComboModel = outputComboModel; - emit inputComboModelChanged(); - emit outputComboModelChanged(); - if (!m_deviceModelsInitialized) { - m_deviceModelsInitialized = true; - emit deviceModelsInitializedChanged(true); - } -} - -void VsAudio::setInputChannelsComboModel(QJsonArray& model) -{ - m_inputChannelsComboModel = model; - emit inputChannelsComboModelChanged(); -} - -void VsAudio::setOutputChannelsComboModel(QJsonArray& model) -{ - m_outputChannelsComboModel = model; - emit outputChannelsComboModelChanged(); -} - -void VsAudio::setInputMixModeComboModel(QJsonArray& model) -{ - m_inputMixModeComboModel = model; - emit inputMixModeComboModelChanged(); -} - -void VsAudio::updateDeviceMessages(AudioInterface& audioInterface) -{ - QString devicesWarningMsg = - QString::fromStdString(audioInterface.getDevicesWarningMsg()); - QString devicesErrorMsg = QString::fromStdString(audioInterface.getDevicesErrorMsg()); - QString devicesWarningHelpUrl = - QString::fromStdString(audioInterface.getDevicesWarningHelpUrl()); - QString devicesErrorHelpUrl = - QString::fromStdString(audioInterface.getDevicesErrorHelpUrl()); - - if (devicesWarningMsg != "") { - qDebug() << "Devices Warning: " << devicesWarningMsg; - if (devicesWarningHelpUrl != "") { - qDebug() << "Learn More: " << devicesWarningHelpUrl; - } - } - - if (devicesErrorMsg != "") { - qDebug() << "Devices Error: " << devicesErrorMsg; - if (devicesErrorHelpUrl != "") { - qDebug() << "Learn More: " << devicesErrorHelpUrl; - } - } - - setDevicesWarningMsg(devicesWarningMsg); - setDevicesErrorMsg(devicesErrorMsg); - setDevicesWarningHelpUrl(devicesWarningHelpUrl); - setDevicesErrorHelpUrl(devicesErrorHelpUrl); - setHighLatencyFlag(audioInterface.getHighLatencyFlag()); -} - -AudioInterface* VsAudio::newAudioInterface(JackTrip* jackTripPtr) -{ - // Create AudioInterface Client Object - AudioInterface* ifPtr = nullptr; - if (m_backend == VsAudio::AudioBackendType::JACK) { - if (!isBackendAvailable()) { - throw std::runtime_error( - "JackTrip was not compiled with support for the Jack backend. " - "In order to use Jack, you'll need to " - "rebuild with Jack support."); - } - if (!jackIsAvailable()) { - throw std::runtime_error( - "Unable to load the Jack client library. " - "In order to use Jack, you'll need to first install it."); - } - qDebug() << "Using JACK backend"; - ifPtr = newJackAudioInterface(jackTripPtr); - } else if (m_backend == VsAudio::AudioBackendType::RTAUDIO) { - if (!isBackendAvailable()) { - throw std::runtime_error( - "JackTrip was not compiled with support for the RtAudio backend. " - "In order to use RtAudio, you'll need to " - "rebuild with RtAudio support."); - } - qDebug() << "Using RtAudio backend"; - ifPtr = newRtAudioInterface(jackTripPtr); - } else { - throw std::runtime_error("Unknown audio backend"); - } - - mHasErrors = false; - ifPtr->setErrorCallback([this, jackTripPtr](const std::string& errorText) { - this->errorCallback(errorText, jackTripPtr); - }); - - // AudioInterface::setup() can return a different buffer size - // if the audio interface doesn't support the one that was requested - if (ifPtr->getBufferSizeInSamples() != uint32_t(getBufferSize())) { - setBufferSize(ifPtr->getBufferSizeInSamples()); - } - - std::cout << "The Sampling Rate is: " << ifPtr->getSampleRate() << std::endl; - std::cout << gPrintSeparator << std::endl; - int AudioBufferSizeInBytes = ifPtr->getBufferSizeInSamples() * sizeof(sample_t); - std::cout << "The Audio Buffer Size is: " << ifPtr->getBufferSizeInSamples() - << " samples" << std::endl; - std::cout << " or: " << AudioBufferSizeInBytes << " bytes" - << std::endl; - std::cout << gPrintSeparator << std::endl; - std::cout << "The Number of Channels is: " << ifPtr->getNumInputChannels() - << std::endl; - std::cout << gPrintSeparator << std::endl; - - // setup audio plugins - appendProcessPlugins(*ifPtr, jackTripPtr != nullptr, getNumInputChannels(), - getNumOutputChannels()); - - return ifPtr; -} - -AudioInterface* VsAudio::newJackAudioInterface([[maybe_unused]] JackTrip* jackTripPtr) -{ - AudioInterface* ifPtr = nullptr; -#ifndef NO_JACK - static const int numJackChannels = 2; - if constexpr (isBackendAvailable() - || isBackendAvailable()) { - QVarLengthArray inputChans; - QVarLengthArray outputChans; - inputChans.resize(numJackChannels); - outputChans.resize(numJackChannels); - - for (int i = 0; i < numJackChannels; i++) { - inputChans[i] = 1 + i; - } - for (int i = 0; i < numJackChannels; i++) { - outputChans[i] = 1 + i; - } - - ifPtr = new JackAudioInterface(inputChans, outputChans, m_audioBitResolution, - jackTripPtr != nullptr, jackTripPtr); - ifPtr->setClientName(QStringLiteral("JackTrip")); -#if defined(__unix__) - AudioInterface::setPipewireLatency(getBufferSize(), getSampleRate()); -#endif - ifPtr->setup(true); - } -#endif - return ifPtr; -} - -AudioInterface* VsAudio::newRtAudioInterface([[maybe_unused]] JackTrip* jackTripPtr) -{ - AudioInterface* ifPtr = nullptr; -#ifdef RT_AUDIO - QVarLengthArray inputChans; - QVarLengthArray outputChans; - inputChans.resize(getNumInputChannels()); - outputChans.resize(getNumOutputChannels()); - - for (int i = 0; i < getNumInputChannels(); i++) { - inputChans[i] = getBaseInputChannel() + i; - } - for (int i = 0; i < getNumOutputChannels(); i++) { - outputChans[i] = getBaseOutputChannel() + i; - } - - ifPtr = new RtAudioInterface( - inputChans, outputChans, - static_cast(getInputMixMode()), - m_audioBitResolution, jackTripPtr != nullptr, jackTripPtr); - ifPtr->setSampleRate(getSampleRate()); - ifPtr->setInputDevice(getInputDevice().toStdString()); - ifPtr->setOutputDevice(getOutputDevice().toStdString()); - ifPtr->setBufferSizeInSamples(getBufferSize()); - - QVector devices = m_audioWorkerPtr->getDevices(); - if (!devices.empty()) - static_cast(ifPtr)->setRtAudioDevices(devices); - -#if defined(__unix__) - AudioInterface::setPipewireLatency(getBufferSize(), ifPtr->getSampleRate()); -#endif - - // Note: setup might change the number of channels and/or buffer size - ifPtr->setup(true); - - // TODO: Add check for if base input channel needs to change - if (jackTripPtr != nullptr && getNumInputChannels() == 2 - && getInputMixMode() == AudioInterface::MIXTOMONO) - jackTripPtr->setNumInputChannels(1); - -#endif // RT_AUDIO - return ifPtr; -} - -void VsAudio::errorCallback(const std::string& errorText, - [[maybe_unused]] JackTrip* jackTripPtr) -{ - const QString errorMsg(QString::fromStdString(errorText)); - setDevicesErrorMsg(errorMsg); -#ifdef _WIN32 - // handle special case for Windows ASIO drivers that trigger - // asynchronous errors shortly after you try opening the - // RtAudio stream with a different sample rate (only for audio tester) - if (jackTripPtr == nullptr && getUseRtAudio() - && errorMsg.contains("sample rate changed")) { - // only refresh devices once - if (mHasErrors) - return; - mHasErrors = true; - // asynchronously refresh devices - refreshDevices(false); - } -#else - mHasErrors = true; -#endif -} - -// VsAudioWorker methods - -VsAudioWorker::VsAudioWorker(VsAudio* ptr) : m_parentPtr(ptr) {} - -void VsAudioWorker::openAudioInterface() -{ -#ifdef __APPLE__ - if (m_parentPtr->m_permissionsPtr->micPermission() != "granted") { - return; - } -#endif - - if constexpr (!(isBackendAvailable() - || isBackendAvailable())) { - return; - } - - if (!m_audioInterfacePtr.isNull()) { - std::cout << "Restarting Audio" << std::endl; - closeAudioInterface(); - } else { - std::cout << "Starting Audio" << std::endl; - } - - unsigned int maxTries = 2; -#ifdef RT_AUDIO - // Update devices, if not already initialized - if (getUseRtAudio()) { - if (!m_parentPtr->getDeviceModelsInitialized()) { - updateDeviceModels(); - maxTries = 1; - } - } -#endif - for (unsigned int tryNum = 0; tryNum < maxTries; ++tryNum) { -#ifdef RT_AUDIO - if (tryNum > 0) { - if (getUseRtAudio()) { - updateDeviceModels(); - } else { - m_parentPtr->setAudioBackend("RtAudio"); - updateDeviceModels(); - } - } -#endif - try { - // create and setup a new audio interface - m_audioInterfacePtr.reset(m_parentPtr->newAudioInterface()); - // success if it doesn't throw - break; - } catch (const std::exception& e) { - emit signalError(QString::fromUtf8(e.what())); - } - } - - if (m_audioInterfacePtr.isNull()) { - return; - } - - // initialize plugins and start the audio callback process - m_audioInterfacePtr->initPlugins(false); - m_audioInterfacePtr->startProcess(); - m_audioInterfacePtr->connectDefaultPorts(); - - m_parentPtr->updateDeviceMessages(*m_audioInterfacePtr); - m_parentPtr->setAudioReady(true); -} - -void VsAudioWorker::closeAudioInterface() -{ - if (m_audioInterfacePtr.isNull()) - return; - std::cout << "Stopping Audio" << std::endl; - try { - m_audioInterfacePtr->stopProcess(); - } catch (const std::exception& e) { - emit signalError(QString::fromUtf8(e.what())); - } - m_audioInterfacePtr.clear(); - m_parentPtr->setAudioReady(false); -} - -#ifdef RT_AUDIO - -void VsAudioWorker::refreshDevices() -{ - if (!getUseRtAudio()) - return; - bool restartAudio = !m_audioInterfacePtr.isNull(); - if (restartAudio) - closeAudioInterface(); - updateDeviceModels(); - if (restartAudio) - openAudioInterface(); -} - -void VsAudioWorker::updateDeviceModels() -{ - if (!getUseRtAudio()) - return; - - // note: audio must not be active when scanning devices - m_parentPtr->setScanningDevices(true); - closeAudioInterface(); - RtAudioInterface::scanDevices(m_devices); - - QStringList inputDeviceCategories; - QStringList outputDeviceCategories; - - getDeviceList(m_devices, m_inputDeviceList, inputDeviceCategories, - m_inputDeviceChannels, true); - getDeviceList(m_devices, m_outputDeviceList, outputDeviceCategories, - m_outputDeviceChannels, false); - - QJsonArray inputComboModel = - formatDeviceList(m_inputDeviceList, inputDeviceCategories, m_inputDeviceChannels); - QJsonArray outputComboModel = formatDeviceList( - m_outputDeviceList, outputDeviceCategories, m_outputDeviceChannels); - - validateDevices(); - - // let VsAudio know that things have been updated - m_parentPtr->setScanningDevices(false); - emit signalUpdatedDeviceModels(inputComboModel, outputComboModel); -} - -void VsAudioWorker::getDeviceList(const QVector& devices, - QStringList& list, QStringList& categories, - QList& channels, bool isInput) -{ - categories.clear(); - channels.clear(); - list.clear(); - - // do not include blacklisted audio interfaces - // these are known to be unstable and cause JackTrip to crash - QVector blacklisted_devices = { -#ifdef _WIN32 - // Realtek ASIO: seems to crash any computer that tries to use it - QString::fromUtf8("Realtek ASIO"), - QString::fromUtf8("Generic Low Latency ASIO Driver"), -#endif - // JackRouter: crashes if not running; use Jack backend instead - QString::fromUtf8("JackRouter"), - }; - - for (int n = 0; n < devices.size(); ++n) { -#ifdef _WIN32 - if (devices[n].api == RtAudio::UNIX_JACK) { - continue; - } -#endif - const QString deviceName(QString::fromStdString(devices[n].name)); - - // Don't include duplicate entries - if (list.contains(deviceName)) { - continue; - } - - // Skip if no channels available - if ((isInput && devices[n].inputChannels == 0) - || (!isInput && devices[n].outputChannels == 0)) { - continue; - } - - // Skip blacklisted devices - if (blacklisted_devices.contains(deviceName)) { - std::cout << "RTAudio: blacklisted " << (isInput ? "input" : "output") - << " device: " << devices[n].name << std::endl; - continue; - } - - // Good to go! - if (isInput) { - list.append(deviceName); - channels.append(devices[n].inputChannels); - } else { - list.append(deviceName); - channels.append(devices[n].outputChannels); - } - - switch (devices[n].api) { - case RtAudio::WINDOWS_ASIO: - categories.append("Low-Latency (ASIO)"); - break; - case RtAudio::WINDOWS_WASAPI: - categories.append("High-Latency (WASAPI)"); - break; - case RtAudio::WINDOWS_DS: - categories.append("High-Latency (DirectSound)"); - break; - case RtAudio::LINUX_ALSA: - categories.append("Low-Latency (ALSA)"); - break; - case RtAudio::LINUX_PULSE: - categories.append("High-Latency (Pulse)"); - break; - case RtAudio::LINUX_OSS: - categories.append("High-Latency (OSS)"); - break; - default: - categories.append(""); - break; - } - } -} - -QJsonArray VsAudioWorker::formatDeviceList(const QStringList& devices, - const QStringList& categories, - const QList& channels) -{ - QStringList uniqueCategories = QStringList(categories); - uniqueCategories.removeDuplicates(); - - bool containsCategories = true; - if (uniqueCategories.size() == 0) { - containsCategories = false; - } else if (uniqueCategories.size() == 1 && uniqueCategories.at(0) == "") { - containsCategories = false; - } - - QJsonArray items; - for (int i = 0; i < uniqueCategories.size(); i++) { - QString category = uniqueCategories.at(i); - - if (containsCategories) { - QJsonObject header = QJsonObject(); - header.insert(QString::fromStdString("text"), category); - header.insert(QString::fromStdString("type"), - QString::fromStdString("header")); - header.insert(QString::fromStdString("category"), category); - items.push_back(header); - } - - for (int j = 0; j < devices.size(); j++) { - if (categories.at(j).toStdString() == category.toStdString()) { - QJsonObject element = QJsonObject(); - element.insert(QString::fromStdString("text"), devices.at(j)); - element.insert(QString::fromStdString("type"), - QString::fromStdString("element")); - element.insert(QString::fromStdString("channels"), channels.at(j)); - element.insert(QString::fromStdString("category"), category); - items.push_back(element); - } - } - } - - return items; -} - -void VsAudioWorker::validateDevices() -{ - if (!getUseRtAudio()) - return; - validateInputDevicesState(); - validateOutputDevicesState(); - emit signalDevicesValidated(); -} - -void VsAudioWorker::validateInputDevicesState() -{ - if (!getUseRtAudio()) { - return; - } - if (m_inputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) { - return; - } - - // Given input device list, check that the currently set device - // actually exists - if (getInputDevice() == QStringLiteral("") - || m_inputDeviceList.indexOf(getInputDevice()) == -1) { - m_parentPtr->setInputDevice(m_inputDeviceList[0]); - } - - // Given the currently selected input device, reset the available input channel - // options - int indexOfInput = m_inputDeviceList.indexOf(getInputDevice()); - if (indexOfInput == -1) { - std::cerr << "Invalid state. Input device index should never be -1" << std::endl; - return; - } - - int numDevicesChannelsAvailable = m_inputDeviceChannels.at(indexOfInput); - if (numDevicesChannelsAvailable < 1) { - std::cerr << "Invalid state. Number of channels should never be less than 1" - << std::endl; - return; - } else if (numDevicesChannelsAvailable == 1) { - // Set the input mix mode to just have "Mono" as the option - QJsonObject inputMixModeComboElement = QJsonObject(); - inputMixModeComboElement.insert(QString::fromStdString("label"), - QString::fromStdString("Mono")); - inputMixModeComboElement.insert(QString::fromStdString("value"), - static_cast(AudioInterface::MONO)); - QJsonArray inputMixModeComboModel; - inputMixModeComboModel.push_back(inputMixModeComboElement); - m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel); - - // Set the input channels combo to only have channel 1 as an option - QJsonObject inputChannelsComboElement; - inputChannelsComboElement.insert(QString::fromStdString("label"), - QString::fromStdString("1")); - inputChannelsComboElement.insert(QString::fromStdString("baseChannel"), - QVariant(0).toInt()); - inputChannelsComboElement.insert(QString::fromStdString("numChannels"), - QVariant(1).toInt()); - QJsonArray inputChannelsComboModel; - inputChannelsComboModel.push_back(inputChannelsComboElement); - m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel); - - // Set the only allowed options for these variables automatically - m_parentPtr->setBaseInputChannel(0); - m_parentPtr->setNumInputChannels(1); - m_parentPtr->setInputMixMode(static_cast(AudioInterface::MONO)); - } else { - // set the input channels selector to have the options based on the currently - // selected device - QJsonArray inputChannelsComboModel; - for (int i = 0; i < numDevicesChannelsAvailable; i++) { - QJsonObject element = QJsonObject(); - element.insert(QString::fromStdString("label"), QVariant(i + 1).toString()); - element.insert(QString::fromStdString("baseChannel"), QVariant(i).toInt()); - element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt()); - inputChannelsComboModel.push_back(element); - } - for (int i = 0; i < numDevicesChannelsAvailable; i++) { - if (i % 2 == 0) { - QJsonObject element = QJsonObject(); - element.insert( - QString::fromStdString("label"), - QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString()); - element.insert(QString::fromStdString("baseChannel"), - QVariant(i).toInt()); - element.insert(QString::fromStdString("numChannels"), - QVariant(2).toInt()); - inputChannelsComboModel.push_back(element); - } - } - m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel); - - // if the current m_baseInputChannel or m_numInputChannels is invalid based on - // this device's option, use the first two channels by default - if (getBaseInputChannel() + getNumInputChannels() > numDevicesChannelsAvailable) { - // we're in the case where numDevicesChannelsAvailable >= 2, so always have - // the ability to use the first 2 channels - m_parentPtr->setBaseInputChannel(0); - m_parentPtr->setNumInputChannels(2); - } - if (getNumInputChannels() != 1) { - // Set the input mix mode to have two options: "Stereo" and "Mix to Mono" if - // we're using 2 channels - QJsonObject inputMixModeComboElement1 = QJsonObject(); - inputMixModeComboElement1.insert(QString::fromStdString("label"), - QString::fromStdString("Stereo")); - inputMixModeComboElement1.insert(QString::fromStdString("value"), - static_cast(AudioInterface::STEREO)); - QJsonObject inputMixModeComboElement2 = QJsonObject(); - inputMixModeComboElement2.insert(QString::fromStdString("label"), - QString::fromStdString("Mix to Mono")); - inputMixModeComboElement2.insert(QString::fromStdString("value"), - static_cast(AudioInterface::MIXTOMONO)); - QJsonArray inputMixModeComboModel; - inputMixModeComboModel.push_back(inputMixModeComboElement1); - inputMixModeComboModel.push_back(inputMixModeComboElement2); - m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel); - - // if m_inputMixMode is an invalid value, set it to "stereo" by default - // given that we are using 2 channels - if (getInputMixMode() != static_cast(AudioInterface::STEREO) - && getInputMixMode() != static_cast(AudioInterface::MIXTOMONO)) { - m_parentPtr->setInputMixMode(static_cast(AudioInterface::STEREO)); - } - } else { - // Set the input mix mode to just have "Mono" as the option if we're using 1 - // channel - QJsonObject inputMixModeComboElement = QJsonObject(); - inputMixModeComboElement.insert(QString::fromStdString("label"), - QString::fromStdString("Mono")); - inputMixModeComboElement.insert(QString::fromStdString("value"), - static_cast(AudioInterface::MONO)); - QJsonArray inputMixModeComboModel; - inputMixModeComboModel.push_back(inputMixModeComboElement); - m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel); - - // if m_inputMixMode is an invalid value, set it to AudioInterface::MONO - if (getInputMixMode() != static_cast(AudioInterface::MONO)) { - m_parentPtr->setInputMixMode(static_cast(AudioInterface::MONO)); - } - } - } -} - -void VsAudioWorker::validateOutputDevicesState() -{ - if (!getUseRtAudio()) { - return; - } - if (m_outputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) { - return; - } - - // Given output device list, check that the currently set device - // actually exists - if (getOutputDevice() == QStringLiteral("") - || m_outputDeviceList.indexOf(getOutputDevice()) == -1) { - m_parentPtr->setOutputDevice(m_outputDeviceList[0]); - } - - // Given the currently selected output device, reset the available output channel - // options - int indexOfOutput = m_outputDeviceList.indexOf(getOutputDevice()); - if (indexOfOutput == -1) { - std::cerr << "Invalid state. Output device index should never be -1" << std::endl; - return; - } - - int numDevicesChannelsAvailable = m_outputDeviceChannels.at(indexOfOutput); - if (numDevicesChannelsAvailable < 1) { - std::cerr << "Invalid state. Number of channels should never be less than 1" - << std::endl; - return; - } else if (numDevicesChannelsAvailable == 1) { - // Set the output channels combo to only have channel 1 as an option - QJsonObject outputChannelsComboElement = QJsonObject(); - outputChannelsComboElement.insert(QString::fromStdString("label"), - QString::fromStdString("1")); - outputChannelsComboElement.insert(QString::fromStdString("baseChannel"), - QVariant(0).toInt()); - outputChannelsComboElement.insert(QString::fromStdString("numChannels"), - QVariant(1).toInt()); - QJsonArray outputChannelsComboModel; - outputChannelsComboModel.push_back(outputChannelsComboElement); - m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel); - - // Set the only allowed options for these variables automatically - m_parentPtr->setBaseOutputChannel(0); - m_parentPtr->setNumOutputChannels(1); - } else { - // set the output channels selector to have the options based on the currently - // selected device - QJsonArray outputChannelsComboModel; - for (int i = 0; i < numDevicesChannelsAvailable; i++) { - if (i % 2 == 0) { - QJsonObject element = QJsonObject(); - element.insert( - QString::fromStdString("label"), - QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString()); - element.insert(QString::fromStdString("baseChannel"), - QVariant(i).toInt()); - element.insert(QString::fromStdString("numChannels"), - QVariant(2).toInt()); - outputChannelsComboModel.push_back(element); - } - } - m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel); - - // if the current m_baseOutputChannel or m_numOutputChannels is invalid based on - // this device's option, use the first two channels by default - if (getBaseOutputChannel() + getNumOutputChannels() - > numDevicesChannelsAvailable) { - // we're in the case where numDevicesChannelsAvailable >= 2, so always have - // the ability to use the first 2 channels - m_parentPtr->setBaseOutputChannel(0); - m_parentPtr->setNumOutputChannels(2); - } - } -} - -#endif // RT_AUDIO diff --git a/src/gui/vsAudio.h b/src/gui/vsAudio.h deleted file mode 100644 index ab53fb6..0000000 --- a/src/gui/vsAudio.h +++ /dev/null @@ -1,454 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsAudio.h - * \author Matt Horton - * \date September 2022 - */ - -#ifndef VSDAUDIO_H -#define VSDAUDIO_H - -#include -#include -#include -#include -#include -#include -#include - -#include "../AudioInterface.h" -#include "../jacktrip_globals.h" - -#ifdef RT_AUDIO -#include "../RtAudioInterface.h" -#endif - -class Analyzer; -class JackTrip; -class Meter; -class Monitor; -class QThread; -class Tone; -class Volume; -class VsPermissions; -class VsAudioWorker; - -class VsAudio : public QObject -{ - Q_OBJECT - - // state shared with QML - Q_PROPERTY(bool audioReady READ getAudioReady NOTIFY signalAudioReadyChanged) - Q_PROPERTY(bool scanningDevices READ getScanningDevices WRITE setScanningDevices - NOTIFY signalScanningDevicesChanged) - Q_PROPERTY(bool feedbackDetectionEnabled READ getFeedbackDetectionEnabled WRITE - setFeedbackDetectionEnabled NOTIFY feedbackDetectionEnabledChanged) - Q_PROPERTY(bool deviceModelsInitialized READ getDeviceModelsInitialized NOTIFY - deviceModelsInitializedChanged) - Q_PROPERTY(bool backendAvailable READ backendAvailable CONSTANT) - Q_PROPERTY(QString audioBackend READ getAudioBackend WRITE setAudioBackend NOTIFY - audioBackendChanged) - Q_PROPERTY( - int sampleRate READ getSampleRate WRITE setSampleRate NOTIFY sampleRateChanged) - Q_PROPERTY( - int bufferSize READ getBufferSize WRITE setBufferSize NOTIFY bufferSizeChanged) - Q_PROPERTY(int bufferStrategy READ getBufferStrategy WRITE setBufferStrategy NOTIFY - bufferStrategyChanged) - Q_PROPERTY(int numInputChannels READ getNumInputChannels WRITE setNumInputChannels - NOTIFY numInputChannelsChanged) - Q_PROPERTY(int numOutputChannels READ getNumOutputChannels WRITE setNumOutputChannels - NOTIFY numOutputChannelsChanged) - Q_PROPERTY(int baseInputChannel READ getBaseInputChannel WRITE setBaseInputChannel - NOTIFY baseInputChannelChanged) - Q_PROPERTY(int baseOutputChannel READ getBaseOutputChannel WRITE setBaseOutputChannel - NOTIFY baseOutputChannelChanged) - Q_PROPERTY(int inputMixMode READ getInputMixMode WRITE setInputMixMode NOTIFY - inputMixModeChanged) - Q_PROPERTY( - bool inputMuted READ getInputMuted WRITE setInputMuted NOTIFY updatedInputMuted) - Q_PROPERTY(bool inputClipped READ getInputClipped NOTIFY updatedInputClipped) - Q_PROPERTY(bool outputClipped READ getOutputClipped NOTIFY updatedOutputClipped) - Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY - updatedInputVolume) - Q_PROPERTY(float outputVolume READ getOutputVolume WRITE setOutputVolume NOTIFY - updatedOutputVolume) - Q_PROPERTY(float monitorVolume READ getMonitorVolume WRITE setMonitorVolume NOTIFY - updatedMonitorVolume) - Q_PROPERTY(QString inputDevice READ getInputDevice WRITE setInputDevice NOTIFY - inputDeviceChanged) - Q_PROPERTY(QString outputDevice READ getOutputDevice WRITE setOutputDevice NOTIFY - outputDeviceChanged) - Q_PROPERTY(QVector inputMeterLevels READ getInputMeterLevels NOTIFY - updatedInputMeterLevels) - Q_PROPERTY(QVector outputMeterLevels READ getOutputMeterLevels NOTIFY - updatedOutputMeterLevels) - Q_PROPERTY( - QJsonArray inputComboModel READ getInputComboModel NOTIFY inputComboModelChanged) - Q_PROPERTY(QJsonArray outputComboModel READ getOutputComboModel NOTIFY - outputComboModelChanged) - Q_PROPERTY(QJsonArray inputChannelsComboModel READ getInputChannelsComboModel NOTIFY - inputChannelsComboModelChanged) - Q_PROPERTY(QJsonArray outputChannelsComboModel READ getOutputChannelsComboModel NOTIFY - outputChannelsComboModelChanged) - Q_PROPERTY(QJsonArray inputMixModeComboModel READ getInputMixModeComboModel NOTIFY - inputMixModeComboModelChanged) - Q_PROPERTY(QStringList feedbackDetectionComboModel READ getFeedbackDetectionComboModel - CONSTANT) - Q_PROPERTY(QStringList bufferSizeComboModel READ getBufferSizeComboModel CONSTANT) - Q_PROPERTY( - QStringList bufferStrategyComboModel READ getBufferStrategyComboModel CONSTANT) - Q_PROPERTY(QStringList audioBackendComboModel READ getAudioBackendComboModel CONSTANT) - Q_PROPERTY( - QString devicesWarning READ getDevicesWarningMsg NOTIFY devicesWarningChanged) - Q_PROPERTY(QString devicesError READ getDevicesErrorMsg NOTIFY devicesErrorChanged) - Q_PROPERTY(QString devicesWarningHelpUrl READ getDevicesWarningHelpUrl NOTIFY - devicesWarningHelpUrlChanged) - Q_PROPERTY(QString devicesErrorHelpUrl READ getDevicesErrorHelpUrl NOTIFY - devicesErrorHelpUrlChanged) - Q_PROPERTY(bool highLatencyFlag READ getHighLatencyFlag NOTIFY highLatencyFlagChanged) - - public: - enum AudioBackendType { - JACK = 0, ///< Jack Mode - RTAUDIO ///< RtAudio Mode - }; - - // Constructor - explicit VsAudio(QObject* parent = nullptr); - virtual ~VsAudio(); - - // allow VirtualStudio to get Permissions to bind to QML view - VsPermissions& getPermissions() { return *m_permissionsPtr; } - VsAudioWorker& getWorker() { return *m_audioWorkerPtr; } - - // allow VirtualStudio to create new audio interfaces - AudioInterface* newAudioInterface(JackTrip* jackTripPtr = nullptr); - - // allow VirtualStudio to load and save settings - void loadSettings(); - void saveSettings(); - - // getters for state shared with QML - bool backendAvailable() const; - bool jackIsAvailable() const; - bool getAudioReady() const { return m_audioReady; } - bool getScanningDevices() const { return m_scanningDevices; } - bool getFeedbackDetectionEnabled() const { return m_feedbackDetectionEnabled; } - bool getDeviceModelsInitialized() const { return m_deviceModelsInitialized; } - bool getUseRtAudio() const { return m_backend == AudioBackendType::RTAUDIO; } - QString getAudioBackend() const - { - return getUseRtAudio() ? QStringLiteral("RtAudio") : QStringLiteral("JACK"); - } - int getSampleRate() const { return m_audioSampleRate; } - int getBufferSize() const { return m_audioBufferSize; } - int getBufferStrategy() const { return m_bufferStrategy; } - int getNumInputChannels() const { return getUseRtAudio() ? m_numInputChannels : 2; } - int getNumOutputChannels() const { return getUseRtAudio() ? m_numOutputChannels : 2; } - int getBaseInputChannel() const { return getUseRtAudio() ? m_baseInputChannel : 0; } - int getBaseOutputChannel() const { return getUseRtAudio() ? m_baseOutputChannel : 0; } - int getInputMixMode() const { return getUseRtAudio() ? m_inputMixMode : 0; } - bool getInputMuted() const { return m_inMuted; } - bool getInputClipped() const { return m_inputClipped; } - bool getOutputClipped() const { return m_outputClipped; } - float getInputVolume() const { return m_inMultiplier; } - float getOutputVolume() const { return m_outMultiplier; } - float getMonitorVolume() const { return m_monMultiplier; } - const QString& getInputDevice() const { return m_inputDevice; } - const QString& getOutputDevice() const { return m_outputDevice; } - const QVector& getInputMeterLevels() const { return m_inputMeterLevels; } - const QVector& getOutputMeterLevels() const { return m_outputMeterLevels; } - const QJsonArray& getInputComboModel() const { return m_inputComboModel; } - const QJsonArray& getOutputComboModel() const { return m_outputComboModel; } - const QJsonArray& getInputChannelsComboModel() const - { - return m_inputChannelsComboModel; - } - const QJsonArray& getOutputChannelsComboModel() const - { - return m_outputChannelsComboModel; - } - const QJsonArray& getInputMixModeComboModel() const - { - return m_inputMixModeComboModel; - } - const QStringList& getFeedbackDetectionComboModel() const - { - return m_feedbackDetectionComboModel; - } - const QStringList& getBufferSizeComboModel() const { return m_bufferSizeComboModel; } - const QStringList& getBufferStrategyComboModel() const - { - return m_bufferStrategyComboModel; - } - const QStringList& getAudioBackendComboModel() const - { - return m_audioBackendComboModel; - } - const QString& getDevicesWarningMsg() const { return m_devicesWarningMsg; } - const QString& getDevicesErrorMsg() const { return m_devicesErrorMsg; } - const QString& getDevicesWarningHelpUrl() const { return m_devicesWarningHelpUrl; } - const QString& getDevicesErrorHelpUrl() const { return m_devicesErrorHelpUrl; } - bool getHighLatencyFlag() const { return m_highLatencyFlag; } - public slots: - - // setters for state shared with QML - void setFeedbackDetectionEnabled(bool enabled); - void setAudioBackend(const QString& backend); - void setSampleRate(int sampleRate); - void setBufferSize(int bufSize); - void setBufferStrategy(int bufStrategy); - void setNumInputChannels(int numChannels); - void setNumOutputChannels(int numChannels); - void setBaseInputChannel(int baseChannel); - void setBaseOutputChannel(int baseChannel); - void setInputMixMode(int mode); - void setInputMuted(bool muted); - void setInputVolume(float multiplier); - void setOutputVolume(float multiplier); - void setMonitorVolume(float multiplier); - void setInputDevice(const QString& device); - void setOutputDevice(const QString& device); - void setDevicesErrorMsg(const QString& msg); - void setDevicesWarningMsg(const QString& msg); - void setDevicesErrorHelpUrl(const QString& url); - void setDevicesWarningHelpUrl(const QString& url); - void setHighLatencyFlag(bool highLatency); - - // public methods accessible by QML - void startAudio(bool block = false); - void stopAudio(bool block = false); - void refreshDevices(bool block = false); - void validateDevices(bool block = false); - void restartAudio(bool block = false) { return startAudio(block); } - void playOutputAudio() { emit signalPlayOutputAudio(); } - - signals: - - // signals for QML state changes - void signalAudioReadyChanged(); - void signalAudioIsReady(); - void signalAudioIsNotReady(); - void signalScanningDevicesChanged(); - void deviceModelsInitializedChanged(bool initialized); - void audioBackendChanged(bool useRtAudio); - void sampleRateChanged(); - void bufferSizeChanged(); - void bufferStrategyChanged(); - void numInputChannelsChanged(int numChannels); - void numOutputChannelsChanged(int numChannels); - void baseInputChannelChanged(int baseChannel); - void baseOutputChannelChanged(int baseChannel); - void inputMixModeChanged(int mode); - void updatedInputMuted(bool muted); - void updatedInputClipped(bool clip); - void updatedOutputClipped(bool clip); - void updatedInputVolume(float multiplier); - void updatedOutputVolume(float multiplier); - void updatedMonitorVolume(float multiplier); - void inputDeviceChanged(QString device); - void outputDeviceChanged(QString device); - void updatedInputMeterLevels(const QVector& levels); - void updatedOutputMeterLevels(const QVector& levels); - void feedbackDetectionEnabledChanged(); - void feedbackDetected(); - void inputComboModelChanged(); - void outputComboModelChanged(); - void inputChannelsComboModelChanged(); - void outputChannelsComboModelChanged(); - void inputMixModeComboModelChanged(); - void devicesWarningChanged(); - void devicesErrorChanged(); - void devicesWarningHelpUrlChanged(); - void devicesErrorHelpUrlChanged(); - void highLatencyFlagChanged(bool highLatencyFlag); - - // other signals to perform actions - void signalPlayOutputAudio(); - void signalStartAudio(); - void signalStopAudio(); - void signalRefreshDevices(); - void signalValidateDevices(); - void signalDevicesValidated(); - - private slots: - void setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel); - void setInputChannelsComboModel(QJsonArray& model); - void setOutputChannelsComboModel(QJsonArray& model); - void setInputMixModeComboModel(QJsonArray& model); - - private: - // private methods - void setAudioReady(bool ready); - void setScanningDevices(bool b); - void detectedFeedbackLoop(); - void updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels); - void updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels); - void appendProcessPlugins(AudioInterface& audioInterface, bool forJackTrip, - int numInputChannels, int numOutputChannels); - void updateDeviceMessages(AudioInterface& audioInterface); - AudioInterface* newJackAudioInterface(JackTrip* jackTripPtr = nullptr); - AudioInterface* newRtAudioInterface(JackTrip* jackTripPtr = nullptr); - void errorCallback(const std::string& errorText, JackTrip* jackTripPtr = nullptr); - - // range for volume meters - static constexpr float m_meterMax = 0.0; - static constexpr float m_meterMin = -64.0; - - // audio bit resolution - static constexpr AudioInterface::audioBitResolutionT m_audioBitResolution = - AudioInterface::BIT16; - - // state shared with QML - AudioBackendType m_backend = AudioBackendType::JACK; - bool m_audioReady = false; - bool m_scanningDevices = false; - bool m_feedbackDetectionEnabled = true; - bool m_deviceModelsInitialized = false; - int m_audioSampleRate = gDefaultSampleRate; - int m_audioBufferSize = - gDefaultBufferSizeInSamples; ///< Audio buffer size to process on each callback - int m_bufferStrategy = 0; - int m_numInputChannels = gDefaultNumInChannels; - int m_numOutputChannels = gDefaultNumOutChannels; - int m_baseInputChannel = 0; - int m_baseOutputChannel = 0; - int m_inputMixMode = 0; - bool m_inMuted = false; - bool m_inputClipped = false; - bool m_outputClipped = false; - float m_inMultiplier = 1.0; - float m_outMultiplier = 1.0; - float m_monMultiplier = 0; - - QString m_inputDevice; - QString m_outputDevice; - QVector m_inputMeterLevels; - QVector m_outputMeterLevels; - QJsonArray m_inputComboModel; - QJsonArray m_outputComboModel; - QJsonArray m_inputChannelsComboModel; - QJsonArray m_outputChannelsComboModel; - QJsonArray m_inputMixModeComboModel; - QString m_devicesWarningMsg = QStringLiteral(""); - QString m_devicesErrorMsg = QStringLiteral(""); - QString m_devicesWarningHelpUrl = QStringLiteral(""); - QString m_devicesErrorHelpUrl = QStringLiteral(""); - bool m_highLatencyFlag = false; - - // other state not shared with QML - QSharedPointer m_permissionsPtr; - QScopedPointer m_audioWorkerPtr; - QThread* m_workerThreadPtr; - QTimer m_inputClipTimer; - QTimer m_outputClipTimer; - Meter* m_inputMeterPluginPtr; - Meter* m_outputMeterPluginPtr; - Volume* m_inputVolumePluginPtr; - Volume* m_outputVolumePluginPtr; - Monitor* m_monitorPluginPtr; - bool mHasErrors; ///< true if one or more error callbacks have been triggered - -#ifndef NO_FEEDBACK - Analyzer* m_outputAnalyzerPluginPtr; -#endif - - QStringList m_audioBackendComboModel = {"JACK", "RtAudio"}; - QStringList m_feedbackDetectionComboModel = {"Enabled", "Disabled"}; - QStringList m_bufferSizeComboModel = {"16", "32", "64", "128", "256", "512", "1024"}; - QStringList m_bufferStrategyComboModel = { - "Adaptable Latency (Old)", "Stable Latency (Old)", "Loss Concealment (Default)"}; - - friend class VsAudioWorker; -}; - -/// VsAudioWorker uses a separate thread to help VsAudio -class VsAudioWorker : public QObject -{ - Q_OBJECT - - public: - VsAudioWorker(VsAudio* ptr); - virtual ~VsAudioWorker() {} - - signals: - void signalDevicesValidated(); - void signalUpdatedDeviceModels(QJsonArray inputComboModel, - QJsonArray outputComboModel); - void signalError(const QString& errorMessage); - - public slots: - void openAudioInterface(); - void closeAudioInterface(); -#ifdef RT_AUDIO - void refreshDevices(); - void validateDevices(); - - private: - void updateDeviceModels(); - void validateInputDevicesState(); - void validateOutputDevicesState(); - static void getDeviceList(const QVector& devices, QStringList& list, - QStringList& categories, QList& channels, - bool isInput); - static QJsonArray formatDeviceList(const QStringList& devices, - const QStringList& categories, - const QList& channels); - QVector m_devices; - - public: - QVector getDevices() const { return m_devices; } -#endif - - private: - // parent getter wrappers - bool getUseRtAudio() const { return m_parentPtr->getUseRtAudio(); } - int getNumInputChannels() const { return m_parentPtr->getNumInputChannels(); } - int getNumOutputChannels() const { return m_parentPtr->getNumOutputChannels(); } - int getBaseInputChannel() const { return m_parentPtr->getBaseInputChannel(); } - int getBaseOutputChannel() const { return m_parentPtr->getBaseOutputChannel(); } - int getSampleRate() const { return m_parentPtr->getSampleRate(); } - int getBufferSize() const { return m_parentPtr->getBufferSize(); } - int getInputMixMode() const { return m_parentPtr->getInputMixMode(); } - const QString& getInputDevice() const { return m_parentPtr->getInputDevice(); } - const QString& getOutputDevice() const { return m_parentPtr->getOutputDevice(); } - - VsAudio* m_parentPtr; - QSharedPointer m_audioInterfacePtr; - QList m_inputDeviceChannels; - QList m_outputDeviceChannels; - QStringList m_inputDeviceList; - QStringList m_outputDeviceList; -}; - -#endif // VSDAUDIO_H diff --git a/src/gui/vsAuth.cpp b/src/gui/vsAuth.cpp deleted file mode 100644 index 970855c..0000000 --- a/src/gui/vsAuth.cpp +++ /dev/null @@ -1,298 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsAuth.cpp - * \author Dominick Hing - * \date May 2023 - */ - -#include "vsAuth.h" - -#include "./vsConstants.h" - -VsAuth::VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api) - : m_clientId(AUTH_CLIENT_ID), m_authorizationServerHost(AUTH_SERVER_HOST) -{ - m_networkAccessManager = networkAccessManager; - m_api = api; - m_deviceCodeFlow.reset(new VsDeviceCodeFlow(networkAccessManager)); - - connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowInitialized, this, - &VsAuth::initializedCodeFlow); - connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowError, this, - &VsAuth::handleAuthFailed); - connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::onCompletedCodeFlow, this, - &VsAuth::codeFlowCompleted); - connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowTimedOut, this, - &VsAuth::codeExpired); - - m_verificationUrl = QStringLiteral("https://auth.jacktrip.org/activate"); -} - -void VsAuth::authenticate(QString currentRefreshToken) -{ - if (currentRefreshToken.isEmpty()) { - // if no refresh token, initialize device flow - m_deviceCodeFlow->grant(); - } else { - m_attemptingRefreshToken = true; - emit updatedAttemptingRefreshToken(m_attemptingRefreshToken); - - // otherwise, use refresh token to gain a new access token - m_refreshToken = currentRefreshToken; - refreshAccessToken(m_refreshToken); - } -} - -void VsAuth::initializedCodeFlow(QString code, QString verificationUrl) -{ - m_verificationCode = code; - m_verificationUrl = verificationUrl; - m_authenticationStage = QStringLiteral("polling"); - - emit updatedAuthenticationStage(m_authenticationStage); - emit updatedVerificationCode(m_verificationCode); - emit updatedVerificationUrl(m_verificationUrl); -} - -void VsAuth::fetchUserInfo(QString accessToken) -{ - QNetworkReply* reply = m_api->getAuth0UserInfo(); - connect(reply, &QNetworkReply::finished, this, [=]() { - if (reply->error() != QNetworkReply::NoError) { - std::cout << "VsAuth::fetchUserInfo Error: " - << reply->errorString().toStdString() << std::endl; - handleAuthFailed(); // handle failure - emit fetchUserInfoFailed(); - reply->deleteLater(); - return; - } - - QByteArray response = reply->readAll(); - QJsonDocument userInfo = QJsonDocument::fromJson(response); - QString userId = userInfo.object()[QStringLiteral("sub")].toString(); - - reply->deleteLater(); - - if (userId.isEmpty()) { - std::cout << "VsAuth::fetchUserInfo Error: empty userId" << std::endl; - handleAuthFailed(); // handle failure - emit fetchUserInfoFailed(); - return; - } - - handleAuthSucceeded(userId, accessToken); - }); -} - -void VsAuth::refreshAccessToken(QString refreshToken) -{ - qDebug() << "Refreshing access token"; - m_authenticationStage = QStringLiteral("refreshing"); - emit updatedAuthenticationStage(m_authenticationStage); - - QNetworkRequest request = QNetworkRequest( - QUrl(QString("https://%1/oauth/token").arg(m_authorizationServerHost))); - - request.setRawHeader(QByteArray("Content-Type"), - QByteArray("application/x-www-form-urlencoded")); - - QString data = QString("grant_type=refresh_token&client_id=%1&refresh_token=%2") - .arg(m_clientId, refreshToken); - - // send request - QNetworkReply* reply = m_networkAccessManager->post(request, data.toUtf8()); - - connect(reply, &QNetworkReply::finished, this, [=]() { - QByteArray buffer = reply->readAll(); - - // Error: failed to get device code - if (reply->error()) { - std::cout << "Failed to get new access token: " << buffer.toStdString() - << std::endl; - handleAuthFailed(); // handle failure - emit refreshTokenFailed(); - reply->deleteLater(); - return; - } - - // parse JSON from string response - QJsonParseError parseError; - QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError); - if (parseError.error) { - std::cout << "Error parsing JSON for Access Token: " - << parseError.errorString().toStdString() << std::endl; - handleAuthFailed(); // handle failure - emit refreshTokenFailed(); - reply->deleteLater(); - return; - } - - // received access token - QJsonObject object = data.object(); - QString accessToken = object.value(QLatin1String("access_token")).toString(); - m_api->setAccessToken(accessToken); // set access token - reply->deleteLater(); - if (m_userId.isEmpty()) { - fetchUserInfo(accessToken); // get user ID from Auth0 - } else { - handleRefreshSucceeded(accessToken); - } - }); -} - -void VsAuth::resetCode() -{ - if (!m_verificationCode.isEmpty()) { - m_deviceCodeFlow->cancelCodeFlow(); - m_deviceCodeFlow->grant(); - } -} - -void VsAuth::codeFlowCompleted(QString accessToken, QString refreshToken) -{ - m_refreshToken = refreshToken; - m_api->setAccessToken(accessToken); - fetchUserInfo(accessToken); -} - -void VsAuth::codeExpired() -{ - emit deviceCodeExpired(); -} - -void VsAuth::handleRefreshSucceeded(QString accessToken) -{ - qDebug() << "Successfully refreshed access token"; - - m_accessToken = accessToken; - m_authenticationStage = QStringLiteral("success"); - m_attemptingRefreshToken = false; - - emit updatedAuthenticationStage(m_authenticationStage); - emit updatedVerificationCode(m_verificationCode); - emit updatedAttemptingRefreshToken(m_attemptingRefreshToken); -} - -void VsAuth::handleAuthSucceeded(QString userId, QString accessToken) -{ - // Success case: we got our access token (either through the refresh token or device - // code flow), and fetched the user ID - std::cout << "Successfully authenticated Virtual Studio user" << std::endl; - std::cout << "User ID: " << userId.toStdString() << std::endl; - - if (m_authenticationStage == QStringLiteral("polling")) { - m_authenticationMethod = QStringLiteral("code flow"); - } else { - m_authenticationMethod = QStringLiteral("refresh token"); - } - - m_userId = userId; - m_verificationCode = QStringLiteral(""); - m_accessToken = accessToken; - m_authenticationStage = QStringLiteral("success"); - m_attemptingRefreshToken = false; - m_isAuthenticated = true; - - emit updatedUserId(m_userId); - emit updatedAuthenticationStage(m_authenticationStage); - emit updatedVerificationCode(m_verificationCode); - emit updatedIsAuthenticated(m_isAuthenticated); - emit updatedAttemptingRefreshToken(m_attemptingRefreshToken); - emit updatedAuthenticationMethod(m_authenticationMethod); - - // notify UI and virtual studio class of success - emit authSucceeded(); -} - -void VsAuth::handleAuthFailed() -{ - // this might get called because there was an error getting the access token, - // or there was an issue fetching the user ID. We need both to say - // that authentication succeeded - std::cout << "Failed to authenticate user" << std::endl; - - m_userId = QStringLiteral(""); - m_verificationCode = QStringLiteral(""); - m_accessToken = QStringLiteral(""); - m_authenticationStage = QStringLiteral("failed"); - m_authenticationMethod = QStringLiteral(""); - m_attemptingRefreshToken = false; - m_isAuthenticated = false; - - emit updatedUserId(m_userId); - emit updatedAuthenticationStage(m_authenticationStage); - emit updatedVerificationCode(m_verificationCode); - emit updatedIsAuthenticated(m_isAuthenticated); - emit updatedAttemptingRefreshToken(m_attemptingRefreshToken); - emit updatedAuthenticationMethod(m_authenticationMethod); - - // notify UI and virtual studio class of failure - emit authFailed(); -} - -void VsAuth::cancelAuthenticationFlow() -{ - qDebug() << "Canceling authentication flow"; - m_deviceCodeFlow->cancelCodeFlow(); - - m_userId = QStringLiteral(""); - m_verificationCode = QStringLiteral(""); - m_accessToken = QStringLiteral(""); - m_authenticationStage = QStringLiteral("unauthenticated"); - m_isAuthenticated = false; - - emit updatedUserId(m_userId); - emit updatedAuthenticationStage(m_authenticationStage); - emit updatedVerificationCode(m_verificationCode); - emit updatedIsAuthenticated(m_isAuthenticated); -} - -void VsAuth::logout() -{ - if (!m_isAuthenticated) { - std::cout << "Warning: attempting to logout while not authenticated" << std::endl; - } - qDebug() << "Logging out"; - - // reset auth state - m_userId = QStringLiteral(""); - m_verificationCode = QStringLiteral(""); - m_accessToken = QStringLiteral(""); - m_authenticationStage = QStringLiteral("unauthenticated"); - m_isAuthenticated = false; - - emit updatedUserId(m_userId); - emit updatedAuthenticationStage(m_authenticationStage); - emit updatedVerificationCode(m_verificationCode); - emit updatedIsAuthenticated(m_isAuthenticated); -} \ No newline at end of file diff --git a/src/gui/vsAuth.h b/src/gui/vsAuth.h deleted file mode 100644 index 61ccb85..0000000 --- a/src/gui/vsAuth.h +++ /dev/null @@ -1,133 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsAuth.h - * \author Dominick Hing - * \date May 2023 - */ - -#ifndef VSAUTH_H -#define VSAUTH_H - -#include -#include -#include -#include -#include - -#include "vsApi.h" -#include "vsDeviceCodeFlow.h" - -class VsAuth : public QObject -{ - Q_OBJECT - - Q_PROPERTY(QString authenticationStage READ authenticationStage NOTIFY - updatedAuthenticationStage); - Q_PROPERTY(QString verificationCode READ deviceCode NOTIFY updatedVerificationCode); - Q_PROPERTY( - QString verificationUrl READ deviceVerificationUrl NOTIFY updatedVerificationUrl); - Q_PROPERTY(bool isAuthenticated READ isAuthenticated NOTIFY updatedIsAuthenticated); - Q_PROPERTY(QString authenticationMethod READ authenticationMethod NOTIFY - updatedAuthenticationMethod); - Q_PROPERTY(bool attemptingRefreshToken READ attemptingRefreshToken NOTIFY - updatedAttemptingRefreshToken); - Q_PROPERTY(QString userId READ userId NOTIFY updatedUserId); - Q_PROPERTY(QString accessToken READ accessToken CONSTANT); - - public: - VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api); - - void authenticate(QString currentRefreshToken); - void refreshAccessToken(QString refreshToken); - Q_INVOKABLE void resetCode(); - void logout(); - - public slots: - void cancelAuthenticationFlow(); - - // getter methods - QString authenticationStage() { return m_authenticationStage; }; - QString deviceCode() { return m_verificationCode; }; - QString deviceVerificationUrl() { return m_verificationUrl; }; - bool isAuthenticated() { return m_isAuthenticated; }; - QString userId() { return m_userId; }; - QString accessToken() { return m_accessToken; }; - QString refreshToken() { return m_refreshToken; }; - QString authenticationMethod() { return m_authenticationMethod; } - bool attemptingRefreshToken() { return m_attemptingRefreshToken; } - - signals: - void updatedAuthenticationStage(QString authenticationStage); - void updatedVerificationCode(QString deviceCode); - void updatedVerificationUrl(QUrl verificationUrl); - void updatedIsAuthenticated(bool isAuthenticated); - void updatedUserId(QString userId); - void updatedAuthenticationMethod(QString grant); - void updatedAttemptingRefreshToken(bool attemptingRefreshToken); - void authSucceeded(); - void authFailed(); - void refreshTokenFailed(); - void fetchUserInfoFailed(); - void deviceCodeExpired(); - - private slots: - void handleRefreshSucceeded(QString accessToken); - void handleAuthSucceeded(QString userId, QString accessToken); - void handleAuthFailed(); - void initializedCodeFlow(QString code, QString verificationUrl); - void codeFlowCompleted(QString accessToken, QString refreshToken); - void codeExpired(); - - private: - void fetchUserInfo(QString accessToken); - - QString m_clientId; - QString m_authorizationServerHost; - - QString m_authenticationStage = QStringLiteral("unauthenticated"); - QString m_verificationCode = QStringLiteral(""); - QString m_verificationUrl; - QString m_authenticationMethod = QStringLiteral(""); - - bool m_attemptingRefreshToken = false; - bool m_isAuthenticated = false; - QString m_userId; - QString m_accessToken; - QString m_refreshToken; - - QNetworkAccessManager* m_networkAccessManager; - VsApi* m_api; - QScopedPointer m_deviceCodeFlow; -}; - -#endif \ No newline at end of file diff --git a/src/gui/vsConstants.h b/src/gui/vsConstants.h deleted file mode 100644 index 63f3f39..0000000 --- a/src/gui/vsConstants.h +++ /dev/null @@ -1,51 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsConstants.h - * \author Nelson Wang - * \date Oct 2022 - */ - -#ifndef VSCONSTANTS_H -#define VSCONSTANTS_H - -#include - -const QString AUTH_AUTHORIZE_URI = QStringLiteral("https://auth.jacktrip.org/authorize"); -const QString AUTH_TOKEN_URI = QStringLiteral("https://auth.jacktrip.org/oauth/token"); -const QString AUTH_AUDIENCE = QStringLiteral("https://api.jacktrip.org"); -const QString AUTH_CLIENT_ID = QStringLiteral("cROUJag0UVKDaJ6jRAKRzlVjKVFNU39I"); -const QString PROD_API_HOST = QStringLiteral("app.jacktrip.com"); -const QString TEST_API_HOST = QStringLiteral("test.jacktrip.com"); -const QString AUTH_SERVER_HOST = QStringLiteral("auth.jacktrip.org"); - -#endif // VSCONSTANTS_H diff --git a/src/gui/vsDeeplink.cpp b/src/gui/vsDeeplink.cpp deleted file mode 100644 index d3cafd1..0000000 --- a/src/gui/vsDeeplink.cpp +++ /dev/null @@ -1,212 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsDeeplink.cpp - * \author Aaron Wyatt, based on code by Matt Horton - * \date February 2023 - */ - -#include "vsDeeplink.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -VsDeeplink::VsDeeplink(const QString& deeplink) : m_deeplink(deeplink) -{ - setUrlScheme(); - checkForInstance(); - QDesktopServices::setUrlHandler(QStringLiteral("jacktrip"), this, "handleUrl"); -} - -VsDeeplink::~VsDeeplink() -{ - QDesktopServices::unsetUrlHandler(QStringLiteral("jacktrip")); -} - -bool VsDeeplink::waitForReady() -{ - while (!m_isReady) { - QTimer timer; - timer.setTimerType(Qt::CoarseTimer); - timer.setSingleShot(true); - - QEventLoop loop; - QObject::connect(this, &VsDeeplink::signalIsReady, &loop, &QEventLoop::quit); - QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); - timer.start(100); // wait for 100ms - loop.exec(); - } - return m_readyToExit; -} - -void VsDeeplink::readyForSignals() -{ - m_readyForSignals = true; - if (!m_deeplink.isEmpty()) { - emit signalDeeplink(m_deeplink); - m_deeplink.clear(); - } -} - -void VsDeeplink::handleUrl(const QUrl& url) -{ - if (m_readyForSignals) { - emit signalDeeplink(url); - } else { - m_deeplink = url; - } -} - -void VsDeeplink::checkForInstance() -{ - // Create socket - m_instanceCheckSocket.reset(new QLocalSocket(this)); - QObject::connect(m_instanceCheckSocket.data(), &QLocalSocket::connected, this, - &VsDeeplink::connectionReceived, Qt::QueuedConnection); - // Create instanceServer to prevent new instances from being created - void (QLocalSocket::*errorFunc)(QLocalSocket::LocalSocketError); -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - errorFunc = &QLocalSocket::error; -#else - errorFunc = &QLocalSocket::errorOccurred; -#endif - QObject::connect(m_instanceCheckSocket.data(), errorFunc, this, - &VsDeeplink::connectionFailed); - // Check for existing instance - m_instanceCheckSocket->connectToServer("jacktripExists"); -} - -void VsDeeplink::connectionReceived() -{ - // another jacktrip instance is running - if (!m_deeplink.isEmpty()) { - // pass deeplink to existing instance before quitting - QString deeplinkStr = m_deeplink.toString(); - QByteArray baDeeplink = deeplinkStr.toLocal8Bit(); - qint64 writeBytes = m_instanceCheckSocket->write(baDeeplink); - if (writeBytes < 0) { - qDebug() << "sending deeplink failed"; - } else { - qDebug() << "Sent deeplink request to remote instance"; - } - - // make sure it isn't processed again - m_deeplink.clear(); - - // End process if another instance exists - m_readyToExit = true; - } - - m_instanceCheckSocket->waitForBytesWritten(); - m_instanceCheckSocket->disconnectFromServer(); // remove next - - // let main thread know we are finished - m_isReady = true; - emit signalIsReady(); -} - -void VsDeeplink::connectionFailed(QLocalSocket::LocalSocketError socketError) -{ - switch (socketError) { - case QLocalSocket::ServerNotFoundError: - case QLocalSocket::SocketTimeoutError: - case QLocalSocket::ConnectionRefusedError: - // no other jacktrip instance is running, so we will take over handling deep links - qDebug() << "Listening for deep link requests"; - m_instanceServer.reset(new QLocalServer(this)); - m_instanceServer->setSocketOptions(QLocalServer::WorldAccessOption); - m_instanceServer->listen("jacktripExists"); - QObject::connect(m_instanceServer.data(), &QLocalServer::newConnection, this, - &VsDeeplink::handleDeeplinkRequest, Qt::QueuedConnection); - break; - case QLocalSocket::PeerClosedError: - break; - default: - qDebug() << m_instanceCheckSocket->errorString(); - } - - // let main thread know we are finished - m_isReady = true; - emit signalIsReady(); -} - -void VsDeeplink::handleDeeplinkRequest() -{ - while (m_instanceServer->hasPendingConnections()) { - // Receive URL from 2nd instance - QLocalSocket* connectedSocket = m_instanceServer->nextPendingConnection(); - - if (connectedSocket == nullptr || !connectedSocket->waitForConnected()) { - qDebug() << "Deeplink socket: never received connection"; - return; - } - - if (!connectedSocket->waitForReadyRead() - && connectedSocket->bytesAvailable() <= 0) { - qDebug() << "Deeplink socket: not ready and no bytes available: " - << connectedSocket->errorString(); - return; - } - - if (connectedSocket->bytesAvailable() < (int)sizeof(quint16)) { - qDebug() << "Deeplink socket: ready but no bytes available"; - break; - } - - QByteArray in(connectedSocket->readAll()); - QString urlString(in); - handleUrl(urlString); - } -} - -void VsDeeplink::setUrlScheme() -{ -#ifdef _WIN32 - // Set url scheme in registry - QString path = QDir::toNativeSeparators(qApp->applicationFilePath()); - - QSettings set("HKEY_CURRENT_USER\\Software\\Classes", QSettings::NativeFormat); - set.beginGroup("jacktrip"); - set.setValue("Default", "URL:JackTrip Protocol"); - set.setValue("DefaultIcon/Default", path); - set.setValue("URL Protocol", ""); - set.setValue("shell/open/command/Default", - QString("\"%1\"").arg(path) + " --gui --deeplink \"%1\""); - set.endGroup(); -#endif -} diff --git a/src/gui/vsDeeplink.h b/src/gui/vsDeeplink.h deleted file mode 100644 index 8eb932e..0000000 --- a/src/gui/vsDeeplink.h +++ /dev/null @@ -1,113 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsDeeplink.h - * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton - * \date August 2023 - */ - -#ifndef __VSDEEPLINK_H__ -#define __VSDEEPLINK_H__ - -#include -#include -#include -#include -#include - -class VsDeeplink : public QObject -{ - Q_OBJECT - - public: - // construct with an instance of the application, to parse command line args - VsDeeplink(const QString& deeplink); - - // virtual destructor since it inherits from QObject - // this is used to unregister url handler - virtual ~VsDeeplink(); - - // blocks main thread until local socket server is ready - // returns true if a deeplink was handled and we should exit now - bool waitForReady(); - - // used to let us know VirtualStudio is ready to process deeplink signals - void readyForSignals(); - - // returns deeplink extracted from command line, if any - const QUrl& getDeeplink() const { return m_deeplink; } - - signals: - - // signalIsReady is emitted when the local socket server is ready - void signalIsReady(); - - // signalDeeplink is emitted when we want the local instance to process a deeplink - void signalDeeplink(const QUrl& url); - - private slots: - - // handleUrl is called to trigger processing of a deeplink - void handleUrl(const QUrl& url); - - // checks to see if another instance of jacktrip is available to process requests. - // if there is, this will send any command line deeplinks to it and exit. - // if there isn't, this will start listening for requests. - void checkForInstance(); - - // called if a connection was established with another instance of VS - void connectionReceived(); - - // called if unable to connect to another instance of VS - void connectionFailed(QLocalSocket::LocalSocketError socketError); - - // called by local socket server to process deeplink requests - void handleDeeplinkRequest(); - - private: - // sets url scheme for windows machines; does nothing on other platforms - static void setUrlScheme(); - - // used to check if there is a virtual studio instance already running - QScopedPointer m_instanceCheckSocket; - - // used to listen for deeplink requests via local socket connections - QScopedPointer m_instanceServer; - - // used to synchronize with main thread at startup - bool m_isReady = false; - bool m_readyForSignals = false; - bool m_readyToExit = false; - QUrl m_deeplink; -}; - -#endif // __VSDEEPLINK_H__ diff --git a/src/gui/vsDevice.cpp b/src/gui/vsDevice.cpp deleted file mode 100644 index 79b99b9..0000000 --- a/src/gui/vsDevice.cpp +++ /dev/null @@ -1,601 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsDevice.cpp - * \author Matt Horton - * \date June 2022 - */ - -#include "vsDevice.h" - -#include - -// Constructor -VsDevice::VsDevice(QSharedPointer& auth, QSharedPointer& api, - QSharedPointer& audio, QObject* parent) - : QObject(parent) - , m_auth(auth) - , m_api(api) - , m_audioConfigPtr(audio) - , m_sendVolumeTimer(this) -{ - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - m_apiPrefix = settings.value(QStringLiteral("ApiPrefix"), "").toString(); - m_apiSecret = settings.value(QStringLiteral("ApiSecret"), "").toString(); - m_appUUID = settings.value(QStringLiteral("AppUUID"), "").toString(); - m_appID = settings.value(QStringLiteral("AppID"), "").toString(); - settings.endGroup(); - - if (!m_appID.isEmpty()) { - std::cout << "Device ID: " << m_appID.toStdString() << std::endl; - } - - m_sendVolumeTimer.setSingleShot(true); - connect(&m_sendVolumeTimer, &QTimer::timeout, this, &VsDevice::sendLevels); - - // Set server levels to stored versions - sendLevels(); -} - -VsDevice::~VsDevice() -{ - m_sendVolumeTimer.stop(); - stopJackTrip(false); -} - -// registerApp idempotently registers an emulated device belonging to the current user -void VsDevice::registerApp() -{ - if (m_appUUID == "") { - m_appUUID = QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces); - } - - // check if device exists - QNetworkReply* reply = m_api->getDevice(m_appID); - connect(reply, &QNetworkReply::finished, this, [=]() { - // Got error - if (reply->error() != QNetworkReply::NoError) { - QVariant statusCode = - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - if (!statusCode.isValid()) { - std::cout << "Error: " << reply->errorString().toStdString() << std::endl; - reply->deleteLater(); - return; - } - - int status = statusCode.toInt(); - // Device does not exist - if (status >= 400 && status < 500) { - std::cout << "Device not found. Creating new device." << std::endl; - - if (m_apiPrefix == "" || m_apiSecret == "") { - m_apiPrefix = randomString(7); - m_apiSecret = randomString(22); - } - - registerJTAsDevice(); - } else { - // Other error status. Won't create device. - std::cout << "Error: " << reply->errorString().toStdString() << std::endl; - reply->deleteLater(); - return; - } - } else if (m_apiPrefix != "" && m_apiSecret != "") { - sendHeartbeat(); - } - - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - settings.setValue(QStringLiteral("AppUUID"), m_appUUID); - settings.setValue(QStringLiteral("ApiPrefix"), m_apiPrefix); - settings.setValue(QStringLiteral("ApiSecret"), m_apiSecret); - settings.endGroup(); - if (!m_appID.isEmpty()) { - updateState(""); - } - - reply->deleteLater(); - }); -} - -// removeApp deletes the emulated device -void VsDevice::removeApp() -{ - if (m_appID.isEmpty()) { - return; - } - - QNetworkReply* reply = m_api->deleteDevice(m_appID); - QEventLoop loop; - connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - loop.exec(); - - if (reply->error() != QNetworkReply::NoError) { - std::cout << "Error: " << reply->errorString().toStdString() << std::endl; - } else { - m_appID.clear(); - m_appUUID.clear(); - m_apiPrefix.clear(); - m_apiSecret.clear(); - - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - settings.remove(QStringLiteral("AppID")); - settings.remove(QStringLiteral("AppUUID")); - settings.remove(QStringLiteral("ApiPrefix")); - settings.remove(QStringLiteral("ApiSecret")); - settings.endGroup(); - } - reply->deleteLater(); -} - -// sendHeartbeat is reponsible for sending liveness heartbeats to the API -void VsDevice::sendHeartbeat() -{ - QString now = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - QJsonObject json = { - {QLatin1String("stats_updated_at"), now}, - {QLatin1String("mac"), m_appUUID}, - {QLatin1String("version"), QLatin1String(gVersion)}, - {QLatin1String("type"), "jacktrip_app"}, - {QLatin1String("apiPrefix"), m_apiPrefix}, - {QLatin1String("apiSecret"), m_apiSecret}, - }; - - // Add stats to heartbeat body - if (!m_pinger.isNull() && m_pinger->active()) { - VsPinger::PingStat stats = m_pinger->getPingStats(); - - // API server expects RTTs to be in int64 nanoseconds, so we must convert - // from milliseconds to nanoseconds - int ns_per_ms = 1000000; - - json.insert(QLatin1String("pkts_sent"), (int)stats.packetsSent); - json.insert(QLatin1String("pkts_recv"), (int)stats.packetsReceived); - json.insert(QLatin1String("min_rtt"), (qint64)(stats.minRtt * ns_per_ms)); - json.insert(QLatin1String("max_rtt"), (qint64)(stats.maxRtt * ns_per_ms)); - json.insert(QLatin1String("avg_rtt"), (qint64)(stats.avgRtt * ns_per_ms)); - json.insert(QLatin1String("stddev_rtt"), (qint64)(stats.stdDevRtt * ns_per_ms)); - json.insert(QLatin1String("high_latency"), - m_audioConfigPtr->getHighLatencyFlag()); - json.insert(QLatin1String("network_outage"), m_networkOutage); - - // For the internal application UI, ms will suffice. No conversion needed - QJsonObject pingStats = {}; - pingStats.insert(QLatin1String("packetsSent"), (int)stats.packetsSent); - pingStats.insert(QLatin1String("packetsReceived"), (int)stats.packetsReceived); - pingStats.insert(QLatin1String("minRtt"), ((int)(10 * stats.minRtt)) / 10.0); - pingStats.insert(QLatin1String("maxRtt"), ((int)(10 * stats.maxRtt)) / 10.0); - pingStats.insert(QLatin1String("avgRtt"), ((int)(10 * stats.avgRtt)) / 10.0); - pingStats.insert(QLatin1String("stdDevRtt"), - ((int)(10 * stats.stdDevRtt)) / 10.0); - pingStats.insert(QLatin1String("highLatency"), - m_audioConfigPtr->getHighLatencyFlag()); - emit updateNetworkStats(pingStats); - } - - QJsonDocument request = QJsonDocument(json); - - if (!m_deviceSocketPtr.isNull() && m_deviceSocketPtr->isValid()) { - // Send heartbeat via websocket - m_deviceSocketPtr->sendMessage(request.toJson()); - } else { - if (enabled()) { - if (m_deviceSocketPtr.isNull()) { - qDebug() << "Heartbeat not sent"; - } else { - qDebug() << "Heartbeat not sent; trying to reopen socket"; - m_deviceSocketPtr->openSocket(); - } - } - } -} - -bool VsDevice::hasTerminated() -{ - return m_jackTrip.isNull(); -} - -// updateState updates the emulated device with the provided state -void VsDevice::updateState(const QString& serverId) -{ - m_deviceAgentConfig.insert("serverId", serverId); - m_deviceAgentConfig.insert("enabled", !serverId.isEmpty()); - QJsonObject json = { - {QLatin1String("serverId"), serverId}, - {QLatin1String("enabled"), !serverId.isEmpty()}, - }; - QJsonDocument request = QJsonDocument(json); - QNetworkReply* reply = m_api->updateDevice(m_appID, request.toJson()); - connect(reply, &QNetworkReply::finished, this, [=]() { - if (reply->error() != QNetworkReply::NoError) { - std::cout << "Error: " << reply->errorString().toStdString() << std::endl; - } - reply->deleteLater(); - }); -} - -void VsDevice::sendLevels() -{ - if (m_appID.isEmpty()) { - return; - } - // Add latest volume and mute values to heartbeat body - QJsonObject json = {{QLatin1String("version"), QLatin1String(gVersion)}, - {QLatin1String("captureVolume"), - (int)(m_audioConfigPtr->getInputVolume() * 100.0)}, - {QLatin1String("captureMute"), m_audioConfigPtr->getInputMuted()}, - {QLatin1String("playbackVolume"), - (int)(m_audioConfigPtr->getOutputVolume() * 100.0)}, - {QLatin1String("playbackMute"), false}, - {QLatin1String("monitorVolume"), - (int)(m_audioConfigPtr->getMonitorVolume() * 100.0)}}; - - QJsonDocument request = QJsonDocument(json); - QNetworkReply* reply = m_api->updateDevice(m_appID, request.toJson()); - connect(reply, &QNetworkReply::finished, this, [=]() { - if (reply->error() != QNetworkReply::NoError) { - std::cout << "Error: " << reply->errorString().toStdString() << std::endl; - } - reply->deleteLater(); - }); -} - -// initJackTrip spawns a new jacktrip process with the desired settings -JackTrip* VsDevice::initJackTrip( - [[maybe_unused]] bool useRtAudio, [[maybe_unused]] std::string input, - [[maybe_unused]] std::string output, [[maybe_unused]] int baseInputChannel, - [[maybe_unused]] int numChannelsIn, [[maybe_unused]] int baseOutputChannel, - [[maybe_unused]] int numChannelsOut, [[maybe_unused]] int inputMixMode, - [[maybe_unused]] int bufferSize, [[maybe_unused]] int bufferStrategy, - VsServerInfo* studioInfo) -{ - m_jackTrip.reset( - new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, baseInputChannel, - numChannelsIn, baseOutputChannel, numChannelsOut, - static_cast(inputMixMode), -#ifdef WAIR // wair - 0, -#endif // endwhere - 4, 1)); - m_jackTrip->setConnectDefaultAudioPorts(true); -#ifdef RT_AUDIO - if (useRtAudio) { - m_jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO); - m_jackTrip->setAudioBufferSizeInSamples(bufferSize); - m_jackTrip->setInputDevice(input); - m_jackTrip->setOutputDevice(output); - } -#endif - m_jackTrip->setSampleRate(studioInfo->sampleRate()); - int bindPort = selectBindPort(); - if (bindPort == 0) { - return 0; - } - m_jackTrip->setBindPorts(bindPort); - m_jackTrip->setRemoteClientName(m_appID); - m_jackTrip->setBufferStrategy(bufferStrategy); - m_jackTrip->setBufferQueueLength(-500); // use -q auto - m_jackTrip->setPeerAddress(studioInfo->host()); - m_jackTrip->setPeerPorts(studioInfo->port()); - m_jackTrip->setPeerHandshakePort(studioInfo->port()); - - QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this, - &VsDevice::handleJackTripError, Qt::QueuedConnection); - QObject::connect(m_jackTrip.data(), &JackTrip::signalError, this, - &VsDevice::handleJackTripError, Qt::QueuedConnection); - - return m_jackTrip.data(); -} - -// startJackTrip starts the current jacktrip process if applicable -void VsDevice::startJackTrip(const VsServerInfo& studioInfo) -{ - m_stopping = false; - m_networkOutage = false; - updateState(studioInfo.id()); - - // setup websocket listener - m_deviceSocketPtr.reset( - new VsWebSocket(QUrl(QStringLiteral("wss://%1/api/devices/%2/heartbeat") - .arg(m_api->getApiHost(), m_appID)), - m_auth->accessToken(), m_apiPrefix, m_apiSecret)); - connect(m_deviceSocketPtr.get(), &VsWebSocket::textMessageReceived, this, - &VsDevice::onTextMessageReceived); - connect(m_deviceSocketPtr.get(), &VsWebSocket::disconnected, this, - &VsDevice::restartDeviceSocket); - m_deviceSocketPtr->openSocket(); - - if (!m_jackTrip.isNull()) { -#ifdef WAIRTOHUB // WAIR - m_jackTrip->startProcess(0); // for WAIR compatibility, ID in jack client name -#else - m_jackTrip->startProcess(); -#endif // endwhere - } - - // intialize the pinger used to generate network latency statistics for - // Virtual Studio - QString host = studioInfo.host(); - if (studioInfo.isManaged()) { - host = studioInfo.sessionId(); - host.append(QString::fromStdString(".jacktrip.cloud")); - } - if (studioInfo.isManaged() || !studioInfo.sessionId().isEmpty()) { - m_pinger.reset(new VsPinger(QString::fromStdString("wss"), host, - QString::fromStdString("/ping"))); - } -} - -// stopJackTrip stops the current jacktrip process if applicable -void VsDevice::stopJackTrip(bool isReconnecting) -{ - // check if another process has already initiated - QMutexLocker stopLock(&m_stopMutex); - if (m_stopping) - return; - m_stopping = true; - - // only clear state if we are not reconnecting - if (!isReconnecting) - updateState(""); - - // stop the Virtual Studio pinger - if (!m_pinger.isNull()) { - m_pinger->stop(); - m_pinger->unsetToken(); - } - - if (!m_jackTrip.isNull()) { - if (!m_deviceSocketPtr.isNull()) { - m_deviceSocketPtr->closeSocket(); - } - m_jackTrip->stop(); - m_jackTrip.reset(); - } -} - -// reconcileAgentConfig updates the internal DeviceAgentConfig structure -void VsDevice::reconcileAgentConfig(QJsonDocument newState) -{ - // Only sync if the incoming type matches DeviceAgentConfig: - // https://github.com/jacktrip/jacktrip-agent/blob/fd3940c293daf16d8467c62b39a30779d21a0a22/pkg/client/devices.go#L87 - QJsonObject newObject = newState.object(); - if (!newObject.contains("enabled")) { - return; - } - for (auto it = newObject.constBegin(); it != newObject.constEnd(); it++) { - // if currently enabled but new config is not enabled, disconnect immediately - if (enabled() && it.key() == "enabled" && !it.value().toBool() - && !m_jackTrip.isNull()) { - stopJackTrip(false); - } - m_deviceAgentConfig.insert(it.key(), it.value()); - } -} - -// syncDeviceSettings updates volume/mute controls against the API -void VsDevice::syncDeviceSettings() -{ - m_sendVolumeTimer.start(100); -} - -// handleJackTripError is a slot intended to be triggered on jacktrip process signals -void VsDevice::handleJackTripError() -{ - stopJackTrip(false); -} - -// onTextMessageReceived is a slot intended to be triggered by new incoming WSS messages -void VsDevice::onTextMessageReceived(const QString& message) -{ - QJsonDocument newState = QJsonDocument::fromJson(message.toUtf8()); - QJsonObject newObj = newState.object(); - - // We have a heartbeat from which we can read the studio auth token - // Use it to set up and start the pinger connection - QString token = newState["authToken"].toString(); - if (!m_pinger.isNull() && !m_pinger->active() && !token.isEmpty()) { - m_pinger->setToken(token); - m_pinger->start(); - } - - // capture (input) volume - m_audioConfigPtr->setInputVolume( - (float)(newObj[QStringLiteral("captureVolume")].toDouble() / 100.0)); - m_audioConfigPtr->setInputMuted(newObj[QStringLiteral("captureMute")].toBool()); - - // playback (output) volume - m_audioConfigPtr->setOutputVolume( - (float)(newObj[QStringLiteral("playbackVolume")].toDouble() / 100.0)); - - // monitor volume - m_audioConfigPtr->setMonitorVolume( - (float)(newObj[QStringLiteral("monitorVolume")].toDouble() / 100.0)); - - reconcileAgentConfig(newState); -} - -void VsDevice::restartDeviceSocket() -{ - if (m_deviceAgentConfig[QStringLiteral("serverId")].toString() != "") { - if (!m_deviceSocketPtr.isNull()) { - m_deviceSocketPtr->openSocket(); - } - } -} - -// registerJTAsDevice creates the emulated device belonging to the current user -void VsDevice::registerJTAsDevice() -{ - /* - REGISTER JT APP AS A DEVICE ON VIRTUAL STUDIO - - Defaults: - period - 128 - set by studio = buffer size - queueBuffer - 0 - set by studio = net queue - devicePort - 4464 - reverb - 0 - off - limiter - false - compressor - false - quality - 2 - high - captureMute - false - unused right now - captureVolume - 100 - unused right now - playbackMute - false - unused right now - playbackVolume - 100 - unused right now - monitorMute - false - unsure if we should enable - monitorVolume - 0 - unsure if we should enable - name - "JackTrip App" - alsaName - "jacktripapp" - overlay - "jacktrip_app" - mac - UUID tied to app session - version - app version - will need to update in heartbeat - apiPrefix - random 7 character string tied to app session - apiSecret - random 22 character string tied to app session - */ - - QJsonObject json = { - // TODO: Fix me - //{QLatin1String("period"), m_bufferOptions[bufferSize()].toInt()}, - {QLatin1String("period"), 128}, - {QLatin1String("queueBuffer"), 0}, - {QLatin1String("devicePort"), 4464}, - {QLatin1String("reverb"), 0}, - {QLatin1String("limiter"), false}, - {QLatin1String("compressor"), false}, - {QLatin1String("quality"), 2}, - {QLatin1String("captureMute"), false}, - {QLatin1String("captureVolume"), 100}, - {QLatin1String("playbackMute"), false}, - {QLatin1String("playbackVolume"), 100}, - {QLatin1String("monitorMute"), false}, - {QLatin1String("monitorVolume"), 100}, - {QLatin1String("alsaName"), "jacktripapp"}, - {QLatin1String("overlay"), "jacktrip_app"}, - {QLatin1String("mac"), m_appUUID}, - {QLatin1String("version"), QLatin1String(gVersion)}, - {QLatin1String("apiPrefix"), m_apiPrefix}, - {QLatin1String("apiSecret"), m_apiSecret}, -#if defined(Q_OS_MACOS) - {QLatin1String("name"), "JackTrip App (macOS)"}, -#elif defined(Q_OS_WIN) - {QLatin1String("name"), "JackTrip App (Windows)"}, -#else - {QLatin1String("name"), "JackTrip App"}, -#endif // Q_OS_WIN - }; - QJsonDocument request = QJsonDocument(json); - - QNetworkReply* reply = m_api->postDevice(request.toJson()); - connect(reply, &QNetworkReply::finished, this, [=]() { - if (reply->error() != QNetworkReply::NoError) { - std::cout << "Error: " << reply->errorString().toStdString() << std::endl; - reply->deleteLater(); - return; - } else { - QJsonDocument response = QJsonDocument::fromJson(reply->readAll()); - QJsonObject newObject = response.object(); - - m_appID = newObject[QStringLiteral("id")].toString(); - - // capture (input) volume - m_audioConfigPtr->setInputVolume( - (float)(newObject[QStringLiteral("captureVolume")].toDouble() / 100.0)); - m_audioConfigPtr->setInputMuted( - newObject[QStringLiteral("captureMute")].toBool()); - - // playback (output) volume - m_audioConfigPtr->setOutputVolume( - (float)(newObject[QStringLiteral("playbackVolume")].toDouble() / 100.0)); - - // monitor volume - m_audioConfigPtr->setMonitorVolume( - (float)(newObject[QStringLiteral("monitorVolume")].toDouble() / 100.0)); - - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - settings.setValue(QStringLiteral("AppID"), m_appID); - settings.endGroup(); - - std::cout << "Device ID: " << m_appID.toStdString() << std::endl; - sendHeartbeat(); - } - - reply->deleteLater(); - }); -} - -// enabled returns whether or not the client is connected to a studio -bool VsDevice::enabled() -{ - return m_deviceAgentConfig[QStringLiteral("enabled")].toBool(); -} - -// randomString generates a random sequence of characters -QString VsDevice::randomString(int stringLength) -{ - QString str = ""; - static bool seeded = false; - QString allow_symbols( - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); - - if (!seeded) { - m_randomizer.seed((QTime::currentTime().msec())); - seeded = true; - } - - for (int i = 0; i < stringLength; ++i) { - str.append(allow_symbols.at(m_randomizer.generate() % (allow_symbols.length()))); - } - - return str; -} - -// selectBindPort finds the next open bind port to use for jacktrip -int VsDevice::selectBindPort() -{ - int candidate = gDefaultPort; - if (m_jackTrip.isNull()) { - return candidate; - } - int attempt = 0; - while (attempt <= 5000) { - candidate = QRandomGenerator::global()->bounded(gBindPortLow, gBindPortHigh + 1); - attempt++; - if (!m_jackTrip->checkIfPortIsBinded(candidate)) { - return candidate; - } - } - return 0; -} diff --git a/src/gui/vsDevice.h b/src/gui/vsDevice.h deleted file mode 100644 index 99a2fe6..0000000 --- a/src/gui/vsDevice.h +++ /dev/null @@ -1,122 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsDevice.h - * \author Matt Horton - * \date June 2022 - */ - -#ifndef VSDEVICE_H -#define VSDEVICE_H - -#include -#include -#include -#include -#include -#include - -#include "../JackTrip.h" -#include "../jacktrip_globals.h" -#include "vsApi.h" -#include "vsAudio.h" -#include "vsAuth.h" -#include "vsConstants.h" -#include "vsPinger.h" -#include "vsServerInfo.h" -#include "vsWebSocket.h" - -class VsDevice : public QObject -{ - Q_OBJECT - - public: - // Constructor - explicit VsDevice(QSharedPointer& auth, QSharedPointer& api, - QSharedPointer& audio, QObject* parent = nullptr); - virtual ~VsDevice(); - - // Public functions - void registerApp(); - void removeApp(); - void sendHeartbeat(); - bool hasTerminated(); - JackTrip* initJackTrip(bool useRtAudio, std::string input, std::string output, - int baseInputChannel, int numChannelsIn, int baseOutputChannel, - int numChannelsOut, int inputMixMode, int bufferSize, - int bufferStrategy, VsServerInfo* studioInfo); - void startJackTrip(const VsServerInfo& studioInfo); - void stopJackTrip(bool isReconnecting = false); - void reconcileAgentConfig(QJsonDocument newState); - void setNetworkOutage(bool outage = true) { m_networkOutage = outage; } - bool getNetworkOutage() const { return m_networkOutage; } - - signals: - void updateNetworkStats(QJsonObject stats); - - public slots: - void syncDeviceSettings(); - - private slots: - void handleJackTripError(); - void onTextMessageReceived(const QString& message); - void restartDeviceSocket(); - void sendLevels(); - - private: - void updateState(const QString& serverId); - void registerJTAsDevice(); - bool enabled(); - int selectBindPort(); - QString randomString(int stringLength); - - QSharedPointer m_auth; - QSharedPointer m_api; - QSharedPointer m_audioConfigPtr; - QScopedPointer m_pinger; - - QString m_appID; - QString m_appUUID; - QString m_token; - QString m_apiPrefix; - QString m_apiSecret; - QMutex m_stopMutex; - QJsonObject m_deviceAgentConfig; - QScopedPointer m_deviceSocketPtr; - QScopedPointer m_jackTrip; - QRandomGenerator m_randomizer; - QTimer m_sendVolumeTimer; - bool m_networkOutage = false; - bool m_stopping = false; -}; - -#endif // VSDEVICE_H diff --git a/src/gui/vsDeviceCodeFlow.cpp b/src/gui/vsDeviceCodeFlow.cpp deleted file mode 100644 index dbc17e5..0000000 --- a/src/gui/vsDeviceCodeFlow.cpp +++ /dev/null @@ -1,249 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsDeviceCodeFlow.cpp - * \author Dominick Hing - * \date May 2023 - */ - -#include "./vsDeviceCodeFlow.h" - -#include "./vsConstants.h" - -VsDeviceCodeFlow::VsDeviceCodeFlow(QNetworkAccessManager* networkAccessManager) - : m_clientId(AUTH_CLIENT_ID) - , m_audience(AUTH_AUDIENCE) - , m_authorizationServerHost(AUTH_SERVER_HOST) - , m_authenticationError(false) - , m_netManager(networkAccessManager) -{ - // start polling when the device flow has been initialized - connect(this, &VsDeviceCodeFlow::deviceCodeFlowInitialized, this, - &VsDeviceCodeFlow::startPolling); - connect(&m_tokenPollingTimer, &QTimer::timeout, this, - &VsDeviceCodeFlow::onPollingTimerTick); - connect(&m_deviceFlowExpirationTimer, &QTimer::timeout, this, - &VsDeviceCodeFlow::onDeviceCodeExpired); - - m_tokenPollingTimer.setSingleShot(false); - m_deviceFlowExpirationTimer.setSingleShot(true); -} - -void VsDeviceCodeFlow::grant() -{ - initDeviceAuthorizationCodeFlow(); -} - -void VsDeviceCodeFlow::initDeviceAuthorizationCodeFlow() -{ - // form initial request for device authorization code - QNetworkRequest request = QNetworkRequest( - QUrl(QString("https://%1/oauth/device/code").arg(m_authorizationServerHost))); - - request.setRawHeader(QByteArray("Content-Type"), - QByteArray("application/x-www-form-urlencoded")); - - QString data = - QString("client_id=%1&scope=%2&audience=%3") - .arg(m_clientId, - QLatin1String("openid profile email offline_access read:servers"), - m_audience); - - // send request - QNetworkReply* reply = m_netManager->post(request, data.toUtf8()); - connect(reply, &QNetworkReply::finished, this, [=]() { - bool success = processDeviceCodeNetworkReply(reply); - if (success) { - // notify success along with user code and verification URL - emit deviceCodeFlowInitialized(m_userCode, m_verificationUriComplete); - } else if (m_authenticationError) { - // notify failure - emit deviceCodeFlowError(); - } - reply->deleteLater(); - }); -} - -void VsDeviceCodeFlow::startPolling() -{ - if (m_pollingInterval <= 0 || m_deviceCodeValidityDuration <= 0) { - std::cout << "Could not start polling. This should not print and indicates a bug." - << std::endl; - return; - } - - // poll on a regular interval, up until the expiration of the code - m_tokenPollingTimer.setInterval(m_pollingInterval * 1000); - m_deviceFlowExpirationTimer.setInterval(m_deviceCodeValidityDuration * 1000); - - m_tokenPollingTimer.start(); - m_deviceFlowExpirationTimer.start(); -} - -void VsDeviceCodeFlow::stopPolling() -{ - if (m_tokenPollingTimer.isActive()) { - m_tokenPollingTimer.stop(); - } - if (m_deviceFlowExpirationTimer.isActive()) { - m_deviceFlowExpirationTimer.stop(); - } -} - -void VsDeviceCodeFlow::onPollingTimerTick() -{ - // form request to /oauth/token - QNetworkRequest request = QNetworkRequest( - QUrl(QString("https://%1/oauth/token").arg(m_authorizationServerHost))); - - request.setRawHeader(QByteArray("Content-Type"), - QByteArray("application/x-www-form-urlencoded")); - - QString data = - QString("client_id=%1&device_code=%2&grant_type=%3") - .arg(m_clientId, m_deviceCode, - QLatin1String("urn:ietf:params:oauth:grant-type:device_code")); - - // send send request for token - QNetworkReply* reply = m_netManager->post(request, data.toUtf8()); - connect(reply, &QNetworkReply::finished, this, [=]() { - bool success = processPollingOAuthTokenNetworkReply(reply); - if (m_authenticationError) { - // shouldn't happen - emit deviceCodeFlowError(); - } else if (success) { - // flow successfully completed - emit onCompletedCodeFlow(m_accessToken, m_refreshToken); - // cleanup - stopPolling(); - cleanupDeviceCodeFlow(); - } - reply->deleteLater(); - }); -} - -void VsDeviceCodeFlow::onDeviceCodeExpired() -{ - emit deviceCodeFlowTimedOut(); - - std::cout << "Device Code has expired." << std::endl; - stopPolling(); - cleanupDeviceCodeFlow(); -} - -void VsDeviceCodeFlow::cancelCodeFlow() -{ - stopPolling(); - cleanupDeviceCodeFlow(); -} - -bool VsDeviceCodeFlow::processDeviceCodeNetworkReply(QNetworkReply* reply) -{ - QByteArray buffer = reply->readAll(); - - // Error: failed to get device code - if (reply->error()) { - std::cout << "Failed to get device code: " << buffer.toStdString() << std::endl; - m_authenticationError = true; - return false; - } - - // parse JSON from string response - QJsonParseError parseError; - QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError); - if (parseError.error) { - std::cout << "Error parsing JSON for Device Code: " - << parseError.errorString().toStdString() << std::endl; - m_authenticationError = true; - return false; - } - - // get fields - QJsonObject object = data.object(); - m_deviceCode = object.value(QLatin1String("device_code")).toString(); - m_userCode = object.value(QLatin1String("user_code")).toString(); - m_verificationUri = object.value(QLatin1String("verification_uri")).toString(); - m_verificationUriComplete = - object.value(QLatin1String("verification_uri_complete")).toString(); - m_pollingInterval = - object.value(QLatin1String("interval")).toInt(2); // default to 2s - m_deviceCodeValidityDuration = - object.value(QLatin1String("expires_in")).toInt(900); // default to 900s - - // return true if success - return true; -} - -bool VsDeviceCodeFlow::processPollingOAuthTokenNetworkReply(QNetworkReply* reply) -{ - QByteArray buffer = reply->readAll(); - - // Error: failed to get device code (this is expected) - if (reply->error()) { - return false; - } - - // parse JSON from string response - QJsonParseError parseError; - QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError); - if (parseError.error) { - std::cout << "Error parsing JSON for access token: " - << parseError.errorString().toStdString() << std::endl; - return false; - } - - // get fields - QJsonObject object = data.object(); - m_idToken = object.value(QLatin1String("id_token")).toString(); - m_accessToken = object.value(QLatin1String("access_token")).toString(); - m_refreshToken = object.value(QLatin1String("refresh_token")).toString(); - m_authenticationError = false; - - // return true if success - return true; -} - -void VsDeviceCodeFlow::cleanupDeviceCodeFlow() -{ - m_deviceCode = QStringLiteral(""); - m_userCode = QStringLiteral(""); - m_verificationUri = QStringLiteral("https://auth.jacktrip.org/activate"); - m_verificationUriComplete = QStringLiteral(""); - - m_pollingInterval = -1; - m_deviceCodeValidityDuration = -1; -} - -QString VsDeviceCodeFlow::accessToken() -{ - return m_accessToken; -} \ No newline at end of file diff --git a/src/gui/vsDeviceCodeFlow.h b/src/gui/vsDeviceCodeFlow.h deleted file mode 100644 index eaadc7a..0000000 --- a/src/gui/vsDeviceCodeFlow.h +++ /dev/null @@ -1,109 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsDeviceCodeFlow.h - * \author Dominick Hing - * \date May 2023 - */ - -#ifndef VSDEVICECODEFLOW_H -#define VSDEVICECODEFLOW_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "vsDeviceCodeFlow.h" - -class VsDeviceCodeFlow : public QObject -{ - Q_OBJECT - - public: - explicit VsDeviceCodeFlow(QNetworkAccessManager* networkAccessManager); - virtual ~VsDeviceCodeFlow() { stopPolling(); } - - void grant(); - void refreshAccessToken(){}; - void initDeviceAuthorizationCodeFlow(); - - bool processDeviceCodeNetworkReply(QNetworkReply* reply); - bool processPollingOAuthTokenNetworkReply(QNetworkReply* reply); - void startPolling(); - void stopPolling(); - void onPollingTimerTick(); - void onDeviceCodeExpired(); - void cancelCodeFlow(); - void cleanupDeviceCodeFlow(); - - bool authenticated(); - QString accessToken(); - - signals: - void deviceCodeFlowInitialized(QString code, QString verificationUrl); - void deviceCodeFlowError(); - void deviceCodeFlowTimedOut(); - void onCompletedCodeFlow(QString accessToken, QString refreshToken); - - private: - QString m_clientId; - QString m_audience; - QString m_authorizationServerHost; - - // state used specifically in the device code flow - QString m_deviceCode; - QString m_userCode; - QString m_verificationUri; - QString m_verificationUriComplete; - int m_pollingInterval = -1; // seconds - int m_deviceCodeValidityDuration = -1; // seconds - - QTimer m_tokenPollingTimer; - QTimer m_deviceFlowExpirationTimer; - - // authentication state variables - bool m_authenticationError; - QString m_refreshToken; - QString m_accessToken; - QString m_idToken; - - QScopedPointer m_netManager; -}; - -#endif // VSDEVICECODEFLOW \ No newline at end of file diff --git a/src/gui/vsMacPermissions.h b/src/gui/vsMacPermissions.h deleted file mode 100644 index 1360fa5..0000000 --- a/src/gui/vsMacPermissions.h +++ /dev/null @@ -1,64 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** -/** - * \file vsMacPermissions.h - * \author Matt Horton - * \date Oct 2022 - */ - -#ifndef __VSMACPERMISSIONS_H__ -#define __VSMACPERMISSIONS_H__ - -#include - -#include -#include -#include - -#include "vsPermissions.h" - -class VsMacPermissions : public VsPermissions -{ - Q_OBJECT - - public: - explicit VsMacPermissions(); - - bool micPermissionChecked() override; - Q_INVOKABLE void getMicPermission() override; - Q_INVOKABLE void openSystemPrivacy(); - - private: - QString m_micPermission = "unknown"; - bool m_micPermissionChecked = false; -}; - -#endif // __VSMACPERMISSIONS_H__ diff --git a/src/gui/vsMacPermissions.mm b/src/gui/vsMacPermissions.mm deleted file mode 100644 index a29c4ba..0000000 --- a/src/gui/vsMacPermissions.mm +++ /dev/null @@ -1,104 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** -/** - * \file vsMacPermissions.mm - * \author Matt Horton - * \date Oct 2022 - */ - -#include "vsMacPermissions.h" -#include -#include -#include -#include -#include - -VsMacPermissions::VsMacPermissions() -{ - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - m_micPermissionChecked = settings.value(QStringLiteral("MicPermissionChecked"), false).toBool(); - settings.endGroup(); -} - -bool VsMacPermissions::micPermissionChecked() -{ - if (m_micPermissionChecked) { - getMicPermission(); - } - return m_micPermissionChecked; -} - -void VsMacPermissions::getMicPermission() -{ - if (@available(macOS 10.14, *)) { - // Request permission to access. - switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]) - { - case AVAuthorizationStatusAuthorized: - { - // The user has previously granted access. - setMicPermission(QStringLiteral("granted")); - break; - } - case AVAuthorizationStatusNotDetermined: - { - // The app hasn't yet asked the user for access. - [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { - if (granted) { - setMicPermission(QStringLiteral("granted")); - } else { - setMicPermission(QStringLiteral("denied")); - } - }]; - setMicPermission(QStringLiteral("unknown")); - break; - } - case AVAuthorizationStatusDenied: - { - // The user has previously denied access. - setMicPermission(QStringLiteral("denied")); - } - case AVAuthorizationStatusRestricted: - { - // The user can't grant access due to restrictions. - setMicPermission(QStringLiteral("denied")); - } - } - } else { - setMicPermission(QStringLiteral("granted")); - } -} - -void VsMacPermissions::openSystemPrivacy() -{ - QDesktopServices::openUrl(QUrl("x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")); -} diff --git a/src/gui/vsPermissions.cpp b/src/gui/vsPermissions.cpp deleted file mode 100644 index a978a5a..0000000 --- a/src/gui/vsPermissions.cpp +++ /dev/null @@ -1,68 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** -/** - * \file vsPermissions.mm - * \author Matt Horton - * \date Oct 2022 - */ - -#include "vsPermissions.h" - -#include -#include -#include - -QString VsPermissions::micPermission() -{ - return m_micPermission; -} - -bool VsPermissions::micPermissionChecked() -{ - return m_micPermissionChecked; -} - -void VsPermissions::getMicPermission() -{ - setMicPermission("granted"); -} - -void VsPermissions::setMicPermission(QString status) -{ - m_micPermission = status; - m_micPermissionChecked = true; - emit micPermissionUpdated(); - - QSettings settings; - settings.beginGroup(QStringLiteral("VirtualStudio")); - settings.setValue(QStringLiteral("MicPermissionChecked"), m_micPermissionChecked); - settings.endGroup(); -} diff --git a/src/gui/vsPermissions.h b/src/gui/vsPermissions.h deleted file mode 100644 index cec2d97..0000000 --- a/src/gui/vsPermissions.h +++ /dev/null @@ -1,70 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** -/** - * \file vsPermissions.h - * \author Matt Horton - * \date Nov 2022 - */ - -#ifndef __VSPERMISSIONS_H__ -#define __VSPERMISSIONS_H__ - -#include -#include -#include - -class VsPermissions : public QObject -{ - Q_OBJECT - Q_PROPERTY(QString micPermission READ micPermission NOTIFY micPermissionUpdated) - - public: - VsPermissions() = default; // define here and there - - QString micPermission(); // define here - virtual bool micPermissionChecked(); // define here and there - Q_INVOKABLE virtual void getMicPermission(); - void setMicPermission(QString status); // define here - - signals: - void micPermissionUpdated(); // leave here - - protected: -#if __APPLE__ - QString m_micPermission = "unknown"; - bool m_micPermissionChecked = false; -#else - QString m_micPermission = "granted"; - bool m_micPermissionChecked = true; -#endif -}; - -#endif // __VSPERMISSIONS_H__ diff --git a/src/gui/vsPing.cpp b/src/gui/vsPing.cpp deleted file mode 100644 index 2d1ec56..0000000 --- a/src/gui/vsPing.cpp +++ /dev/null @@ -1,82 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsPinger.cpp - * \author Dominick Hing - * \date July 2022 - */ - -#include "vsPing.h" - -#include - -using std::cout; -using std::endl; - -// NOTE: It's better not to use -// using namespace std; -// because some functions (like exit()) get confused with QT functions - -//******************************************************************************* -VsPing::VsPing(uint32_t pingNum, uint32_t timeout_msec) : mPingNumber(pingNum) -{ - connect(&mTimer, &QTimer::timeout, this, &VsPing::onTimeout); - - mTimer.setTimerType(Qt::PreciseTimer); - mTimer.setSingleShot(true); - mTimer.setInterval(timeout_msec); - mTimer.start(); -} - -void VsPing::send() -{ - QDateTime now = QDateTime::currentDateTime(); - mSent = now; -} - -void VsPing::receive() -{ - QDateTime now = QDateTime::currentDateTime(); - if (!mTimedOut) { - mTimer.stop(); - mReceivedReply = true; - mReceived = now; - } -} - -void VsPing::onTimeout() -{ - if (!mReceivedReply) { - mTimedOut = true; - emit timeout(mPingNumber); - } -} diff --git a/src/gui/vsPing.h b/src/gui/vsPing.h deleted file mode 100644 index 228f814..0000000 --- a/src/gui/vsPing.h +++ /dev/null @@ -1,84 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsPing.h - * \author Dominick Hing - * \date July 2022 - */ - -#ifndef VSPING_H -#define VSPING_H - -#include -#include -#include -#include -#include -#include - -/** \brief A helper class for VsPinger - * - */ -class VsPing : public QObject -{ - Q_OBJECT; - - public: - explicit VsPing(uint32_t pingNum, uint32_t timeout_msec); - virtual ~VsPing() { mTimer.stop(); } - uint32_t pingNumber() { return mPingNumber; } - - QDateTime sentTimestamp() { return mSent; } - QDateTime receivedTimestamp() { return mReceived; } - bool receivedReply() { return mReceivedReply; } - bool timedOut() { return mTimedOut; } - - void send(); - void receive(); - - private: - uint32_t mPingNumber; - QDateTime mSent; - QDateTime mReceived; - - QTimer mTimer; - bool mTimedOut = false; - bool mReceivedReply = false; - - public slots: - void onTimeout(); - - signals: - void timeout(uint32_t pingNum); -}; - -#endif // VSPING_H \ No newline at end of file diff --git a/src/gui/vsPinger.cpp b/src/gui/vsPinger.cpp deleted file mode 100644 index 95207b6..0000000 --- a/src/gui/vsPinger.cpp +++ /dev/null @@ -1,317 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsPinger.cpp - * \author Dominick Hing - * \date July 2022 - */ - -#include "vsPinger.h" - -#include - -using std::cout; -using std::endl; - -// NOTE: It's better not to use -// using namespace std; -// because some functions (like exit()) get confused with QT functions - -//******************************************************************************* -VsPinger::VsPinger(QString scheme, QString host, QString path) -{ - mURL.setScheme(scheme); - mURL.setHost(host); - mURL.setPath(path); - - mTimer.setTimerType(Qt::PreciseTimer); - - connect(&mSocket, &QWebSocket::binaryMessageReceived, this, - &VsPinger::onReceivePingMessage); - connect(&mSocket, &QWebSocket::connected, this, &VsPinger::onConnected); -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - connect(&mSocket, - QOverload::of(&QWebSocket::errorOccurred), this, - &VsPinger::onError); -#else - connect(&mSocket, QOverload::of(&QWebSocket::error), - this, &VsPinger::onError); -#endif - connect(&mTimer, &QTimer::timeout, this, &VsPinger::onPingTimer); -} - -//******************************************************************************* -void VsPinger::start() -{ - // fail to start if no token is supplied - if (mToken.toStdString() == "") { - std::cout << "Error: auth token is not set" << std::endl; - return; - } - - mTimer.setInterval(mPingInterval); - - QString authVal = "Bearer "; - authVal.append(mToken); - - QNetworkRequest req = QNetworkRequest(QUrl(mURL)); - req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket")); - req.setRawHeader(QByteArray("Connection"), QByteArray("upgrade")); - req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8()); - mSocket.open(req); - - mStarted = true; -} - -//******************************************************************************* -void VsPinger::stop() -{ - mStarted = false; - mError = false; - mTimer.stop(); - mSocket.close(QWebSocketProtocol::CloseCodeNormal, NULL); -} - -//******************************************************************************* -void VsPinger::setToken(QString token) -{ - if (mStarted) { - std::cout << "Error: cannot set token while pinger is active." << std::endl; - return; - } - - mToken = token; - mAuthorized = true; -}; - -//******************************************************************************* -void VsPinger::unsetToken() -{ - if (mStarted) { - std::cout << "Error: cannot unset token while pinger is active." << std::endl; - return; - } - - mToken = QString(); - mAuthorized = false; -} - -//******************************************************************************* -void VsPinger::sendPingMessage(const QByteArray& message) -{ - if (mAuthorized && !mError) { - mSocket.sendBinaryMessage(message); - } -} - -//******************************************************************************* -void VsPinger::updateStats() -{ - PingStat stat; - stat.packetsReceived = 0; - stat.packetsSent = 0; - - uint32_t count = 0; - - std::vector vec_expired; - std::vector vec_rtt; - std::map::reverse_iterator it; - for (it = mPings.rbegin(); it != mPings.rend(); ++it) { - VsPing* ping = it->second; - - // mark this ping as ready to delete, since it will no longer be used in stats - if (count >= mPingNumPerInterval) { - vec_expired.push_back(ping->pingNumber()); - count++; - } else if (ping->timedOut() || ping->receivedReply()) { - // Only include in statistics pings that have timed out or been received. - // All others are pending and are not considered in statistics - stat.packetsSent++; - if (ping->receivedReply()) { - stat.packetsReceived++; - } - - QDateTime sent = ping->sentTimestamp(); - QDateTime received = ping->receivedTimestamp(); - qint64 diff = sent.msecsTo(received); - - // don't include case where dif = 0 in stats, mark as expired instead - if (diff != 0) { - vec_rtt.push_back(diff); - } else { - vec_expired.push_back(ping->pingNumber()); - } - - count++; - } - } - - // Deleted pings marked as expired by freeing the Ping object - // and clearing the map item - for (std::vector::iterator it_expired = vec_expired.begin(); - it_expired != vec_expired.end(); it_expired++) { - uint32_t expiredPingNum = *it_expired; - delete mPings.at(expiredPingNum); - mPings.erase(expiredPingNum); - } - - // Update RTT stats - double min_rtt = 0.0; - double max_rtt = 0.0; - double avg_rtt = 0.0; - double stddev_rtt = 0.0; - - // avoid edge case due to min_rtt and max_rtt being at the numeric limits - // when vector size is 0 - if (vec_rtt.size() == 0) { - stat.maxRtt = 0; - stat.minRtt = 0; - stat.avgRtt = 0; - stat.stdDevRtt = 0; - - // Update mStats - mStats = stat; - return; - } - - for (std::vector::iterator it_rtt = vec_rtt.begin(); it_rtt != vec_rtt.end(); - it_rtt++) { - double rtt = (double)*it_rtt; - if (rtt < min_rtt || min_rtt == 0.0) { - min_rtt = rtt; - } - if (rtt > max_rtt || max_rtt == 0.0) { - max_rtt = rtt; - } - - avg_rtt += rtt / vec_rtt.size(); - } - - for (std::vector::iterator it_rtt = vec_rtt.begin(); it_rtt != vec_rtt.end(); - it_rtt++) { - double rtt = (double)*it_rtt; - stddev_rtt += (rtt - avg_rtt) * (rtt - avg_rtt); - } - stddev_rtt /= vec_rtt.size(); - stddev_rtt = sqrt(stddev_rtt); - - stat.maxRtt = max_rtt; - stat.minRtt = min_rtt; - stat.avgRtt = avg_rtt; - stat.stdDevRtt = stddev_rtt; - - // Update mStats - mStats = stat; - return; -} - -//******************************************************************************* -VsPinger::PingStat VsPinger::getPingStats() -{ - return mStats; -} - -//******************************************************************************* -void VsPinger::onError(QAbstractSocket::SocketError error) -{ - cout << "WebSocket Error: " << error << endl; - mError = true; - mStarted = false; - mTimer.stop(); -} - -//******************************************************************************* -void VsPinger::onConnected() -{ - // start the ping timer after the connection is established - mTimer.start(); -} - -//******************************************************************************* -void VsPinger::onPingTimer() -{ - updateStats(); - - QByteArray bytes = QByteArray::number(mPingCount); - QDateTime now = QDateTime::currentDateTime(); - this->sendPingMessage(bytes); - - VsPing* ping = new VsPing(mPingCount, mPingInterval); - ping->send(); - mPings[mPingCount] = ping; - - connect(ping, &VsPing::timeout, this, &VsPinger::onPingTimeout); - - mLastPacketSent = mPingCount; - mPingCount++; -} - -//******************************************************************************* -void VsPinger::onPingTimeout(uint32_t pingNum) -{ - std::map::iterator it = mPings.find(pingNum); - if (it == mPings.end()) { - return; - } - - updateStats(); -} - -//******************************************************************************* -void VsPinger::onReceivePingMessage(const QByteArray& message) -{ - QDateTime now = QDateTime::currentDateTime(); - uint32_t pingNum = message.toUInt(); - - // locate the appropriate corresponding ping message - std::map::iterator it = mPings.find(pingNum); - if (it == mPings.end()) { - return; - } - - VsPing* ping = (*it).second; - - // do not apply to pings that have timed out - if (!ping->timedOut()) { - // update ping data - ping->receive(); - - // update vsPinger - mHasReceivedPing = true; - mLastPacketReceived = pingNum; - if (pingNum > mLargestPingNumReceived) { - mLargestPingNumReceived = pingNum; - } - } - - updateStats(); -} \ No newline at end of file diff --git a/src/gui/vsPinger.h b/src/gui/vsPinger.h deleted file mode 100644 index c171e7d..0000000 --- a/src/gui/vsPinger.h +++ /dev/null @@ -1,122 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsPinger.h - * \author Dominick Hing - * \date July 2022 - */ - -#ifndef VSPINGER_H -#define VSPINGER_H - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "vsPing.h" - -/** \brief VsPinger for generating latency statistics between - * Virtual Studio devices and Virtual Studio Servers - * - */ -class VsPinger : public QObject -{ - Q_OBJECT; - - public: - /** \brief The class constructor - * \param scheme The protocol scheme for the pinger - * \param host The hostname of the server - * \param path The path to ping the server on - */ - explicit VsPinger(QString scheme, QString host, QString path); - virtual ~VsPinger() { stop(); } - void start(); - void stop(); - bool active() { return mStarted; }; - void setToken(QString token); - void unsetToken(); - - struct PingStat { - uint32_t packetsReceived = 0; - uint32_t packetsSent = 0; - double minRtt = 0.0; - double maxRtt = 0.0; - double avgRtt = 0.0; - double stdDevRtt = 0.0; - }; - - PingStat getPingStats(); - - private: - QWebSocket mSocket; - QUrl mURL; - QString mToken; - bool mAuthorized = false; - bool mStarted = false; - bool mError = false; - - QTimer mTimer; - uint32_t mPingCount = 0; - const uint32_t mPingNumPerInterval = 5; - const uint32_t mPingInterval = 1000; - const uint32_t mPingTimeout = 1000; - - std::map mPings; - - uint32_t mLastPacketSent; - uint32_t mLastPacketReceived; - uint32_t mLargestPingNumReceived = - 0; // is 0 if no ping has been received, otherwise, is the largest ping number - // received - bool mHasReceivedPing = false; // used for edge case where we have't received a ping - // yet (mLargestPingNumReceived = 0) - - PingStat mStats; - - void sendPingMessage(const QByteArray& message); - void updateStats(); - - private slots: - void onError(QAbstractSocket::SocketError error); - void onConnected(); - void onPingTimer(); - void onPingTimeout(uint32_t pingNum); - void onReceivePingMessage(const QByteArray& message); -}; - -#endif // VSPINGER_H \ No newline at end of file diff --git a/src/gui/vsQmlClipboard.h b/src/gui/vsQmlClipboard.h deleted file mode 100644 index 285b350..0000000 --- a/src/gui/vsQmlClipboard.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef VSQMLCLIPBOARD_H -#define VSQMLCLIPBOARD_H - -#include -#include -#include - -class VsQmlClipboard : public QObject -{ - Q_OBJECT - public: - explicit VsQmlClipboard(QObject* parent = 0) : QObject(parent) - { - clipboard = QApplication::clipboard(); - } - - Q_INVOKABLE void setText(QString text) - { - clipboard->setText(text, QClipboard::Clipboard); - } - - private: - QClipboard* clipboard; -}; - -#endif // VSQMLCLIPBOARD_H \ No newline at end of file diff --git a/src/gui/vsQuickView.cpp b/src/gui/vsQuickView.cpp deleted file mode 100644 index e0a9281..0000000 --- a/src/gui/vsQuickView.cpp +++ /dev/null @@ -1,68 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsQuickView.cpp - * \author Aaron Wyatt - * \date March 2022 - */ - -#include "vsQuickView.h" - -#include -#include - -VsQuickView::VsQuickView(QWindow* parent) : QQuickView(parent) -{ -#ifdef Q_OS_MACOS - auto* quit = new QAction("&Quit", this); - - QMenuBar* menuBar = new QMenuBar(nullptr); - QMenu* appName = menuBar->addMenu("&JackTrip"); - appName->addAction(quit); - - connect(quit, &QAction::triggered, this, &VsQuickView::closeWindow); -#endif -} - -bool VsQuickView::event(QEvent* event) -{ - if (event->type() == QEvent::Close || event->type() == QEvent::Quit) { - emit windowClose(); - event->ignore(); - } - return QQuickView::event(event); -} - -void VsQuickView::closeWindow() -{ - emit windowClose(); -} diff --git a/src/gui/vsQuickView.h b/src/gui/vsQuickView.h deleted file mode 100644 index ab80a73..0000000 --- a/src/gui/vsQuickView.h +++ /dev/null @@ -1,64 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsQuickView.h - * \author Aaron Wyatt - * \date March 2022 - */ - -#ifndef VSQUICKVIEW_H -#define VSQUICKVIEW_H - -#include -#ifdef Q_OS_MACOS -#include -#include -#include -#include -#endif - -class VsQuickView : public QQuickView -{ - Q_OBJECT - - public: - VsQuickView(QWindow* parent = nullptr); - bool event(QEvent* event) override; - - signals: - void windowClose(); - - private slots: - void closeWindow(); -}; - -#endif // VSQUICKVIEW_H diff --git a/src/gui/vsServerInfo.cpp b/src/gui/vsServerInfo.cpp deleted file mode 100644 index 9ea5010..0000000 --- a/src/gui/vsServerInfo.cpp +++ /dev/null @@ -1,321 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsServerInfo.cpp - * \author Aaron Wyatt - * \date March 2022 - */ - -#include "vsServerInfo.h" - -VsServerInfo::VsServerInfo(QObject* parent) : QObject(parent) {} - -VsServerInfo& VsServerInfo::operator=(const VsServerInfo& info) -{ - m_section = info.m_section; - m_name = info.m_name; - m_host = info.m_host; - m_port = info.m_port; - m_enabled = info.m_enabled; - m_owner = info.m_owner; - m_admin = info.m_admin; - m_isManaged = info.m_isManaged; - m_isPublic = info.m_isPublic; - m_region = info.m_region; - m_period = info.m_period; - m_sampleRate = info.m_sampleRate; - m_queueBuffer = info.m_queueBuffer; - m_bannerURL = info.m_bannerURL; - m_id = info.m_id; - m_sessionId = info.m_sessionId; - m_streamId = info.m_streamId; - m_status = info.m_status; - m_cloudId = info.m_cloudId; - m_inviteKey = info.m_inviteKey; - return *this; -} - -VsServerInfo::serverSectionT VsServerInfo::section() -{ - return m_section; -} - -QString VsServerInfo::type() const -{ - if (m_section == YOUR_STUDIOS) { - return QStringLiteral("Your Studios"); - } else if (m_section == SUBSCRIBED_STUDIOS) { - return QStringLiteral("Subscribed Studios"); - } else { - return QStringLiteral("Public Studios"); - } -} - -void VsServerInfo::setSection(serverSectionT section) -{ - m_section = section; -} - -QString VsServerInfo::name() const -{ - return m_name; -} - -void VsServerInfo::setName(const QString& name) -{ - m_name = name; -} - -QString VsServerInfo::host() const -{ - return m_host; -} - -QString VsServerInfo::status() const -{ - return m_status; -} - -bool VsServerInfo::canConnect() const -{ - return !m_host.isEmpty() && m_status == "Ready"; -} - -bool VsServerInfo::canStart() const -{ - return m_owner || m_admin; -} - -void VsServerInfo::setHost(const QString& host) -{ - m_host = host; - emit canConnectChanged(); -} - -void VsServerInfo::setStatus(const QString& status) -{ - m_status = status; - emit canConnectChanged(); -} - -quint16 VsServerInfo::port() const -{ - return m_port; -} - -void VsServerInfo::setPort(quint16 port) -{ - m_port = port; -} - -bool VsServerInfo::enabled() const -{ - return m_enabled; -} - -void VsServerInfo::setEnabled(bool enabled) -{ - m_enabled = enabled; -} - -bool VsServerInfo::isOwner() const -{ - return m_owner; -} - -void VsServerInfo::setIsOwner(bool owner) -{ - m_owner = owner; -} - -bool VsServerInfo::isAdmin() const -{ - return m_admin; -} - -void VsServerInfo::setIsAdmin(bool admin) -{ - m_admin = admin; -} - -bool VsServerInfo::isPublic() const -{ - return m_isPublic; -} - -void VsServerInfo::setIsPublic(bool isPublic) -{ - m_isPublic = isPublic; -} - -QString VsServerInfo::region() const -{ - return m_region; -} - -QString VsServerInfo::flag() const -{ - QStringList parts = m_region.split(QStringLiteral("-")); - if (parts.count() > 1) { - QString countryCode = parts.at(1).toUpper(); - if (countryCode == QStringLiteral("TF")) { - countryCode = QStringLiteral("TW"); - } - return QStringLiteral("flags/%1.svg").arg(countryCode); - } - // Have a fallback here - return QStringLiteral("flags/US.svg"); -} - -QString VsServerInfo::location() const -{ - return m_region; -} - -void VsServerInfo::setRegion(const QString& region) -{ - m_region = region; -} - -bool VsServerInfo::isManaged() const -{ - return m_isManaged; -} - -void VsServerInfo::setIsManaged(bool isManaged) -{ - m_isManaged = isManaged; -} - -quint16 VsServerInfo::period() const -{ - return m_period; -} - -void VsServerInfo::setPeriod(quint16 period) -{ - m_period = period; -} - -quint32 VsServerInfo::sampleRate() const -{ - return m_sampleRate; -} - -void VsServerInfo::setSampleRate(quint32 sampleRate) -{ - m_sampleRate = sampleRate; -} - -quint16 VsServerInfo::queueBuffer() const -{ - return m_queueBuffer; -} - -void VsServerInfo::setQueueBuffer(quint16 queueBuffer) -{ - m_queueBuffer = queueBuffer; -} - -QString VsServerInfo::bannerURL() const -{ - return m_bannerURL; -} - -void VsServerInfo::setBannerURL(const QString& bannerURL) -{ - m_bannerURL = bannerURL; -} - -QString VsServerInfo::id() const -{ - return m_id; -} - -void VsServerInfo::setId(const QString& id) -{ - m_id = id; -} - -QString VsServerInfo::sessionId() const -{ - return m_sessionId; -} - -void VsServerInfo::setSessionId(const QString& sessionId) -{ - m_sessionId = (sessionId == "undefined") ? "" : sessionId; -} - -QString VsServerInfo::streamId() const -{ - return m_streamId; -} - -void VsServerInfo::setStreamId(const QString& streamId) -{ - m_streamId = (streamId == "undefined") ? "" : streamId; -} - -QString VsServerInfo::inviteKey() const -{ - return m_inviteKey; -} - -void VsServerInfo::setInviteKey(const QString& inviteKey) -{ - m_inviteKey = inviteKey; -} - -QString VsServerInfo::cloudId() const -{ - return m_cloudId; -} - -void VsServerInfo::setCloudId(const QString& cloudId) -{ - m_cloudId = cloudId; -} - -bool VsServerInfo::operator<(const VsServerInfo& other) const -{ - if (status() == QStringLiteral("Ready")) { - if (other.status() != QStringLiteral("Ready")) { - return true; - } - } else if (other.status() == QStringLiteral("Ready")) { - return false; - } - return name() < other.name(); -} - -VsServerInfo::~VsServerInfo() = default; diff --git a/src/gui/vsServerInfo.h b/src/gui/vsServerInfo.h deleted file mode 100644 index 5112c64..0000000 --- a/src/gui/vsServerInfo.h +++ /dev/null @@ -1,164 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsServerInfo.h - * \author Aaron Wyatt - * \date March 2022 - */ - -#ifndef VSSERVERINFO_H -#define VSSERVERINFO_H - -#include - -class VsServerInfo : public QObject -{ - Q_OBJECT - - Q_PROPERTY(QString type READ type CONSTANT) - Q_PROPERTY(QString name READ name CONSTANT) - // Q_PROPERTY(QString host READ host CONSTANT) - Q_PROPERTY(bool canConnect READ canConnect NOTIFY canConnectChanged) - Q_PROPERTY(bool canStart READ canStart CONSTANT) - // Q_PROPERTY(quint16 port READ port CONSTANT) - Q_PROPERTY(bool isPublic READ isPublic CONSTANT) - Q_PROPERTY(QString flag READ flag CONSTANT) - Q_PROPERTY(QString bannerURL READ bannerURL CONSTANT) - Q_PROPERTY(QString location READ location CONSTANT) - Q_PROPERTY(bool isAdmin READ isAdmin CONSTANT) - Q_PROPERTY(bool isManaged READ isManaged CONSTANT) - Q_PROPERTY(quint16 period READ period CONSTANT) - Q_PROPERTY(quint32 sampleRate READ sampleRate CONSTANT) - Q_PROPERTY(quint16 queueBuffer READ queueBuffer CONSTANT) - Q_PROPERTY(QString sessionId READ sessionId CONSTANT) - Q_PROPERTY(QString streamId READ streamId CONSTANT) - Q_PROPERTY(QString status READ status CONSTANT) - Q_PROPERTY(bool enabled READ enabled CONSTANT) - Q_PROPERTY(QString cloudId READ cloudId CONSTANT) - Q_PROPERTY(QString id READ id CONSTANT) - Q_PROPERTY(QString inviteKey READ inviteKey CONSTANT) - - public: - enum serverSectionT { YOUR_STUDIOS, SUBSCRIBED_STUDIOS, PUBLIC_STUDIOS }; - - explicit VsServerInfo(QObject* parent = nullptr); - VsServerInfo& operator=(const VsServerInfo& info); - ~VsServerInfo() override; - - serverSectionT section(); - QString type() const; - void setSection(serverSectionT section); - QString name() const; - void setName(const QString& name); - QString host() const; - bool canConnect() const; - bool canStart() const; - void setHost(const QString& host); - quint16 port() const; - void setPort(quint16 port); - bool enabled() const; - void setEnabled(bool enabled); - bool isOwner() const; - void setIsOwner(bool owner); - bool isAdmin() const; - void setIsAdmin(bool admin); - bool isPublic() const; - void setIsPublic(bool isPublic); - QString region() const; - QString flag() const; - QString location() const; - void setRegion(const QString& region); - bool isManaged() const; - void setIsManaged(bool isManageable); - quint16 period() const; - void setPeriod(quint16 period); - quint32 sampleRate() const; - void setSampleRate(quint32 sampleRate); - quint16 queueBuffer() const; - void setQueueBuffer(quint16 queueBuffer); - QString bannerURL() const; - void setBannerURL(const QString& bannerURL); - QString id() const; - void setId(const QString& id); - QString sessionId() const; - void setSessionId(const QString& sessionId); - QString streamId() const; - void setStreamId(const QString& streamId); - QString status() const; - void setStatus(const QString& status); - QString inviteKey() const; - void setInviteKey(const QString& inviteKey); - QString cloudId() const; - void setCloudId(const QString& cloudId); - bool operator<(const VsServerInfo& other) const; - - signals: - void canConnectChanged(); - - private: - serverSectionT m_section = PUBLIC_STUDIOS; - QString m_name; - QString m_host; - quint16 m_port; - bool m_enabled; - bool m_owner; - bool m_admin; - bool m_isManaged; - bool m_isPublic; - QString m_region; - quint16 m_period; - quint32 m_sampleRate; - quint16 m_queueBuffer; - QString m_bannerURL; - QString m_id; - QString m_sessionId; - QString m_streamId; - QString m_status; - QString m_cloudId; - QString m_inviteKey; - - /* Remaining JSON fields - "loopback": true, - "stereo": true, - "type": "JackTrip", - "size": "c5.large", - "mixBranch": "main", - "mixCode": "SimpleMix(~maxClients).masterVolume_(1).connect.start;", - "ownerId": "string", - "subStatus": "Active", - "createdAt": "2021-09-07T17:15:38Z", - "expiresAt": "2021-09-07T17:15:38Z", - "updatedAt": "2021-09-07T17:15:38Z" - */ -}; - -#endif // VSSERVERINFO_H diff --git a/src/gui/vsWebSocket.cpp b/src/gui/vsWebSocket.cpp deleted file mode 100644 index d3ef34d..0000000 --- a/src/gui/vsWebSocket.cpp +++ /dev/null @@ -1,146 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsWebSocket.cpp - * \author Matt Horton - * \date June 2022 - */ - -#include "vsWebSocket.h" - -#include -#include - -// Constructor -VsWebSocket::VsWebSocket(const QUrl& url, QString token, QString apiPrefix, - QString apiSecret, QObject* parent) - : QObject(parent) - , m_url(url) - , m_token(token) - , m_apiPrefix(apiPrefix) - , m_apiSecret(apiSecret) -{ - m_webSocket.reset(new QWebSocket()); - connect(m_webSocket.get(), &QWebSocket::disconnected, this, - &VsWebSocket::disconnected); - connect(m_webSocket.get(), - QOverload&>::of(&QWebSocket::sslErrors), this, - &VsWebSocket::onSslErrors); -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - connect(m_webSocket.get(), - QOverload::of(&QWebSocket::errorOccurred), this, - &VsWebSocket::onError); -#else - connect(m_webSocket.get(), - QOverload::of(&QWebSocket::error), this, - &VsWebSocket::onError); -#endif - connect(m_webSocket.get(), &QWebSocket::textMessageReceived, this, - &VsWebSocket::textMessageReceived); -} - -VsWebSocket::~VsWebSocket() -{ - if (isValid()) { - closeSocket(); - } - if (!m_webSocket.isNull()) { - m_webSocket->disconnect(); - m_webSocket.reset(); - } -} - -void VsWebSocket::openSocket() -{ - if (isValid()) { - return; - } - - QNetworkRequest req = QNetworkRequest(QUrl(m_url)); - QString authVal = "Bearer "; - authVal.append(m_token); - req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket")); - req.setRawHeader(QByteArray("Connection"), QByteArray("Upgrade")); - req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8()); - req.setRawHeader(QByteArray("Origin"), QByteArray("http://jacktrip.local")); - req.setRawHeader(QByteArray("APIPrefix"), m_apiPrefix.toUtf8()); - req.setRawHeader(QByteArray("APISecret"), m_apiSecret.toUtf8()); - - if (!m_webSocket.isNull()) { - m_webSocket->open(req); - qDebug() << "Opened websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery); - } -} - -void VsWebSocket::closeSocket() -{ - if (!m_webSocket.isNull() - && m_webSocket->state() != QAbstractSocket::UnconnectedState) { - qDebug() << "Closing websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery); - m_webSocket->abort(); - } -} - -void VsWebSocket::onError(QAbstractSocket::SocketError error) -{ - // RemoteHostClosedError may be expected due to finite connection durations - // ConnectionRefusedError may be expected if the server-side endpoint is closed - if (error != QAbstractSocket::RemoteHostClosedError) { - qDebug() << "Websocket error: " << error; - } - if (!m_webSocket.isNull()) { - m_webSocket->abort(); - } -} - -void VsWebSocket::onSslErrors(const QList& errors) -{ - for (int i = 0; i < errors.size(); ++i) { - qDebug() << "SSL error: " << errors.at(i); - } - if (!m_webSocket.isNull()) { - m_webSocket->abort(); - } -} - -void VsWebSocket::sendMessage(const QByteArray& message) -{ - if (isValid()) { - m_webSocket->sendBinaryMessage(message); - } -} - -bool VsWebSocket::isValid() -{ - return !m_webSocket.isNull() - && m_webSocket->state() == QAbstractSocket::ConnectedState; -} diff --git a/src/gui/vsWebSocket.h b/src/gui/vsWebSocket.h deleted file mode 100644 index 61b19c6..0000000 --- a/src/gui/vsWebSocket.h +++ /dev/null @@ -1,81 +0,0 @@ -//***************************************************************** -/* - JackTrip: A System for High-Quality Audio Network Performance - over the Internet - - Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe. - SoundWIRE group at CCRMA, Stanford University. - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ -//***************************************************************** - -/** - * \file vsWebSocket.h - * \author Matt Horton - * \date June 2022 - */ - -#ifndef VSWEBSOCKET_H -#define VSWEBSOCKET_H - -#include -#include -#include -#include -#include -#include -#include - -class VsWebSocket : public QObject -{ - Q_OBJECT - - public: - // Constructor - explicit VsWebSocket(const QUrl& url, QString token, QString apiPrefix, - QString apiSecret, QObject* parent = nullptr); - virtual ~VsWebSocket(); - - // Public functions - void openSocket(); - void closeSocket(); - void sendMessage(const QByteArray& message); - bool isValid(); - - signals: - void textMessageReceived(const QString& message); - void disconnected(); - - private slots: - void onError(QAbstractSocket::SocketError error); - void onSslErrors(const QList& errors); - - private: - QScopedPointer m_webSocket; - QUrl m_url; - QString m_token; - QString m_apiPrefix; - QString m_apiSecret; -}; - -#endif // VSWEBSOCKET_H diff --git a/src/gui/warning.svg b/src/gui/warning.svg deleted file mode 100644 index e532c5f..0000000 --- a/src/gui/warning.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/gui/wedge.svg b/src/gui/wedge.svg deleted file mode 100644 index 230cdd2..0000000 --- a/src/gui/wedge.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - diff --git a/src/gui/wedge_inactive.svg b/src/gui/wedge_inactive.svg deleted file mode 100644 index 68ecbcf..0000000 --- a/src/gui/wedge_inactive.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - diff --git a/src/images/icon_128.png b/src/images/icon_128.png new file mode 100644 index 0000000..0f877b9 Binary files /dev/null and b/src/images/icon_128.png differ diff --git a/src/images/icon_256.png b/src/images/icon_256.png new file mode 100644 index 0000000..f51508b Binary files /dev/null and b/src/images/icon_256.png differ diff --git a/src/images/icon_32.png b/src/images/icon_32.png new file mode 100644 index 0000000..9344dff Binary files /dev/null and b/src/images/icon_32.png differ diff --git a/src/images/images.qrc b/src/images/images.qrc new file mode 100644 index 0000000..12f7129 --- /dev/null +++ b/src/images/images.qrc @@ -0,0 +1,7 @@ + + + icon_256.png + icon_128.png + icon_32.png + + diff --git a/src/jacktrip_globals.cpp b/src/jacktrip_globals.cpp index 6a31fbf..abd3017 100644 --- a/src/jacktrip_globals.cpp +++ b/src/jacktrip_globals.cpp @@ -152,11 +152,70 @@ void setRealtimeProcessPriority(int bufferSize, int sampleRate) #elif defined(_WIN32) void setRealtimeProcessPriority() { - if (SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS) == 0) { - std::cerr << "Failed to set process priority class." << std::endl; + if (SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS) == 0 + || GetPriorityClass(GetCurrentProcess()) != REALTIME_PRIORITY_CLASS) { + std::string priority = "unknown"; + switch (GetPriorityClass(GetCurrentProcess())) { + case ABOVE_NORMAL_PRIORITY_CLASS: + priority = "above normal"; + break; + case BELOW_NORMAL_PRIORITY_CLASS: + priority = "below normal"; + break; + case HIGH_PRIORITY_CLASS: + priority = "high"; + break; + case IDLE_PRIORITY_CLASS: + priority = "idle"; + break; + case NORMAL_PRIORITY_CLASS: + priority = "high"; + break; + case REALTIME_PRIORITY_CLASS: + priority = "realtime"; + break; + } + std::cerr << "Failed to set process priority class (priority = " << priority + << ")" << std::endl; + } else { + std::cout << "Set process priority class to realtime" << std::endl; } - if (SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL) == 0) { - std::cerr << "Failed to set thread priority." << std::endl; + if (SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL) == 0 + || GetThreadPriority(GetCurrentThread()) != THREAD_PRIORITY_TIME_CRITICAL) { + std::string priority = "unknown"; + switch (GetThreadPriority(GetCurrentThread())) { + case THREAD_MODE_BACKGROUND_BEGIN: + priority = "background begin"; + break; + case THREAD_MODE_BACKGROUND_END: + priority = "background end"; + break; + case THREAD_PRIORITY_ABOVE_NORMAL: + priority = "above normal"; + break; + case THREAD_PRIORITY_BELOW_NORMAL: + priority = "below normal"; + break; + case THREAD_PRIORITY_HIGHEST: + priority = "highest"; + break; + case THREAD_PRIORITY_IDLE: + priority = "idle"; + break; + case THREAD_PRIORITY_LOWEST: + priority = "lowest"; + break; + case THREAD_PRIORITY_NORMAL: + priority = "normal"; + break; + case THREAD_PRIORITY_TIME_CRITICAL: + priority = "time critical"; + break; + } + std::cerr << "Failed to set thread priority (priority = " << priority << ")" + << std::endl; + } else { + std::cout << "Set thread priority to time critical" << std::endl; } } #else diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h index 0e587de..e61162a 100644 --- a/src/jacktrip_globals.h +++ b/src/jacktrip_globals.h @@ -40,7 +40,7 @@ #include "AudioInterface.h" -constexpr const char* const gVersion = "2.3.1"; ///< JackTrip version +constexpr const char* const gVersion = "2.4.0"; ///< JackTrip version //******************************************************************************* /// \name Default Values @@ -87,8 +87,7 @@ constexpr uint32_t gDefaultBufferSizeInSamples = 128; constexpr const char* gDefaultLocalAddress = ""; constexpr int gDefaultRedundancy = 1; constexpr int gTimeOutMultiThreadedServer = 10000; // seconds -constexpr int gWaitCounter = 60; -constexpr int gUdpWaitTimeout = 30; // milliseconds +constexpr int gUdpWaitTimeout = 50; // milliseconds //@} //******************************************************************************* diff --git a/src/main.cpp b/src/main.cpp index 58d91dd..8ef5908 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -35,41 +35,7 @@ * \date July 2020 */ -#ifndef NO_GUI -#include -#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include -#endif - -#ifndef NO_UPDATER -#include "dblsqd/feed.h" -#include "dblsqd/update_dialog.h" -#endif - -#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include -#include -#include -#include -#include -#include -#include -#include -#include -// TODO: Add support for QtWebView -// #include -#include - -#include "JTApplication.h" -#include "gui/virtualstudio.h" -#include "gui/vsDeeplink.h" -#include "gui/vsQmlClipboard.h" -#endif // NO_VS && QT_VERSION - -#include "gui/qjacktrip.h" -#else #include -#endif #include #include #include @@ -79,23 +45,21 @@ #include "UdpHubListener.h" #include "jacktrip_globals.h" +#ifndef NO_GUI +#include "UserInterface.h" +#endif + #ifdef _WIN32 #include #include #include #endif -#ifndef NO_GUI -#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -static QTextStream* ts; -static QFile outFile; -#endif // NO_VS && QT_VERSION -#endif // NO_GUI - QCoreApplication* createApplication(int& argc, char* argv[]) { // Check for some specific, GUI related command line options. bool forceGui = false; + bool testGui = false; for (int i = 1; i < argc; i++) { if (strncmp(argv[i], "--gui", 5) == 0 || strncmp(argv[i], "--deeplink", 10) == 0 || strncmp(argv[i], "--classic-gui", 13) == 0 @@ -105,115 +69,37 @@ QCoreApplication* createApplication(int& argc, char* argv[]) // Command line option to test if the binary has been built with GUI support. // Exits immediately. Exits with an error if GUI support has not been built // in. -#ifdef NO_GUI - std::cout << "This version of JackTrip has been built without GUI support." - << std::endl; - std::cout << "(To run JackTrip normally, please omit the --test-gui option.)" - << std::endl; - std::exit(1); -#else - std::cout << "This version of JackTrip has been built with GUI support." - << std::endl; - std::cout << "(To run JackTrip normally, please omit the --test-gui option.)" - << std::endl; - std::exit(0); -#endif + testGui = true; } } - // If we have command line arguments and aren't forcing the GUI run on the command - // line. - if (argc == 1 || forceGui) { #ifdef NO_GUI - if (forceGui) { - std::cout << "This version of jacktrip has not been built with GUI support." - << std::endl; - std::exit(1); - } else { - return new QCoreApplication(argc, argv); - } -#else -#if defined(__unix__) - // Check if X or Wayland environment variables are set. - if (std::getenv("WAYLAND_DISPLAY") == nullptr - && std::getenv("DISPLAY") == nullptr) { - std::cout << "ERROR: Display not found. Make sure X or Wayland is running or " - "try running jacktrip in command line mode." - << std::endl; - std::cout << "(To display a list of command line options run \"jacktrip -h\")" - << std::endl; - std::exit(1); - } -#endif -#if defined(Q_OS_MACOS) && !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - // Turn on high DPI support. -#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) - JTApplication::setAttribute(Qt::AA_EnableHighDpiScaling); -#endif - // Fix for display scaling like 125% or 150% on Windows -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - QGuiApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -#endif // QT_VERSION - - // Initialize webengine - QtWebEngineQuick::initialize(); - // TODO: Add support for QtWebView - // qputenv("QT_WEBVIEW_PLUGIN", "native"); - // QtWebView::initialize(); - - return new JTApplication(argc, argv); -#else - // Turn on high DPI support. -#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) - QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); -#endif - // Fix for display scaling like 125% or 150% on Windows -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) -#if defined(NO_VS) && defined(_WIN32) - QGuiApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::RoundPreferFloor); + if (testGui) { + std::cout << "This version of JackTrip has been built without GUI support." + << std::endl; + std::cout << "(To run JackTrip normally, please omit the --test-gui option.)" + << std::endl; + std::exit(1); + } + if (forceGui) { + std::cout << "This version of jacktrip has not been built with GUI support." + << std::endl; + std::exit(1); + } #else - QGuiApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -#endif // NO_VS -#endif // QT_VERSION - -#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - // Enables resource sharing between the OpenGL contexts - QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); - // QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); - // QCoreApplication::setAttribute(Qt::AA_UseOpenGLES); - - // QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D11); - QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); - - // Initialize webengine - QtWebEngineQuick::initialize(); - // TODO: Add support for QtWebView - // qputenv("QT_WEBVIEW_PLUGIN", "native"); - // QtWebView::initialize(); -#endif - - return new QApplication(argc, argv); -#endif // Q_OS_MACOS -#endif // NO_GUI - } else { - return new QCoreApplication(argc, argv); + if (testGui) { + std::cout << "This version of JackTrip has been built with GUI support." + << std::endl; + std::cout << "(To run JackTrip normally, please omit the --test-gui option.)" + << std::endl; + std::exit(0); } -} + if (forceGui || argc == 1) { + return UserInterface::createApplication(argc, argv); + } +#endif -void qtMessageHandler([[maybe_unused]] QtMsgType type, - [[maybe_unused]] const QMessageLogContext& context, - const QString& msg) -{ - std::cerr << msg.toStdString() << std::endl; -#ifndef NO_GUI -#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - // Writes to file in order to debug bundles and executables - *ts << msg << Qt::endl; -#endif // NO_VS && QT_VERSION -#endif // NO_GUI + return new QCoreApplication(argc, argv); } void outputError(const QString& msg) @@ -259,64 +145,6 @@ BOOL WINAPI windowsCtrlHandler(DWORD fdwCtrlType) return false; } } - -bool isRunFromCmd() -{ - // Get our parent process pid - HANDLE h = NULL; - PROCESSENTRY32 pe; - ZeroMemory(&pe, sizeof(PROCESSENTRY32)); - DWORD pid = GetCurrentProcessId(); - DWORD ppid = 0; - pe.dwSize = sizeof(PROCESSENTRY32); - h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (Process32First(h, &pe)) { - do { - // Loop through the list of processes until we find ours. - if (pe.th32ProcessID == pid) { - ppid = pe.th32ParentProcessID; - break; - } - } while (Process32Next(h, &pe)); - } - CloseHandle(h); - - // Get the name of our parent process; - char pname[MAX_PATH] = {0}; - DWORD size = MAX_PATH; - h = NULL; - h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, ppid); - if (h) { - if (QueryFullProcessImageNameA(h, 0, pname, &size)) { - CloseHandle(h); - - // Check if our parent process is a command line. - if (size >= 14 && strncmp(pname + size - 14, "powershell.exe", 14) == 0) { - return true; - } - if (size >= 7 && strncmp(pname + size - 7, "cmd.exe", 7) == 0) { - return true; - } - if (size >= 6 && strncmp(pname + size - 6, "wt.exe", 6) == 0) { - return true; - } - // a few extras for msys/cygwin/etc - if (size >= 8 && strncmp(pname + size - 8, "bash.exe", 8) == 0) { - return true; - } - if (size >= 6 && strncmp(pname + size - 6, "sh.exe", 6) == 0) { - return true; - } - if (size >= 7 && strncmp(pname + size - 7, "zsh.exe", 7) == 0) { - return true; - } - } else { - CloseHandle(h); - } - } - - return false; -} #endif int main(int argc, char* argv[]) @@ -324,217 +152,84 @@ int main(int argc, char* argv[]) QScopedPointer app(createApplication(argc, argv)); QScopedPointer jackTrip; QScopedPointer udpHub; -#ifndef NO_GUI - QSharedPointer window; -#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QQuickStyle::setStyle("Basic"); -#endif // QT_VERSION - -#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QSharedPointer vsPtr; - QScopedPointer vsDeeplinkPtr; -#endif // NO_VS && QT_VERSION + QSharedPointer cliSettings; -#if defined(Q_OS_MACOS) && !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - if (qobject_cast(app.data())) { -#else - if (qobject_cast(app.data())) { -#endif - // Start the GUI if there are no command line options. -#ifdef _WIN32 - // Remove the console that appears if we're on windows and not running from a - // command line. - if (!isRunFromCmd()) { - std::cout << "This extra window is caused by a bug in Microsoft Windows. " - << "It can safely be ignored or closed." << std::endl - << std::endl - << "To fix this bug, please upgrade to the latest version of " - << "Windows Terminal available in the Microsoft App Store:" - << std::endl - << "https://aka.ms/terminal" << std::endl; - - FreeConsole(); - } -#endif // _WIN32 - app->setOrganizationName(QStringLiteral("jacktrip")); - app->setOrganizationDomain(QStringLiteral("jacktrip.org")); - app->setApplicationName(QStringLiteral("JackTrip")); - app->setApplicationVersion(gVersion); - - QSharedPointer cliSettings; +#ifndef NO_GUI + QScopedPointer interface; + QApplication* guiApp = dynamic_cast(app.data()); + if (guiApp != nullptr) { + // Start the GUI cliSettings.reset(new Settings(true)); cliSettings->parseInput(argc, argv); + interface.reset(new UserInterface(cliSettings)); + interface->start(guiApp); + return app->exec(); + } +#endif // NO_GUI -#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - // Register clipboard Qml type - qmlRegisterType("VS", 1, 0, "Clipboard"); - - // prepare handler for deeplinks jacktrip://join/ - vsDeeplinkPtr.reset(new VsDeeplink(cliSettings->getDeeplink())); - if (!vsDeeplinkPtr->getDeeplink().isEmpty()) { - bool readyForExit = vsDeeplinkPtr->waitForReady(); - if (readyForExit) - return 0; - } - - // Check which mode we are running in - QSettings settings; - int uiMode = QJackTrip::UNSET; - if (!vsDeeplinkPtr->getDeeplink().isEmpty()) { - uiMode = QJackTrip::VIRTUAL_STUDIO; - } else if (cliSettings->guiForceClassicMode()) { - uiMode = QJackTrip::STANDARD; - // force settings change; otherwise, virtual studio - // window will still be displayed - settings.setValue(QStringLiteral("UiMode"), uiMode); - } else { - uiMode = settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt(); - } - - window.reset(new QJackTrip(cliSettings, !vsDeeplinkPtr->getDeeplink().isEmpty())); -#else - window.reset(new QJackTrip(cliSettings)); -#endif // NO_VS - QObject::connect(window.data(), &QJackTrip::signalExit, app.data(), - &QCoreApplication::quit, Qt::QueuedConnection); - -#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - vsPtr.reset(new VirtualStudio(uiMode == QJackTrip::UNSET)); - QObject::connect(vsPtr.data(), &VirtualStudio::signalExit, app.data(), - &QCoreApplication::quit, Qt::QueuedConnection); - vsPtr->setStandardWindow(window); - vsPtr->setCLISettings(cliSettings); - window->setVs(vsPtr); - QObject::connect(vsDeeplinkPtr.get(), &VsDeeplink::signalDeeplink, vsPtr.get(), - &VirtualStudio::handleDeeplinkRequest, Qt::QueuedConnection); - vsDeeplinkPtr->readyForSignals(); - - if (uiMode == QJackTrip::UNSET) { - vsPtr->show(); - } else if (uiMode == QJackTrip::VIRTUAL_STUDIO) { - vsPtr->show(); - } else { - window->show(); - } - - // Log to file - QString logPath( - QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); - QDir logDir; - if (!logDir.exists(logPath)) { - logDir.mkpath(logPath); - } - QString fileLoc(logPath.append("/log.txt")); - qDebug() << "Log file location:" << fileLoc; - outFile.setFileName(fileLoc); - if (!outFile.open(QIODevice::WriteOnly | QIODevice::Append)) { - qDebug() << "Log file open failed:" << outFile.errorString(); - } - ts = new QTextStream(&outFile); - qInstallMessageHandler(qtMessageHandler); -#else - window->show(); -#endif // NO_VS + // Otherwise use the non-GUI version, and parse our command line. + try { + cliSettings.reset(new Settings(false)); + cliSettings->parseInput(argc, argv); -#if !defined(NO_UPDATER) && !defined(__unix__) #ifndef PSI -#if defined(NO_VS) || QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - - // This wasn't set up earlier in NO_VS builds. Create it here. - QSettings settings; -#endif - QString updateChannel = settings.value(QStringLiteral("UpdateChannel"), "stable") - .toString() - .toLower(); - QString baseUrl = QStringLiteral("https://files.jacktrip.org/app-releases/%1") - .arg(updateChannel); -#else - QString baseUrl = QStringLiteral("https://nuages.psi-borg.org/jacktrip"); -#endif // PSI - // Setup auto-update feed - dblsqd::Feed* feed = new dblsqd::Feed(); -#ifdef Q_OS_WIN - feed->setUrl(QUrl(QString("%1/%2-manifests.json").arg(baseUrl, "win"))); -#endif -#ifdef Q_OS_MACOS - feed->setUrl(QUrl(QString("%1/%2-manifests.json").arg(baseUrl, "mac"))); -#endif - if (feed) { - dblsqd::UpdateDialog* updateDialog = new dblsqd::UpdateDialog(feed); - updateDialog->setIcon(":/qjacktrip/icon.png"); + if (gVerboseFlag) { + QLoggingCategory::setFilterRules(QStringLiteral("*.debug=true")); } -#endif // NO_UPDATER - } else { -#endif // NO_GUI - // Otherwise use the non-GUI version, and parse our command line. - try { - Settings settings; - settings.parseInput(argc, argv); - -#ifndef PSI - if (gVerboseFlag) { - QLoggingCategory::setFilterRules(QStringLiteral("*.debug=true")); - } #endif - // Either start our hub server or our jacktrip process as appropriate. - if (settings.isHubServer()) { - udpHub.reset(settings.getConfiguredHubServer()); - if (gVerboseFlag) - std::cout << "Settings:startJackTrip before udphub->start" - << std::endl; - QObject::connect(udpHub.data(), &UdpHubListener::signalStopped, - app.data(), &QCoreApplication::quit, - Qt::QueuedConnection); - QObject::connect(udpHub.data(), &UdpHubListener::signalError, - outputError); - QObject::connect(udpHub.data(), &UdpHubListener::signalError, app.data(), - &QCoreApplication::quit, Qt::QueuedConnection); + // Either start our hub server or our jacktrip process as appropriate. + if (cliSettings->isHubServer()) { + udpHub.reset(cliSettings->getConfiguredHubServer()); + if (gVerboseFlag) + std::cout << "Settings:startJackTrip before udphub->start" << std::endl; + QObject::connect(udpHub.data(), &UdpHubListener::signalStopped, app.data(), + &QCoreApplication::quit, Qt::QueuedConnection); + QObject::connect(udpHub.data(), &UdpHubListener::signalError, outputError); + QObject::connect(udpHub.data(), &UdpHubListener::signalError, app.data(), + &QCoreApplication::quit, Qt::QueuedConnection); #ifndef _WIN32 - setupUnixSignalHandler(UdpHubListener::sigIntHandler); + setupUnixSignalHandler(UdpHubListener::sigIntHandler); #else isHubServer = true; SetConsoleCtrlHandler(windowsCtrlHandler, true); -#endif - udpHub->start(); - } else { - jackTrip.reset(settings.getConfiguredJackTrip()); - if (gVerboseFlag) - std::cout << "Settings:startJackTrip before mJackTrip->startProcess" - << std::endl; - QObject::connect(jackTrip.data(), &JackTrip::signalProcessesStopped, - app.data(), &QCoreApplication::quit, - Qt::QueuedConnection); - QObject::connect(jackTrip.data(), &JackTrip::signalError, outputError); - QObject::connect(jackTrip.data(), &JackTrip::signalError, app.data(), - &QCoreApplication::quit, Qt::QueuedConnection); +#endif // _WIN32 + udpHub->start(); + } else { + jackTrip.reset(cliSettings->getConfiguredJackTrip()); + if (gVerboseFlag) + std::cout << "Settings:startJackTrip before mJackTrip->startProcess" + << std::endl; + QObject::connect(jackTrip.data(), &JackTrip::signalProcessesStopped, + app.data(), &QCoreApplication::quit, Qt::QueuedConnection); + QObject::connect(jackTrip.data(), &JackTrip::signalError, outputError); + QObject::connect(jackTrip.data(), &JackTrip::signalError, app.data(), + &QCoreApplication::quit, Qt::QueuedConnection); + #ifndef _WIN32 - setupUnixSignalHandler(JackTrip::sigIntHandler); + setupUnixSignalHandler(JackTrip::sigIntHandler); #else std::cout << SetConsoleCtrlHandler(windowsCtrlHandler, true) << std::endl; -#endif -#ifdef WAIRTOHUB // WAIR - jackTrip->startProcess( - 0); // for WAIR compatibility, ID in jack client name +#endif // _WIN32 + +#ifdef WAIRTOHUB // WAIR + jackTrip->startProcess(0); // for WAIR compatibility, ID in jack client name #else jackTrip->startProcess(); #endif // endwhere - } - - if (gVerboseFlag) - std::cout << "step 6" << std::endl; - if (gVerboseFlag) - std::cout << "jmain before app->exec()" << std::endl; - } catch (const std::exception& e) { - std::cerr << "ERROR:" << std::endl; - std::cerr << e.what() << std::endl; - std::cerr << "Exiting JackTrip..." << std::endl; - std::cerr << gPrintSeparator << std::endl; - return -1; } -#ifndef NO_GUI + + if (gVerboseFlag) + std::cout << "step 6" << std::endl; + if (gVerboseFlag) + std::cout << "jmain before app->exec()" << std::endl; + } catch (const std::exception& e) { + std::cerr << "ERROR:" << std::endl; + std::cerr << e.what() << std::endl; + std::cerr << "Exiting JackTrip..." << std::endl; + std::cerr << gPrintSeparator << std::endl; + return -1; } -#endif // NO_GUI return app->exec(); } diff --git a/src/vs/AboutWindow.qml b/src/vs/AboutWindow.qml new file mode 100644 index 0000000..da21499 --- /dev/null +++ b/src/vs/AboutWindow.qml @@ -0,0 +1,145 @@ +import QtQuick +import QtQuick.Controls + +Window { + id: aboutWindow + title: "About JackTrip" + visible: false + + width: 600 * virtualstudio.uiScale + height: 570 * virtualstudio.uiScale + + property int fontTitle: 20 + property int fontMedium: 12 + property int fontSmall: 10 + property int fontTiny: 8 + + property string buttonColour: "#F2F3F3" + property string buttonHoverColour: "#E7E8E8" + property string buttonPressedColour: "#E7E8E8" + property string buttonStroke: "#EAEBEB" + property string buttonHoverStroke: "#B0B5B5" + property string buttonPressedStroke: "#B0B5B5" + + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string textAreaTextColour: virtualstudio.darkMode ? "#A6A6A6" : "#757575" + property string textAreaColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + + Rectangle { + width: parent.width + height: parent.height + color: backgroundColour + + Rectangle { + id: aboutJackTripLogo + x: 0; y: 0; + width: 122 * virtualstudio.uiScale + height: 108 * virtualstudio.uiScale + color: backgroundColour + Image { + id: aboutLogoImage + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + source: "logo.svg" + width: 42 * virtualstudio.uiScale; height: 76 * virtualstudio.uiScale + sourceSize: Qt.size(aboutLogoImage.width,aboutLogoImage.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + } + + Item { + id: aboutContent + x: 122 * virtualstudio.uiScale; + y: 0; + width: parent.width - aboutJackTripLogo.width - (32 * virtualstudio.uiScale) + height: parent.height + + Text { + id: aboutHeader + anchors.top: parent.top + anchors.topMargin: 16 * virtualstudio.uiScale + anchors.left: parent.left + width: parent.width + text: "JackTrip Desktop App" + font {family: "Poppins"; pixelSize: fontTitle * virtualstudio.fontScale * virtualstudio.uiScale; bold: true } + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + Text { + id: aboutVersion + anchors.top: aboutHeader.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + anchors.left: parent.left + width: parent.width + text: "Version " + virtualstudio.versionString + font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true } + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + Text { + id: aboutBuildInfo + anchors.top: aboutVersion.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + anchors.left: parent.left + width: parent.width + text: virtualstudio.buildString + font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; } + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + Text { + id: aboutCopyright + anchors.top: aboutBuildInfo.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + anchors.left: parent.left + width: parent.width + text: virtualstudio.copyrightString + font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale; } + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + Button { + id: aboutCloseButton + anchors.top: aboutCopyright.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + anchors.left: parent.left + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + onClicked: () => { + aboutWindow.visible = false; + } + + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: aboutCloseButton.down ? buttonPressedColour : (aboutCloseButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: aboutCloseButton.down ? buttonPressedStroke : (aboutCloseButton.hovered ? buttonHoverStroke : buttonStroke) + } + + Text { + text: "Close" + font.family: "Poppins" + font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + + Connections { + target: virtualstudio + + function onOpenAboutWindow() { + aboutWindow.visible = true; + } + } +} diff --git a/src/vs/AppIcon.qml b/src/vs/AppIcon.qml new file mode 100644 index 0000000..32eb8aa --- /dev/null +++ b/src/vs/AppIcon.qml @@ -0,0 +1,29 @@ +import QtQuick +import QtQuick.Controls + +Item { + id: appIcon + + property alias icon: btn.icon + property string color: "" + property string defaultColor: virtualstudio.darkMode ? "#CCCCCC" : "#333333" + signal clicked() + + Button { + id: btn + anchors.fill: parent + anchors.centerIn: parent + topInset: 0 + leftInset: 0 + rightInset: 0 + bottomInset: 0 + padding: 0 + + background: Rectangle { color: "transparent" } + icon.color: color ? color : defaultColor + icon.width: parent.width + icon.height: parent.height + display: AbstractButton.IconOnly + onClicked: appIcon.clicked() + } +} diff --git a/src/vs/AudioInterfaceMode.h b/src/vs/AudioInterfaceMode.h new file mode 100644 index 0000000..b1aaebe --- /dev/null +++ b/src/vs/AudioInterfaceMode.h @@ -0,0 +1,87 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file AudioInterfaceMode.h + * \author Matt Horton + * \date December 2022 + */ + +#ifndef AUDIOINTERFACEMODE_H +#define AUDIOINTERFACEMODE_H + +enum class AudioInterfaceMode { + JACK, ///< Jack Mode + RTAUDIO, ///< RtAudio Mode + ALL, + NONE +}; + +#ifdef RT_AUDIO +#ifndef NO_JACK +constexpr AudioInterfaceMode mode = AudioInterfaceMode::ALL; +#else +constexpr AudioInterfaceMode mode = AudioInterfaceMode::RTAUDIO; +#endif +#else +#ifndef NO_JACK +constexpr AudioInterfaceMode mode = AudioInterfaceMode::JACK; +#else +constexpr AudioInterfaceMode mode = AudioInterfaceMode::NONE; +#endif +#endif + +template +constexpr auto isBackendAvailable() +{ + if constexpr (backend == AudioInterfaceMode::RTAUDIO) { + if (mode == AudioInterfaceMode::RTAUDIO || mode == AudioInterfaceMode::ALL) { + return true; + } else { + return false; + } + } else if constexpr (backend == AudioInterfaceMode::JACK) { + if (mode == AudioInterfaceMode::JACK || mode == AudioInterfaceMode::ALL) { + return true; + } else { + return false; + } + } else if constexpr (backend == AudioInterfaceMode::ALL) { + if (mode == AudioInterfaceMode::ALL) { + return true; + } else { + return false; + } + } else { + return false; + } +} + +#endif // AUDIOINTERFACEMODE_H diff --git a/src/vs/AudioSettings.qml b/src/vs/AudioSettings.qml new file mode 100644 index 0000000..0b56065 --- /dev/null +++ b/src/vs/AudioSettings.qml @@ -0,0 +1,803 @@ +import QtQuick +import QtQuick.Controls + +Rectangle { + width: parent.width + height: parent.height + color: backgroundColour + + property bool connected: false + property bool showMeters: true + property bool showTestAudio: true + + property int fontBig: 20 + property int fontMedium: 13 + property int fontSmall: 11 + property int fontExtraSmall: 8 + + property int leftMargin: 48 + property int rightMargin: 16 + property int bottomToolTipMargin: 8 + property int rightToolTipMargin: 4 + property int buttonWidth: 103 + property int buttonHeight: 25 + + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" + property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" + property string toolTipTextColour: textColour + + property string errorFlagColour: "#DB0A0A" + property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + + function getCurrentInputDeviceIndex () { + if (audio.inputDevice === "") { + return audio.inputComboModel.findIndex(elem => elem.type === "element"); + } + + let idx = audio.inputComboModel.findIndex(elem => elem.type === "element" && elem.text === audio.inputDevice); + if (idx < 0) { + idx = audio.inputComboModel.findIndex(elem => elem.type === "element"); + } + return idx; + } + + function getCurrentOutputDeviceIndex() { + if (audio.outputDevice === "") { + return audio.outputComboModel.findIndex(elem => elem.type === "element"); + } + + let idx = audio.outputComboModel.findIndex(elem => elem.type === "element" && elem.text === audio.outputDevice); + if (idx < 0) { + idx = audio.outputComboModel.findIndex(elem => elem.type === "element"); + } + return idx; + } + + function getCurrentInputChannelsIndex() { + let idx = audio.inputChannelsComboModel.findIndex(elem => elem.baseChannel === audio.baseInputChannel + && elem.numChannels === audio.numInputChannels); + if (idx < 0) { + idx = 0; + } + return idx; + } + + function getCurrentOutputChannelsIndex() { + let idx = audio.outputChannelsComboModel.findIndex(elem => elem.baseChannel === audio.baseOutputChannel + && elem.numChannels === audio.numOutputChannels); + if (idx < 0) { + idx = 0; + } + return idx; + } + + function getCurrentMixModeIndex() { + let idx = audio.inputMixModeComboModel.findIndex(elem => elem.value === audio.inputMixMode); + if (idx < 0) { + idx = 0; + } + return idx; + } + + Loader { + anchors.fill: parent + sourceComponent: audio.audioBackend == "JACK" ? usingJACK : ((!audio.deviceModelsInitialized || audio.scanningDevices) ? scanningDevices : usingRtAudio); + } + + Component { + id: usingRtAudio + + Item { + anchors.top: parent.top + anchors.topMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: 24 * virtualstudio.uiScale + anchors.right: parent.right + + Rectangle { + id: leftSpacer + x: 0; y: 0 + width: 144 * virtualstudio.uiScale + height: 0 + color: "transparent" + } + + Text { + id: outputLabel + x: 0; y: 0 + text: "Output Device" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + bottomPadding: 10 * virtualstudio.uiScale + color: textColour + } + + InfoTooltip { + id: outputHelpIcon + anchors.left: outputLabel.right + anchors.bottom: outputLabel.top + anchors.bottomMargin: -8 * virtualstudio.uiScale + size: 16 * virtualstudio.uiScale + content: qsTr("How you'll hear the studio audio") + } + + AppIcon { + id: headphonesIcon + anchors.left: outputLabel.left + anchors.top: outputLabel.bottom + width: 28 * virtualstudio.uiScale + height: 28 * virtualstudio.uiScale + icon.source: "headphones.svg" + } + + ComboBox { + id: outputCombo + anchors.left: leftSpacer.right + anchors.verticalCenter: outputLabel.verticalCenter + anchors.rightMargin: rightMargin * virtualstudio.uiScale + width: parent.width - leftSpacer.width - rightMargin * virtualstudio.uiScale + model: audio.outputComboModel + currentIndex: getCurrentOutputDeviceIndex() + delegate: ItemDelegate { + required property var modelData + required property int index + + leftPadding: 0 + + width: parent.width + contentItem: Text { + leftPadding: modelData.type === "element" && outputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12 + text: modelData.text + font.bold: modelData.type === "header" + } + highlighted: outputCombo.highlightedIndex === index + MouseArea { + anchors.fill: parent + onClicked: { + if (modelData.type == "element") { + outputCombo.currentIndex = index + outputCombo.popup.close() + audio.outputDevice = modelData.text + if (modelData.category.startsWith("Low-Latency")) { + let inputComboIdx = inputCombo.model.findIndex(it => it.category.startsWith("Low-Latency") && it.text === modelData.text); + if (inputComboIdx !== null && inputComboIdx !== undefined) { + inputCombo.currentIndex = inputComboIdx; + audio.inputDevice = modelData.text + } + } + if (connected) { + virtualstudio.triggerReconnect(false); + } else { + audio.validateDevices() + audio.restartAudio() + } + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: outputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: outputCombo.model[outputCombo.currentIndex]!=undefined && outputCombo.model[outputCombo.currentIndex].text ? outputCombo.model[outputCombo.currentIndex].text : "" + } + } + + Meter { + id: outputDeviceMeters + anchors.left: outputCombo.left + anchors.right: outputCombo.right + anchors.top: outputCombo.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + height: 24 * virtualstudio.uiScale + model: showMeters ? audio.outputMeterLevels : [0, 0] + clipped: audio.outputClipped + visible: showMeters + enabled: audio.audioReady && !Boolean(audio.devicesError) + } + + VolumeSlider { + id: outputSlider + anchors.left: outputCombo.left + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.top: outputDeviceMeters.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + height: 30 * virtualstudio.uiScale + labelText: "Studio" + tooltipText: "How loudly you hear other participants" + showLabel: false + sliderEnabled: true + visible: showMeters + } + + Text { + id: outputChannelsLabel + anchors.left: outputCombo.left + anchors.right: outputCombo.horizontalCenter + anchors.top: showMeters ? outputSlider.bottom : outputCombo.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + textFormat: Text.RichText + text: "Output Channel(s)" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: outputChannelsCombo + anchors.left: outputCombo.left + anchors.right: outputCombo.horizontalCenter + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: outputChannelsLabel.bottom + anchors.topMargin: 4 * virtualstudio.uiScale + model: audio.outputChannelsComboModel + enabled: audio.outputChannelsComboModel.length > 1 + currentIndex: getCurrentOutputChannelsIndex() + delegate: ItemDelegate { + required property var modelData + required property int index + width: parent.width + contentItem: Text { + text: modelData.label + } + highlighted: outputChannelsCombo.highlightedIndex === index + MouseArea { + anchors.fill: parent + onClicked: { + outputChannelsCombo.currentIndex = index + outputChannelsCombo.popup.close() + audio.baseOutputChannel = modelData.baseChannel + audio.numOutputChannels = modelData.numChannels + if (connected) { + virtualstudio.triggerReconnect(false); + } else { + audio.validateDevices() + audio.restartAudio() + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: inputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: outputChannelsCombo.model[outputChannelsCombo.currentIndex].label || "" + } + } + + Button { + id: testOutputAudioButton + visible: showTestAudio + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: testOutputAudioButton.down ? buttonPressedColour : (testOutputAudioButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: testOutputAudioButton.down || testOutputAudioButton.hovered ? buttonPressedStroke : (testOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { audio.playOutputAudio() } + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: outputChannelsCombo.verticalCenter + width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Play Test Tone" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Rectangle { + id: divider1 + anchors.top: showTestAudio ? testOutputAudioButton.bottom : outputChannelsCombo.bottom + anchors.topMargin: 24 * virtualstudio.uiScale + width: parent.width - x - (16 * virtualstudio.uiScale); height: 2 * virtualstudio.uiScale + color: "#E0E0E0" + } + + Text { + id: inputLabel + anchors.left: outputLabel.left + anchors.top: divider1.bottom + anchors.topMargin: 24 * virtualstudio.uiScale + text: "Input Device" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + bottomPadding: 10 * virtualstudio.uiScale + color: textColour + } + + InfoTooltip { + id: inputHelpIcon + anchors.left: inputLabel.right + anchors.bottom: inputLabel.top + anchors.bottomMargin: -8 * virtualstudio.uiScale + size: 16 * virtualstudio.uiScale + content: qsTr("Audio sent to the studio (microphone, instrument, mixer, etc.)") + } + + AppIcon { + id: microphoneIcon + anchors.left: inputLabel.left + anchors.top: inputLabel.bottom + width: 32 * virtualstudio.uiScale + height: 32 * virtualstudio.uiScale + icon.source: "mic.svg" + } + + ComboBox { + id: inputCombo + model: audio.inputComboModel + currentIndex: getCurrentInputDeviceIndex() + anchors.left: outputCombo.left + anchors.right: outputCombo.right + anchors.verticalCenter: inputLabel.verticalCenter + delegate: ItemDelegate { + required property var modelData + required property int index + + leftPadding: 0 + + width: parent.width + contentItem: Text { + leftPadding: modelData.type === "element" && inputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12 + text: modelData.text + font.bold: modelData.type === "header" + } + highlighted: inputCombo.highlightedIndex === index + MouseArea { + anchors.fill: parent + onClicked: { + if (modelData.type == "element") { + inputCombo.currentIndex = index + inputCombo.popup.close() + audio.inputDevice = modelData.text + if (modelData.category.startsWith("Low-Latency")) { + let outputComboIdx = outputCombo.model.findIndex(it => it.category.startsWith("Low-Latency") && it.text === modelData.text); + if (outputComboIdx !== null && outputComboIdx !== undefined) { + outputCombo.currentIndex = outputComboIdx; + audio.outputDevice = modelData.text + } + } + if (connected) { + virtualstudio.triggerReconnect(false); + } else { + audio.validateDevices() + audio.restartAudio() + } + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: inputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: inputCombo.model[inputCombo.currentIndex] != undefined && inputCombo.model[inputCombo.currentIndex].text ? inputCombo.model[inputCombo.currentIndex].text : "" + } + } + + Meter { + id: inputDeviceMeters + anchors.left: inputCombo.left + anchors.right: inputCombo.right + anchors.top: inputCombo.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + height: 24 * virtualstudio.uiScale + model: showMeters ? audio.inputMeterLevels : [0, 0] + clipped: audio.inputClipped + visible: showMeters + enabled: audio.audioReady && !Boolean(audio.devicesError) + } + + VolumeSlider { + id: inputSlider + anchors.left: inputCombo.left + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.top: inputDeviceMeters.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + height: 30 * virtualstudio.uiScale + labelText: "Send" + tooltipText: "How loudly other participants hear you" + showLabel: false + sliderEnabled: true + visible: showMeters + } + + Button { + id: hiddenInputButton + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: inputSlider.verticalCenter + width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + visible: false + } + + Text { + id: inputChannelsLabel + anchors.left: inputCombo.left + anchors.right: inputCombo.horizontalCenter + anchors.top: showMeters ? inputSlider.bottom : inputCombo.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + textFormat: Text.RichText + text: "Input Channel(s)" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: inputChannelsCombo + anchors.left: inputCombo.left + anchors.right: inputCombo.horizontalCenter + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: inputChannelsLabel.bottom + anchors.topMargin: 4 * virtualstudio.uiScale + model: audio.inputChannelsComboModel + enabled: audio.inputChannelsComboModel.length > 1 + currentIndex: getCurrentInputChannelsIndex() + delegate: ItemDelegate { + required property var modelData + required property int index + width: parent.width + contentItem: Text { + text: modelData.label + } + highlighted: inputChannelsCombo.highlightedIndex === index + MouseArea { + anchors.fill: parent + onClicked: { + inputChannelsCombo.currentIndex = index + inputChannelsCombo.popup.close() + audio.baseInputChannel = modelData.baseChannel + audio.numInputChannels = modelData.numChannels + if (connected) { + virtualstudio.triggerReconnect(false); + } else { + audio.validateDevices() + audio.restartAudio() + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: inputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: inputChannelsCombo.model[inputChannelsCombo.currentIndex].label || "" + } + } + + Text { + id: inputMixModeLabel + anchors.left: inputCombo.horizontalCenter + anchors.right: inputCombo.right + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: showMeters ? inputSlider.bottom : inputCombo.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + textFormat: Text.RichText + text: "Mono / Stereo" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: inputMixModeCombo + anchors.left: inputCombo.horizontalCenter + anchors.right: inputCombo.right + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.top: inputMixModeLabel.bottom + anchors.topMargin: 4 * virtualstudio.uiScale + model: audio.inputMixModeComboModel + enabled: audio.inputMixModeComboModel.length > 1 + currentIndex: getCurrentMixModeIndex() + delegate: ItemDelegate { + required property var modelData + required property int index + width: parent.width + contentItem: Text { + text: modelData.label + } + highlighted: inputMixModeCombo.highlightedIndex === index + MouseArea { + anchors.fill: parent + onClicked: { + inputMixModeCombo.currentIndex = index + inputMixModeCombo.popup.close() + audio.inputMixMode = audio.inputMixModeComboModel[index].value + if (connected) { + virtualstudio.triggerReconnect(false); + } else { + audio.validateDevices() + audio.restartAudio() + } + } + } + } + contentItem: Text { + leftPadding: 12 + font: inputCombo.font + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: inputMixModeCombo.model[inputMixModeCombo.currentIndex].label || "" + } + } + + Text { + id: inputChannelHelpMessage + anchors.left: inputChannelsCombo.left + anchors.leftMargin: 2 * virtualstudio.uiScale + anchors.right: inputChannelsCombo.right + anchors.top: inputChannelsCombo.bottom + anchors.topMargin: 8 * virtualstudio.uiScale + textFormat: Text.RichText + wrapMode: Text.WordWrap + text: audio.inputChannelsComboModel.length > 1 ? "Choose up to 2 channels" : "Only 1 channel available" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Text { + id: inputMixModeHelpMessage + anchors.left: inputMixModeCombo.left + anchors.leftMargin: 2 * virtualstudio.uiScale + anchors.right: inputMixModeCombo.right + anchors.top: inputMixModeCombo.bottom + anchors.topMargin: 8 * virtualstudio.uiScale + textFormat: Text.RichText + wrapMode: Text.WordWrap + text: (() => { + if (audio.inputMixMode === 2) { + return "Treat the channels as Left and Right signals, coming through each speaker separately."; + } else if (audio.inputMixMode === 3) { + return "Combine the channels into one central channel coming through both speakers."; + } else if (audio.inputMixMode === 1) { + return "Send a single channel of audio"; + } else { + return ""; + } + })() + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Connections { + target: audio + // anything that sets currentIndex to the value of a function needs + // to be manually updated whenever there is a change to any vars it uses + function onInputDeviceChanged() { + inputCombo.currentIndex = getCurrentInputDeviceIndex(); + } + function onOutputDeviceChanged() { + outputCombo.currentIndex = getCurrentOutputDeviceIndex(); + } + function onNumInputChannelsChanged() { + inputChannelsCombo.currentIndex = getCurrentInputChannelsIndex(); + } + function onBaseInputChannelChanged() { + inputChannelsCombo.currentIndex = getCurrentInputChannelsIndex(); + } + function onNumOutputChannelsChanged() { + outputChannelsCombo.currentIndex = getCurrentOutputChannelsIndex(); + } + function onBaseOutputChannelChanged() { + outputChannelsCombo.currentIndex = getCurrentOutputChannelsIndex(); + } + function onInputMixModeChanged() { + inputMixModeCombo.currentIndex = getCurrentMixModeIndex(); + } + } + } + } + + Component { + id: usingJACK + + Item { + anchors.top: parent.top + anchors.topMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: leftMargin * virtualstudio.uiScale + anchors.right: parent.right + + Text { + id: jackLabel + x: 0; y: 0 + width: parent.width - rightMargin * virtualstudio.uiScale + text: "Using JACK for audio input and output. Use QjackCtl to adjust your sample rate, buffer, and device settings." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + color: textColour + } + + Text { + id: jackOutputLabel + anchors.left: jackLabel.left + anchors.top: jackLabel.bottom + anchors.topMargin: 48 * virtualstudio.uiScale + width: 144 * virtualstudio.uiScale + text: "Output Volume" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + bottomPadding: 10 * virtualstudio.uiScale + color: textColour + } + + AppIcon { + id: jackHeadphonesIcon + anchors.left: jackOutputLabel.left + anchors.top: jackOutputLabel.bottom + width: 28 * virtualstudio.uiScale + height: 28 * virtualstudio.uiScale + icon.source: "headphones.svg" + } + + Meter { + id: jackOutputMeters + anchors.left: jackOutputLabel.right + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: jackOutputLabel.verticalCenter + height: 24 * virtualstudio.uiScale + model: showMeters ? audio.outputMeterLevels : [0, 0] + clipped: audio.outputClipped + enabled: audio.audioReady && !Boolean(audio.devicesError) + } + + Button { + id: jackTestOutputAudioButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: jackTestOutputAudioButton.down ? buttonPressedColour : (jackTestOutputAudioButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: jackTestOutputAudioButton.down ? buttonPressedStroke : (jackTestOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { audio.playOutputAudio() } + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: jackOutputVolumeSlider.verticalCenter + width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Play Test Tone" + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + VolumeSlider { + id: jackOutputVolumeSlider + anchors.left: jackOutputMeters.left + anchors.right: jackTestOutputAudioButton.left + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.top: jackOutputMeters.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + height: 30 * virtualstudio.uiScale + labelText: "Studio" + tooltipText: "How loudly you hear other participants" + showLabel: false + sliderEnabled: true + } + + Text { + id: jackInputLabel + anchors.left: jackLabel.left + anchors.top: jackOutputVolumeSlider.bottom + anchors.topMargin: 48 * virtualstudio.uiScale + width: 144 * virtualstudio.uiScale + text: "Input Volume" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + bottomPadding: 10 * virtualstudio.uiScale + color: textColour + } + + AppIcon { + id: jackMicrophoneIcon + anchors.left: jackInputLabel.left + anchors.top: jackInputLabel.bottom + width: 32 * virtualstudio.uiScale + height: 32 * virtualstudio.uiScale + icon.source: "mic.svg" + } + + Meter { + id: jackInputMeters + anchors.left: jackInputLabel.right + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: jackInputLabel.verticalCenter + height: 24 * virtualstudio.uiScale + model: showMeters ? audio.inputMeterLevels : [0, 0] + clipped: audio.inputClipped + enabled: audio.audioReady && !Boolean(audio.devicesError) + } + + VolumeSlider { + id: jackInputVolumeSlider + anchors.left: jackInputMeters.left + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.top: jackInputMeters.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + height: 30 * virtualstudio.uiScale + labelText: "Send" + tooltipText: "How loudly other participants hear you" + showLabel: false + sliderEnabled: true + } + + Button { + id: jackHiddenInputButton + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: jackInputVolumeSlider.verticalCenter + width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + visible: false + } + } + } + + Component { + id: noBackend + + Item { + anchors.top: parent.top + anchors.topMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: leftMargin * virtualstudio.uiScale + anchors.right: parent.right + + Text { + id: noBackendLabel + x: 0; y: 0 + width: parent.width - (16 * virtualstudio.uiScale) + text: "JackTrip has been compiled without an audio backend. Please rebuild with the rtaudio flag or without the nojack flag." + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + color: textColour + } + } + } + + Component { + id: scanningDevices + + Item { + anchors.top: parent.top + anchors.topMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: leftMargin * virtualstudio.uiScale + anchors.right: parent.right + + Text { + id: scanningDevicesLabel + x: 0; y: 0 + width: parent.width - (16 * virtualstudio.uiScale) + text: "Scanning audio devices..." + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + color: textColour + } + } + } +} diff --git a/src/vs/Browse.qml b/src/vs/Browse.qml new file mode 100644 index 0000000..4974347 --- /dev/null +++ b/src/vs/Browse.qml @@ -0,0 +1,248 @@ +import QtQuick +import QtQuick.Controls + +Item { + width: parent.width; height: parent.height + clip: true + + Rectangle { + width: parent.width; height: parent.height + color: backgroundColour + } + + property int buttonHeight: 25 + property int buttonWidth: 103 + property int extraSettingsButtonWidth: 16 + property int emptyListMessageWidth: 450 + property int createMessageTopMargin: 16 + property int createButtonTopMargin: 24 + property int fontBig: 28 + property int fontMedium: 11 + + property int scrollY: 0 + + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string createButtonStroke: virtualstudio.darkMode ? "#AB0F0F" : "#0F0D0D" + + function refresh() { + scrollY = studioListView.contentY; + var currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY); + if (currentIndex == -1) { + currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY + (16 * virtualstudio.uiScale)); + } + virtualstudio.refreshStudios(currentIndex, true) + } + + Component { + id: footer + Rectangle { + height: 16 * virtualstudio.uiScale + x: 16 * virtualstudio.uiScale + width: parent.width - (2 * x) + color: backgroundColour + } + } + + ListView { + id: studioListView + x:0; y: 0; width: parent.width - (2 * x); height: parent.height - 36 * virtualstudio.uiScale + spacing: 16 * virtualstudio.uiScale + header: footer + footer: footer + model: virtualstudio.serverModel + clip: true + boundsBehavior: Flickable.StopAtBounds + delegate: Studio { + x: 16 * virtualstudio.uiScale + width: studioListView.width - (2 * x) + serverLocation: virtualstudio.regions[modelData.location] ? "in " + virtualstudio.regions[modelData.location].label : "" + flagImage: modelData.bannerURL ? modelData.bannerURL : modelData.flag + studioName: modelData.name + publicStudio: modelData.isPublic + admin: modelData.isAdmin + available: modelData.canConnect + connected: false + studioId: modelData.id ? modelData.id : "" + streamId: modelData.streamId ? modelData.streamId : "" + inviteKeyString: modelData.inviteKey ? modelData.inviteKey : "" + sampleRate: modelData.sampleRate + } + + section { property: "modelData.type"; criteria: ViewSection.FullString; delegate: SectionHeading {} } + + // Show sectionHeading if there are no Studios in list + SectionHeading { + id: emptyListSectionHeading + listIsEmpty: true + visible: parent.count == 0 + } + + Text { + id: emptyListMessage + visible: parent.count == 0 + text: virtualstudio.refreshInProgress ? "Loading Studios..." : "No studios found that match your filter criteria." + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: emptyListMessageWidth + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: emptyListSectionHeading.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + + Button { + id: resetFiltersButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: resetFiltersButton.down ? buttonPressedColour : (resetFiltersButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: resetFiltersButton.down ? buttonPressedStroke : (resetFiltersButton.hovered ? buttonHoverStroke : buttonStroke) + } + visible: parent.count == 0 + onClicked: { + virtualstudio.showSelfHosted = true; + virtualstudio.showInactive = true; + refresh(); + } + anchors.top: emptyListMessage.bottom + anchors.topMargin: createButtonTopMargin + anchors.horizontalCenter: emptyListMessage.horizontalCenter + width: 120 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale + Text { + text: "Reset Filters" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + // Disable momentum scroll + MouseArea { + z: -1 + anchors.fill: parent + onWheel: function (wheel) { + // trackpad + studioListView.contentY -= wheel.pixelDelta.y; + // mouse wheel + studioListView.contentY -= wheel.angleDelta.y; + studioListView.returnToBounds(); + } + } + + Component.onCompleted: { + // Customize scroll properties on different platforms + if (Qt.platform.os == "linux" || Qt.platform.os == "osx" || + Qt.platform.os == "unix" || Qt.platform.os == "windows") { + var scrollBar = Qt.createQmlObject('import QtQuick.Controls; ScrollBar{}', + studioListView, + "dynamicSnippet1"); + scrollBar.policy = ScrollBar.AlwaysOn; + ScrollBar.vertical = scrollBar; + } + } + } + + Rectangle { + x: 0; y: parent.height - 36 * virtualstudio.uiScale; width: parent.width; height: 36 * virtualstudio.uiScale + border.color: "#33979797" + color: backgroundColour + + Button { + id: refreshButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { refresh() } + anchors.verticalCenter: parent.verticalCenter + x: 16 * virtualstudio.uiScale + width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale + Text { + text: "Refresh List" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Button { + id: aboutButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: aboutButton.down ? buttonPressedColour : (aboutButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: aboutButton.down ? buttonPressedStroke : (aboutButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.showAbout() } + anchors.verticalCenter: parent.verticalCenter + x: parent.width - ((230 + extraSettingsButtonWidth) * virtualstudio.uiScale) + width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale + Text { + text: "About" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Button { + id: settingsButton + text: "Settings" + palette.buttonText: textColour + icon { + source: "cog.svg"; + color: textColour; + } + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: settingsButton.down ? buttonPressedColour : (settingsButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: settingsButton.down ? buttonPressedStroke : (settingsButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.windowState = "settings"; audio.startAudio(); } + display: AbstractButton.TextBesideIcon + font { + family: "Poppins"; + pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; + } + leftPadding: 0 + rightPadding: 4 + spacing: 0 + anchors.verticalCenter: parent.verticalCenter + x: parent.width - ((119 + extraSettingsButtonWidth) * virtualstudio.uiScale) + width: (buttonWidth + extraSettingsButtonWidth) * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale + } + } + + AboutWindow { + } + + FeedbackSurvey { + } + + Connections { + target: virtualstudio + // Need to do this to avoid layout issues with our section header. + function onNewScale() { + studioListView.positionViewAtEnd(); + studioListView.positionViewAtBeginning(); + scrollY = studioListView.contentY; + } + function onRefreshFinished(index) { + if (index == -1) { + studioListView.contentY = scrollY + } else { + studioListView.positionViewAtIndex(index, ListView.Beginning); + } + } + } +} diff --git a/src/vs/ChangeDevices.qml b/src/vs/ChangeDevices.qml new file mode 100644 index 0000000..1a8dcfe --- /dev/null +++ b/src/vs/ChangeDevices.qml @@ -0,0 +1,115 @@ +import QtQuick +import QtQuick.Controls + +Rectangle { + width: parent.width; height: parent.height + color: backgroundColour + clip: true + + property int fontBig: 28 + property int fontMedium: 12 + property int fontSmall: 10 + property int fontTiny: 8 + + property int rightMargin: 16 + property int bottomToolTipMargin: 8 + property int rightToolTipMargin: 4 + + property string saveButtonText: "#000000" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0" + property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0 + property real muteButtonMutedLightnessValue: 0.24 + property real muteButtonMutedSaturationValue: 0.73 + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1" + property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" + property string toolTipTextColour: textColour + + property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" + property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + + property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" + + MouseArea { + anchors.fill: parent + propagateComposedEvents: false + } + + Rectangle { + id: audioSettingsView + width: parent.width; + height: parent.height; + color: backgroundColour + radius: 6 * virtualstudio.uiScale + + DeviceRefreshButton { + id: refreshButton + anchors.top: parent.top; + anchors.topMargin: 16 * virtualstudio.uiScale; + anchors.right: parent.right; + anchors.rightMargin: 16 * virtualstudio.uiScale; + enabled: !audio.scanningDevices + onDeviceRefresh: function () { + virtualstudio.triggerReconnect(true); + } + } + + Text { + text: "Restarting Audio" + anchors.verticalCenter: refreshButton.verticalCenter + anchors.right: refreshButton.left; + anchors.rightMargin: 16 * virtualstudio.uiScale; + font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + visible: audio.scanningDevices + } + + AudioSettings { + id: audioSettings + showMeters: false + showTestAudio: false + connected: true + height: 300 * virtualstudio.uiScale + anchors.top: refreshButton.bottom; + anchors.topMargin: 16 * virtualstudio.uiScale; + } + } + + Button { + id: backButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour) + } + onClicked: { + virtualstudio.saveSettings(); + virtualstudio.windowState = "connected"; + } + anchors.bottom: parent.bottom + anchors.bottomMargin: 16 * virtualstudio.uiScale; + anchors.left: parent.left + anchors.leftMargin: 16 * virtualstudio.uiScale; + width: 150 * virtualstudio.uiScale; height: 36 * virtualstudio.uiScale + + Text { + text: "Back" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + DeviceWarning { + id: deviceWarning + anchors.left: backButton.right + anchors.leftMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.bottomMargin: 16 * virtualstudio.uiScale; + visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) + } +} diff --git a/src/vs/Connected.qml b/src/vs/Connected.qml new file mode 100644 index 0000000..b95af9f --- /dev/null +++ b/src/vs/Connected.qml @@ -0,0 +1,85 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import org.jacktrip.jacktrip 1.0 + +Item { + width: parent.width; height: parent.height + clip: true + + property bool connecting: false + + property int leftHeaderMargin: 16 + property int fontBig: 28 + property int fontMedium: 12 + property int fontSmall: 10 + property int fontTiny: 8 + + property int bodyMargin: 60 + property int rightMargin: 16 + property int bottomToolTipMargin: 8 + property int rightToolTipMargin: 4 + + property string studioStatus: (virtualstudio.currentStudio.id === "" ? "" : virtualstudio.currentStudio.status) + property bool showReadyScreen: studioStatus === "Ready" + property bool showStartingScreen: studioStatus === "Starting" + property bool showStoppingScreen: (virtualstudio.currentStudio.id === "" ? false : (virtualstudio.currentStudio.isAdmin && !virtualstudio.currentStudio.enabled && virtualstudio.currentStudio.cloudId !== "")) + property bool showWaitingScreen: !showStoppingScreen && !showStartingScreen && !showReadyScreen + + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + + property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" + property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string saveButtonBackgroundColour: "#F2F3F3" + property string saveButtonPressedColour: "#E7E8E8" + property string saveButtonStroke: "#EAEBEB" + property string saveButtonPressedStroke: "#B0B5B5" + property string saveButtonText: "#000000" + + property string muteButtonMutedColor: "#FCB6B6" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0" + property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0 + property real muteButtonMutedLightnessValue: 0.24 + property real muteButtonMutedSaturationValue: 0.73 + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1" + property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" + property string toolTipTextColour: textColour + property string warningTextColour: "#DB0A0A" + property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" + + property string meterGreen: "#61C554" + property string meterYellow: "#F5BF4F" + property string meterRed: "#F21B1B" + + property bool isUsingRtAudio: audio.audioBackend == "RtAudio" + + Loader { + id: studioWebLoader + anchors.top: parent.top + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: deviceControlsGroup.top + + property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" + property string studioId: virtualstudio.currentStudio.id + + source: accessToken && studioId ? "Web.qml" : "WebNull.qml" + } + + DeviceControlsGroup { + id: deviceControlsGroup + anchors.bottom: footer.top + } + + Footer { + id: footer + anchors.bottom: parent.bottom + } +} diff --git a/src/vs/CreateStudio.qml b/src/vs/CreateStudio.qml new file mode 100644 index 0000000..7458dd6 --- /dev/null +++ b/src/vs/CreateStudio.qml @@ -0,0 +1,125 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtWebEngine +import org.jacktrip.jacktrip 1.0 + +Item { + width: parent.width; height: parent.height + clip: true + + property int fontMedium: 12 + property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + + Loader { + id: webLoader + anchors.top: parent.top + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: footer.top + property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" + sourceComponent: virtualstudio.windowState === "create_studio" && accessToken ? createStudioWeb : createStudioNull + } + + Component { + id: createStudioNull + Rectangle { + anchors.fill: parent + color: backgroundColour + } + } + + Component { + id: createStudioWeb + WebEngineView { + id: webEngineView + anchors.fill: parent + settings.javascriptCanAccessClipboard: true + settings.javascriptCanPaste: true + settings.screenCaptureEnabled: true + profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}` + url: `https://${virtualstudio.apiHost === "test.jacktrip.com" ? "next-test.jacktrip.com" : "www.jacktrip.com"}/app/studios/create?accessToken=${accessToken}&userId=${auth.userId}` + + onContextMenuRequested: function(request) { + // this disables the default context menu: https://doc.qt.io/qt-6.2/qml-qtwebengine-contextmenurequest.html#accepted-prop + request.accepted = true; + } + + onNewWindowRequested: function(request) { + Qt.openUrlExternally(request.requestedUrl); + } + + onFeaturePermissionRequested: function(securityOrigin, feature) { + webEngineView.grantFeaturePermission(securityOrigin, feature, true); + } + + onRenderProcessTerminated: function(terminationStatus, exitCode) { + var status = ""; + switch (terminationStatus) { + case WebEngineView.NormalTerminationStatus: + status = "(normal exit)"; + break; + case WebEngineView.AbnormalTerminationStatus: + status = "(abnormal exit)"; + break; + case WebEngineView.CrashedTerminationStatus: + status = "(crashed)"; + break; + case WebEngineView.KilledTerminationStatus: + status = "(killed)"; + break; + } + console.log("Render process exited with code " + exitCode + " " + status); + } + } + } + + Rectangle { + id: footer + anchors.bottom: parent.bottom + width: parent.width + height: 48 + color: backgroundColour + + RowLayout { + id: layout + anchors.fill: parent + visible: virtualstudio.serverModel.length > 0 + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + + Button { + id: backButton + anchors.centerIn: parent + width: 180 * virtualstudio.uiScale + height: 36 * virtualstudio.uiScale + background: Rectangle { + radius: 8 * virtualstudio.uiScale + color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour) + } + onClicked: virtualstudio.windowState = "browse" + + Text { + text: "Back to Studios" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + } + } + + Rectangle { + id: backgroundBorder + width: parent.width + height: 1 + y: parent.height - footer.height + color: buttonStroke + } + } +} diff --git a/src/vs/DeviceControls.qml b/src/vs/DeviceControls.qml new file mode 100644 index 0000000..38e1745 --- /dev/null +++ b/src/vs/DeviceControls.qml @@ -0,0 +1,197 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects + +Item { + width: parent.width + height: parent.height + clip: true + + required property bool isInput + property string muteButtonMutedColor: "#FCB6B6" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#D5D9D9" + + Component { + id: controlIndicator + + Button { + id: iconButton + width: 24 * virtualstudio.uiScale + height: 24 * virtualstudio.uiScale + + anchors.left: parent.left + anchors.leftMargin: 8 * virtualstudio.uiScale + + background: Rectangle { + color: isInput ? (audio.inputMuted ? muteButtonMutedColor : buttonColour) : "transparent" + width: 24 * virtualstudio.uiScale + radius: 4 * virtualstudio.uiScale + } + + onClicked: isInput ? audio.inputMuted = !audio.inputMuted : console.log() + + AppIcon { + id: iconImage + anchors.centerIn: parent + width: 24 * virtualstudio.uiScale + height: 24 * virtualstudio.uiScale + icon.source: isInput ? (audio.inputMuted ? "micoff.svg" : "mic.svg") : "headphones.svg" + color: isInput ? (audio.inputMuted ? "red" : ( virtualstudio.darkMode ? "#CCCCCC" : "#333333" )) : (virtualstudio.darkMode ? "#CCCCCC" : "#333333") + onClicked: isInput ? audio.inputMuted = !audio.inputMuted : console.log() + } + + ToolTip { + visible: isInput && iconButton.hovered + x: iconButton.x + iconButton.width + y: iconButton.y + iconButton.height + + contentItem: Text { + text: audio.inputMuted ? qsTr("Click to unmute yourself") : qsTr("Click to mute yourself") + font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + background: Rectangle { + color: toolTipBackgroundColour + radius: 4 + layer.enabled: true + layer.effect: Glow { + color: "#66000000" + transparentBorder: true + } + } + } + } + } + + Component { + id: inputControls + + ColumnLayout { + anchors.fill: parent + spacing: 2 * virtualstudio.uiScale + + VolumeSlider { + Layout.fillWidth: true + Layout.fillHeight: true + labelText: "Send" + tooltipText: "How loudly other participants hear you" + sliderEnabled: !audio.inputMuted + } + + DeviceWarning { + id: deviceWarning + visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) + } + } + } + + Component { + id: outputControls + + ColumnLayout { + anchors.fill: parent + spacing: 4 * virtualstudio.uiScale + + VolumeSlider { + Layout.fillWidth: true + Layout.fillHeight: true + labelText: "Studio" + tooltipText: "How loudly you hear other participants" + sliderEnabled: true + } + + VolumeSlider { + Layout.fillWidth: true + Layout.fillHeight: true + labelText: "Monitor" + tooltipText: "How loudly you hear yourself" + sliderEnabled: true + } + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 5 * virtualstudio.uiScale + + Item { + Layout.topMargin: 5 * virtualstudio.uiScale + Layout.preferredHeight: 30 * virtualstudio.uiScale + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + spacing: 8 * virtualstudio.uiScale + + Item { + Layout.fillHeight: true + Layout.preferredWidth: 100 * virtualstudio.uiScale + + Loader { + id: typeIconIndicator + anchors.left: parent.left + sourceComponent: controlIndicator + } + + Text { + id: label + anchors.left: parent.left + anchors.leftMargin: 36 * virtualstudio.uiScale + + text: isInput ? "Input" : "Output" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + InfoTooltip { + content: isInput ? qsTr("Audio sent to the studio (microphone, instrument, mixer, etc.)") : qsTr("How you'll hear the studio audio") + size: 16 + anchors.left: label.right + anchors.leftMargin: 2 * virtualstudio.uiScale + anchors.verticalCenter: label.verticalCenter + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: 200 * virtualstudio.uiScale + + Meter { + anchors.fill: parent + anchors.rightMargin: 8 * virtualstudio.uiScale + model: isInput ? audio.inputMeterLevels : audio.outputMeterLevels + clipped: isInput ? audio.inputClipped : audio.outputClipped + enabled: true + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.bottomMargin: 5 * virtualstudio.uiScale + + RowLayout { + anchors.fill: parent + spacing: 8 * virtualstudio.uiScale + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.leftMargin: 8 * virtualstudio.uiScale + Layout.rightMargin: 8 * virtualstudio.uiScale + + Loader { + anchors.fill: parent + anchors.top: parent.top + sourceComponent: isInput ? inputControls : outputControls + } + } + } + } + } +} diff --git a/src/vs/DeviceControlsGroup.qml b/src/vs/DeviceControlsGroup.qml new file mode 100644 index 0000000..dc250c8 --- /dev/null +++ b/src/vs/DeviceControlsGroup.qml @@ -0,0 +1,383 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Rectangle { + property string disabledButtonText: "#D3D4D4" + property string saveButtonText: "#000000" + property int fullHeight: 88 * virtualstudio.uiScale + property int minimumHeight: 48 * virtualstudio.uiScale + + property bool isUsingRtAudio: audio.audioBackend == "RtAudio" + property bool isReady: virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready" + property bool showDeviceControls: getShowDeviceControls() + + id: deviceControlsGroup + width: parent.width + height: isReady ? (showDeviceControls ? fullHeight : (feedbackDetectedModal.visible ? minimumHeight : 0)) : minimumHeight; + color: backgroundColour + + function getShowDeviceControls () { + // self-managed servers do not support minified controls so keep it full size + return (!virtualstudio.currentStudio.isManaged && virtualstudio.currentStudio.sessionId === "") || (!virtualstudio.collapseDeviceControls && isReady); + } + + MouseArea { + anchors.fill: parent + propagateComposedEvents: false + } + + RowLayout { + id: layout + anchors.fill: parent + spacing: 2 + visible: !feedbackDetectedModal.visible + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + visible: !isReady + + Button { + id: backButton + anchors.centerIn: parent + width: 180 * virtualstudio.uiScale + height: 36 * virtualstudio.uiScale + background: Rectangle { + radius: 8 * virtualstudio.uiScale + color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour) + } + onClicked: virtualstudio.disconnect() + + Text { + text: "Back to Studios" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + visible: showDeviceControls + + DeviceControls { + isInput: true + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + visible: showDeviceControls + + DeviceControls { + isInput: false + } + } + + Item { + Layout.fillHeight: true + Layout.preferredWidth: 48 * virtualstudio.uiScale + visible: showDeviceControls + + ColumnLayout { + anchors.fill: parent + spacing: 2 + + Item { + Layout.preferredHeight: 24 * virtualstudio.uiScale + Layout.preferredWidth: 24 * virtualstudio.uiScale + Layout.topMargin: 2 * virtualstudio.uiScale + Layout.rightMargin: 2 * virtualstudio.uiScale + Layout.alignment: Qt.AlignRight | Qt.AlignTop + + Button { + id: closeDeviceControlsButton + visible: virtualstudio.currentStudio.isManaged || virtualstudio.currentStudio.sessionId !== "" + width: 24 * virtualstudio.uiScale + height: 24 * virtualstudio.uiScale + background: Rectangle { + color: backgroundColour + } + anchors.top: parent.top + anchors.right: parent.right + onClicked: { + virtualstudio.collapseDeviceControls = true; + } + + AppIcon { + id: closeDeviceControlsIcon + anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } + width: 24 * virtualstudio.uiScale + height: 24 * virtualstudio.uiScale + color: closeDeviceControlsButton.hovered ? textColour : browserButtonHoverColour + icon.source: "close.svg" + onClicked: { + virtualstudio.collapseDeviceControls = true; + } + } + } + } + + Item { + visible: isUsingRtAudio + Layout.preferredWidth: 40 * virtualstudio.uiScale + Layout.preferredHeight: 64 * virtualstudio.uiScale + Layout.bottomMargin: 5 * virtualstudio.uiScale + Layout.topMargin: 2 * virtualstudio.uiScale + Layout.rightMargin: 2 * virtualstudio.uiScale + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + + Button { + id: changeDevicesButton + width: 36 * virtualstudio.uiScale + height: 36 * virtualstudio.uiScale + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + background: Rectangle { + radius: 8 * virtualstudio.uiScale + color: changeDevicesButton.down ? browserButtonPressedColour : (changeDevicesButton.hovered ? browserButtonHoverColour : browserButtonColour) + } + onClicked: { + virtualstudio.windowState = "change_devices" + if (!audio.deviceModelsInitialized) { + audio.refreshDevices(); + } + } + + AppIcon { + id: changeDevicesIcon + anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } + width: 20 * virtualstudio.uiScale + height: 20 * virtualstudio.uiScale + icon.source: "cog.svg" + onClicked: { + virtualstudio.windowState = "change_devices" + if (!audio.deviceModelsInitialized) { + audio.refreshDevices(); + } + } + } + } + + Text { + anchors.top: changeDevicesButton.bottom + text: "Devices" + font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale} + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + } + } + } + + Rectangle { + id: backgroundBorder + width: parent.width + height: 1 + anchors.top: layout.top + color: strokeColor + } + + Popup { + id: feedbackDetectedModal + padding: 1 + width: parent.width + height: parent.height + anchors.centerIn: parent + dim: false + modal: false + focus: true + closePolicy: Popup.NoAutoClose + + background: Rectangle { + anchors.fill: parent + color: "transparent" + border.width: 1 + border.color: buttonStroke + clip: true + } + + contentItem: Rectangle { + width: parent.width + height: 232 * virtualstudio.uiScale + color: backgroundColour + + Item { + id: feedbackDetectedContent + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: 16 * virtualstudio.uiScale + anchors.right: parent.right + + AppIcon { + id: feedbackWarningIcon + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 10 * virtualstudio.uiScale + width: 32 * virtualstudio.uiScale + height: 32 * virtualstudio.uiScale + icon.source: "warning.svg" + color: "#F21B1B" + visible: showDeviceControls + } + + AppIcon { + id: feedbackWarningIconMinified + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + height: 24 * virtualstudio.uiScale + width: 24 * virtualstudio.uiScale + icon.source: "warning.svg" + color: "#F21B1B" + visible: !showDeviceControls + } + + Text { + id: feedbackDetectedHeader + anchors.top: parent.top + anchors.topMargin: 10 * virtualstudio.uiScale + anchors.left: feedbackWarningIcon.right + anchors.leftMargin: 16 * virtualstudio.uiScale + width: parent.width + text: "Audio feedback detected!" + font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale; bold: true } + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + visible: showDeviceControls + } + + Text { + id: feedbackDetectedText + anchors.top: feedbackDetectedHeader.bottom + anchors.topMargin: 4 * virtualstudio.uiScale + anchors.left: feedbackWarningIcon.right + anchors.leftMargin: 16 * virtualstudio.uiScale + width: parent.width + text: "JackTrip detected a feedback loop. Your monitor and input volume have automatically been disabled." + font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + visible: showDeviceControls + } + + Text { + id: feedbackDetectedTextMinified + anchors.verticalCenter: parent.verticalCenter + anchors.left: feedbackWarningIcon.right + anchors.leftMargin: 16 * virtualstudio.uiScale + width: parent.width + text: "JackTrip detected a feedback loop. Your monitor and input volume have automatically been disabled." + font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + visible: !showDeviceControls + } + + Text { + id: feedbackDetectedText2 + anchors.top: feedbackDetectedText.bottom + anchors.topMargin: 2 * virtualstudio.uiScale + anchors.left: feedbackWarningIcon.right + anchors.leftMargin: 16 * virtualstudio.uiScale + width: parent.width + text: "You can disable this behavior under Settings > Advanced" + textFormat: Text.RichText + font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + visible: showDeviceControls + } + + Button { + id: closeFeedbackDetectedModalButton + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + width: 128 * virtualstudio.uiScale; + height: 30 * virtualstudio.uiScale + onClicked: feedbackDetectedModal.close() + + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour) + border.width: 1 + border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke) + } + + Text { + text: "Ok" + font.family: "Poppins" + font.pixelSize: showDeviceControls ? fontSmall * virtualstudio.fontScale * virtualstudio.uiScale : fontTiny * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + visible: showDeviceControls + } + + Button { + id: closeFeedbackDetectedModalButtonMinified + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + width: 80 * virtualstudio.uiScale + height: 24 * virtualstudio.uiScale + onClicked: feedbackDetectedModal.close() + + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour) + border.width: 1 + border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke) + } + + Text { + text: "Ok" + font.family: "Poppins" + font.pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + visible: !showDeviceControls + } + } + } + } + + Connections { + target: virtualstudio + + function onFeedbackDetected() { + if (virtualstudio.windowState === "connected") { + feedbackDetectedModal.visible = true; + } + } + + function onCollapseDeviceControlsChanged(collapseDeviceControls) { + showDeviceControls = getShowDeviceControls() + } + + function onCurrentStudioChanged(currentStudio) { + isReady = virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready" + showDeviceControls = getShowDeviceControls() + } + + function onConnectionStateChanged(connectionState) { + isReady = virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready" + showDeviceControls = getShowDeviceControls() + } + } +} \ No newline at end of file diff --git a/src/vs/DeviceRefreshButton.qml b/src/vs/DeviceRefreshButton.qml new file mode 100644 index 0000000..1c3ed65 --- /dev/null +++ b/src/vs/DeviceRefreshButton.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtQuick.Controls + +Button { + id: refreshButton + text: "Refresh Devices" + + property int fontExtraSmall: 8 + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property var onDeviceRefresh: function () { audio.refreshDevices(); }; + + width: 144 * virtualstudio.uiScale; + height: 30 * virtualstudio.uiScale + palette.buttonText: textColour + display: AbstractButton.TextBesideIcon + + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke) + } + icon { + source: "refresh.svg"; + color: textColour; + } + onClicked: { onDeviceRefresh(); } + font { + family: "Poppins" + pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale + } +} diff --git a/src/vs/DeviceWarning.qml b/src/vs/DeviceWarning.qml new file mode 100644 index 0000000..da00b20 --- /dev/null +++ b/src/vs/DeviceWarning.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Controls + +Item { + height: 28 * virtualstudio.uiScale + property string devicesWarningColour: "#F21B1B" + + function getTooltip () { + var result = ""; + if (Boolean(audio.devicesError)) { + result = audio.devicesError; + if (audio.devicesErrorHelpUrl) { + result += " Click for more info." + } + } else if (Boolean(audio.devicesWarning)) { + result = audio.devicesWarning; + if (audio.devicesWarningHelpUrl) { + result += " Click for more info." + } + } + return result; + } + + AppIcon { + id: devicesWarningIcon + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: parent.height + height: parent.height + icon.source: "warning.svg" + color: devicesWarningColour + visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) + } + + Text { + id: warningOrErrorText + text: Boolean(audio.devicesError) ? "Audio Configuration Error" : "Audio Configuration Warning" + anchors.left: devicesWarningIcon.right + anchors.leftMargin: 4 * virtualstudio.uiScale + anchors.verticalCenter: devicesWarningIcon.verticalCenter + visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) + font { family: "Poppins"; pixelSize: 9 * virtualstudio.fontScale * virtualstudio.uiScale } + color: devicesWarningColour + } + + InfoTooltip { + id: devicesWarningTooltip + anchors.left: warningOrErrorText.right + anchors.leftMargin: 2 * virtualstudio.uiScale + anchors.top: devicesWarningIcon.top + content: getTooltip() + iconColor: devicesWarningColour + size: 16 * virtualstudio.uiScale + visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) + } + + MouseArea { + id: devicesWarningToolTipArea + anchors.top: devicesWarningIcon.top + anchors.bottom: devicesWarningIcon.bottom + anchors.left: devicesWarningIcon.left + anchors.right: devicesWarningTooltip.right + hoverEnabled: true + onEntered: devicesWarningTooltip.showToolTip = true + onExited: devicesWarningTooltip.showToolTip = false + onClicked: { + if (Boolean(audio.devicesError) && audio.devicesErrorHelpUrl !== "") { + virtualstudio.openLink(audio.devicesErrorHelpUrl); + } else if (Boolean(audio.devicesWarning) && audio.devicesWarningHelpUrl !== "") { + virtualstudio.openLink(audio.devicesWarningHelpUrl); + } + } + } +} diff --git a/src/vs/DeviceWarningModal.qml b/src/vs/DeviceWarningModal.qml new file mode 100644 index 0000000..f56dfa7 --- /dev/null +++ b/src/vs/DeviceWarningModal.qml @@ -0,0 +1,164 @@ +import QtQuick +import QtQuick.Controls + +Item { + anchors.centerIn: parent + width: 480 * virtualstudio.uiScale + + property int fontMedium: 12 + property int fontSmall: 10 + + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string devicesWarningColour: "#F21B1B" + + Popup { + id: deviceWarningPopup + padding: 1 + width: parent.width + height: 350 * virtualstudio.uiScale + anchors.centerIn: parent + modal: true + focus: true + + background: Rectangle { + anchors.fill: parent + color: "transparent" + radius: 6 * virtualstudio.uiScale + border.width: 1 + border.color: buttonStroke + clip: true + } + + contentItem: Rectangle { + width: parent.width + height: parent.height + color: backgroundColour + radius: 6 * virtualstudio.uiScale + + Item { + id: deviceWarningPopupContent + anchors.top: parent.top + anchors.topMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + AppIcon { + id: devicesWarningIcon + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + width: 56 * virtualstudio.uiScale + height: 56 * virtualstudio.uiScale + icon.source: "warning.svg" + color: devicesWarningColour + } + + Text { + id: deviceWarningPopupHeader + anchors.top: devicesWarningIcon.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + width: parent.width + text: "Audio Configuration Warning" + font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true } + horizontalAlignment: Text.AlignHCenter + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + Text { + id: devicesWarningText + anchors.top: deviceWarningPopupHeader.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - (32 * virtualstudio.uiScale) + text: qsTr(audio.devicesWarning) + font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + horizontalAlignment: Text.AlignHCenter + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + LearnMoreButton { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: devicesWarningText.bottom + anchors.topMargin: 24 * virtualstudio.uiScale + url: Boolean(audio.devicesErrorHelpUrl) ? audio.devicesErrorHelpUrl : audio.devicesWarningHelpUrl + visible: Boolean(audio.devicesErrorHelpUrl) || Boolean(audio.devicesWarningHelpUrl) + } + + Button { + id: backButton + anchors.left: parent.left + anchors.leftMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.bottomMargin: 24 * virtualstudio.uiScale + width: 160 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + onClicked: () => { + deviceWarningPopup.close(); + } + + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: backButton.down ? buttonPressedStroke : (backButton.hovered ? buttonHoverStroke : buttonStroke) + } + + Text { + text: "Back to Settings" + font.family: "Poppins" + font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + Button { + id: connectButton + anchors.right: parent.right + anchors.rightMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.bottomMargin: 24 * virtualstudio.uiScale + width: 160 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + onClicked: () => { + deviceWarningPopup.close(); + audio.stopAudio(true); + virtualstudio.studioToJoin = virtualstudio.currentStudio.id; + virtualstudio.windowState = "connected"; + virtualstudio.saveSettings(); + virtualstudio.joinStudio(); + } + + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: connectButton.down ? buttonPressedColour : (connectButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: connectButton.down ? buttonPressedStroke : (connectButton.hovered ? buttonHoverStroke : buttonStroke) + } + + Text { + text: "Connect to Session" + font.family: "Poppins" + font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + function open () { + deviceWarningPopup.open(); + } +} diff --git a/src/vs/Failed.qml b/src/vs/Failed.qml new file mode 100644 index 0000000..956406d --- /dev/null +++ b/src/vs/Failed.qml @@ -0,0 +1,79 @@ +import QtQuick +import QtQuick.Controls + +Item { + width: parent.width; height: parent.height + clip: true + + property int leftMargin: 16 + property int fontBig: 28 + property int fontMedium: 18 + property int fontSmall: 11 + + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1" + property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5" + property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5" + property string buttonStroke: virtualstudio.darkMode ? "#9C9C9C" : "#A4A7A7" + property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A" + property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A" + property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A" + + AppIcon { + id: ohnoImage + y: 60 + anchors.horizontalCenter: parent.horizontalCenter + width: 180 + height: 180 + icon.source: "sentiment_very_dissatisfied.svg" + } + + Text { + id: ohnoHeader + text: "Oh no!" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: ohnoImage.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + } + + Text { + id: ohnoMessage + text: virtualstudio.failedMessage || "Unable to process request - please try again later." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 400 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: ohnoHeader.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Button { + id: backButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour) + border.width: backButton.down ? 1 : 0 + border.color: buttonStroke + layer.enabled: !backButton.down + } + onClicked: { virtualstudio.windowState = "browse" } + width: 256 * virtualstudio.uiScale + height: 42 * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: ohnoMessage.bottom + anchors.topMargin: 60 * virtualstudio.uiScale + Text { + text: "Back" + font.family: "Poppins" + font.pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + color: backButton.down ? buttonTextPressed : (backButton.hovered ? buttonTextHover : buttonTextColour) + } + visible: true + } +} diff --git a/src/vs/FeedbackSurvey.qml b/src/vs/FeedbackSurvey.qml new file mode 100644 index 0000000..05731f3 --- /dev/null +++ b/src/vs/FeedbackSurvey.qml @@ -0,0 +1,421 @@ +import QtQuick +import QtQuick.Controls + +Item { + id: userFeedbackSurvey + + anchors.centerIn: parent + width: 480 * virtualstudio.uiScale + height: 232 * virtualstudio.uiScale + + property int leftHeaderMargin: 16 + property int fontBig: 28 + property int fontMedium: 12 + property int fontSmall: 10 + property int fontTiny: 8 + property int bodyMargin: 60 + property int rightMargin: 16 + property int bottomToolTipMargin: 8 + property int rightToolTipMargin: 4 + + property string buttonColour: "#F2F3F3" + property string buttonHoverColour: "#E7E8E8" + property string buttonPressedColour: "#E7E8E8" + property string buttonStroke: "#EAEBEB" + property string buttonHoverStroke: "#B0B5B5" + property string buttonPressedStroke: "#B0B5B5" + + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string textAreaTextColour: virtualstudio.darkMode ? "#A6A6A6" : "#757575" + property string textAreaColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + + property string serverId: "" + + property int rating: 0 + property int hover: star1MouseArea.containsMouse ? 1 : star2MouseArea.containsMouse ? 2 : star3MouseArea.containsMouse ? 3 : star4MouseArea.containsMouse ? 4 : star5MouseArea.containsMouse ? 5 : 0 + property int currentView: (hover > 0 ? hover : rating) + property bool submitted: false; + + property string message: "" + + Popup { + id: userFeedbackModal + padding: 1 + width: parent.width + height: 300 * virtualstudio.uiScale + anchors.centerIn: parent + modal: true + focus: true + closePolicy: Popup.NoAutoClose + + background: Rectangle { + anchors.fill: parent + color: "transparent" + radius: 6 * virtualstudio.uiScale + border.width: 1 + border.color: buttonStroke + clip: true + } + + contentItem: Rectangle { + width: parent.width + height: parent.height + color: backgroundColour + radius: 6 * virtualstudio.uiScale + + Item { + id: userFeedbackSurveyContent + anchors.top: parent.top + anchors.topMargin: 24 * virtualstudio.uiScale + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + visible: !submitted + + Text { + id: userFeedbackSurveyHeader + anchors.top: parent.top + anchors.topMargin: 16 * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + text: "How did your session go?" + font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true } + horizontalAlignment: Text.AlignHCenter + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + Text { + id: ratingItemInstructions + anchors.top: userFeedbackSurveyHeader.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + text: "Rate your session on a scale of 1 to 5" + font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + horizontalAlignment: Text.AlignHCenter + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + Item { + id: ratingItem + anchors.top: ratingItemInstructions.bottom + anchors.topMargin: 4 * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + height: 40 * virtualstudio.uiScale + width: 200 * virtualstudio.uiScale + + Item { + id: star1Container + anchors.verticalCenter: parent.verticalCenter + anchors.right: star2Container.left + width: parent.width / 5 + height: parent.height + + AppIcon { + id: star1Icon + anchors.centerIn: parent + width: currentView >= 1 ? parent.width : 20 * virtualstudio.uiScale + height: currentView >= 1 ? parent.height : 20 * virtualstudio.uiScale + icon.source: "star.svg" + color: currentView >= 1 ? "#faaf00" : "#606060" + } + + MouseArea { + id: star1MouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: () => { + if (rating === 1) { + rating = 0; + } else { + rating = 1; + } + } + } + } + + Item { + id: star2Container + anchors.verticalCenter: parent.verticalCenter + anchors.right: star3Container.left + width: parent.width / 5 + height: parent.height + + AppIcon { + id: star2Icon + anchors.centerIn: parent + width: currentView >= 2 ? parent.width : 20 * virtualstudio.uiScale + height: currentView >= 2 ? parent.height : 20 * virtualstudio.uiScale + icon.source: "star.svg" + color: currentView >= 2 ? "#faaf00" : "#606060" + } + + MouseArea { + id: star2MouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: () => { + if (rating === 2) { + rating = 0; + } else { + rating = 2; + } + } + } + } + + Item { + id: star3Container + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width / 5 + height: parent.height + + AppIcon { + id: star3Icon + anchors.centerIn: parent + width: currentView >= 3 ? parent.width : 20 * virtualstudio.uiScale + height: currentView >= 3 ? parent.height : 20 * virtualstudio.uiScale + icon.source: "star.svg" + color: currentView >= 3 ? "#faaf00" : "#606060" + } + + MouseArea { + id: star3MouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: () => { + if (rating === 3) { + rating = 0; + } else { + rating = 3; + } + } + } + } + + Item { + id: star4Container + anchors.verticalCenter: parent.verticalCenter + anchors.left: star3Container.right + width: parent.width / 5 + height: parent.height + + AppIcon { + id: star4Icon + anchors.centerIn: parent + width: currentView >= 4 ? parent.width : 20 * virtualstudio.uiScale + height: currentView >= 4 ? parent.height : 20 * virtualstudio.uiScale + icon.source: "star.svg" + color: currentView >= 4 ? "#faaf00" : "#606060" + } + + MouseArea { + id: star4MouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: () => { + if (rating === 4) { + rating = 0; + } else { + rating = 4; + } + } + } + } + + Item { + id: star5Container + anchors.verticalCenter: parent.verticalCenter + anchors.left: star4Container.right + width: parent.width / 5 + height: parent.height + + AppIcon { + id: star5Icon + anchors.centerIn: parent + width: currentView >= 5 ? parent.width : 20 * virtualstudio.uiScale + height: currentView >= 5 ? parent.height : 20 * virtualstudio.uiScale + icon.source: "star.svg" + color: currentView >= 5 ? "#faaf00" : "#606060" + } + + MouseArea { + id: star5MouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: () => { + if (rating === 5) { + rating = 0; + } else { + rating = 5; + } + } + } + } + } + + ScrollView { + id: messageBoxScrollArea + anchors.left: parent.left + anchors.leftMargin: 32 * virtualstudio.uiScale + anchors.right: parent.right + anchors.rightMargin: 32 * virtualstudio.uiScale + anchors.top: ratingItem.bottom + anchors.topMargin: 12 * virtualstudio.uiScale + height: 64 * virtualstudio.uiScale + + TextArea { + id: messageBox + placeholderText: qsTr("(Optional) Let us know how we can improve your experience.") + placeholderTextColor: textAreaTextColour + color: textColour + background: Rectangle { + color: textAreaColour + radius: 6 * virtualstudio.uiScale + border.width: 1 + border.color: buttonStroke + } + } + } + + Item { + id: buttonsArea + height: 32 * virtualstudio.uiScale + width: 324 * virtualstudio.uiScale + anchors.horizontalCenter: messageBoxScrollArea.horizontalCenter + anchors.top: messageBoxScrollArea.bottom + anchors.topMargin: 24 * virtualstudio.uiScale + + Button { + id: userFeedbackButton + anchors.right: buttonsArea.right + anchors.horizontalCenter: buttonsArea.horizontalCenter + anchors.verticalCenter: parent.buttonsArea + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + onClicked: () => { + if (rating === 0 && messageBox.text === "") { + userFeedbackModal.close(); + serverId = ""; + messageBox.clear(); + return; + } + virtualstudio.collectFeedbackSurvey(serverId, rating, messageBox.text); + submitted = true; + rating = 0; + serverId = ""; + messageBox.clear(); + userFeedbackModal.height = 150 * virtualstudio.uiScale + submittedFeedbackTimer.start(); + } + + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: userFeedbackButton.down ? buttonPressedColour : (userFeedbackButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: userFeedbackButton.down ? buttonPressedStroke : (userFeedbackButton.hovered ? buttonHoverStroke : buttonStroke) + } + + Text { + text: (rating === 0 && messageBox.text === "") ? "Dismiss" : "Submit" + font.family: "Poppins" + font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: "#000000" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + Timer { + id: submittedFeedbackTimer + interval: 5000; running: false; repeat: false + onTriggered: () => { + submitted = false; + userFeedbackModal.height = 300 * virtualstudio.uiScale + userFeedbackModal.close(); + } + } + } + } + + Item { + id: submittedFeedbackContent + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + visible: submitted + + Text { + id: submittedFeedbackHeader + anchors.top: submittedFeedbackContent.top + anchors.topMargin: 24 * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + text: "Thank you!" + font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true } + horizontalAlignment: Text.AlignHCenter + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + Text { + id: submittedFeedbackText + anchors.top: submittedFeedbackHeader.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + text: "Your feedback has been recorded." + font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + horizontalAlignment: Text.AlignHCenter + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + Button { + id: closeButtonFeedback + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: submittedFeedbackText.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + onClicked: () => { + submitted = false; + userFeedbackModal.height = 300 * virtualstudio.uiScale + userFeedbackModal.close(); + } + + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: closeButtonFeedback.down ? buttonPressedColour : (closeButtonFeedback.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: closeButtonFeedback.down ? buttonPressedStroke : (closeButtonFeedback.hovered ? buttonHoverStroke : buttonStroke) + } + + Text { + text: "Close" + font.family: "Poppins" + font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + Connections { + target: virtualstudio + + function onOpenFeedbackSurveyModal(serverId) { + userFeedbackSurvey.serverId = serverId; + userFeedbackModal.open(); + } + } +} diff --git a/src/vs/FirstLaunch.qml b/src/vs/FirstLaunch.qml new file mode 100644 index 0000000..e5623b5 --- /dev/null +++ b/src/vs/FirstLaunch.qml @@ -0,0 +1,128 @@ +import QtQuick +import QtQuick.Controls + +Item { + width: parent.width; height: parent.height + clip: true + + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1" + property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1" + property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5" + property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5" + property string buttonStroke: virtualstudio.darkMode ? "#636060" : "#DEDFDF" + property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" + property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" + + Image { + id: jtlogo + source: "logo.svg" + anchors.horizontalCenter: parent.horizontalCenter + y: 35 * virtualstudio.uiScale + width: 50 * virtualstudio.uiScale; height: 92 * virtualstudio.uiScale + sourceSize: Qt.size(jtlogo.width,jtlogo.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + y: 168 * virtualstudio.uiScale + text: "Sign in with a Virtual Studio account?" + font.family: "Poppins" + font.pixelSize: 17 * virtualstudio.fontScale * virtualstudio.uiScale + color: textColour + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + y: 219 * virtualstudio.uiScale + text: "You'll be able to change your mind later" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + color: textColour + } + + Button { + id: vsButton + background: Rectangle { + radius: 10 * virtualstudio.uiScale + color: vsButton.down ? buttonPressedColour : (vsButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: vsButton.down ? buttonPressedStroke : (vsButton.hovered ? buttonHoverStroke : buttonStroke) + layer.enabled: vsButton.hovered && !vsButton.down + } + onClicked: { virtualstudio.toVirtualStudioMode(); } + x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale + width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale + Text { + text: "Yes" + font.family: "Poppins" + font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: "#000000" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + Text { + text: "• Seamless Audio & Video
• Recording & Livestreaming
• No Servers Required" + textFormat: Text.StyledText + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + x: parent.width / 2 - (265 * virtualstudio.uiScale); + y: 355 * virtualstudio.uiScale; + width: 230 * virtualstudio.uiScale + padding: 0 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: textColour + } + Image { + source: "JTVS.png" + x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale + width: 234 * virtualstudio.uiScale; height: 195 * virtualstudio.uiScale; + } + + Button { + id: standardButton + background: Rectangle { + radius: 10 * virtualstudio.uiScale + color: standardButton.down ? buttonPressedColour : (standardButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: standardButton.down ? buttonPressedStroke : (standardButton.hovered ? buttonHoverStroke : buttonStroke) + layer.enabled: standardButton.hovered && !standardButton.down + } + onClicked: { virtualstudio.toClassicMode(); } + x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale + width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale + Text { + text: "No" + font.family: "Poppins" + font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: "#000000" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + Image { + source: "JTOriginal.png" + x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale + width: 234 * virtualstudio.uiScale; height: 337.37 * virtualstudio.uiScale; + } + Text { + text: virtualstudio.psiBuild ? "• Connect via IP address
• Run a local hub server
• The Standard JackTrip experience" : + "• Connect via IP address
• Run a local hub server
• The Classic JackTrip experience" + textFormat: Text.StyledText + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + x: parent.width / 2 + (32 * virtualstudio.uiScale); + y: 355 * virtualstudio.uiScale; + width: 230 * virtualstudio.uiScale + padding: 0 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: textColour + } +} diff --git a/src/vs/Footer.qml b/src/vs/Footer.qml new file mode 100644 index 0000000..18f8292 --- /dev/null +++ b/src/vs/Footer.qml @@ -0,0 +1,180 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Rectangle { + id: footer + width: parent.width + height: 24 + anchors.bottom: parent.bottom + color: backgroundColour + clip: true + + property string statsOrange: "#b26a00" + property string connectionStateColor: getConnectionStateColor() + property variant networkStatsText: getNetworkStatsText() + + function getConnectionStateColor() { + if (virtualstudio.connectionState == "Connected") { + return meterGreen + } + if (virtualstudio.connectionState.includes("Disconnected") || virtualstudio.connectionState.includes("Error")) { + return meterRed + } + if (studioStatus === "Starting" || virtualstudio.connectionState == "Connecting..." || virtualstudio.connectionState == "Reconnecting...") { + return meterYellow + } + return "grey" + } + + function getNetworkStatsText() { + let minRtt = virtualstudio.networkStats.minRtt; + let maxRtt = virtualstudio.networkStats.maxRtt; + let avgRtt = virtualstudio.networkStats.avgRtt; + + let texts = ["Unstable", "Please plug into Ethernet & turn off WIFI.", meterRed]; + if (virtualstudio.networkOutage) { + return texts; + } + + texts = ["Measuring...", "", "grey"]; + if (!minRtt || !maxRtt) { + return texts; + } + + texts[1] = "" + minRtt + " ms - " + maxRtt + " ms, avg " + avgRtt + " ms"; + let quality = "Poor"; + let color = meterRed; + if (avgRtt < 10 && maxRtt < 15) { + quality = "Excellent"; + color = meterGreen; + } else if (avgRtt < 20 && maxRtt < 30) { + quality = "Good"; + color = meterYellow; + } else if (avgRtt < 30 && maxRtt < 40) { + quality = "Fair"; + color = statsOrange; + } + + texts[0] = quality + texts[2] = color; + return texts; + } + + MouseArea { + anchors.fill: parent + propagateComposedEvents: false + } + + RowLayout { + id: layout + anchors.fill: parent + spacing: 4 + + Rectangle { + color: backgroundColour + Layout.minimumWidth: 256 + Layout.preferredWidth: 512 + Layout.maximumWidth: 640 + Layout.fillHeight: true + Layout.fillWidth: true + visible: studioStatus === "Ready" + + AppIcon { + id: connectionQualityIcon + anchors.left: parent.left + anchors.leftMargin: 8 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + width: 20 * virtualstudio.uiScale + height: 20 * virtualstudio.uiScale + icon.source: "speed.svg" + } + + Text { + id: connectionQualityText + anchors.left: connectionQualityIcon.right + anchors.leftMargin: 4 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + text: "Connection:" + font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Text { + id: connectionQualityName + anchors.left: connectionQualityText.right + anchors.leftMargin: 2 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + text: networkStatsText[0] + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: networkStatsText[2] + } + + Text { + id: connectionQualityTime + anchors.left: connectionQualityName.right + anchors.leftMargin: 8 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + text: networkStatsText[1] + font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + + Rectangle { + color: backgroundColour + Layout.minimumWidth: 96 + Layout.preferredWidth: 128 + Layout.maximumWidth: 160 + Layout.fillHeight: true + + Rectangle { + id: connectionStatusDot + anchors.right: connectionStatusText.left + anchors.rightMargin: 4 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + width: 12 + height: connectionStatusDot.width + radius: connectionStatusDot.height / 2 + color: connectionStateColor + } + + Text { + id: connectionStatusText + anchors.right: parent.right + anchors.rightMargin: 8 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + text: studioStatus === "Starting" ? "Starting..." : virtualstudio.connectionState + font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + } + } + + Rectangle { + id: backgroundBorder + width: parent.width + height: 1 + y: parent.height - footer.height + color: buttonStroke + } + + Connections { + target: virtualstudio + + function onConnectionStateChanged() { + connectionStatusDot.color = getConnectionStateColor() + } + function onNetworkStatsChanged() { + networkStatsText = getNetworkStatsText(); + } + function onUpdatedNetworkOutage() { + networkStatsText = getNetworkStatsText(); + } + } +} diff --git a/src/vs/InfoTooltip.qml b/src/vs/InfoTooltip.qml new file mode 100644 index 0000000..5d5c95e --- /dev/null +++ b/src/vs/InfoTooltip.qml @@ -0,0 +1,63 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects + +Item { + required property string content + required property int size + + width: size * virtualstudio.uiScale + height: size * virtualstudio.uiScale + + property string iconSource: "help.svg" + property string iconColor: "" + property string backgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" + property bool showToolTip: false + + Item { + anchors.fill: parent + + AppIcon { + id: tooltipIcon + anchors.centerIn: parent + width: parent.width + height: parent.height + icon.source: iconSource + color: iconColor + } + + MouseArea { + id: mouseArea + anchors.fill: tooltipIcon + hoverEnabled: true + onEntered: showToolTip = true + onExited: showToolTip = false + } + + ToolTip { + visible: showToolTip + x: tooltipIcon.x + tooltipIcon.width + y: tooltipIcon.y + tooltipIcon.height + + contentItem: Text { + text: content + font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + background: Rectangle { + color: backgroundColour + radius: 4 + layer.enabled: true + layer.effect: Glow { + radius: 8 + color: "#66000000" + transparentBorder: true + } + } + } + } +} diff --git a/src/vs/JTApplication.h b/src/vs/JTApplication.h new file mode 100644 index 0000000..8b62c2e --- /dev/null +++ b/src/vs/JTApplication.h @@ -0,0 +1,65 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file JTApplication.h + * \author Matt Hortoon + * \date July 2022 + */ + +#ifndef JTAPPLICATION_H +#define JTAPPLICATION_H + +#include +#include +#include +#include +#include +#include + +class JTApplication : public QApplication +{ + Q_OBJECT + + public: + JTApplication(int& argc, char** argv) : QApplication(argc, argv) {} + + bool event(QEvent* event) override + { + if (event->type() == QEvent::FileOpen) { + QFileOpenEvent* openEvent = static_cast(event); + + QDesktopServices::openUrl(openEvent->url()); + } + return QApplication::event(event); + } +}; + +#endif // JTAPPLICATION_H diff --git a/src/vs/JTOriginal.png b/src/vs/JTOriginal.png new file mode 100644 index 0000000..a6c92cd Binary files /dev/null and b/src/vs/JTOriginal.png differ diff --git a/src/vs/JTVS.png b/src/vs/JTVS.png new file mode 100644 index 0000000..05573f2 Binary files /dev/null and b/src/vs/JTVS.png differ diff --git a/src/vs/LearnMoreButton.qml b/src/vs/LearnMoreButton.qml new file mode 100644 index 0000000..e9a4a78 --- /dev/null +++ b/src/vs/LearnMoreButton.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtQuick.Controls + +Button { + property string url + property string buttonText: "Learn more" + + width: 150 * virtualstudio.uiScale; + height: 30 * virtualstudio.uiScale + + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + + onClicked: { + virtualstudio.openLink(url); + } + + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: parent.down ? buttonPressedColour : (parent.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: parent.down ? buttonPressedStroke : (parent.hovered ? buttonHoverStroke : buttonStroke) + } + + Text { + text: buttonText + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } +} diff --git a/src/vs/Login.qml b/src/vs/Login.qml new file mode 100644 index 0000000..7ca4dd0 --- /dev/null +++ b/src/vs/Login.qml @@ -0,0 +1,357 @@ +import QtQuick +import QtQuick.Controls +import VS 1.0 + +Item { + width: parent.width; height: parent.height + clip: true + + state: auth.authenticationStage + states: [ + State { + name: "unauthenticated" + }, + State { + name: "refreshing" + }, + State { + name: "polling" + }, + State { + name: "success" + }, + State { + name: "failed" + } + ] + + Rectangle { + width: parent.width; height: parent.height + color: backgroundColour + } + + property bool codeCopied: false + property int numFailures: 0; + + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1" + property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5" + property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" + property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5" + property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A" + property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A" + property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A" + property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1" + property string linkTextColour: virtualstudio.darkMode ? "#8B8D8D" : "#272525" + property string toolTipTextColour: codeCopied ? "#FAFBFB" : textColour + property string toolTipBackgroundColour: codeCopied ? "#57B147" : (virtualstudio.darkMode ? "#323232" : "#F3F3F3") + property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string disabledButtonText: "#D3D4D4" + property string errorTextColour: "#DB0A0A" + + property bool showCodeFlow: (loginScreen.state === "unauthenticated" && !auth.attemptingRefreshToken) || (loginScreen.state === "polling" || loginScreen.state === "failed" || (loginScreen.state === "success" && auth.authenticationMethod === "code flow")) + property bool showLoading: (loginScreen.state === "unauthenticated" ** auth.attemptingRefreshToken) || loginScreen.state === "refreshing" || (loginScreen.state === "success" && auth.authenticationMethod === "refresh token") + + Clipboard { + id: clipboard + } + + function getVerificationCodeText() { + if (Boolean(auth.verificationCode)) { + return auth.verificationCode; + } + if (numFailures < 5) { + return "Loading..."; + } + var result; + if (auth.errorMessage.startsWith("Host") && auth.errorMessage.endsWith("not found")) { + result = "Your Internet connection is offline."; + } else { + result = "There was an error trying to sign in."; + } + result += "
Please try again."; + return result; + } + + Item { + id: loginScreenHeader + anchors.horizontalCenter: parent.horizontalCenter + y: showCodeFlow ? 48 * virtualstudio.uiScale : 144 * virtualstudio.uiScale + + Image { + id: loginLogo + source: "logo.svg" + x: parent.width / 2 - (150 * virtualstudio.uiScale); + width: 42 * virtualstudio.uiScale; height: 76 * virtualstudio.uiScale + sourceSize: Qt.size(loginLogo.width,loginLogo.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + + Image { + source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png" + anchors.bottom: loginLogo.bottom + x: parent.width / 2 - (88 * virtualstudio.uiScale) + width: 238 * virtualstudio.uiScale; height: 56 * virtualstudio.uiScale + } + + Text { + text: "Virtual Studio" + font.family: "Poppins" + font.pixelSize: 24 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + y: 80 * virtualstudio.uiScale + color: textColour + } + } + + Item { + id: codeFlow + anchors.horizontalCenter: parent.horizontalCenter + y: 68 * virtualstudio.uiScale + height: parent.height - codeFlow.y + visible: showCodeFlow + width: parent.width + + Text { + id: deviceVerificationExplanation + text: `To get started, please sign in and confirm the following code using your web browser. Return here when you are done.` + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + y: 128 * virtualstudio.uiScale + width: 500 * virtualstudio.uiScale; + visible: Boolean(auth.verificationCode) + color: textColour + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + textFormat: Text.RichText + onLinkActivated: link => { + if (!Boolean(auth.verificationCode)) { + return; + } + virtualstudio.openLink(link) + } + } + + AppIcon { + id: successIcon + y: 224 * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + width: 96 * virtualstudio.uiScale + height: 96 * virtualstudio.uiScale + icon.source: "check.svg" + color: "green" + visible: loginScreen.state === "success" + } + + Text { + id: deviceVerificationCode + text: getVerificationCodeText() + font.family: "Poppins" + font.pixelSize: 20 * virtualstudio.fontScale * virtualstudio.uiScale + font.letterSpacing: Boolean(auth.verificationCode) ? 8 : 1 + anchors.horizontalCenter: parent.horizontalCenter + y: 196 * virtualstudio.uiScale + width: 540 * virtualstudio.uiScale; + visible: !auth.isAuthenticated + color: Boolean(auth.verificationCode) ? textColour : disabledButtonText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + + Timer { + id: copiedResetTimer + interval: 2000; running: false; repeat: false + onTriggered: codeCopied = false; + } + + MouseArea { + id: deviceVerificationCodeMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: Boolean(auth.verificationCode) + hoverEnabled: true + onClicked: () => { + codeCopied = true; + clipboard.setText(auth.verificationCode); + copiedResetTimer.restart() + } + } + + ToolTip { + parent: deviceVerificationCode + visible: loginScreen.state === "polling" && deviceVerificationCodeMouseArea.containsMouse + delay: 100 + contentItem: Rectangle { + color: toolTipBackgroundColour + radius: 3 + anchors.fill: parent + layer.enabled: true + border.width: 1 + border.color: tooltipStroke + + Text { + anchors.centerIn: parent + font { family: "Poppins"; pixelSize: 8 * virtualstudio.fontScale * virtualstudio.uiScale} + text: codeCopied ? qsTr("📋 Copied code to clipboard") : qsTr("📋 Copy code to Clipboard") + color: toolTipTextColour + } + } + background: Rectangle { + color: "transparent" + } + } + } + + Button { + id: loginButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: loginButton.down ? buttonPressedColour : (loginButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: loginButton.down ? buttonPressedStroke : (loginButton.hovered ? buttonHoverStroke : buttonStroke) + layer.enabled: !loginButton.down + } + onClicked: { + if (auth.verificationCode && auth.verificationUrl) { + virtualstudio.openLink(auth.verificationUrl); + } + } + anchors.horizontalCenter: parent.horizontalCenter + y: 260 * virtualstudio.uiScale + width: 263 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale + Text { + text: "Sign In" + font.family: "Poppins" + font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + color: loginButton.down ? buttonTextPressed : (loginButton.hovered ? buttonTextHover : buttonTextColour) + } + visible: !auth.isAuthenticated && Boolean(auth.verificationCode) + } + + Text { + id: authFailedText + text: auth.errorMessage + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: loginScreenFooter.top + anchors.bottomMargin: 16 * virtualstudio.uiScale + visible: (loginScreen.state === "failed" && numFailures >= 5) && loginScreen.state !== "success" + color: errorTextColour + } + + Item { + id: loginScreenFooter + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 24 * virtualstudio.uiScale + width: parent.width + height: 48 * virtualstudio.uiScale + + property bool showBackButton: !virtualstudio.vsFtux + + Item { + id: backButton + visible: parent.showBackButton + anchors.verticalCenter: parent.verticalCenter + x: (parent.x + parent.width / 2) - backButton.width - 8 * virtualstudio.uiScale + width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale + Text { + text: "Back" + font.family: "Poppins" + font.underline: true + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + color: textColour + } + MouseArea { + anchors.fill: parent + onClicked: () => { if (!auth.isAuthenticated) { virtualstudio.windowState = "start"; } } + cursorShape: Qt.PointingHandCursor + } + } + + Item { + id: resetCodeButton + visible: loginScreen.state == "failed" || (!auth.isAuthenticated && auth.verificationCode) + x: parent.showBackButton ? (parent.x + parent.width / 2) + 8 * virtualstudio.uiScale : (parent.x + parent.width / 2) - resetCodeButton.width / 2 + anchors.verticalCenter: parent.verticalCenter + width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale + Text { + text: auth.verificationCode ? "Reset Code" : "Retry" + font.family: "Poppins" + font.underline: true + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + color: textColour + } + MouseArea { + anchors.fill: parent + onClicked: () => { + if (auth.verificationCode) { + auth.resetCode(); + } else { + numFailures = 0; + virtualstudio.login(); + } + } + cursorShape: Qt.PointingHandCursor + } + } + } + } + + Item { + id: refreshToken + anchors.horizontalCenter: parent.horizontalCenter + y: 108 * virtualstudio.uiScale + visible: showLoading + + Text { + id: loadingViaRefreshToken + text: "Logging In..."; + font.family: "Poppins" + font.pixelSize: 20 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + y: 208 * virtualstudio.uiScale + width: 360 * virtualstudio.uiScale; + color: textColour + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + + Timer { + id: retryTimer + running: false + repeat: false + interval: 300 + onTriggered: { virtualstudio.login(); } + } + + Connections { + target: auth + function onUpdatedAuthenticationStage (stage) { + loginScreen.state = stage; + if (stage === "failed") { + numFailures = numFailures + 1; + if (numFailures < 5) { + retryTimer.restart(); + } + } + if (stage === "success") { + numFailures = 0; + } + } + } +} diff --git a/src/vs/Meter.qml b/src/vs/Meter.qml new file mode 100644 index 0000000..292cef9 --- /dev/null +++ b/src/vs/Meter.qml @@ -0,0 +1,53 @@ +import QtQuick +import QtQuick.Controls + +Item { + required property var model + property int bins: Math.max(15, width/20) + property int innerMargin: 2 * virtualstudio.uiScale + property int boxRadius: 3 * virtualstudio.uiScale + property int boxThickness: 12 + required property bool clipped + property bool enabled: true + property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : (virtualstudio.darkMode ? "#7b7979" : "#EAECEC") + property string meterRed: "#F21B1B" + + Item { + id: meters + width: parent.width - boxThickness - innerMargin + height: parent.height + + MeterBars { + id: leftchannel + x: 0 + y: 0 + width: parent.width + height: boxThickness + level: parent.parent.model[0] + enabled: parent.parent.enabled + } + + MeterBars { + id: rightchannel + x: 0; + anchors.top: leftchannel.bottom + anchors.topMargin: innerMargin + width: parent.width + height: boxThickness + level: parent.parent.model[1] + enabled: parent.parent.enabled + } + } + + Rectangle { + id: clipIndicator + y: 0 + anchors.left: meters.right + anchors.leftMargin: innerMargin + + width: boxThickness + height: leftchannel.height + rightchannel.height + innerMargin + radius: boxRadius + color: clipped ? meterRed : meterColor + } +} \ No newline at end of file diff --git a/src/vs/MeterBars.qml b/src/vs/MeterBars.qml new file mode 100644 index 0000000..cbdbf0f --- /dev/null +++ b/src/vs/MeterBars.qml @@ -0,0 +1,49 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + required property var level + required property var enabled + property int boxHeight: height + property int boxWidth: (width / bins) - innerMargin + property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : (virtualstudio.darkMode ? "#7b7979" : "#EAECEC") + property string meterGreen: "#61C554" + property string meterYellow: "#F5BF4F" + property string meterRed: "#F21B1B" + + function getBoxColor (idx) { + // Case where the meter should not be filled + if (!enabled || level <= (idx / bins)) { + return meterColor; + } + // Case where the meter should be filled + let fillColor = meterGreen; + if (idx > 0.5*bins && idx <= 0.8*bins) { + fillColor = meterYellow; + } else if (idx > 0.8*bins) { + fillColor = meterRed; + } + return fillColor; + } + + RowLayout { + anchors.fill: parent + spacing: innerMargin + + Repeater { + model: bins + Rectangle { + Layout.fillHeight: true + Layout.fillWidth: true + x: (boxWidth) * index + innerMargin * index; + y: 0 + z: 1 + width: boxWidth + height: boxHeight + color: getBoxColor(index) + radius: boxRadius + } + } + } +} diff --git a/src/vs/Permissions.qml b/src/vs/Permissions.qml new file mode 100644 index 0000000..a4a5b14 --- /dev/null +++ b/src/vs/Permissions.qml @@ -0,0 +1,204 @@ +import QtQuick +import QtQuick.Controls + +Item { + width: parent.width; height: parent.height + clip: true + + property int fontBig: 20 + property int fontMedium: 13 + property int fontSmall: 11 + property int fontExtraSmall: 8 + + property string saveButtonPressedColour: "#E7E8E8" + property string saveButtonPressedStroke: "#B0B5B5" + property string saveButtonBackgroundColour: "#F2F3F3" + property string saveButtonStroke: "#EAEBEB" + property string saveButtonText: "#000000" + + Item { + id: requestMicPermissionsItem + width: parent.width; height: parent.height + visible: permissions.micPermission == "unknown" + + AppIcon { + id: microphonePrompt + y: 60 + anchors.horizontalCenter: parent.horizontalCenter + width: 260 + height: 250 + icon.source: "Prompt.svg" + } + + Image { + id: micLogo + source: "logo.svg" + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: microphonePrompt.top + anchors.topMargin: 18 * virtualstudio.uiScale + width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale + sourceSize: Qt.size(micLogo.width,micLogo.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + + Button { + id: showPromptButton + width: 112 * virtualstudio.uiScale + height: 30 * virtualstudio.uiScale + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: showPromptButton.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 2 + border.color: showPromptButton.down || showPromptButton.hovered ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: showPromptButton.hovered && !showPromptButton.down + } + onClicked: { + permissions.getMicPermission(); + } + anchors.right: microphonePrompt.right + anchors.rightMargin: 13.5 * virtualstudio.uiScale + anchors.bottomMargin: 17 * virtualstudio.uiScale + anchors.bottom: microphonePrompt.bottom + Text { + text: "OK" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: saveButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + Text { + id: micPermissionsHeader + text: "JackTrip needs your sounds!" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: microphonePrompt.bottom + anchors.topMargin: 48 * virtualstudio.uiScale + } + + Text { + id: micPermissionsSubheader1 + text: "JackTrip requires permission to use your microphone." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 400 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: micPermissionsHeader.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: micPermissionsSubheader2 + text: "Click ‘OK’ to give JackTrip access to your microphone, instrument, or other audio device." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 400 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: micPermissionsSubheader1.bottom + anchors.topMargin: 24 * virtualstudio.uiScale + } + } + + Item { + id: noMicItem + width: parent.width; height: parent.height + visible: permissions.micPermission == "denied" + + AppIcon { + id: noMic + y: 60 + anchors.horizontalCenter: parent.horizontalCenter + width: 109.27 + height: 170 + icon.source: "micoff.svg" + } + + Button { + id: openSettingsButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: openSettingsButton.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: openSettingsButton.down || openSettingsButton.hovered ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: openSettingsButton.hovered && !openSettingsButton.down + } + onClicked: { + permissions.openSystemPrivacy(); + } + anchors.right: parent.right + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.bottomMargin: 16 * virtualstudio.uiScale + anchors.bottom: parent.bottom + width: 200 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Open Privacy Settings" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: saveButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + Text { + id: noMicHeader + text: "JackTrip can't hear you!" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: noMic.bottom + anchors.topMargin: 48 * virtualstudio.uiScale + } + + Text { + id: noMicSubheader1 + text: "JackTrip requires permission to use your microphone." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 400 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: noMicHeader.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: noMicSubheader2 + text: "Click 'Open Privacy Settings' to give JackTrip permission to access your microphone, instrument, or other audio device." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 400 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: noMicSubheader1.bottom + anchors.topMargin: 24 * virtualstudio.uiScale + } + } + + Connections { + target: permissions + + function onMicPermissionUpdated() { + if (permissions.micPermission === "granted") { + if (virtualstudio.studioToJoin === "") { + virtualstudio.windowState = "browse"; + } else { + virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected"; + virtualstudio.joinStudio(); + } + } + } + } + +} diff --git a/src/vs/Poppins-Bold.ttf b/src/vs/Poppins-Bold.ttf new file mode 100644 index 0000000..00559ee Binary files /dev/null and b/src/vs/Poppins-Bold.ttf differ diff --git a/src/vs/Poppins-Regular.ttf b/src/vs/Poppins-Regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/src/vs/Poppins-Regular.ttf differ diff --git a/src/vs/Prompt.svg b/src/vs/Prompt.svg new file mode 100644 index 0000000..110d116 --- /dev/null +++ b/src/vs/Prompt.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/vs/Recommendations.qml b/src/vs/Recommendations.qml new file mode 100644 index 0000000..43d98be --- /dev/null +++ b/src/vs/Recommendations.qml @@ -0,0 +1,563 @@ +import QtQuick +import QtQuick.Controls + +Item { + width: parent.width; height: parent.height + clip: true + + property int fontBig: 20 + property int fontMedium: 13 + property int fontSmall: 11 + property int fontExtraSmall: 8 + + property int buttonWidth: 103 + property int buttonHeight: 25 + + property int leftMargin: 48 + property int rightMargin: 16 + + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string saveButtonBackgroundColour: "#F2F3F3" + property string saveButtonPressedColour: "#E7E8E8" + property string saveButtonStroke: "#EAEBEB" + property string saveButtonPressedStroke: "#B0B5B5" + property string saveButtonText: "#000000" + property string checkboxStroke: "#0062cc" + property string checkboxPressedStroke: "#007AFF" + property string disabledButtonText: "#D3D4D4" + property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" + + property bool currShowRecommendations: virtualstudio.showWarnings + property string recommendationScreen: virtualstudio.showWarnings ? "ethernet" : ( permissions.micPermission == "unknown" ? "microphone" : "acknowledged") + property bool onWindows: Qt.platform.os === "windows" + + Rectangle { + id: recommendationsHeader + x: -1 + y: 0 + + width: parent.width + 2 + height: 64 + + color: backgroundColour + border.color: "#33979797" + + Image { + source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png" + anchors.left: parent.left + anchors.leftMargin: 32 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + width: 119 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale + } + + Text { + id: gettingStartedText1 + visible: recommendationScreen === "ethernet" + text: "Getting Started with JackTrip (1/5)" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.right: parent.right + anchors.rightMargin: 32 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + } + + Text { + id: gettingStartedText2 + visible: recommendationScreen === "fiber" + text: "Getting Started with JackTrip (2/5)" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.right: parent.right + anchors.rightMargin: 32 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + } + + Text { + id: gettingStartedText3 + visible: recommendationScreen === "audiointerface" + text: "Getting Started with JackTrip (3/5)" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.right: parent.right + anchors.rightMargin: 32 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + } + + Text { + id: gettingStartedText4 + visible: recommendationScreen === "headphones" + text: "Getting Started with JackTrip (4/5)" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.right: parent.right + anchors.rightMargin: 32 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + } + + Text { + id: gettingStartedText5 + visible: recommendationScreen === "acknowledged" + text: "Getting Started with JackTrip (5/5)" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.right: parent.right + anchors.rightMargin: 32 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + } + } + + Item { + id: ethernetRecommendationItem + width: parent.width; height: parent.height + visible: recommendationScreen == "ethernet" + + AppIcon { + id: ethernetRecommendationLogo + y: 90 + anchors.horizontalCenter: parent.horizontalCenter + width: 179 + height: 128 + icon.source: "ethernet.svg" + } + + Text { + id: ethernetRecommendationHeader1 + text: "Wired Ethernet Recommended" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: ethernetRecommendationLogo.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: ethernetRecommendationSubheader1 + text: "JackTrip works best when you connect your computer directly to your Internet router via a wired ethernet cable." + + "

" + + "WiFi works OK for some people, but generates significantly more latency and audio glitches." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 600 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: ethernetRecommendationHeader1.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + LearnMoreButton { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: ethernetRecommendationSubheader1.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + url: "https://support.jacktrip.com/wired-internet-versus-wi-fi" + } + + Button { + id: okButtonEthernet + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: okButtonEthernet.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: okButtonEthernet.down || okButtonEthernet.hovered ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: okButtonEthernet.hovered && !okButtonEthernet.down + } + onClicked: { recommendationScreen = "fiber" } + anchors.right: parent.right + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.bottomMargin: 16 * virtualstudio.uiScale + anchors.bottom: parent.bottom + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Continue" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: saveButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Item { + id: fiberRecommendationItem + width: parent.width; height: parent.height + visible: recommendationScreen == "fiber" + + AppIcon { + id: fiberRecommendationLogo + y: 90 + anchors.horizontalCenter: parent.horizontalCenter + width: 179 + height: 128 + icon.source: "networkCheck.svg" + } + + Text { + id: fiberRecommendationHeader + text: "Fiber Internet Recommended" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: fiberRecommendationLogo.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: fiberRecommendationSubheader + text: "A Fiber Internet connection from your Internet Service Provider (ISP) will give you the best experience while using JackTrip." + + "

" + + "It's OK to use JackTrip with Cable and DSL, but these types of Internet connections introduce significantly more latency." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 600 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: fiberRecommendationHeader.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + LearnMoreButton { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: fiberRecommendationSubheader.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + url: "https://support.jacktrip.com/how-to-optimize-latency-when-using-jacktrip" + } + + Button { + id: okButtonFiber + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: okButtonFiber.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: okButtonFiber.down || okButtonFiber.hovered ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: okButtonFiber.hovered && !okButtonFiber.down + } + onClicked: { recommendationScreen = "audiointerface" } + anchors.right: parent.right + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.bottomMargin: 16 * virtualstudio.uiScale + anchors.bottom: parent.bottom + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Continue" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: saveButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Item { + id: headphoneRecommendationItem + width: parent.width; height: parent.height + visible: recommendationScreen == "headphones" + + AppIcon { + id: headphoneWarningLogo + y: 90 + anchors.horizontalCenter: parent.horizontalCenter + width: 118 + height: 128 + icon.source: "headphones.svg" + } + + Text { + id: headphoneRecommendationHeader1 + text: "Wired Headphones Required" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: headphoneWarningLogo.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: headphoneRecommendationSubheader1 + text: "JackTrip requires the use of wired headphones." + + "

" + + "Using speakers will generate echos and loud feedback loops." + + "

" + + "Wireless and bluetooth headphones introduce higher latency." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 600 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: headphoneRecommendationHeader1.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Button { + id: okButtonHeadphones + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: okButtonHeadphones.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: okButtonHeadphones.down || okButtonHeadphones.hovered ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: okButtonHeadphones.hovered && !okButtonHeadphones.down + } + onClicked: { + recommendationScreen = "acknowledged"; + } + anchors.right: parent.right + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.bottomMargin: 16 * virtualstudio.uiScale + anchors.bottom: parent.bottom + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Continue" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: saveButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Item { + id: audioInterfaceRecommendationItem + width: parent.width; height: parent.height + visible: recommendationScreen == "audiointerface" + + AppIcon { + id: audioInterfaceRecommendationLogo + y: 90 + anchors.horizontalCenter: parent.horizontalCenter + width: 118 + height: 128 + icon.source: "externalMic.svg" + } + + Text { + id: audioInterfaceRecommendationHeaderNonWindows + visible: !onWindows + text: "Use Recommended Audio Devices" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: audioInterfaceRecommendationLogo.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: audioInterfaceRecommendationSubheaderNonWindows + visible: !onWindows + text: "Many audio devices are too slow to work well with JackTrip." + + "

" + + "We recommend external USB or Thunderbolt interfaces released within the past " + + "few years for the best quality, low latency and glitch-free sound." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 600 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: audioInterfaceRecommendationHeaderNonWindows.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: audioInterfaceRecommendationHeaderWindows + visible: onWindows + text: "Use Recommended Audio Devices" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: audioInterfaceRecommendationLogo.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Text { + id: audioInterfaceRecommendationSubheaderWindows + visible: onWindows + text: "Many audio devices are too slow to work well with JackTrip." + + "
" + + "ASIO drivers are required for low latency on Windows. " + + "

" + + "We recommend external USB or Thunderbolt interfaces released within the past " + + "few years for the best quality, low latency and glitch-free sound." + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 600 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: audioInterfaceRecommendationHeaderWindows.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + LearnMoreButton { + width: 250 * virtualstudio.uiScale; + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: onWindows ? audioInterfaceRecommendationSubheaderWindows.bottom : audioInterfaceRecommendationSubheaderNonWindows.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + buttonText: "See recommended devices" + url: "https://support.jacktrip.com/recommended-audio-interfaces" + } + + Button { + id: okButtonAudioInterface + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: okButtonAudioInterface.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: okButtonAudioInterface.down || okButtonAudioInterface.hovered ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: okButtonAudioInterface.hovered && !okButtonAudioInterface.down + } + onClicked: { + recommendationScreen = "headphones"; + } + anchors.right: parent.right + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.bottomMargin: 16 * virtualstudio.uiScale + anchors.bottom: parent.bottom + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Continue" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: saveButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Item { + id: acknowledgedRecommendationItem + width: parent.width; height: parent.height + visible: recommendationScreen == "acknowledged" + + Text { + id: acknowledgedHeader + text: "Remind Me Again Next Time?" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 176 * virtualstudio.uiScale + } + + Text { + id: acknowledgedSubheader + text: "Would you like to review the getting started recommendations again the next time you start JackTrip?" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 600 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: acknowledgedHeader.bottom + anchors.topMargin: 32 * virtualstudio.uiScale + } + + Item { + id: acknowledgedButtonsContainer + width: 320 * virtualstudio.uiScale + + anchors.top: acknowledgedSubheader.bottom + anchors.topMargin: 64 * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + + Button { + id: acknowledgedYesButton + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: acknowledgedYesButton.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: acknowledgedYesButton.down || acknowledgedYesButton.hovered ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: acknowledgedYesButton.hovered && !acknowledgedYesButton.down + } + onClicked: { + virtualstudio.showWarnings = true; + virtualstudio.saveSettings(); + if (permissions.micPermission !== "granted") { + virtualstudio.windowState = "permissions"; + } else if (virtualstudio.studioToJoin === "") { + virtualstudio.windowState = "browse"; + } else { + virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected"; + virtualstudio.joinStudio(); + } + } + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Yes" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: saveButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + Button { + id: acknowledgedNoButton + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: acknowledgedNoButton.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: acknowledgedNoButton.down || acknowledgedNoButton.hovered ? saveButtonPressedStroke : saveButtonStroke + layer.enabled: acknowledgedNoButton.hovered && !acknowledgedNoButton.down + } + onClicked: { + virtualstudio.showWarnings = false; + virtualstudio.saveSettings(); + if (permissions.micPermission !== "granted") { + virtualstudio.windowState = "permissions"; + } else if (virtualstudio.studioToJoin === "") { + virtualstudio.windowState = "browse"; + } else { + virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected"; + virtualstudio.joinStudio(); + } + } + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "No" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: saveButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + } + + + Text { + id: acknowledgedSettingsInfo + text: "You can change this setting at any time under Settings > Advanced" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + width: 600 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: acknowledgedButtonsContainer.bottom + anchors.topMargin: 64 * virtualstudio.uiScale + } + } +} diff --git a/src/vs/SectionHeading.qml b/src/vs/SectionHeading.qml new file mode 100644 index 0000000..474f0b5 --- /dev/null +++ b/src/vs/SectionHeading.qml @@ -0,0 +1,161 @@ +import QtQuick +import QtQuick.Controls + +Rectangle { + property string filterStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + + property bool listIsEmpty: false + // required property string section: section (for 5.15) + color: "transparent" + height: 72 * virtualstudio.uiScale + x: 16 * virtualstudio.uiScale + y: listIsEmpty ? 16 * virtualstudio.uiScale : 0 + width: listIsEmpty ? parent.width - (2 * x) : ListView.view.width - (2 * x) + Text { + id: sectionText + //anchors.bottom: parent.bottom + y: 12 * virtualstudio.uiScale + // text: parent.section (for 5.15) + width: parent.width - 332 * virtualstudio.uiScale + fontSizeMode: Text.HorizontalFit + text: listIsEmpty ? "No Studios" : section + font { family: "Poppins"; pixelSize: 28 * virtualstudio.fontScale * virtualstudio.uiScale; weight: Font.Bold } + color: textColour + verticalAlignment: Text.AlignBottom + } + Button { + id: createButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: createButton.down ? "#E7E8E8" : "#F2F3F3" + border.width: 1 + border.color: createButton.down ? "#B0B5B5" : "#EAEBEB" + layer.enabled: createButton.hovered && !createButton.down + } + onClicked: { virtualstudio.windowState = "create_studio"; } + anchors.right: filterButton.left + anchors.rightMargin: 16 + anchors.verticalCenter: sectionText.verticalCenter + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Create Studio" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: "#000000" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + visible: listIsEmpty ? true : (section == virtualstudio.logoSection ? true : false) + } + Button { + id: filterButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: filterButton.down ? "#E7E8E8" : "#F2F3F3" + border.width: 1 + border.color: filterButton.down ? "#B0B5B5" : "#EAEBEB" + layer.enabled: filterButton.hovered && !filterButton.down + } + onClicked: { filterMenu.open(); } + anchors.right: parent.right + anchors.verticalCenter: sectionText.verticalCenter + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Filter Studios" + font.family: "Poppins" + font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + visible: listIsEmpty ? true : (section == virtualstudio.logoSection ? true : false) + + Popup { + id: filterMenu + y: Math.round(parent.height + 8) + rightMargin: 16 * virtualstudio.uiScale + width: 210 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale + modal: false + focus: false + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: "#F6F8F8" + border.width: 1 + border.color: filterStroke + layer.enabled: true + } + contentItem: Column { + anchors.fill: parent + CheckBox { + id: inactiveCheckbox + text: qsTr("Show my inactive Studios") + checkState: virtualstudio.showInactive ? Qt.Checked : Qt.Unchecked + onClicked: { virtualstudio.showInactive = inactiveCheckbox.checkState == Qt.Checked; + refresh(); + } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: inactiveCheckbox.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: inactiveCheckbox.down ? "#007AFF" : "#0062cc" + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: inactiveCheckbox.down ? "#007AFF" : "#0062cc" + visible: inactiveCheckbox.checked + } + } + contentItem: Text { + text: inactiveCheckbox.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: inactiveCheckbox.indicator.width + inactiveCheckbox.spacing + } + } + CheckBox { + id: selfHostedCheckbox + text: qsTr("Show self-hosted Studios") + checkState: virtualstudio.showSelfHosted ? Qt.Checked : Qt.Unchecked + onClicked: { virtualstudio.showSelfHosted = selfHostedCheckbox.checkState == Qt.Checked; + refresh(); + } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: selfHostedCheckbox.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC" + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC" + visible: selfHostedCheckbox.checked + } + } + contentItem: Text { + text: selfHostedCheckbox.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: selfHostedCheckbox.indicator.width + selfHostedCheckbox.spacing + } + } + } + } + } +} \ No newline at end of file diff --git a/src/vs/Settings.qml b/src/vs/Settings.qml new file mode 100644 index 0000000..e969637 --- /dev/null +++ b/src/vs/Settings.qml @@ -0,0 +1,866 @@ +import QtQuick +import QtQuick.Controls + +Item { + width: parent.width; height: parent.height + clip: true + + Rectangle { + width: parent.width; height: parent.height + color: backgroundColour + } + + property int fontBig: 20 + property int fontMedium: 13 + property int fontSmall: 11 + property int fontExtraSmall: 8 + + property int leftMargin: 48 + property int rightMargin: 16 + property int buttonWidth: 103 + property int buttonHeight: 25 + + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string sliderColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC" + property string sliderPressedColour: virtualstudio.darkMode ? "#ACAFAF" : "#DEE0E0" + property string sliderTrackColour: virtualstudio.darkMode ? "#5B5858" : "light gray" + property string sliderActiveTrackColour: virtualstudio.darkMode ? "light gray" : "black" + property string warningTextColour: "#DB0A0A" + property string checkboxStroke: "#0062cc" + property string checkboxPressedStroke: "#007AFF" + property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" + + property string errorFlagColour: "#DB0A0A" + property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + + property string settingsGroupView: "Audio" + + function getCurrentBufferSizeIndex () { + let bufferSize = audio.bufferSize; + let idx = audio.bufferSizeComboModel.findIndex(elem => parseInt(elem) === bufferSize); + if (idx < 0) { + idx = 0; + } + return idx; + } + + function getCurrentAudioBackendIndex () { + let idx = audio.audioBackendComboModel.findIndex(elem => elem === audio.audioBackend); + if (idx < 0) { + idx = 0; + } + return idx; + } + + function getQueueBufferString () { + if (audio.queueBuffer == 0) { + return "auto"; + } + return audio.queueBuffer + " ms"; + } + + Rectangle { + id: audioSettingsView + width: 0.8 * parent.width + height: parent.height - header.height + x: 0.2 * window.width + y: header.height + visible: settingsGroupView == "Audio" + + AudioSettings { + id: audioSettings + } + } + + ToolBar { + id: header + width: parent.width + height: 64 * virtualstudio.uiScale + + background: Rectangle { + border.color: "#33979797" + color: backgroundColour + width: parent.width + } + + contentItem: Item { + id: headerContent + width: header.width + height: header.height + + Label { + id: pageTitle + text: "Settings" + height: headerContent.height; + anchors.left: headerContent.left; + anchors.leftMargin: 32 * virtualstudio.uiScale + elide: Label.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + DeviceRefreshButton { + id: refreshButton + anchors.verticalCenter: pageTitle.verticalCenter; + anchors.right: headerContent.right; + anchors.rightMargin: 16 * virtualstudio.uiScale; + visible: audio.audioBackend == "RtAudio" && settingsGroupView == "Audio" + enabled: audio.audioReady && !audio.scanningDevices + } + + Text { + text: "Restarting Audio" + anchors.verticalCenter: pageTitle.verticalCenter; + anchors.right: refreshButton.left; + anchors.rightMargin: 16 * virtualstudio.uiScale; + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + visible: !audio.audioReady + } + } + } + + Drawer { + id: drawer + width: 0.2 * parent.width + height: parent.height - header.height + y: header.height-1 + modal: false + interactive: false + visible: virtualstudio.windowState == "settings" + + background: Rectangle { + border.color: "#33979797" + color: backgroundColour + } + + ButtonGroup { + buttons: viewControls.children + onClicked: function(button) { + settingsGroupView = button.text + } + } + + Column { + id: viewControls + width: parent.width + spacing: 24 * virtualstudio.uiScale + anchors.centerIn: parent + Button { + id: audioBtn + text: "Audio" + width: parent.width + contentItem: Item { + implicitWidth: audioButtonText.implicitWidth + implicitHeight: audioButtonText.implicitHeight + + Label { + id: audioButtonText + text: audioBtn.text + width: Boolean(audio.devicesError) ? parent.width - 16 * virtualstudio.uiScale : parent.width + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: textColour + } + + Rectangle { + id: audioDevicesErrorFlag + anchors.left: audioButtonText.right + anchors.verticalCenter: audioButtonText.verticalCenter + anchors.rightMargin: 16 * virtualstudio.uiScale + width: 8 * virtualstudio.uiScale + height: 8 * virtualstudio.uiScale + color: errorFlagColour + radius: 4 * virtualstudio.uiScale + visible: Boolean(audio.devicesError) + } + } + background: Rectangle { + width: parent.width + color: audioBtn.down ? buttonPressedColour : (audioBtn.hovered || settingsGroupView == "Audio" ? buttonHoverColour : backgroundColour) + } + } + Button { + id: appearanceBtn + text: "Appearance" + width: parent.width + contentItem: Item { + implicitWidth: appearanceButtonText.implicitWidth + implicitHeight: appearanceButtonText.implicitHeight + + Label { + id: appearanceButtonText + text: appearanceBtn.text + width: parent.width + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: textColour + } + } + + + background: Rectangle { + width: parent.width + color: appearanceBtn.down ? buttonPressedColour : (appearanceBtn.hovered || settingsGroupView == "Appearance" ? buttonHoverColour : backgroundColour) + } + } + Button { + id: advancedBtn + text: "Advanced" + width: parent.width + contentItem: Item { + implicitWidth: advancedButtonText.implicitWidth + implicitHeight: advancedButtonText.implicitHeight + + Label { + id: advancedButtonText + text: advancedBtn.text + width: parent.width + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: textColour + } + } + background: Rectangle { + width: parent.width + color: advancedBtn.down ? buttonPressedColour : (advancedBtn.hovered || settingsGroupView == "Advanced" ? buttonHoverColour : backgroundColour) + } + } + Button { + id: profileBtn + text: "Profile" + width: parent.width + contentItem: Item { + + implicitWidth: profileButtonText.implicitWidth + implicitHeight: profileButtonText.implicitHeight + + Label { + id: profileButtonText + text: profileBtn.text + width: parent.width + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: textColour + } + } + background: Rectangle { + width: parent.width + color: profileBtn.down ? buttonPressedColour : (profileBtn.hovered || settingsGroupView == "Profile" ? buttonHoverColour : backgroundColour) + } + } + } + + Column { + id: appVersion + width: parent.width + spacing: 24 * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + + Text { + text: "Version " + virtualstudio.versionString + font { family: "Poppins"; pixelSize: 9 * virtualstudio.fontScale * virtualstudio.uiScale} + color: textColour + opacity: 0.8 + width: parent.width + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + bottomPadding: 5 * virtualstudio.uiScale + } + } + } + + Rectangle { + id: appearanceSettingsView + width: 0.8 * parent.width + height: parent.height - header.height + x: 0.2 * window.width + y: header.height + color: backgroundColour + visible: settingsGroupView == "Appearance" + + Slider { + id: scaleSlider + x: 220 * virtualstudio.uiScale; + y: 100 * virtualstudio.uiScale + width: backendCombo.width + from: 1; to: 1.25; value: virtualstudio.uiScale + onMoved: { virtualstudio.uiScale = value } + + background: Rectangle { + x: scaleSlider.leftPadding + y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2 + implicitWidth: parent.width + implicitHeight: 6 + width: scaleSlider.availableWidth + height: implicitHeight + radius: 4 + color: sliderTrackColour + + Rectangle { + width: scaleSlider.visualPosition * parent.width + height: parent.height + color: sliderActiveTrackColour + radius: 4 + } + } + + handle: Rectangle { + x: scaleSlider.leftPadding + scaleSlider.visualPosition * (scaleSlider.availableWidth - width) + y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2 + implicitWidth: 26 * virtualstudio.uiScale + implicitHeight: 26 * virtualstudio.uiScale + radius: 13 * virtualstudio.uiScale + color: scaleSlider.pressed ? sliderPressedColour : sliderColour + border.color: buttonStroke + } + } + + Text { + anchors.verticalCenter: scaleSlider.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Scale Interface" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Button { + id: darkButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: darkButton.down ? buttonPressedColour : (darkButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: darkButton.down ? buttonPressedStroke : (darkButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.darkMode = !virtualstudio.darkMode; } + x: parent.width - (232 * virtualstudio.uiScale); + y: scaleSlider.y + (48 * virtualstudio.uiScale) + width: 216 * virtualstudio.uiScale; + height: 30 * virtualstudio.uiScale + Text { + text: virtualstudio.darkMode ? "Switch to Light Mode" : "Switch to Dark Mode" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Text { + anchors.verticalCenter: darkButton.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Color Theme" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + } + + Rectangle { + id: advancedSettingsView + width: 0.8 * parent.width + height: parent.height - header.height + x: 0.2 * window.width + y: header.height + color: backgroundColour + visible: settingsGroupView == "Advanced" + + Button { + id: modeButton + visible: virtualstudio.hasClassicMode + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: modeButton.down ? buttonPressedColour : (modeButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { + // essentially the same here as clicking the cancel button + audio.stopAudio(); + virtualstudio.windowState = "browse"; + virtualstudio.loadSettings(); + audio.validateDevices(); + + // switch mode + virtualstudio.toClassicMode(); + } + x: 220 * virtualstudio.uiScale; + y: 48 * virtualstudio.uiScale + width: 216 * virtualstudio.uiScale; + height: 30 * virtualstudio.uiScale + Text { + text: virtualstudio.psiBuild ? "Switch to Standard Mode" : "Switch to Classic Mode" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Text { + visible: virtualstudio.hasClassicMode + anchors.verticalCenter: modeButton.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Display Mode" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: updateChannelCombo + x: 220 * virtualstudio.uiScale; + y: virtualstudio.hasClassicMode ? (modeButton.y + (48 * virtualstudio.uiScale)) : (48 * virtualstudio.uiScale) + width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale + model: virtualstudio.updateChannelComboModel + currentIndex: virtualstudio.updateChannel == "stable" ? 0 : 1 + onActivated: { virtualstudio.updateChannel = currentIndex == 0 ? "stable": "edge" } + font.family: "Poppins" + enabled: !virtualstudio.noUpdater + } + + Text { + anchors.verticalCenter: updateChannelCombo.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Update Channel" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: backendCombo + model: audio.audioBackendComboModel + enabled: audio.audioBackendComboModel.length > 1 + currentIndex: getCurrentAudioBackendIndex() + onActivated: { + audio.audioBackend = currentText + audio.restartAudio(); + } + x: 220 * virtualstudio.uiScale; y: updateChannelCombo.y + (48 * virtualstudio.uiScale) + width: updateChannelCombo.width; height: updateChannelCombo.height + } + + Text { + id: backendLabel + anchors.verticalCenter: backendCombo.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Audio Backend" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + ComboBox { + id: bufferCombo + x: 220 * virtualstudio.uiScale; y: backendCombo.y + (48 * virtualstudio.uiScale) + width: backendCombo.width; height: updateChannelCombo.height + model: audio.bufferSizeComboModel + currentIndex: getCurrentBufferSizeIndex() + onActivated: { + audio.bufferSize = parseInt(currentText, 10); + audio.restartAudio(); + } + font.family: "Poppins" + } + + Text { + anchors.verticalCenter: bufferCombo.verticalCenter + x: 48 * virtualstudio.uiScale + text: "Buffer Size" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Slider { + id: queueBufferSlider + value: audio.queueBuffer + onMoved: { + audio.queueBuffer = value; + } + from: 0 + to: 128 + stepSize: 1 + padding: 0 + x: updateChannelCombo.x + queueBufferText.width + y: bufferCombo.y + (54 * virtualstudio.uiScale) + width: updateChannelCombo.width - queueBufferText.width + + background: Rectangle { + x: queueBufferSlider.leftPadding + y: queueBufferSlider.topPadding + queueBufferSlider.availableHeight / 2 - height / 2 + implicitWidth: parent.width + implicitHeight: 6 + width: queueBufferSlider.availableWidth + height: implicitHeight + radius: 4 + color: sliderTrackColour + + Rectangle { + width: queueBufferSlider.visualPosition * parent.width + height: parent.height + color: sliderActiveTrackColour + radius: 4 + } + } + + handle: Rectangle { + x: queueBufferSlider.leftPadding + queueBufferSlider.visualPosition * (queueBufferSlider.availableWidth - width) + y: queueBufferSlider.topPadding + queueBufferSlider.availableHeight / 2 - height / 2 + implicitWidth: 26 * virtualstudio.uiScale + implicitHeight: 26 * virtualstudio.uiScale + radius: 13 * virtualstudio.uiScale + color: queueBufferSlider.pressed ? sliderPressedColour : sliderColour + border.color: buttonStroke + } + } + + Text { + id: queueBufferText + width: (64 * virtualstudio.uiScale) + x: updateChannelCombo.x; + anchors.verticalCenter: queueBufferSlider.verticalCenter + text: getQueueBufferString() + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Text { + id: queueBufferLabel + anchors.verticalCenter: queueBufferSlider.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Adjust Latency" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + InfoTooltip { + id: tooltip + content: "JackTrip analyzes your Internet connection to find the best balance between audio latency and quality. Add additional latency to further improve quality." + size: 16 + anchors.left: queueBufferLabel.right + anchors.leftMargin: 2 * virtualstudio.uiScale + anchors.verticalCenter: queueBufferSlider.verticalCenter + } + + CheckBox { + id: feedbackDetection + checked: audio.feedbackDetectionEnabled + text: qsTr("Automatically mute when feedback is detected") + x: updateChannelCombo.x; + y: queueBufferSlider.y + (48 * virtualstudio.uiScale) + onClicked: { audio.feedbackDetectionEnabled = feedbackDetection.checkState == Qt.Checked; } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: feedbackDetection.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: feedbackDetection.down || feedbackDetection.hovered ? checkboxPressedStroke : checkboxStroke + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: feedbackDetection.down || feedbackDetection.hovered ? checkboxPressedStroke : checkboxStroke + visible: feedbackDetection.checked + } + } + contentItem: Text { + text: feedbackDetection.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: feedbackDetection.indicator.width + feedbackDetection.spacing + color: textColour + } + } + + Text { + anchors.verticalCenter: feedbackDetection.verticalCenter + x: 48 * virtualstudio.uiScale + text: "Detect Feedback" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + CheckBox { + id: showStartupSetup + checked: virtualstudio.showDeviceSetup + text: qsTr("Show device setup screen before connecting to a studio") + x: updateChannelCombo.x; y: feedbackDetection.y + (48 * virtualstudio.uiScale) + onClicked: { virtualstudio.showDeviceSetup = showStartupSetup.checkState == Qt.Checked; } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: showStartupSetup.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: showStartupSetup.down || showStartupSetup.hovered ? checkboxPressedStroke : checkboxStroke + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: showStartupSetup.down || showStartupSetup.hovered ? checkboxPressedStroke : checkboxStroke + visible: showStartupSetup.checked + } + } + contentItem: Text { + text: showStartupSetup.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: showStartupSetup.indicator.width + showStartupSetup.spacing + color: textColour + } + } + + Text { + anchors.verticalCenter: showStartupSetup.verticalCenter + x: 48 * virtualstudio.uiScale + text: "Device Setup" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + CheckBox { + id: showStartupWarnings + checked: virtualstudio.showWarnings + text: qsTr("Show recommendations on startup again next time") + x: updateChannelCombo.x; y: showStartupSetup.y + (48 * virtualstudio.uiScale) + onClicked: { virtualstudio.showWarnings = showStartupWarnings.checkState == Qt.Checked; } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: showStartupWarnings.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: showStartupWarnings.down || showStartupWarnings.hovered ? checkboxPressedStroke : checkboxStroke + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: showStartupWarnings.down || showStartupWarnings.hovered ? checkboxPressedStroke : checkboxStroke + visible: showStartupWarnings.checked + } + } + contentItem: Text { + text: showStartupWarnings.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: showStartupWarnings.indicator.width + showStartupWarnings.spacing + color: textColour + } + } + + Text { + anchors.verticalCenter: showStartupWarnings.verticalCenter + x: 48 * virtualstudio.uiScale + text: "Recommendations" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + } + + Rectangle { + id: profileSettingsView + width: 0.8 * parent.width + height: parent.height - header.height + x: 0.2 * window.width + y: header.height + color: backgroundColour + visible: settingsGroupView == "Profile" + + Image { + id: profilePicture + width: 96; height: 96 + y: 60 * virtualstudio.uiScale + source: virtualstudio.userMetadata.picture ? virtualstudio.userMetadata.picture : "" + anchors.horizontalCenter: parent.horizontalCenter + fillMode: Image.PreserveAspectCrop + } + + Text { + id: displayName + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: profilePicture.bottom + text: virtualstudio.userMetadata.user_metadata ? ( virtualstudio.userMetadata.user_metadata.display_name ? virtualstudio.userMetadata.user_metadata.display_name : virtualstudio.userMetadata.nickname ) : virtualstudio.userMetadata.name || "" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Text { + id: email + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: displayName.bottom + text: virtualstudio.userMetadata.email ? virtualstudio.userMetadata.email : "" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Button { + id: editButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: editButton.down ? buttonPressedColour : (editButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: editButton.down ? buttonPressedStroke : (editButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.editProfile(); } + anchors.horizontalCenter: parent.horizontalCenter + y: email.y + (56 * virtualstudio.uiScale) + width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Edit Profile" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Button { + id: logoutButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: logoutButton.down ? buttonPressedColour : (logoutButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: logoutButton.down ? buttonPressedStroke : (logoutButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { virtualstudio.logout(); } + anchors.horizontalCenter: parent.horizontalCenter + y: editButton.y + (48 * virtualstudio.uiScale) + width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Log Out" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Button { + id: testModeButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: testModeButton.down ? buttonPressedColour : (testModeButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: testModeButton.down ? buttonPressedStroke : (testModeButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { + virtualstudio.testMode = !virtualstudio.testMode; + + // behave like "Cancel" and switch back to browse mode + audio.stopAudio(); + virtualstudio.windowState = "browse"; + virtualstudio.loadSettings(); + audio.validateDevices(); + } + anchors.horizontalCenter: parent.horizontalCenter + y: logoutButton.y + (48 * virtualstudio.uiScale) + width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + visible: virtualstudio.userMetadata.email ? ( virtualstudio.userMetadata.email.endsWith("@jacktrip.org") ? true : false ) : false + Text { + text: virtualstudio.testMode ? "Switch to Prod Mode" : "Switch to Test Mode" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + } + + Rectangle { + x: -1; y: parent.height - (36 * virtualstudio.uiScale) + width: parent.width; height: (36 * virtualstudio.uiScale) + border.color: "#33979797" + color: backgroundColour + + Button { + id: cancelButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: cancelButton.down ? buttonPressedColour : (cancelButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: cancelButton.down ? buttonPressedStroke : (cancelButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { + audio.stopAudio(); + virtualstudio.windowState = "browse"; + virtualstudio.loadSettings(); + audio.validateDevices(); + } + anchors.verticalCenter: parent.verticalCenter + x: parent.width - (230 * virtualstudio.uiScale) + width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale + Text { + text: "Cancel" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Button { + id: saveButton + enabled: !Boolean(audio.devicesError) + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: saveButton.down ? buttonPressedColour : (saveButton.hovered ? buttonHoverColour : buttonColour) + border.width: 1 + border.color: saveButton.down ? buttonPressedStroke : (saveButton.hovered ? buttonHoverStroke : buttonStroke) + } + onClicked: { + audio.stopAudio(); + virtualstudio.windowState = "browse"; + virtualstudio.saveSettings(); + } + anchors.verticalCenter: parent.verticalCenter + x: parent.width - (119 * virtualstudio.uiScale) + width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale + Text { + text: "Save" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: Boolean(audio.devicesError) ? disabledButtonTextColour : textColour + } + } + + DeviceWarning { + id: deviceWarning + x: (0.2 * window.width) + 16 * virtualstudio.uiScale + anchors.verticalCenter: parent.verticalCenter + visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) + } + } + + Connections { + target: audio + // anything that sets currentIndex to the value of a function needs + // to be manually updated whenever there is a change to any vars it uses + function onBufferSizeChanged() { + bufferCombo.currentIndex = getCurrentBufferSizeIndex(); + } + function onAudioBackendChanged() { + backendCombo.currentIndex = getCurrentAudioBackendIndex(); + } + } +} diff --git a/src/vs/Setup.qml b/src/vs/Setup.qml new file mode 100644 index 0000000..a28a8dd --- /dev/null +++ b/src/vs/Setup.qml @@ -0,0 +1,209 @@ +import QtQuick +import QtQuick.Controls + +Item { + width: parent.width; height: parent.height + clip: true + + property int fontBig: 20 + property int fontMedium: 13 + property int fontSmall: 11 + property int fontExtraSmall: 8 + + property int leftMargin: 48 + property int rightMargin: 16 + + property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC" + property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4" + property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0" + property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" + property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + property string saveButtonBackgroundColour: "#F2F3F3" + property string saveButtonPressedColour: "#E7E8E8" + property string saveButtonStroke: "#EAEBEB" + property string saveButtonPressedStroke: "#B0B5B5" + property string saveButtonText: "#000000" + property string checkboxStroke: "#0062cc" + property string checkboxPressedStroke: "#007AFF" + property string disabledButtonText: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" + + Item { + id: setupItem + width: parent.width; height: parent.height + + property bool isUsingRtAudio: audio.audioBackend == "RtAudio" + + Text { + id: pageTitle + x: 16 * virtualstudio.uiScale; + y: 16 * virtualstudio.uiScale + text: "Choose your audio devices" + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + DeviceRefreshButton { + id: refreshButton + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.verticalCenter: pageTitle.verticalCenter + visible: parent.isUsingRtAudio + enabled: audio.audioReady && !audio.scanningDevices + } + + Text { + text: "Restarting Audio" + anchors.verticalCenter: pageTitle.verticalCenter; + anchors.right: refreshButton.left; + anchors.rightMargin: 16 * virtualstudio.uiScale; + font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + visible: !audio.audioReady + } + + AudioSettings { + id: audioSettings + width: parent.width + anchors.top: pageTitle.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + } + + Rectangle { + id: headerBorder + width: parent.width + height: 1 + anchors.top: audioSettings.top + color: strokeColor + } + + Rectangle { + id: footerBorder + width: parent.width + height: 1 + anchors.top: audioSettings.bottom + color: strokeColor + } + + Rectangle { + property int footerHeight: (30 + (rightMargin * 2)) * virtualstudio.uiScale; + x: -1; y: parent.height - footerHeight; + width: parent.width; height: footerHeight; + border.color: "#33979797" + color: backgroundColour + + Button { + id: backButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: backButton.down ? buttonPressedColour : buttonColour + border.width: 1 + border.color: backButton.down || backButton.hovered ? buttonPressedStroke : buttonStroke + } + onClicked: { virtualstudio.windowState = "browse"; virtualstudio.studioToJoin = ""; audio.stopAudio(); } + anchors.left: parent.left + anchors.leftMargin: 16 * virtualstudio.uiScale + anchors.bottomMargin: rightMargin * virtualstudio.uiScale + anchors.bottom: parent.bottom + width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Back" + font.family: "Poppins" + font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale + color: textColour + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + DeviceWarning { + id: deviceWarning + anchors.left: backButton.right + anchors.leftMargin: 16 * virtualstudio.uiScale + anchors.verticalCenter: backButton.verticalCenter + visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning) + } + + Button { + id: saveButton + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: saveButton.down ? saveButtonPressedColour : saveButtonBackgroundColour + border.width: 1 + border.color: saveButton.down || saveButton.hovered ? saveButtonPressedStroke : saveButtonStroke + } + enabled: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady + onClicked: { + if (Boolean(audio.devicesWarning)) { + deviceWarningModal.open(); + } else { + audio.stopAudio(true); + virtualstudio.studioToJoin = virtualstudio.currentStudio.id; + virtualstudio.windowState = "connected"; + virtualstudio.saveSettings(); + virtualstudio.joinStudio(); + } + } + anchors.right: parent.right + anchors.rightMargin: rightMargin * virtualstudio.uiScale + anchors.bottomMargin: rightMargin * virtualstudio.uiScale + anchors.bottom: parent.bottom + width: 160 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + Text { + text: "Connect to Session" + font.family: "Poppins" + font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale + font.weight: Font.Bold + color: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady ? saveButtonText : disabledButtonText + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + CheckBox { + id: showAgainCheckbox + checked: virtualstudio.showDeviceSetup + visible: audio.backendAvailable + text: qsTr("Ask again next time") + anchors.right: saveButton.left + anchors.rightMargin: 16 * virtualstudio.uiScale + anchors.verticalCenter: saveButton.verticalCenter + onClicked: { virtualstudio.showDeviceSetup = showAgainCheckbox.checkState == Qt.Checked } + indicator: Rectangle { + implicitWidth: 16 * virtualstudio.uiScale + implicitHeight: 16 * virtualstudio.uiScale + x: showAgainCheckbox.leftPadding + y: parent.height / 2 - height / 2 + radius: 3 * virtualstudio.uiScale + border.color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke + + Rectangle { + width: 10 * virtualstudio.uiScale + height: 10 * virtualstudio.uiScale + x: 3 * virtualstudio.uiScale + y: 3 * virtualstudio.uiScale + radius: 2 * virtualstudio.uiScale + color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke + visible: showAgainCheckbox.checked + } + } + + contentItem: Text { + text: showAgainCheckbox.text + font.family: "Poppins" + font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + leftPadding: showAgainCheckbox.indicator.width + showAgainCheckbox.spacing + color: textColour + } + } + } + + DeviceWarningModal { + id: deviceWarningModal + } + } +} diff --git a/src/vs/Studio.qml b/src/vs/Studio.qml new file mode 100644 index 0000000..726ac52 --- /dev/null +++ b/src/vs/Studio.qml @@ -0,0 +1,402 @@ +import QtQuick +import QtQuick.Controls +import Qt5Compat.GraphicalEffects +import VS 1.0 + +Rectangle { + width: 664; height: 83 * virtualstudio.uiScale + radius: 6 * virtualstudio.uiScale + color: backgroundColour + + property string serverLocation: "Germany - Berlin" + property string flagImage: "flags/DE.svg" + property string hostname: "app.jacktrip.com" + property string studioName: "Test Studio" + property string studioId: "" + property string streamId: "" + property string inviteKeyString: "" + property int sampleRate: 48000 + property bool publicStudio: false + property bool admin: false + property bool available: true + property bool connected: false + property bool inviteCopied: false + + property int leftMargin: 81 + property int topMargin: 13 + property int bottomToolTipMargin: 8 + property int rightToolTipMargin: 4 + + property real fontBig: 18 + property real fontMedium: 11 + property real fontSmall: 8 + + property string backgroundColour: virtualstudio.darkMode ? "#494646" : "#F4F6F6" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1" + property string inviteToolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3" + property string inviteToolTipTextColour: textColour + property string inviteCopiedBackgroundColour: "#57B147" + property string inviteCopiedTextColour: "#FAFBFB" + property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + + property string baseButtonColour: virtualstudio.darkMode ? "#F0F1F1" : "#EAEBEB" + property string baseButtonHoverColour: virtualstudio.darkMode ? "#CCCDCD" : "#D3D3D3" + property string baseButtonPressedColour: virtualstudio.darkMode ? "#E4E5E5" : "#EAEBEB" + property string baseButtonStroke: virtualstudio.darkMode ? "#8B8D8D" : "#949494" + + property string joinAvailableColour: virtualstudio.darkMode ? "#E2EBE0" : "#C4F4BE" + property string joinAvailableHoverColour: virtualstudio.darkMode ? "#BAC7B8" : "#B0DCAB" + property string joinAvailablePressedColour: virtualstudio.darkMode ? "#D8E2D6" : "#BAE8B5" + property string joinAvailableStroke: virtualstudio.darkMode ? "#748F70" : "#5DB752" + + property string joinUnavailableColour: baseButtonColour + property string joinUnavailableHoverColour: baseButtonHoverColour + property string joinUnavailablePressedColour: baseButtonPressedColour + property string joinUnavailableStroke: baseButtonStroke + + property string startColour: virtualstudio.darkMode ? "#E2EBE0" : "#C4F4BE" + property string startHoverColour: virtualstudio.darkMode ? "#BAC7B8" : "#B0DCAB" + property string startPressedColour: virtualstudio.darkMode ? "#D8E2D6" : "#BAE8B5" + property string startStroke: virtualstudio.darkMode ? "#748F70" : "#5DB752" + + property string manageColour: baseButtonColour + property string manageHoverColour: baseButtonHoverColour + property string managePressedColour: baseButtonPressedColour + property string manageStroke: baseButtonStroke + + property string leaveColour: virtualstudio.darkMode ? "#FCB6B6" : "#FCB6B6" + property string leaveHoverColour: virtualstudio.darkMode ? "#D49696" : "#E3A4A4" + property string leavePressedColour: virtualstudio.darkMode ? "#F2AEAE" : "#EFADAD" + property string leaveStroke: virtualstudio.darkMode ? "#A65959" : "#C95E5E" + + property string studioStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797" + + border.width: 1 + border.color: studioStroke + + Clipboard { + id: clipboard + } + + Rectangle { + id: shadow + anchors.fill: parent + color: "transparent" + radius: 6 + } + + Rectangle { + width: 12 * virtualstudio.uiScale; height: parent.height + radius: width / 2 + color: available ? "#0C1424" : "#B3B3B3" + } + + Image { + id: wedge + source: available ? "wedge.svg" : "wedge_inactive.svg" + x: 6; y: 0; width: 52 * virtualstudio.uiScale; height: 83 * virtualstudio.uiScale + sourceSize: Qt.size(wedge.width,wedge.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + + Image { + id: studioLogo + source: "logo.svg" + x: 8; y: 11; width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale + sourceSize: Qt.size(studioLogo.width,studioLogo.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + + Rectangle { + x: 33 * virtualstudio.uiScale; y: 8 * virtualstudio.uiScale + width: 32 * virtualstudio.uiScale; height: width + radius: width / 2 + color: available ? "#0C1424" : "#B3B3B3" + } + + Image { + id: flag + source: flagImage + x: 30 * virtualstudio.uiScale; y: 9 * virtualstudio.uiScale + width: 40 * virtualstudio.uiScale; height: width / 4 * 3 + fillMode: Image.PreserveAspectCrop + layer.enabled: true + layer.effect: OpacityMask { + maskSource: mask + } + + AppIcon { + id: defaultFlag + anchors.fill: parent + width: 32 * virtualstudio.uiScale + height: 32 * virtualstudio.uiScale + icon.source: "language.svg" + color: "white" + visible: flag.status != Image.Ready + } + } + + Rectangle { + id: mask + x: 0 ; y: 0 ; width: flag.width; height: flag.height + visible: false + color: "#00000000" + Rectangle { + x: 7 * virtualstudio.uiScale; y: 3 * virtualstudio.uiScale + width:24 * virtualstudio.uiScale; height: width + radius: width / 2 + } + } + + Text { + x: leftMargin * virtualstudio.uiScale; y: 11 * virtualstudio.uiScale; + width: (admin || connected) ? parent.width - (310 * virtualstudio.uiScale) : parent.width - (233 * virtualstudio.uiScale) + text: studioName + fontSizeMode: Text.HorizontalFit + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + color: textColour + } + + Rectangle { + id: publicRect + x: leftMargin * virtualstudio.uiScale; y: 52 * virtualstudio.uiScale + width: 14 * virtualstudio.uiScale; height: width + radius: 2 * virtualstudio.uiScale + color: publicStudio ? "#0095FF" : "#FF9800" + Image { + id: pubPriv + source: publicStudio ? "public.svg" : "private.svg" + x: 1 * virtualstudio.uiScale; y: x; width: 12 * virtualstudio.uiScale; height: width + sourceSize: Qt.size(pubPriv.width,pubPriv.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + } + + Text { + anchors.verticalCenter: publicRect.verticalCenter + x: (leftMargin + 22) * virtualstudio.uiScale + width: (admin || connected) ? parent.width - (255 * virtualstudio.uiScale) : parent.width - (178 * virtualstudio.uiScale) + text: publicStudio ? "Public hub studio " + serverLocation : "Private hub studio " + serverLocation + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + elide: Text.ElideRight + color: textColour + } + + Button { + id: joinButton + x: (admin || connected) ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale) + y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width + background: Rectangle { + radius: width / 2 + color: available ? (joinButton.down ? joinAvailablePressedColour : (joinButton.hovered ? joinAvailableHoverColour : joinAvailableColour)) + : (joinButton.down ? joinUnavailablePressedColour : (joinButton.hovered ? joinUnavailableHoverColour : joinUnavailableColour)) + border.width: joinButton.down ? 1 : 0 + border.color: available ? joinAvailableStroke : joinUnavailableStroke + } + visible: !connected + onClicked: { + virtualstudio.studioToJoin = studioId; + virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected"; + virtualstudio.joinStudio(); + } + Image { + id: join + width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale + anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } + source: "join.svg" + sourceSize: Qt.size(join.width,join.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + } + + Button { + id: leaveButton + x: (admin || connected) ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale) + y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width + background: Rectangle { + radius: width / 2 + color: leaveButton.down ? leavePressedColour : (leaveButton.hovered ? leaveHoverColour : leaveColour) + border.width: leaveButton.down ? 1 : 0 + border.color: leaveStroke + } + visible: connected + onClicked: { + virtualstudio.disconnect(); + } + Image { + id: leave + width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale + anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } + source: "leave.svg" + sourceSize: Qt.size(leave.width,leave.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + } + + Text { + anchors.horizontalCenter: joinButton.horizontalCenter + y: 56 * virtualstudio.uiScale + text: connected ? "Leave" : "Join" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} + visible: true + color: textColour + } + + Button { + id: inviteButton + x: (admin || connected) ? parent.width - (142 * virtualstudio.uiScale) : parent.width - (65 * virtualstudio.uiScale) + y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width + background: Rectangle { + radius: width / 2 + color: inviteButton.down ? managePressedColour : (inviteButton.hovered ? manageHoverColour : manageColour) + border.width: inviteButton.down ? 1 : 0 + border.color: manageStroke + } + Timer { + id: copiedResetTimer + interval: 3000; running: false; repeat: false + onTriggered: inviteCopied = false; + } + onClicked: { + inviteCopied = true; + if (virtualstudio.testMode) { + hostname = "test.jacktrip.com"; + } + if (!inviteKeyString) { + clipboard.setText(qsTr("https://" + hostname + "/studios/" + studioId + "?invited=true")); + } else { + clipboard.setText(qsTr("https://" + hostname + "/studios/" + studioId + "?invited=" + inviteKeyString)); + } + copiedResetTimer.restart() + } + visible: true + Image { + id: shareImg + width: 24 * virtualstudio.uiScale; height: width + anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } + source: "share.svg" + sourceSize: Qt.size(shareImg.width,shareImg.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + ToolTip { + parent: inviteButton + visible: !inviteCopied && inviteButton.hovered + bottomPadding: bottomToolTipMargin * virtualstudio.uiScale + rightPadding: rightToolTipMargin * virtualstudio.uiScale + delay: 100 + contentItem: Rectangle { + color: inviteToolTipBackgroundColour + radius: 3 + anchors.fill: parent + anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale + anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale + layer.enabled: true + border.width: 1 + border.color: tooltipStroke + + Text { + anchors.centerIn: parent + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale} + text: qsTr("Copy invite link for Studio") + color: inviteToolTipTextColour + } + } + background: Rectangle { + color: "transparent" + } + } + ToolTip { + parent: inviteButton + visible: inviteCopied + bottomPadding: bottomToolTipMargin * virtualstudio.uiScale + rightPadding: rightToolTipMargin * virtualstudio.uiScale + delay: 100 + contentItem: Rectangle { + color: inviteCopiedBackgroundColour + radius: 3 + anchors.fill: parent + anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale + anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale + layer.enabled: true + border.width: 1 + border.color: tooltipStroke + + Text { + anchors.centerIn: parent + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale} + text: qsTr("📋 Copied invitation link to Clipboard") + color: inviteCopiedTextColour + } + } + background: Rectangle { + color: "transparent" + } + } + } + + Text { + anchors.horizontalCenter: inviteButton.horizontalCenter + y: 56 * virtualstudio.uiScale + text: "Invite" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + visible: true + color: textColour + } + + Button { + id: manageButton + x: parent.width - (65 * virtualstudio.uiScale); y: topMargin * virtualstudio.uiScale + width: 40 * virtualstudio.uiScale; height: width + background: Rectangle { + radius: width / 2 + color: manageButton.down ? managePressedColour : (manageButton.hovered ? manageHoverColour : manageColour) + border.width: manageButton.down ? 1 : 0 + border.color: manageStroke + } + onClicked: { + var url = ""; + if (streamId === "") { + if (virtualstudio.testMode) { + url = "https://test.jacktrip.com/studios/" + studioId; + } else { + url = "https://app.jacktrip.com/studios/" + studioId; + } + } else { + if (virtualstudio.testMode) { + url = "https://next-test.jacktrip.com/@" + streamId + "/dashboard"; + } else { + url = "https://www.jacktrip.com/@" + streamId + "/dashboard"; + } + } + virtualstudio.openLink(qsTr(url)); + } + visible: admin || connected + Image { + id: manageImg + width: 20 * virtualstudio.uiScale; height: width + anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } + source: "manage.svg" + sourceSize: Qt.size(manageImg.width,manageImg.height) + fillMode: Image.PreserveAspectFit + smooth: true + } + } + + Text { + anchors.horizontalCenter: manageButton.horizontalCenter + y: 56 * virtualstudio.uiScale + text: "Manage" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + visible: admin || connected + color: textColour + } +} diff --git a/src/vs/VolumeSlider.qml b/src/vs/VolumeSlider.qml new file mode 100644 index 0000000..1f2a467 --- /dev/null +++ b/src/vs/VolumeSlider.qml @@ -0,0 +1,122 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Item { + width: parent.width + height: parent.height + + required property string labelText + required property string tooltipText + required property bool sliderEnabled + property bool showLabel: true + property int fontSize: 8 + + property string iconColor: virtualstudio.darkMode ? "#ACAFAF" : "gray" + property string sliderPressedColour: virtualstudio.darkMode ? "#BABCBC" : "#EAECEC" + + Item { + anchors.fill: parent + anchors.verticalCenter: parent.verticalCenter + + Text { + id: label + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 40 * virtualstudio.uiScale + horizontalAlignment: Text.AlignRight + text: labelText + font {family: "Poppins"; weight: Font.Medium; pixelSize: fontSize * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + visible: showLabel + } + + InfoTooltip { + id: tooltip + content: tooltipText + size: 16 + anchors.left: label.right + anchors.leftMargin: 2 * virtualstudio.uiScale + anchors.verticalCenter: label.verticalCenter + visible: showLabel + } + + AppIcon { + id: quieterIcon + anchors.left: showLabel ? tooltip.right : parent.left + anchors.leftMargin: showLabel ? 8 * virtualstudio.uiScale : 0 + anchors.verticalCenter: label.verticalCenter + width: 16 + height: 16 + icon.source: "quiet.svg" + color: iconColor + } + + AppIcon { + id: louderIcon + anchors.right: parent.right + anchors.verticalCenter: label.verticalCenter + width: 18 + height: 18 + icon.source: "loud.svg" + color: iconColor + } + + Slider { + id: slider + value: labelText == "Monitor" ? audio.monitorVolume : (labelText == "Studio" ? audio.outputVolume : audio.inputVolume ) + onMoved: { + if (labelText == "Monitor") { + audio.monitorVolume = value; + } else if (labelText == "Studio") { + audio.outputVolume = value; + } else { + audio.inputVolume = value; + } + } + enabled: sliderEnabled + from: 0.0 + to: 1.0 + stepSize: 0.01 + padding: 0 + anchors.left: quieterIcon.right + anchors.leftMargin: 4 * virtualstudio.uiScale + anchors.right: louderIcon.left + anchors.rightMargin: 4 * virtualstudio.uiScale + anchors.verticalCenter: label.verticalCenter + + background: Rectangle { + x: slider.leftPadding + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitWidth: parent.width + implicitHeight: 8 * virtualstudio.uiScale + width: slider.availableWidth + height: implicitHeight + radius: 4 + color: "gray" + + Rectangle { + width: slider.visualPosition * parent.width + height: parent.height + radius: 4 + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.0; color: sliderEnabled ? "#67C6F3" : "light gray" } + GradientStop { position: 1.0; color: sliderEnabled ? "#00897b" : "light gray" } + } + } + } + + handle: Rectangle { + x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitWidth: 18 * virtualstudio.uiScale + implicitHeight: 18 * virtualstudio.uiScale + radius: implicitWidth / 2 + color: slider.pressed ? sliderPressedColour : "white" + border.width: 3 + border.color: sliderEnabled ? "#00897b" : "light gray" + } + } + } +} diff --git a/src/vs/Web.qml b/src/vs/Web.qml new file mode 100644 index 0000000..13927f8 --- /dev/null +++ b/src/vs/Web.qml @@ -0,0 +1,10 @@ +import QtQuick +import QtQuick.Controls + +Loader { + anchors.fill: parent + source: "WebEngine.qml" + + // TODO: Add support for QtWebView + // source: useWebEngine ? "WebEngine.qml" : "WebView.qml" +} diff --git a/src/vs/WebEngine.qml b/src/vs/WebEngine.qml new file mode 100644 index 0000000..a45b459 --- /dev/null +++ b/src/vs/WebEngine.qml @@ -0,0 +1,121 @@ +import QtQuick +import QtQuick.Controls +import QtWebEngine + +Item { + width: parent.width; height: parent.height + clip: true + + function contentScriptFactory (port) { + return ` + // add script tag for qwebchannel + document.head.addEventListener("initqwebchannel", () => { + + var script = document.createElement("script"); + script.onload = function () { + var url = "ws://localhost:${port}"; + var socket = new WebSocket(url); + + socket.onclose = function() { + console.error("[QT] web channel closed"); + }; + socket.onerror = function(event) { + console.error("[QT] web channel error: " + event.type); + }; + socket.onopen = function() { + new QWebChannel(socket, function(channel) { + console.log("[QT] Socket opened"); + + // make core object accessible globally + window.virtualstudio = channel.objects.virtualstudio; + window.auth = channel.objects.auth; + window.clipboard = channel.objects.clipboard; + + const event = new CustomEvent("qwebchannelinitialized"); + document.head.dispatchEvent(event); + console.log("[QT] Dispatched qwebchannelinitialized event"); + console.log("[QT] Connected to WebChannel, ready to send/receive messages!"); + }); + } + } + script.setAttribute("src", "qrc:///qtwebchannel/qwebchannel.js"); + script.setAttribute("type", "text/javascript"); + document.head.appendChild(script); + console.log("[QT] Added qwebchannel initialization script to DOM."); + }); + console.log("[QT] Added initqwebchannel event listener"); + ` + } + + Rectangle { + id: web + anchors.fill: parent + color: backgroundColour + + property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" + property string studioId: virtualstudio.currentStudio.id + + WebEngineView { + id: webEngineView + anchors.fill: parent + settings.javascriptCanAccessClipboard: true + settings.javascriptCanPaste: true + settings.screenCaptureEnabled: true + profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}` + url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}` + + // useful for debugging + // onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID) { + // console.log(level, message, lineNumber, sourceID); + // } + + // useful for debugging + // onLoadingChanged: function(loadRequest) { + // console.log("onLoadingChanged", loadRequest.errorCode, loadRequest.errorDomain, loadRequest.errorString, loadRequest.status, loadRequest.url); + // } + + onContextMenuRequested: function(request) { + // this disables the default context menu: https://doc.qt.io/qt-6.2/qml-qtwebengine-contextmenurequest.html#accepted-prop + request.accepted = true; + } + + onNewWindowRequested: function(request) { + Qt.openUrlExternally(request.requestedUrl); + } + + onFeaturePermissionRequested: function(securityOrigin, feature) { + webEngineView.grantFeaturePermission(securityOrigin, feature, true); + } + + onRenderProcessTerminated: function(terminationStatus, exitCode) { + var status = ""; + switch (terminationStatus) { + case WebEngineView.NormalTerminationStatus: + status = "(normal exit)"; + break; + case WebEngineView.AbnormalTerminationStatus: + status = "(abnormal exit)"; + break; + case WebEngineView.CrashedTerminationStatus: + status = "(crashed)"; + break; + case WebEngineView.KilledTerminationStatus: + status = "(killed)"; + break; + } + console.log("Render process exited with code " + exitCode + " " + status); + } + + onNavigationRequested: function(request) { + webEngineView.userScripts.collection = [ + { + name: "script", + sourceCode: contentScriptFactory(virtualstudio.webChannelPort), + injectionPoint: WebEngineScript.DocumentReady, + worldId: WebEngineScript.MainWorld + } + ] + } + } + } +} diff --git a/src/vs/WebNull.qml b/src/vs/WebNull.qml new file mode 100644 index 0000000..6f1c6de --- /dev/null +++ b/src/vs/WebNull.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Controls + +Item { + width: parent.width; height: parent.height + clip: true + + Item { + id: webNull + anchors.fill: parent + } +} diff --git a/src/vs/WebSocketTransport.cpp b/src/vs/WebSocketTransport.cpp new file mode 100644 index 0000000..d9c9688 --- /dev/null +++ b/src/vs/WebSocketTransport.cpp @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, +*author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "WebSocketTransport.h" + +#include +#include +#include +#include + +/*! + \brief QWebChannelAbstractSocket implementation that uses a QWebSocket internally. + The transport delegates all messages received over the QWebSocket over its + textMessageReceived signal. Analogously, all calls to sendTextMessage will + be send over the QWebSocket to the remote client. +*/ + +QT_BEGIN_NAMESPACE + +/*! + Construct the transport object and wrap the given socket. + The socket is also set as the parent of the transport object. +*/ +WebSocketTransport::WebSocketTransport(QWebSocket* socket) + : QWebChannelAbstractTransport(socket), m_socket(socket) +{ + connect(socket, &QWebSocket::textMessageReceived, this, + &WebSocketTransport::textMessageReceived); +} + +/*! + Destroys the WebSocketTransport. +*/ +WebSocketTransport::~WebSocketTransport() {} + +/*! + Serialize the JSON message and send it as a text message via the WebSocket to the + client. +*/ +void WebSocketTransport::sendMessage(const QJsonObject& message) +{ + QJsonDocument doc(message); + m_socket->sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); +} + +/*! + Deserialize the stringified JSON messageData and emit messageReceived. +*/ +void WebSocketTransport::textMessageReceived(const QString& messageData) +{ + QJsonParseError error; + QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error); + if (error.error) { + qWarning() << "Failed to parse text message as JSON object:" << messageData + << "Error is:" << error.errorString(); + return; + } else if (!message.isObject()) { + qWarning() << "Received JSON message that is not an object: " << messageData; + return; + } + emit messageReceived(message.object(), this); +} + +QT_END_NAMESPACE \ No newline at end of file diff --git a/src/vs/WebSocketTransport.h b/src/vs/WebSocketTransport.h new file mode 100644 index 0000000..4f93b23 --- /dev/null +++ b/src/vs/WebSocketTransport.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, +*author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef WEBSOCKETTRANSPORT_H +#define WEBSOCKETTRANSPORT_H + +#include + +QT_BEGIN_NAMESPACE + +class QWebSocket; +class WebSocketTransport : public QWebChannelAbstractTransport +{ + Q_OBJECT + public: + explicit WebSocketTransport(QWebSocket* socket); + virtual ~WebSocketTransport(); + + void sendMessage(const QJsonObject& message) Q_DECL_OVERRIDE; + + private Q_SLOTS: + void textMessageReceived(const QString& message); + + private: + QWebSocket* m_socket; +}; + +QT_END_NAMESPACE + +#endif // WEBSOCKETTRANSPORT_H \ No newline at end of file diff --git a/src/vs/WebView.qml b/src/vs/WebView.qml new file mode 100644 index 0000000..871a1d9 --- /dev/null +++ b/src/vs/WebView.qml @@ -0,0 +1,23 @@ +import QtQuick +import QtQuick.Controls +import QtWebView + +Item { + width: parent.width; height: parent.height + clip: true + + Item { + id: web + anchors.fill: parent + + property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" + property string studioId: virtualstudio.currentStudio.id + + WebView { + id: webEngineView + anchors.fill: parent + httpUserAgent: `JackTrip/${virtualstudio.versionString}` + url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}` + } + } +} diff --git a/src/vs/check.svg b/src/vs/check.svg new file mode 100644 index 0000000..333cf07 --- /dev/null +++ b/src/vs/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/close.svg b/src/vs/close.svg new file mode 100644 index 0000000..e96a4e7 --- /dev/null +++ b/src/vs/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/cog.svg b/src/vs/cog.svg new file mode 100644 index 0000000..ba5645a --- /dev/null +++ b/src/vs/cog.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/ethernet.svg b/src/vs/ethernet.svg new file mode 100644 index 0000000..d35645b --- /dev/null +++ b/src/vs/ethernet.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/expand_less.svg b/src/vs/expand_less.svg new file mode 100644 index 0000000..08039b6 --- /dev/null +++ b/src/vs/expand_less.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/expand_more.svg b/src/vs/expand_more.svg new file mode 100644 index 0000000..71b70fd --- /dev/null +++ b/src/vs/expand_more.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/externalMic.svg b/src/vs/externalMic.svg new file mode 100644 index 0000000..ad8e7dd --- /dev/null +++ b/src/vs/externalMic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/AE.svg b/src/vs/flags/AE.svg new file mode 100644 index 0000000..59ddafd --- /dev/null +++ b/src/vs/flags/AE.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/AU.svg b/src/vs/flags/AU.svg new file mode 100644 index 0000000..f91b013 --- /dev/null +++ b/src/vs/flags/AU.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/BE.svg b/src/vs/flags/BE.svg new file mode 100644 index 0000000..cc1b013 --- /dev/null +++ b/src/vs/flags/BE.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/BR.svg b/src/vs/flags/BR.svg new file mode 100644 index 0000000..f4dbb02 --- /dev/null +++ b/src/vs/flags/BR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/CA.svg b/src/vs/flags/CA.svg new file mode 100644 index 0000000..457d316 --- /dev/null +++ b/src/vs/flags/CA.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/CH.svg b/src/vs/flags/CH.svg new file mode 100644 index 0000000..498b7d1 --- /dev/null +++ b/src/vs/flags/CH.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/DE.svg b/src/vs/flags/DE.svg new file mode 100644 index 0000000..df0775b --- /dev/null +++ b/src/vs/flags/DE.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/FR.svg b/src/vs/flags/FR.svg new file mode 100644 index 0000000..9f02836 --- /dev/null +++ b/src/vs/flags/FR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/GB.svg b/src/vs/flags/GB.svg new file mode 100644 index 0000000..4ada58a --- /dev/null +++ b/src/vs/flags/GB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/HK.svg b/src/vs/flags/HK.svg new file mode 100644 index 0000000..284a722 --- /dev/null +++ b/src/vs/flags/HK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/ID.svg b/src/vs/flags/ID.svg new file mode 100644 index 0000000..45d3745 --- /dev/null +++ b/src/vs/flags/ID.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/IT.svg b/src/vs/flags/IT.svg new file mode 100644 index 0000000..17b1314 --- /dev/null +++ b/src/vs/flags/IT.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/JP.svg b/src/vs/flags/JP.svg new file mode 100644 index 0000000..92eb885 --- /dev/null +++ b/src/vs/flags/JP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/RO.svg b/src/vs/flags/RO.svg new file mode 100644 index 0000000..fabf12e --- /dev/null +++ b/src/vs/flags/RO.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/SE.svg b/src/vs/flags/SE.svg new file mode 100644 index 0000000..7ec1787 --- /dev/null +++ b/src/vs/flags/SE.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/SG.svg b/src/vs/flags/SG.svg new file mode 100644 index 0000000..c374c47 --- /dev/null +++ b/src/vs/flags/SG.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/TW.svg b/src/vs/flags/TW.svg new file mode 100644 index 0000000..c3660f1 --- /dev/null +++ b/src/vs/flags/TW.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/US.svg b/src/vs/flags/US.svg new file mode 100644 index 0000000..dc427e7 --- /dev/null +++ b/src/vs/flags/US.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/flags/ZA.svg b/src/vs/flags/ZA.svg new file mode 100644 index 0000000..1b294c9 --- /dev/null +++ b/src/vs/flags/ZA.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/headphones.svg b/src/vs/headphones.svg new file mode 100644 index 0000000..8041f67 --- /dev/null +++ b/src/vs/headphones.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/help.svg b/src/vs/help.svg new file mode 100644 index 0000000..d2a8d02 --- /dev/null +++ b/src/vs/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/jacktrip white.png b/src/vs/jacktrip white.png new file mode 100644 index 0000000..7ba69b0 Binary files /dev/null and b/src/vs/jacktrip white.png differ diff --git a/src/vs/jacktrip.png b/src/vs/jacktrip.png new file mode 100644 index 0000000..c4b998a Binary files /dev/null and b/src/vs/jacktrip.png differ diff --git a/src/vs/join.svg b/src/vs/join.svg new file mode 100644 index 0000000..bc50d97 --- /dev/null +++ b/src/vs/join.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/language.svg b/src/vs/language.svg new file mode 100644 index 0000000..8273aed --- /dev/null +++ b/src/vs/language.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/leave.svg b/src/vs/leave.svg new file mode 100644 index 0000000..c44f7e7 --- /dev/null +++ b/src/vs/leave.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/logo.svg b/src/vs/logo.svg new file mode 100644 index 0000000..508c81b --- /dev/null +++ b/src/vs/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/loud.svg b/src/vs/loud.svg new file mode 100644 index 0000000..b3aeb16 --- /dev/null +++ b/src/vs/loud.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/vs/manage.svg b/src/vs/manage.svg new file mode 100644 index 0000000..7d9ae31 --- /dev/null +++ b/src/vs/manage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/mic.svg b/src/vs/mic.svg new file mode 100644 index 0000000..520b7f9 --- /dev/null +++ b/src/vs/mic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/micoff.svg b/src/vs/micoff.svg new file mode 100644 index 0000000..8816538 --- /dev/null +++ b/src/vs/micoff.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/network.svg b/src/vs/network.svg new file mode 100644 index 0000000..4aa8827 --- /dev/null +++ b/src/vs/network.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/vs/networkCheck.svg b/src/vs/networkCheck.svg new file mode 100644 index 0000000..3b402a6 --- /dev/null +++ b/src/vs/networkCheck.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/private.svg b/src/vs/private.svg new file mode 100644 index 0000000..c08eb3d --- /dev/null +++ b/src/vs/private.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/public.svg b/src/vs/public.svg new file mode 100644 index 0000000..1407d57 --- /dev/null +++ b/src/vs/public.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/quiet.svg b/src/vs/quiet.svg new file mode 100644 index 0000000..b2ea070 --- /dev/null +++ b/src/vs/quiet.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/vs/refresh.svg b/src/vs/refresh.svg new file mode 100644 index 0000000..d2c5a92 --- /dev/null +++ b/src/vs/refresh.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/vs/sentiment_very_dissatisfied.svg b/src/vs/sentiment_very_dissatisfied.svg new file mode 100644 index 0000000..5af1e4b --- /dev/null +++ b/src/vs/sentiment_very_dissatisfied.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/share.svg b/src/vs/share.svg new file mode 100644 index 0000000..77f6e6c --- /dev/null +++ b/src/vs/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/speed.svg b/src/vs/speed.svg new file mode 100644 index 0000000..7ab86e4 --- /dev/null +++ b/src/vs/speed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/vs/star.svg b/src/vs/star.svg new file mode 100644 index 0000000..971dd91 --- /dev/null +++ b/src/vs/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/start.svg b/src/vs/start.svg new file mode 100644 index 0000000..f75a587 --- /dev/null +++ b/src/vs/start.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/video.svg b/src/vs/video.svg new file mode 100644 index 0000000..a1b644f --- /dev/null +++ b/src/vs/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/virtualstudio.cpp b/src/vs/virtualstudio.cpp new file mode 100644 index 0000000..6f27a67 --- /dev/null +++ b/src/vs/virtualstudio.cpp @@ -0,0 +1,1726 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file virtualstudio.cpp + * \author Matt Horton, based on code by Aaron Wyatt + * \date March 2022 + */ + +#include "virtualstudio.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// TODO: remove me; including this to work-around this bug +// https://bugreports.qt.io/browse/QTBUG-55199 +#include + +#include "../JackTrip.h" +#include "../Settings.h" +#include "../jacktrip_globals.h" +#include "JTApplication.h" +#include "WebSocketTransport.h" +#include "vsApi.h" +#include "vsAudio.h" +#include "vsAuth.h" +#include "vsDeeplink.h" +#include "vsDevice.h" +#include "vsQmlClipboard.h" +#include "vsWebSocket.h" + +#ifdef _WIN32 +#include +#endif + +#if defined(JACKTRIP_BUILD_INFO) +#define STR(s) #s +#define TO_STRING(s) STR(s) +const QString jackTripBuildInfo = QLatin1String(TO_STRING(JACKTRIP_BUILD_INFO)); +#else +const QString jackTripBuildInfo = QLatin1String(""); +#endif + +static QTextStream* ts; +static QFile outFile; + +void qtMessageHandler([[maybe_unused]] QtMsgType type, + [[maybe_unused]] const QMessageLogContext& context, + const QString& msg) +{ + std::cerr << msg.toStdString() << std::endl; + // Writes to file in order to debug bundles and executables + *ts << msg << Qt::endl; +} + +VirtualStudio::VirtualStudio(UserInterface& parent) + : QObject() + , m_interface(parent) + , m_view(new VsQuickView) + , m_audioConfigPtr( + new VsAudio(this)) // this needs to be constructed before loadSettings() +{ + // load or initialize persisted settings + loadSettings(); + + // TODO: remove me; this is a hack for this bug + // https://bugreports.qt.io/browse/QTBUG-55199 + QSvgGenerator svgImageHack; + + // use a singleton QNetworkAccessManager + // WARNING: using a raw pointer and intentionally leaking this because + // it crashes at shutdown if you try to destruct it directly or try + // calling QObject::deleteLater() + m_networkAccessManagerPtr = new QNetworkAccessManager; + + // instantiate API + m_api.reset(new VsApi(m_networkAccessManagerPtr)); + m_api->setApiHost(PROD_API_HOST); + if (m_testMode) { + m_api->setApiHost(TEST_API_HOST); + } + + // instantiate auth + m_auth.reset(new VsAuth(m_networkAccessManagerPtr, m_api.data())); + connect(m_auth.data(), &VsAuth::authSucceeded, this, + &VirtualStudio::slotAuthSucceeded); + connect(m_auth.data(), &VsAuth::refreshTokenFailed, this, [=]() { + m_auth->authenticate(QStringLiteral("")); // retry without using refresh token + }); + connect(m_auth.data(), &VsAuth::fetchUserInfoFailed, this, [=]() { + m_auth->authenticate(QStringLiteral("")); // retry without using refresh token + }); + connect(m_auth.data(), &VsAuth::deviceCodeExpired, this, [=]() { + m_auth->authenticate(QStringLiteral("")); // retry without using refresh token + }); + + m_webChannelServer.reset(new QWebSocketServer( + QStringLiteral("Qt6 Virtual Studio Server"), QWebSocketServer::NonSecureMode)); + connect(m_webChannelServer.data(), &QWebSocketServer::newConnection, this, [=]() { + m_webChannel->connectTo( + new WebSocketTransport(m_webChannelServer->nextPendingConnection())); + }); + + m_webChannel.reset(new QWebChannel()); + m_webChannel->registerObject(QStringLiteral("virtualstudio"), this); + + // Load our font for our qml interface + QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Regular.ttf")); + QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Bold.ttf")); + + // Set our font scaling to convert points to pixels + m_fontScale = float(4.0 / 3.0); + + // Initialize timer needed for network outage indicator + m_networkOutageTimer.setTimerType(Qt::CoarseTimer); + m_networkOutageTimer.setSingleShot(true); + m_networkOutageTimer.setInterval(5000); + m_networkOutageTimer.callOnTimeout([&]() { + if (m_devicePtr.isNull()) + return; + m_devicePtr->setNetworkOutage(false); + emit updatedNetworkOutage(false); + }); + + // register QML types + qmlRegisterType("org.jacktrip.jacktrip", 1, 0, "VsServerInfo"); + + // Register clipboard Qml type + qmlRegisterType("VS", 1, 0, "Clipboard"); + + // setup QML view + m_view->engine()->rootContext()->setContextProperty(QStringLiteral("virtualstudio"), + this); + m_view->engine()->rootContext()->setContextProperty(QStringLiteral("auth"), + m_auth.get()); + m_view->engine()->rootContext()->setContextProperty(QStringLiteral("audio"), + m_audioConfigPtr.get()); + m_view->engine()->rootContext()->setContextProperty( + QStringLiteral("permissions"), + QVariant::fromValue(&m_audioConfigPtr->getPermissions())); + m_view->setSource(QUrl(QStringLiteral("qrc:/vs/vs.qml"))); + m_view->setMinimumSize(QSize(800, 640)); + // m_view->setMaximumSize(QSize(696, 577)); + m_view->setResizeMode(QQuickView::SizeRootObjectToView); + m_view->resize(800 * m_uiScale, 640 * m_uiScale); + + // Connect our timers + connect(this, &VirtualStudio::scheduleStudioRefresh, this, + &VirtualStudio::refreshStudios, Qt::QueuedConnection); + connect(&m_heartbeatTimer, &QTimer::timeout, this, &VirtualStudio::sendHeartbeat, + Qt::QueuedConnection); + + // QueuedConnection since refreshFinished is sometimes signaled from a network reply + // thread + connect(this, &VirtualStudio::refreshFinished, this, &VirtualStudio::joinStudio, + Qt::QueuedConnection); + + // handle audio config errors + connect(&m_audioConfigPtr->getWorker(), &VsAudioWorker::signalError, this, + &VirtualStudio::processError, Qt::QueuedConnection); + + // when connected to server, trigger UI modal when feedback is detected + connect(m_audioConfigPtr.get(), &VsAudio::feedbackDetected, this, + &VirtualStudio::detectedFeedbackLoop, Qt::QueuedConnection); + + // call exit() when the UI window is closed + connect(m_view.get(), &VsQuickView::windowClose, this, &VirtualStudio::exit, + Qt::QueuedConnection); + + // prepare handler for deeplinks jacktrip://join/ + m_deepLinkPtr.reset(new VsDeeplink(m_interface.getSettings().getDeeplink())); + if (!m_deepLinkPtr->getDeeplink().isEmpty()) { + bool readyForExit = m_deepLinkPtr->waitForReady(); + if (readyForExit) + std::exit(0); + } + + // Log to file + QString logPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + QDir logDir; + if (!logDir.exists(logPath)) { + logDir.mkpath(logPath); + } + QString fileLoc(logPath.append("/log.txt")); + qDebug() << "Log file location:" << fileLoc; + outFile.setFileName(fileLoc); + if (!outFile.open(QIODevice::WriteOnly | QIODevice::Append)) { + qDebug() << "Log file open failed:" << outFile.errorString(); + } + ts = new QTextStream(&outFile); + qInstallMessageHandler(qtMessageHandler); + + // Get ready for deep link signals + QObject::connect(m_deepLinkPtr.get(), &VsDeeplink::signalDeeplink, this, + &VirtualStudio::handleDeeplinkRequest, Qt::QueuedConnection); + m_deepLinkPtr->readyForSignals(); +} + +void VirtualStudio::show() +{ + if (m_checkSsl) { + // Check our available SSL version + QString sslVersion = QSslSocket::sslLibraryVersionString(); + // Important: this needs to be output with qDebug rather than to std::cout + // otherwise it may get passed to an existing JackTrip instance in place of our + // deeplink. (Need to find the root cause of this.) + qDebug() << "SSL Library: " << sslVersion; + if (sslVersion.isEmpty()) { + QMessageBox msgBox; + msgBox.setText( + QStringLiteral("OpenSSL was not found. You will not be able to connect " + "to the Virtual Studio server.")); + msgBox.setWindowTitle(QStringLiteral("SSL Error")); + msgBox.exec(); + } + m_checkSsl = false; + } + + while (m_view->status() == QQuickView::Loading) { + // I don't think there is any need to load network data, but just in case + // See https://doc.qt.io/qt-6/qquickview.html#Status-enum + qDebug() << "JackTrip is still loading the QML view"; + QThread::sleep(1); + } + + if (m_view->status() != QQuickView::Ready) { + QMessageBox msgBox; + msgBox.setText( + "JackTrip detected that some modules required for the " + "Virtual Studio mode are missing on your system. " + "Click \"OK\" to proceed to classic mode.\n\n" + "Details: JackTrip failed to load the QML view. " + "This is likely caused by missing QML plugins. " + "Please consult support.jacktrip.com for possible solutions."); + msgBox.setWindowTitle(QStringLiteral("JackTrip Is Missing QML Modules")); + connect(&msgBox, &QMessageBox::finished, this, &VirtualStudio::toClassicMode, + Qt::QueuedConnection); + msgBox.exec(); + return; + } + + raiseToTop(); +} + +void VirtualStudio::hide() +{ + if (!m_view.isNull()) + m_view->hide(); +} + +void VirtualStudio::raiseToTop() +{ + if (m_view->status() != QQuickView::Ready) + return; + m_view->show(); // Restore from systray + m_view->raise(); // raise to top + m_view->requestActivate(); // focus on window +} + +int VirtualStudio::webChannelPort() +{ + return m_webChannelPort; +} + +bool VirtualStudio::hasRefreshToken() +{ + return !m_refreshToken.isEmpty(); +} + +QString VirtualStudio::versionString() +{ + return QLatin1String(gVersion); +} + +QString VirtualStudio::buildString() +{ + QString result; + if (!jackTripBuildInfo.isEmpty()) { + result += "Build "; + result += jackTripBuildInfo; + result += "
\n"; + } + result += "Qt version "; + result += QT_VERSION_STR; +#ifdef QT_OPENSOURCE + result += " (open source)"; +#else + result += " (commercial)"; +#endif + return result; +} + +QString VirtualStudio::copyrightString() +{ + QString result; + bool gplLicense = false; +#ifdef QT_OPENSOURCE + gplLicense = true; +#endif + + result += + "Copyright © 2008-2024 Juan-Pablo Caceres, Chris Chafe, et al. SoundWIRE " + "group at CCRMA, Stanford University.

\n"; + result += + "Virtual Studio interface and integration Copyright © 2022-2024 JackTrip " + "Labs, Inc.

\n"; + + if (hasClassicMode()) { + gplLicense = true; + result += + "Classic mode graphical user interface component originally released as " + "QJackTrip, Copyright © 2020 Aaron Wyatt.

\n"; + } + + if (m_audioConfigPtr->asioIsAvailable()) { + result += + "This build of JackTrip includes support for ASIO. ASIO is a trademark and " + "software of Steinberg Media Technologies GmbH.

"; + } + + result += + "This app is free and open source software provided "as is" under the "; + result += (gplLicense ? "GPL" : "MIT"); + result += " license, without warranty of any kind.\n"; + result += "See the included LICENSE.md file for more information.

\n"; + + return result; +} + +QString VirtualStudio::logoSection() +{ + return m_logoSection; +} + +QString VirtualStudio::connectedErrorMsg() +{ + return m_connectedErrorMsg; +} + +void VirtualStudio::setConnectedErrorMsg(const QString& msg) +{ + if (m_connectedErrorMsg == msg) + return; + m_connectedErrorMsg = msg; + emit connectedErrorMsgChanged(); +} + +bool VirtualStudio::networkOutage() +{ + return m_devicePtr.isNull() ? false : m_devicePtr->getNetworkOutage(); +} + +QJsonObject VirtualStudio::regions() +{ + return m_regions; +} + +QJsonObject VirtualStudio::userMetadata() +{ + return m_userMetadata; +} + +QString VirtualStudio::connectionState() +{ + return m_connectionState; +} + +QJsonObject VirtualStudio::networkStats() +{ + return m_networkStats; +} + +QString VirtualStudio::updateChannel() +{ + return m_updateChannel; +} + +void VirtualStudio::setUpdateChannel(const QString& channel) +{ + if (m_updateChannel == channel) + return; + m_updateChannel = channel; + emit updateChannelChanged(); +} + +bool VirtualStudio::showInactive() +{ + return m_showInactive; +} + +void VirtualStudio::setShowInactive(bool inactive) +{ + if (m_showInactive == inactive) + return; + m_showInactive = inactive; + emit showInactiveChanged(); + + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("ShowInactive"), m_showInactive); + settings.endGroup(); +} + +bool VirtualStudio::showSelfHosted() +{ + return m_showSelfHosted; +} + +void VirtualStudio::setShowSelfHosted(bool selfHosted) +{ + if (m_showSelfHosted == selfHosted) + return; + m_showSelfHosted = selfHosted; + emit showSelfHostedChanged(); + + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("ShowSelfHosted"), m_showSelfHosted); + settings.endGroup(); +} + +bool VirtualStudio::showDeviceSetup() +{ + return m_showDeviceSetup; +} + +void VirtualStudio::setShowDeviceSetup(bool show) +{ + if (m_showDeviceSetup == show) + return; + m_showDeviceSetup = show; + emit showDeviceSetupChanged(); +} + +QString VirtualStudio::windowState() +{ + return m_windowState; +} + +void VirtualStudio::setWindowState(QString state) +{ + if (m_windowState == state) + return; + m_windowState = state; + // refresh studio list if navigating to browse window + // only if user id is empty (edge case for when logging in) + if (m_windowState == "browse" && !m_userId.isEmpty()) { + // schedule studio refresh instead of doing it now + // just to reduce risk of running into a deadlock + emit scheduleStudioRefresh(-1, false); + } + emit windowStateUpdated(); +} + +QString VirtualStudio::apiHost() +{ + return m_apiHost; +} + +void VirtualStudio::setApiHost(QString host) +{ + if (m_apiHost == host) + return; + m_apiHost = host; + emit apiHostChanged(); +} + +bool VirtualStudio::vsFtux() +{ + return m_vsFtux; +} + +bool VirtualStudio::isExiting() +{ + return m_isExiting; +} + +bool VirtualStudio::refreshInProgress() +{ + return m_refreshInProgress; +} + +void VirtualStudio::setRefreshInProgress(bool b) +{ + if (m_refreshInProgress == b) + return; + m_refreshInProgress = b; + emit refreshInProgressChanged(); +} + +void VirtualStudio::collectFeedbackSurvey(QString serverId, int rating, QString message) +{ + QJsonObject feedback; + + QString sysInfo = QString("[platform=%1").arg(QSysInfo::prettyProductName()); +#ifdef RT_AUDIO + QString inputDevice = + QString::fromStdString(m_audioConfigPtr->getInputDevice().toStdString()); + if (!inputDevice.isEmpty()) { + sysInfo.append(QString(",input=%1").arg(inputDevice)); + } + QString outputDevice = + QString::fromStdString(m_audioConfigPtr->getOutputDevice().toStdString()); + if (!outputDevice.isEmpty()) { + sysInfo.append(QString(",output=%1").arg(outputDevice)); + } +#endif + sysInfo.append("]"); + + feedback.insert(QStringLiteral("rating"), rating); + if (message.isEmpty()) { + feedback.insert(QStringLiteral("message"), sysInfo); + } else { + feedback.insert(QStringLiteral("message"), message + " " + sysInfo); + } + + QJsonDocument data = QJsonDocument(feedback); + m_api->submitServerFeedback(serverId, data.toJson()); + return; +} + +bool VirtualStudio::showWarnings() +{ + return m_showWarnings; +} + +void VirtualStudio::setShowWarnings(bool show) +{ + if (m_showWarnings == show) + return; + m_showWarnings = show; + emit showWarningsChanged(); +} + +float VirtualStudio::fontScale() +{ + return m_fontScale; +} + +float VirtualStudio::uiScale() +{ + return m_uiScale; +} + +void VirtualStudio::setUiScale(float scale) +{ + if (scale == m_uiScale) + return; + m_uiScale = scale; + emit uiScaleChanged(); +} + +bool VirtualStudio::darkMode() +{ + return m_darkMode; +} + +void VirtualStudio::setDarkMode(bool dark) +{ + if (dark == m_darkMode) + return; + m_darkMode = dark; + emit darkModeChanged(); +} + +bool VirtualStudio::collapseDeviceControls() +{ + return m_collapseDeviceControls; +} + +void VirtualStudio::setCollapseDeviceControls(bool collapseDeviceControls) +{ + if (m_collapseDeviceControls == collapseDeviceControls) + return; + m_collapseDeviceControls = collapseDeviceControls; + emit collapseDeviceControlsChanged(collapseDeviceControls); +} + +bool VirtualStudio::testMode() +{ + return m_testMode; +} + +void VirtualStudio::setTestMode(bool test) +{ + if (m_testMode == test) + return; + + QString userEmail = m_userMetadata[QStringLiteral("email")].toString(); + if (m_userMetadata.isEmpty() || userEmail == "" + || !userEmail.endsWith("@jacktrip.org")) { + qDebug() << "Not allowed"; + return; + } + + // deregister app + if (!m_devicePtr.isNull()) { + m_devicePtr->removeApp(); + m_devicePtr->disconnect(); + m_devicePtr.reset(); + } + + m_testMode = test; + + // clear existing auth state + m_auth->logout(); + + // Clear existing registers - any existing instance data will be overwritten + // when m_auth->authenticate finishes and slotAuthSucceeded() is called again + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("TestMode"), m_testMode); + settings.remove(QStringLiteral("RefreshToken")); + settings.remove(QStringLiteral("UserId")); + settings.endGroup(); + + // stop timers, clear data, etc. + resetState(); + + // clear user data + m_userMetadata = QJsonObject(); + m_userId.clear(); + + // re-run authentication. This should not require another browser flow since + // we're starting with the existing refresh token + m_auth->authenticate(m_refreshToken); + emit testModeChanged(); +} + +QString VirtualStudio::studioToJoin() +{ + return m_studioToJoin; +} + +void VirtualStudio::setStudioToJoin(const QString& id) +{ + if (m_studioToJoin == id) + return; + m_studioToJoin = id; + emit studioToJoinChanged(); +} + +bool VirtualStudio::noUpdater() +{ +#ifdef NO_UPDATER + return true; +#else + return false; +#endif +} + +bool VirtualStudio::psiBuild() +{ +#ifdef PSI + return true; +#else + return false; +#endif +} + +QString VirtualStudio::failedMessage() +{ + return m_failedMessage; +} + +void VirtualStudio::joinStudio() +{ + // nothing to do unless on setup or connected windows with studio to join + if ((m_windowState != "setup" && m_windowState != "connected") + || !m_auth->isAuthenticated() || m_studioToJoin.isEmpty()) + return; + + // make sure we've retrieved a list of servers + QMutexLocker locker(&m_refreshMutex); + if (m_servers.isEmpty()) { + // No servers yet. Making sure we have them. + // refreshStudios emits refreshFinished which + // will come back to this function. + locker.unlock(); + refreshStudios(-1, true); + return; + } + + // pop studioToJoin + const QString targetId = m_studioToJoin; + setStudioToJoin(""); + + // stop audio if already running (settings or setup windows) + m_audioConfigPtr->stopAudio(true); + + // find and populate data for current studio + VsServerInfoPointer sPtr; + for (const VsServerInfoPointer& s : m_servers) { + if (s->id() == targetId) { + sPtr = s; + break; + } + } + locker.unlock(); + + if (sPtr.isNull()) { + m_failedMessage = "Unable to find studio " + targetId; + emit failedMessageChanged(); + emit failed(); + return; + } + + m_currentStudio = *sPtr; + emit currentStudioChanged(); + + if (m_windowState == "setup") { + m_audioConfigPtr->setSampleRate(m_currentStudio.sampleRate()); + m_audioConfigPtr->startAudio(); + return; + } + + // m_windowState == "connected" + connectToStudio(); +} + +bool VirtualStudio::hasClassicMode() +{ +#ifdef NO_CLASSIC + return false; +#else + return true; +#endif // NO_CLASSIC +} + +void VirtualStudio::toVirtualStudioMode() +{ + QSettings settings; + settings.setValue(QStringLiteral("UiMode"), UserInterface::MODE_VS); + setWindowState(QStringLiteral("login")); + login(); +} + +void VirtualStudio::toClassicMode() +{ + if (hasClassicMode()) { + m_interface.setMode(UserInterface::MODE_CLASSIC); + + QSettings settings; + settings.setValue(QStringLiteral("UiMode"), UserInterface::MODE_CLASSIC); + + // stop timers, clear data, etc. + resetState(); + setWindowState(QStringLiteral("start")); + m_auth->logout(); + } else { + std::cerr << "JackTrip was not built with support for Classic mode." << std::endl; + } +} + +void VirtualStudio::login() +{ + if (m_refreshToken.isEmpty()) { + m_auth->authenticate(QStringLiteral("")); + } else { + m_auth->authenticate(m_refreshToken); + } +} + +void VirtualStudio::logout() +{ + // deregister app + if (!m_devicePtr.isNull()) { + m_devicePtr->removeApp(); + m_devicePtr->disconnect(); + m_devicePtr.reset(); + } + + QUrl logoutURL = QUrl("https://auth.jacktrip.org/v2/logout"); + QUrlQuery query; + query.addQueryItem(QStringLiteral("client_id"), AUTH_CLIENT_ID); + if (m_testMode) { + query.addQueryItem(QStringLiteral("returnTo"), + QStringLiteral("https://next-test.jacktrip.com/")); + } else { + query.addQueryItem(QStringLiteral("returnTo"), + QStringLiteral("https://www.jacktrip.com/")); + } + + logoutURL.setQuery(query); + QDesktopServices::openUrl(logoutURL); + + m_auth->logout(); + + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.remove(QStringLiteral("RefreshToken")); + settings.remove(QStringLiteral("UserId")); + settings.endGroup(); + + // stop timers, clear data, etc. + resetState(); + + // clear user data + m_refreshToken.clear(); + m_userMetadata = QJsonObject(); + m_userId.clear(); + emit hasRefreshTokenChanged(); + + // reset window state + setWindowState(QStringLiteral("login")); + login(); // called to retrieve new code flow token +} + +void VirtualStudio::loadSettings() +{ + QSettings settings; + setUpdateChannel( + settings.value(QStringLiteral("UpdateChannel"), "stable").toString().toLower()); + + settings.beginGroup(QStringLiteral("VirtualStudio")); + m_refreshToken = settings.value(QStringLiteral("RefreshToken"), "").toString(); + m_userId = settings.value(QStringLiteral("UserId"), "").toString(); + m_testMode = settings.value(QStringLiteral("TestMode"), false).toBool(); + m_showInactive = settings.value(QStringLiteral("ShowInactive"), true).toBool(); + m_showSelfHosted = settings.value(QStringLiteral("ShowSelfHosted"), true).toBool(); + + // use setters to emit signals for these if they change; otherwise, the + // user interface will not revert back after cancelling settings changes + setUiScale(settings.value(QStringLiteral("UiScale"), 1).toFloat()); + setDarkMode(settings.value(QStringLiteral("DarkMode"), false).toBool()); + setShowDeviceSetup(settings.value(QStringLiteral("ShowDeviceSetup"), true).toBool()); + setShowWarnings(settings.value(QStringLiteral("ShowWarnings"), true).toBool()); + settings.endGroup(); + + m_audioConfigPtr->loadSettings(); +} + +void VirtualStudio::saveSettings() +{ + QSettings settings; + settings.setValue(QStringLiteral("UpdateChannel"), m_updateChannel); + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("UiScale"), m_uiScale); + settings.setValue(QStringLiteral("DarkMode"), m_darkMode); + settings.setValue(QStringLiteral("ShowDeviceSetup"), m_showDeviceSetup); + settings.setValue(QStringLiteral("ShowWarnings"), m_showWarnings); + settings.endGroup(); + + m_audioConfigPtr->saveSettings(); +} + +void VirtualStudio::connectToStudio() +{ + m_networkStats = QJsonObject(); + emit networkStatsChanged(); + + m_onConnectedScreen = true; + + m_studioSocketPtr.reset(new VsWebSocket( + QUrl(QStringLiteral("wss://%1/api/servers/%2?auth_code=%3") + .arg(m_api->getApiHost(), m_currentStudio.id(), m_auth->accessToken())), + m_auth->accessToken(), QString(), QString())); + connect(m_studioSocketPtr.get(), &VsWebSocket::textMessageReceived, this, + &VirtualStudio::handleWebsocketMessage); + connect(m_studioSocketPtr.get(), &VsWebSocket::disconnected, this, + &VirtualStudio::restartStudioSocket); + m_studioSocketPtr->openSocket(); + + // Check if we have an address for our server + if (m_currentStudio.status() != "Ready") { + m_connectionState = QStringLiteral("Waiting..."); + emit connectionStateChanged(); + } else { + completeConnection(); + } + + m_reconnectState = ReconnectState::NOT_RECONNECTING; +} + +void VirtualStudio::completeConnection() +{ + // sanity check + if (m_currentStudio.id() == "" + || m_currentStudio.status() == QStringLiteral("Disabled")) { + processError("Studio session has ended"); + return; + } + + // these shouldn't happen + if (m_currentStudio.status() != "Ready") { + processError("Studio session is not ready"); + return; + } + if (m_currentStudio.host().isEmpty()) { + processError("Studio host is unknown"); + return; + } + + // always connect with audio device controls open + setCollapseDeviceControls(false); + + m_jackTripRunning = true; + m_connectionState = QStringLiteral("Preparing audio..."); + emit connectionStateChanged(); + try { + bool useRtAudio = m_audioConfigPtr->getUseRtAudio(); + std::string input = ""; + std::string output = ""; + int buffer_size = 0; + int inputMixMode = -1; + int baseInputChannel = 0; + int numInputChannels = 2; + int baseOutputChannel = 0; + int numOutputChannels = 2; +#ifdef RT_AUDIO + if (useRtAudio) { + // pre-populate device cache and validate first, if using rtaudio + if (!m_audioConfigPtr->getDeviceModelsInitialized()) + m_audioConfigPtr->refreshDevices(true); + // initialize jacktrip using audio settings + input = m_audioConfigPtr->getInputDevice().toStdString(); + output = m_audioConfigPtr->getOutputDevice().toStdString(); + buffer_size = m_audioConfigPtr->getBufferSize(); + inputMixMode = m_audioConfigPtr->getInputMixMode(); + baseInputChannel = m_audioConfigPtr->getBaseInputChannel(); + numInputChannels = m_audioConfigPtr->getNumInputChannels(); + baseOutputChannel = m_audioConfigPtr->getBaseOutputChannel(); + numOutputChannels = m_audioConfigPtr->getNumOutputChannels(); + } +#endif + + // adjust queueBuffer config setting to map to auto headroom + int queue_buffer = m_audioConfigPtr->getQueueBuffer(); + if (queue_buffer <= 0) { + queue_buffer = -500; + } else { + queue_buffer *= -1; + } + + // create a new JackTrip instance + JackTrip* jackTrip = m_devicePtr->initJackTrip( + useRtAudio, input, output, baseInputChannel, numInputChannels, + baseOutputChannel, numOutputChannels, inputMixMode, buffer_size, queue_buffer, + &m_currentStudio); + if (jackTrip == 0) { + processError("Could not bind port"); + return; + } + jackTrip->setIOStatTimeout(m_interface.getSettings().getIOStatTimeout()); + m_audioConfigPtr->setSampleRate(jackTrip->getSampleRate()); + + // this passes ownership to JackTrip + jackTrip->setAudioInterface(m_audioConfigPtr->newAudioInterface(jackTrip)); + + QObject::connect(jackTrip, &JackTrip::signalProcessesStopped, this, + &VirtualStudio::connectionFinished, Qt::QueuedConnection); + QObject::connect(jackTrip, &JackTrip::signalError, this, + &VirtualStudio::processError, Qt::QueuedConnection); + QObject::connect(jackTrip, &JackTrip::signalReceivedConnectionFromPeer, this, + &VirtualStudio::receivedConnectionFromPeer, + Qt::QueuedConnection); + QObject::connect(jackTrip, &JackTrip::signalUdpWaitingTooLong, this, + &VirtualStudio::udpWaitingTooLong, Qt::QueuedConnection); + + m_connectionState = QStringLiteral("Connecting..."); + emit connectionStateChanged(); + m_devicePtr->startJackTrip(m_currentStudio); + + // update device error messages and warnings based on latest results + // this is necessary because we may have never loaded audio settings, + // or the state may have changed via the connected change devices screen + m_audioConfigPtr->setDevicesWarningMsg(jackTrip->getDevicesWarningMsg()); + m_audioConfigPtr->setDevicesErrorMsg(jackTrip->getDevicesErrorMsg()); + m_audioConfigPtr->setDevicesWarningHelpUrl(jackTrip->getDevicesWarningHelpUrl()); + m_audioConfigPtr->setDevicesErrorHelpUrl(jackTrip->getDevicesErrorHelpUrl()); + m_audioConfigPtr->setHighLatencyFlag(jackTrip->getHighLatencyFlag()); + + } catch (const std::exception& e) { + // Let the user know what our exception was. + m_connectionState = QStringLiteral("JackTrip Error"); + emit connectionStateChanged(); + + processError(QString::fromUtf8(e.what())); + return; + } + + m_interface.enableNap(); +} + +void VirtualStudio::triggerReconnect(bool refresh) +{ + if (!m_jackTripRunning || m_devicePtr.isNull()) { + if (refresh) + m_audioConfigPtr->refreshDevices(true); + else + m_audioConfigPtr->validateDevices(); + return; + } + + // this needs to be synchronous to avoid both trying + // to use the audio interfaces at the same time + // note that connectionFinished() checks m_reconnectState + // and uses that to update audio, then reconnect + m_reconnectState = refresh ? ReconnectState::RECONNECTING_REFRESH + : ReconnectState::RECONNECTING_VALIDATE; + m_connectionState = QStringLiteral("Reconnecting..."); + emit connectionStateChanged(); + + // keep device enabled while stopping jacktrip + m_devicePtr->stopJackTrip(true); +} + +void VirtualStudio::disconnect() +{ + // stop jackrip if it's running + if (m_jackTripRunning) { + m_devicePtr->stopJackTrip(false); + // persist any volume level or device changes + m_audioConfigPtr->saveSettings(); + } + + m_connectionState = QStringLiteral("Disconnected"); + emit connectionStateChanged(); + setConnectedErrorMsg(""); + + if (m_isExiting) { + emit signalExit(); + return; + } + + // if this occurs on the setup or settings screen (for example, due to an issue with + // devices) then don't emit disconnected, as that would move you back to the "Browse" + // screen + if (m_onConnectedScreen) { + m_onConnectedScreen = false; + emit disconnected(); + } + + if (!m_jackTripRunning) { + return; + } + m_jackTripRunning = false; + + if (!m_studioSocketPtr.isNull()) { + m_studioSocketPtr->closeSocket(); + m_studioSocketPtr->disconnect(); + m_studioSocketPtr.reset(); + } + + // reset network statistics + m_networkStats = QJsonObject(); + + if (!m_currentStudio.id().isEmpty()) { + emit openFeedbackSurveyModal(m_currentStudio.id()); + m_currentStudio.setId(""); + emit currentStudioChanged(); + } + + m_interface.enableNap(); +} + +void VirtualStudio::editProfile() +{ + QUrl url = QUrl(QStringLiteral("https://www.jacktrip.com/profile")); + if (testMode()) { + url = QUrl(QStringLiteral("https://next-test.jacktrip.com/profile")); + } + QDesktopServices::openUrl(url); +} + +void VirtualStudio::showAbout() +{ + emit openAboutWindow(); +} + +void VirtualStudio::openLink(const QString& link) +{ + QUrl url = QUrl(link); + QDesktopServices::openUrl(url); +} + +void VirtualStudio::handleDeeplinkRequest(const QUrl& link) +{ + // check link is valid + QString studioId; + if (link.scheme() != QLatin1String("jacktrip") || link.path().length() <= 1) { + qDebug() << "Ignoring invalid deeplink to" << link; + return; + } + if (link.host() == QLatin1String("join")) { + studioId = link.path().remove(0, 1); + } else if (link.host().isEmpty() && link.path().startsWith("join/")) { + studioId = link.path().remove(0, 5); + } else { + qDebug() << "Ignoring invalid deeplink to" << link; + return; + } + + // check if already connected (ignore) + if (m_windowState == "connected" || m_windowState == "change_devices") { + qDebug() << "Already connected; ignoring deeplink to" << link; + return; + } + + if (m_windowState == "setup" + && (m_studioToJoin == studioId || m_currentStudio.id() == studioId)) { + qDebug() << "Already preparing to connect; ignoring deeplink to" << link; + return; + } + + qDebug() << "Handling deeplink to" << link; + setStudioToJoin(studioId); + + // Switch to virtual studio mode, if necessary + // Note that this doesn't change the startup preference + if (m_interface.getMode() != UserInterface::MODE_VS) { + m_interface.setMode(UserInterface::MODE_VS); + } else { + raiseToTop(); + } + + // automatically navigate if on certain window screens + // note that the studio creation happens inside of the web view, + // and the app doesn't really know anything about it. we depend + // on the web app triggering a deep link join event, which is + // handled here. it's unlikely that the new studio has been + // noticed yet, so we don't join right away; otherwise we'd just + // get an unknown studio error. instead, we trigger a refresh and + // rely on it to kick off the join afterwards. + if (m_windowState == "browse" || m_windowState == "create_studio" + || m_windowState == "settings" || m_windowState == "setup" + || m_windowState == "failed") { + if (showDeviceSetup()) { + setWindowState("setup"); + } else { + setWindowState("connected"); + } + refreshStudios(0, true); + } + + // otherwise, assume we are on setup screens and can let the normal flow handle it +} + +void VirtualStudio::exit() +{ + // if multiple close events are received, emit the signalExit to force-quit the app + if (m_isExiting) { + emit signalExit(); + } + + // triggering isExitingChanged will force any WebEngine things to close properly + m_isExiting = true; + emit isExitingChanged(); + + // stop timers, clear data, etc. + resetState(); + + if (m_onConnectedScreen) { + // manually disconnect on self-managed studios + if (!m_currentStudio.id().isEmpty() && !m_currentStudio.isManaged()) { + disconnect(); + } + } else { + emit signalExit(); + } +} + +void VirtualStudio::slotAuthSucceeded() +{ + // Make sure window is on top (instead of browser, during first auth) + raiseToTop(); + + // Determine which API host to use + m_apiHost = PROD_API_HOST; + if (m_testMode) { + m_apiHost = TEST_API_HOST; + } + m_api->setApiHost(m_apiHost); + + // Get refresh token and userId + m_refreshToken = m_auth->refreshToken(); + m_userId = m_auth->userId(); + emit hasRefreshTokenChanged(); + + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("RefreshToken"), m_refreshToken); + settings.setValue(QStringLiteral("UserId"), m_userId); + settings.endGroup(); + + // initialize new VsDevice and wire up signals/slots before registering app + m_devicePtr.reset(new VsDevice(m_auth, m_api, m_audioConfigPtr)); + connect(m_devicePtr.get(), &VsDevice::updateNetworkStats, this, + &VirtualStudio::updatedStats); + connect(m_audioConfigPtr.get(), &VsAudio::updatedInputVolume, m_devicePtr.get(), + &VsDevice::syncDeviceSettings); + connect(m_audioConfigPtr.get(), &VsAudio::updatedInputMuted, m_devicePtr.get(), + &VsDevice::syncDeviceSettings); + connect(m_audioConfigPtr.get(), &VsAudio::updatedOutputVolume, m_devicePtr.get(), + &VsDevice::syncDeviceSettings); + connect(m_audioConfigPtr.get(), &VsAudio::updatedMonitorVolume, m_devicePtr.get(), + &VsDevice::syncDeviceSettings); + + m_devicePtr->registerApp(); + + if (!m_webChannelServer->listen(QHostAddress::LocalHost)) { + // shouldn't happen + std::cout << "ERROR: Failed to start server!" << std::endl; + } + m_webChannelPort = m_webChannelServer->serverPort(); + emit webChannelPortChanged(m_webChannelPort); + std::cout << "QWebChannel listening on port: " << m_webChannelPort << std::endl; + + getRegions(); + getUserMetadata(); // calls refreshStudios(-1, false) +} + +void VirtualStudio::connectionFinished() +{ + if (!m_devicePtr.isNull() + && (m_reconnectState == ReconnectState::RECONNECTING_VALIDATE + || m_reconnectState == ReconnectState::RECONNECTING_REFRESH)) { + if (m_devicePtr->hasTerminated()) { + if (m_reconnectState == ReconnectState::RECONNECTING_REFRESH) { + m_audioConfigPtr->refreshDevices(true); + } else { + m_audioConfigPtr->validateDevices(true); + } + connectToStudio(); + } + return; + } + m_reconnectState = ReconnectState::NOT_RECONNECTING; + + // use disconnect function to handle reset of all internal flags and timers + disconnect(); +} + +void VirtualStudio::processError(const QString& errorMessage) +{ + static const QString RtAudioErrorMsg = QStringLiteral("RtAudio Error"); + static const QString JackAudioErrorMsg = + QStringLiteral("The Jack server was shut down"); + + const bool shouldSwitchToRtAudio = + (errorMessage == QLatin1String("Maybe the JACK server is not running?")); + + QMessageBox msgBox; + if (shouldSwitchToRtAudio) { + // Report the other end quitting as a regular occurance rather than an error. + msgBox.setText("The JACK server is not running. Switching back to RtAudio."); + msgBox.setWindowTitle(QStringLiteral("No JACK server")); + } else if (errorMessage == QLatin1String("Peer Stopped")) { + // Report the other end quitting as a regular occurance rather than an error. + msgBox.setText("The Studio has been stopped."); + msgBox.setWindowTitle(QStringLiteral("Disconnected")); + } else if (errorMessage.startsWith(RtAudioErrorMsg)) { + if (errorMessage.length() > RtAudioErrorMsg.length() + 2) { + const QString details(errorMessage.sliced(RtAudioErrorMsg.length() + 2)); + if (details.contains(QStringLiteral("device was disconnected")) + || details.contains( + QStringLiteral("Unable to retrieve capture buffer"))) { + msgBox.setText(QStringLiteral("Your audio interface was disconnected.")); + } else { + msgBox.setText(details); + } + } else { + msgBox.setText(errorMessage); + } + msgBox.setWindowTitle(QStringLiteral("Audio Interface Error")); + } else if (errorMessage.startsWith(JackAudioErrorMsg)) { + if (errorMessage.length() > JackAudioErrorMsg.length() + 2) { + msgBox.setText(errorMessage.sliced(JackAudioErrorMsg.length() + 2)); + } else { + msgBox.setText(QStringLiteral("The JACK Audio Server was stopped.")); + } + msgBox.setWindowTitle(QStringLiteral("Jack Audio Error")); + } else { + msgBox.setText(QStringLiteral("Error: ").append(errorMessage)); + msgBox.setWindowTitle(QStringLiteral("Doh!")); + } + msgBox.exec(); + + if (m_jackTripRunning) + connectionFinished(); +} + +void VirtualStudio::receivedConnectionFromPeer() +{ + // Connect via API + m_connectionState = QStringLiteral("Connected"); + emit connectionStateChanged(); + std::cout << "Received connection" << std::endl; + emit connected(); +} + +void VirtualStudio::handleWebsocketMessage(const QString& msg) +{ + QJsonObject serverState = QJsonDocument::fromJson(msg.toUtf8()).object(); + QString serverStatus = serverState[QStringLiteral("status")].toString(); + bool serverEnabled = serverState[QStringLiteral("enabled")].toBool(); + QString serverCloudId = serverState[QStringLiteral("cloudId")].toString(); + + // server notifications are also transmitted along this websocket, so ignore data if + // it contains "message" + QString message = serverState[QStringLiteral("message")].toString(); + if (!message.isEmpty()) { + return; + } + if (m_currentStudio.id() == "") { + return; + } + m_currentStudio.setStatus(serverStatus); + m_currentStudio.setEnabled(serverEnabled); + m_currentStudio.setCloudId(serverCloudId); + if (!m_jackTripRunning) { + if (serverStatus == QLatin1String("Ready") && m_onConnectedScreen) { + m_currentStudio.setHost(serverState[QStringLiteral("serverHost")].toString()); + m_currentStudio.setPort(serverState[QStringLiteral("serverPort")].toInt()); + m_currentStudio.setSessionId( + serverState[QStringLiteral("sessionId")].toString()); + completeConnection(); + } + } + + emit currentStudioChanged(); +} + +void VirtualStudio::restartStudioSocket() +{ + if (m_onConnectedScreen) { + if (!m_studioSocketPtr.isNull()) { + m_studioSocketPtr->openSocket(); + } + } +} + +void VirtualStudio::updatedStats(const QJsonObject& stats) +{ + QJsonObject newStats; + for (int i = 0; i < stats.keys().size(); i++) { + QString key = stats.keys().at(i); + newStats.insert(key, stats[key].toDouble()); + } + + m_networkStats = newStats; + emit networkStatsChanged(); + return; +} + +void VirtualStudio::udpWaitingTooLong() +{ + if (m_devicePtr.isNull()) + return; + m_networkOutageTimer.start(); + m_devicePtr->setNetworkOutage(true); + emit updatedNetworkOutage(true); +} + +void VirtualStudio::sendHeartbeat() +{ + if (!m_devicePtr.isNull() && m_connectionState != "Connecting..." + && m_connectionState != "Preparing audio...") { + m_devicePtr->sendHeartbeat(); + } +} + +void VirtualStudio::resetState() +{ + m_webChannelServer->close(); + m_heartbeatTimer.stop(); + m_startTimer.stop(); + m_networkOutageTimer.stop(); + m_firstRefresh = true; +} + +void VirtualStudio::refreshStudios(int index, bool signalRefresh) +{ + // user id is required for retrieval of subscriptions + if (m_userId.isEmpty()) { + std::cerr << "Studio refresh cancelled due to empty user id" << std::endl; + return; + } + + // only allow one thread to refresh at a time + QMutexLocker refreshLock(&m_refreshMutex); + if (m_refreshInProgress) + return; + std::cout << "Refreshing list of studios" << std::endl; + setRefreshInProgress(true); + refreshLock.unlock(); + + // get subscriptions first; this is required for studio groupings + QNetworkReply* reply = m_api->getSubscriptions(m_userId); + connect(reply, &QNetworkReply::finished, this, [&, reply, signalRefresh, index]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + reply->deleteLater(); + return; + } + + QByteArray response = reply->readAll(); + QJsonDocument subscriptionList = QJsonDocument::fromJson(response); + if (!subscriptionList.isArray()) { + std::cout << "Error: Not an array" << std::endl; + reply->deleteLater(); + return; + } + m_subscribedServers.clear(); + QJsonArray subscriptions = subscriptionList.array(); + for (int i = 0; i < subscriptions.count(); i++) { + m_subscribedServers.insert( + subscriptions.at(i)[QStringLiteral("serverId")].toString(), true); + } + reply->deleteLater(); + + // done getting subscriptions; get list of servers next + QNetworkReply* serversReply = m_api->getServers(); + connect(serversReply, &QNetworkReply::finished, this, + [&, serversReply, signalRefresh, index]() { + handleServerUpdate(serversReply, signalRefresh, index); + }); + }); +} + +void VirtualStudio::handleServerUpdate(QNetworkReply* reply, bool signalRefresh, + int index) +{ + if (reply->error() != QNetworkReply::NoError) { + if (signalRefresh) { + emit refreshFinished(index); + } + std::cerr << "Error: " << reply->errorString().toStdString() << std::endl; + reply->deleteLater(); + QMutexLocker refreshLock(&m_refreshMutex); + setRefreshInProgress(false); + return; + } + + // Get the serverId of the server at the top of our screen if we know it + QString topServerId; + if (index >= 0 && index < m_serverModel.count()) { + topServerId = m_serverModel.at(index)->id(); + } + + QByteArray response = reply->readAll(); + QJsonDocument serverList = QJsonDocument::fromJson(response); + reply->deleteLater(); + if (!serverList.isArray()) { + if (signalRefresh) { + emit refreshFinished(index); + } + std::cerr << "Error: Not an array" << std::endl; + QMutexLocker refreshLock(&m_refreshMutex); + setRefreshInProgress(false); + return; + } + + QJsonArray servers = serverList.array(); + // Divide our servers by category initially so that they're easier to sort + QVector yourServers; + QVector subServers; + QVector pubServers; + int skippedStudios = 0; + + QMutexLocker refreshLock(&m_refreshMutex); // protect m_servers + m_servers.clear(); + for (int i = 0; i < servers.count(); i++) { + if (servers.at(i)[QStringLiteral("type")].toString().contains( + QStringLiteral("JackTrip"))) { + QSharedPointer serverInfo(new VsServerInfo(this)); + serverInfo->setIsAdmin(servers.at(i)[QStringLiteral("admin")].toBool()); + serverInfo->setName(servers.at(i)[QStringLiteral("name")].toString()); + serverInfo->setHost(servers.at(i)[QStringLiteral("serverHost")].toString()); + serverInfo->setIsManaged(servers.at(i)[QStringLiteral("managed")].toBool()); + serverInfo->setStatus(servers.at(i)[QStringLiteral("status")].toString()); + serverInfo->setPort(servers.at(i)[QStringLiteral("serverPort")].toInt()); + serverInfo->setIsPublic(servers.at(i)[QStringLiteral("public")].toBool()); + serverInfo->setRegion(servers.at(i)[QStringLiteral("region")].toString()); + serverInfo->setPeriod(servers.at(i)[QStringLiteral("period")].toInt()); + serverInfo->setSampleRate( + servers.at(i)[QStringLiteral("sampleRate")].toInt()); + serverInfo->setQueueBuffer( + servers.at(i)[QStringLiteral("queueBuffer")].toInt()); + serverInfo->setBannerURL( + servers.at(i)[QStringLiteral("bannerURL")].toString()); + serverInfo->setId(servers.at(i)[QStringLiteral("id")].toString()); + serverInfo->setSessionId( + servers.at(i)[QStringLiteral("sessionId")].toString()); + serverInfo->setStreamId(servers.at(i)[QStringLiteral("streamId")].toString()); + serverInfo->setInviteKey( + servers.at(i)[QStringLiteral("inviteKey")].toString()); + serverInfo->setCloudId(servers.at(i)[QStringLiteral("cloudId")].toString()); + serverInfo->setEnabled(servers.at(i)[QStringLiteral("enabled")].toBool()); + serverInfo->setIsOwner(servers.at(i)[QStringLiteral("owner")].toBool()); + + // Always add servers to m_servers + m_servers.append(serverInfo); + + // Only add servers to the model that we want to show + if (serverInfo->isAdmin() || serverInfo->isOwner()) { + if (filterStudio(*serverInfo)) { + ++skippedStudios; + } else { + yourServers.append(serverInfo); + serverInfo->setSection(VsServerInfo::YOUR_STUDIOS); + } + } else if (m_subscribedServers.contains(serverInfo->id())) { + if (filterStudio(*serverInfo)) { + ++skippedStudios; + } else { + subServers.append(serverInfo); + serverInfo->setSection(VsServerInfo::SUBSCRIBED_STUDIOS); + } + } else { + if (!filterStudio(*serverInfo) && serverInfo->enabled()) { + pubServers.append(serverInfo); + serverInfo->setSection(VsServerInfo::PUBLIC_STUDIOS); + } + // don't count public studios in skipped count + } + } + } + refreshLock.unlock(); + + // sort studios in each section by name + auto serverSorter = [](VsServerInfoPointer first, VsServerInfoPointer second) { + return *first < *second; + }; + std::sort(yourServers.begin(), yourServers.end(), serverSorter); + std::sort(subServers.begin(), subServers.end(), serverSorter); + std::sort(pubServers.begin(), pubServers.end(), serverSorter); + + // If we don't have any owned servers, move the JackTrip logo to an + // appropriate section header. + if (yourServers.isEmpty()) { + if (subServers.isEmpty()) { + m_logoSection = QStringLiteral("Public Studios"); + if (skippedStudios == 0 && m_windowState == "browse") { + // Transition to create studio page + setWindowState("create_studio"); + } + } else { + m_logoSection = QStringLiteral("Subscribed Studios"); + } + } else { + m_logoSection = QStringLiteral("Your Studios"); + } + emit logoSectionChanged(); + + m_serverModel.clear(); + for (const VsServerInfoPointer& s : yourServers) { + m_serverModel.append(s.get()); + } + for (const VsServerInfoPointer& s : subServers) { + m_serverModel.append(s.get()); + } + for (const VsServerInfoPointer& s : pubServers) { + m_serverModel.append(s.get()); + } + emit serverModelChanged(); + + index = -1; + if (!topServerId.isEmpty()) { + for (int i = 0; i < m_serverModel.count(); i++) { + if (m_serverModel.at(i)->id() == topServerId) { + index = i; + break; + } + } + } + + if (m_firstRefresh) { + m_heartbeatTimer.setInterval(5000); + m_heartbeatTimer.start(); + m_firstRefresh = false; + } + setRefreshInProgress(false); + if (signalRefresh) { + emit refreshFinished(index); + } +} + +bool VirtualStudio::filterStudio(const VsServerInfo& serverInfo) const +{ + // Return true if we want to filter the studio out of the display model + bool activeStudio = serverInfo.status() == QLatin1String("Ready"); + bool hostedStudio = serverInfo.isManaged(); + if (!m_showSelfHosted && !hostedStudio) { + return true; + } + if (!m_showInactive && !activeStudio) { + return true; + } + return false; +} + +void VirtualStudio::getRegions() +{ + QNetworkReply* reply = m_api->getRegions(m_userId); + connect(reply, &QNetworkReply::finished, this, [&, reply]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + reply->deleteLater(); + return; + } + + m_regions = QJsonDocument::fromJson(reply->readAll()).object(); + emit regionsChanged(); + reply->deleteLater(); + }); +} + +void VirtualStudio::getUserMetadata() +{ + QNetworkReply* reply = m_api->getUser(m_userId); + connect(reply, &QNetworkReply::finished, this, [&, reply]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + reply->deleteLater(); + return; + } + + m_userMetadata = QJsonDocument::fromJson(reply->readAll()).object(); + emit userMetadataChanged(); + reply->deleteLater(); + refreshStudios(-1, false); + }); +} + +bool VirtualStudio::readyToJoin() +{ + // FTUX shows warnings and device setup views + // if any of these enabled, do not immediately join + return m_windowState == "connected" + && (m_connectionState == QStringLiteral("Waiting...") + || m_connectionState == QStringLiteral("Disconnected")); +} + +void VirtualStudio::detectedFeedbackLoop() +{ + emit feedbackDetected(); +} + +VirtualStudio::~VirtualStudio() +{ + QDesktopServices::unsetUrlHandler("jacktrip"); + // close the window + m_view.reset(); + // stop the audio worker thread before destructing other things + m_audioConfigPtr.reset(); + // stop device and corresponding threads + m_devicePtr.reset(); +} + +QApplication* VirtualStudio::createApplication(int& argc, char* argv[]) +{ +#if defined(Q_OS_WIN) + // Fix for display scaling like 125% or 150% on Windows + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + + // Enables resource sharing between the OpenGL contexts + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + // QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); + // QCoreApplication::setAttribute(Qt::AA_UseOpenGLES); + + // QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D11); + QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); +#endif + + QQuickStyle::setStyle("Basic"); + + // Initialize webengine + QtWebEngineQuick::initialize(); + // TODO: Add support for QtWebView + // qputenv("QT_WEBVIEW_PLUGIN", "native"); + // QtWebView::initialize(); + +#if defined(Q_OS_MACOS) + return new JTApplication(argc, argv); +#else + return new QApplication(argc, argv); +#endif +} diff --git a/src/vs/virtualstudio.h b/src/vs/virtualstudio.h new file mode 100644 index 0000000..8861af5 --- /dev/null +++ b/src/vs/virtualstudio.h @@ -0,0 +1,335 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file virtualstudio.h + * \author Matt Horton, based on code by Aaron Wyatt + * \date March 2022 + */ + +#ifndef VIRTUALSTUDIO_H +#define VIRTUALSTUDIO_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../Settings.h" +#include "../UserInterface.h" +#include "vsConstants.h" +#include "vsQuickView.h" +#include "vsServerInfo.h" + +class JackTrip; +class VsAudio; +class VsApi; +class VsAuth; +class VsDevice; +class VsDeeplink; +class VsWebSocket; + +typedef QSharedPointer VsServerInfoPointer; + +class VirtualStudio : public QObject +{ + Q_OBJECT + Q_PROPERTY(int webChannelPort READ webChannelPort NOTIFY webChannelPortChanged) + Q_PROPERTY(bool hasRefreshToken READ hasRefreshToken NOTIFY hasRefreshTokenChanged) + Q_PROPERTY(QString versionString READ versionString CONSTANT) + Q_PROPERTY(QString buildString READ buildString CONSTANT) + Q_PROPERTY(QString copyrightString READ copyrightString CONSTANT) + Q_PROPERTY(QString logoSection READ logoSection NOTIFY logoSectionChanged) + Q_PROPERTY( + QString connectedErrorMsg READ connectedErrorMsg NOTIFY connectedErrorMsgChanged) + + Q_PROPERTY( + QVector serverModel READ getServerModel NOTIFY serverModelChanged) + Q_PROPERTY(VsServerInfo* currentStudio READ currentStudio NOTIFY currentStudioChanged) + Q_PROPERTY(QString studioToJoin READ studioToJoin WRITE setStudioToJoin NOTIFY + studioToJoinChanged) + Q_PROPERTY(QJsonObject regions READ regions NOTIFY regionsChanged) + Q_PROPERTY(QJsonObject userMetadata READ userMetadata NOTIFY userMetadataChanged) + Q_PROPERTY(bool showInactive READ showInactive WRITE setShowInactive NOTIFY + showInactiveChanged) + Q_PROPERTY(bool showSelfHosted READ showSelfHosted WRITE setShowSelfHosted NOTIFY + showSelfHostedChanged) + Q_PROPERTY(QString connectionState READ connectionState NOTIFY connectionStateChanged) + Q_PROPERTY(QJsonObject networkStats READ networkStats NOTIFY networkStatsChanged) + Q_PROPERTY(bool networkOutage READ networkOutage NOTIFY updatedNetworkOutage) + + Q_PROPERTY(QString updateChannel READ updateChannel WRITE setUpdateChannel NOTIFY + updateChannelChanged) + Q_PROPERTY(float fontScale READ fontScale CONSTANT) + Q_PROPERTY(float uiScale READ uiScale WRITE setUiScale NOTIFY uiScaleChanged) + Q_PROPERTY(bool darkMode READ darkMode WRITE setDarkMode NOTIFY darkModeChanged) + Q_PROPERTY(bool collapseDeviceControls READ collapseDeviceControls WRITE + setCollapseDeviceControls NOTIFY collapseDeviceControlsChanged) + Q_PROPERTY(bool testMode READ testMode WRITE setTestMode NOTIFY testModeChanged) + Q_PROPERTY(bool showDeviceSetup READ showDeviceSetup WRITE setShowDeviceSetup NOTIFY + showDeviceSetupChanged) + Q_PROPERTY(bool showWarnings READ showWarnings WRITE setShowWarnings NOTIFY + showWarningsChanged) + Q_PROPERTY(bool isExiting READ isExiting NOTIFY isExitingChanged) + Q_PROPERTY( + bool refreshInProgress READ refreshInProgress NOTIFY refreshInProgressChanged) + Q_PROPERTY(bool noUpdater READ noUpdater CONSTANT) + Q_PROPERTY(bool psiBuild READ psiBuild CONSTANT) + Q_PROPERTY(QString failedMessage READ failedMessage NOTIFY failedMessageChanged) + Q_PROPERTY(QString windowState READ windowState WRITE setWindowState NOTIFY + windowStateUpdated) + Q_PROPERTY(QString apiHost READ apiHost WRITE setApiHost NOTIFY apiHostChanged) + Q_PROPERTY(bool vsFtux READ vsFtux CONSTANT) + Q_PROPERTY(bool hasClassicMode READ hasClassicMode CONSTANT) + Q_PROPERTY( + QStringList updateChannelComboModel READ getUpdateChannelComboModel CONSTANT) + + public: + explicit VirtualStudio(UserInterface& parent); + ~VirtualStudio() override; + + void show(); + void hide(); + void raiseToTop(); + + int webChannelPort(); + bool hasRefreshToken(); + QString versionString(); + QString buildString(); + QString copyrightString(); + QString logoSection(); + QString connectedErrorMsg(); + void setConnectedErrorMsg(const QString& msg); + const QVector& getServerModel() const { return m_serverModel; } + VsServerInfo* currentStudio() { return &m_currentStudio; } + QJsonObject regions(); + QJsonObject userMetadata(); + QString connectionState(); + QJsonObject networkStats(); + QString updateChannel(); + void setUpdateChannel(const QString& channel); + bool showInactive(); + void setShowInactive(bool inactive); + bool showSelfHosted(); + void setShowSelfHosted(bool selfHosted); + float fontScale(); + float uiScale(); + void setUiScale(float scale); + bool darkMode(); + void setDarkMode(bool dark); + bool collapseDeviceControls(); + void setCollapseDeviceControls(bool collapseDeviceControls); + bool testMode(); + void setTestMode(bool test); + QString studioToJoin(); + void setStudioToJoin(const QString& id); + bool showDeviceSetup(); + void setShowDeviceSetup(bool show); + bool showWarnings(); + void setShowWarnings(bool show); + bool noUpdater(); + bool psiBuild(); + QString failedMessage(); + bool networkOutage(); + bool backendAvailable(); + QString windowState(); + QString apiHost(); + void setApiHost(QString host); + bool vsFtux(); + bool hasClassicMode(); + bool isExiting(); + bool refreshInProgress(); + void setRefreshInProgress(bool b); + + static QApplication* createApplication(int& argc, char* argv[]); + + const QStringList& getUpdateChannelComboModel() const + { + return m_updateChannelOptions; + } + + public slots: + void toVirtualStudioMode(); + void toClassicMode(); + void login(); + void logout(); + void refreshStudios(int index, bool signalRefresh = false); + void loadSettings(); + void saveSettings(); + void triggerReconnect(bool refresh); + void editProfile(); + void showAbout(); + void openLink(const QString& url); + void handleDeeplinkRequest(const QUrl& url); + void udpWaitingTooLong(); + void setWindowState(QString state); + void joinStudio(); + void disconnect(); + void collectFeedbackSurvey(QString serverId, int rating, QString message); + + signals: + void failed(); + void connected(); + void disconnected(); + void refreshFinished(int index); + void webChannelPortChanged(int webChannelPort); + void hasRefreshTokenChanged(); + void logoSectionChanged(); + void connectedErrorMsgChanged(); + void serverModelChanged(); + void currentStudioChanged(); + void regionsChanged(); + void userMetadataChanged(); + void showInactiveChanged(); + void showSelfHostedChanged(); + void connectionStateChanged(); + void networkStatsChanged(); + void updateChannelChanged(); + void showDeviceSetupChanged(); + void showWarningsChanged(); + void uiScaleChanged(); + void collapseDeviceControlsChanged(bool collapseDeviceControls); + void newScale(); + void darkModeChanged(); + void testModeChanged(); + void signalExit(); + void failedMessageChanged(); + void studioToJoinChanged(); + void updatedNetworkOutage(bool outage); + void windowStateUpdated(); + void isExitingChanged(); + void scheduleStudioRefresh(int index, bool signalRefresh); + void refreshInProgressChanged(); + void apiHostChanged(); + void feedbackDetected(); + void openFeedbackSurveyModal(QString serverId); + void openAboutWindow(); + + private slots: + void slotAuthSucceeded(); + void receivedConnectionFromPeer(); + void handleWebsocketMessage(const QString& msg); + void restartStudioSocket(); + void updatedStats(const QJsonObject& stats); + void processError(const QString& errorMessage); + void detectedFeedbackLoop(); + void sendHeartbeat(); + void connectionFinished(); + void exit(); + + private: + void resetState(); + void handleServerUpdate(QNetworkReply* reply, bool signalRefresh, int index); + bool filterStudio(const VsServerInfo& serverInfo) const; + void getRegions(); + void getUserMetadata(); + bool readyToJoin(); + void connectToStudio(); + void completeConnection(); + + private: + enum ReconnectState { + NOT_RECONNECTING = 0, + RECONNECTING_VALIDATE, + RECONNECTING_REFRESH + }; + + UserInterface& m_interface; + VsServerInfo m_currentStudio; + QNetworkAccessManager* m_networkAccessManagerPtr; + QScopedPointer m_view; + QSharedPointer m_deepLinkPtr; + QSharedPointer m_auth; + QSharedPointer m_api; + QScopedPointer m_devicePtr; + QScopedPointer m_studioSocketPtr; + QSharedPointer m_audioConfigPtr; + QVector m_servers; + QVector m_serverModel; //< qml doesn't like smart pointers + QScopedPointer m_webChannelServer; + QScopedPointer m_webChannel; + QMap m_subscribedServers; + QJsonObject m_regions; + QJsonObject m_userMetadata; + QJsonObject m_networkStats; + QTimer m_startTimer; + QTimer m_heartbeatTimer; + QTimer m_networkOutageTimer; + QMutex m_refreshMutex; + QString m_studioToJoin; + QString m_updateChannel; + QString m_refreshToken; + QString m_userId; + QString m_apiHost = PROD_API_HOST; + ReconnectState m_reconnectState = ReconnectState::NOT_RECONNECTING; + + bool m_firstRefresh = true; + bool m_jackTripRunning = false; + bool m_checkSsl = true; + bool m_refreshInProgress = false; + bool m_onConnectedScreen = false; + bool m_isExiting = false; + bool m_showInactive = true; + bool m_showSelfHosted = true; + bool m_showDeviceSetup = true; + bool m_showWarnings = true; + bool m_darkMode = false; + bool m_collapseDeviceControls = false; + bool m_testMode = false; + bool m_authenticated = false; + float m_fontScale = 1; + float m_uiScale = 1; + uint32_t m_webChannelPort = 1; + + QString m_failedMessage = QStringLiteral(""); + QString m_windowState = QStringLiteral(""); + QString m_connectedErrorMsg = QStringLiteral(""); + QString m_logoSection = QStringLiteral("Your Studios"); + QString m_connectionState = QStringLiteral("Waiting..."); + QStringList m_updateChannelOptions = {"Stable", "Edge"}; + +#if defined(VS_FTUX) || defined(NO_CLASSIC) + bool m_vsFtux = true; +#else + bool m_vsFtux = false; +#endif +}; + +#endif // VIRTUALSTUDIO_H diff --git a/src/vs/vs.qml b/src/vs/vs.qml new file mode 100644 index 0000000..360a12a --- /dev/null +++ b/src/vs/vs.qml @@ -0,0 +1,323 @@ +import QtQuick +import QtQuick.Controls + +Rectangle { + property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB" + property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" + + color: backgroundColour + state: virtualstudio.windowState + anchors.fill: parent + + id: window + states: [ + State { + name: "start" + PropertyChanges { target: startScreen; x: 0 } + PropertyChanges { target: loginScreen; x: window.width; } + PropertyChanges { target: recommendationsScreen; x: window.width } + PropertyChanges { target: permissionsScreen; x: window.width } + PropertyChanges { target: setupScreen; x: window.width } + PropertyChanges { target: browseScreen; x: window.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: changeDevicesScreen; x: 2*window.width } + PropertyChanges { target: failedScreen; x: window.width } + }, + + State { + name: "login" + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: loginScreen; x: 0; } + PropertyChanges { target: recommendationsScreen; x: window.width } + PropertyChanges { target: permissionsScreen; x: window.width } + PropertyChanges { target: setupScreen; x: window.width } + PropertyChanges { target: browseScreen; x: window.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: changeDevicesScreen; x: 2*window.width } + PropertyChanges { target: failedScreen; x: window.width } + }, + + State { + name: "recommendations" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: recommendationsScreen; x: 0 } + PropertyChanges { target: permissionsScreen; x: window.width } + PropertyChanges { target: setupScreen; x: window.width } + PropertyChanges { target: browseScreen; x: window.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: changeDevicesScreen; x: 2*window.width } + PropertyChanges { target: failedScreen; x: window.width } + }, + + State { + name: "permissions" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } + PropertyChanges { target: permissionsScreen; x: 0 } + PropertyChanges { target: setupScreen; x: window.width } + PropertyChanges { target: browseScreen; x: window.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: changeDevicesScreen; x: 2*window.width } + PropertyChanges { target: failedScreen; x: window.width } + }, + + State { + name: "setup" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } + PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } + PropertyChanges { target: setupScreen; x: 0 } + PropertyChanges { target: browseScreen; x: -browseScreen.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: changeDevicesScreen; x: 2*window.width } + PropertyChanges { target: failedScreen; x: window.width } + }, + + State { + name: "browse" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } + PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } + PropertyChanges { target: setupScreen; x: window.width } + PropertyChanges { target: browseScreen; x: 0 } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: changeDevicesScreen; x: 2*window.width } + PropertyChanges { target: failedScreen; x: window.width } + }, + + State { + name: "settings" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } + PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } + PropertyChanges { target: setupScreen; x: window.width } + PropertyChanges { target: browseScreen; x: -browseScreen.width } + PropertyChanges { target: settingsScreen; x: 0 } + PropertyChanges { target: createStudioScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: changeDevicesScreen; x: 2*window.width } + PropertyChanges { target: failedScreen; x: window.width } + }, + + State { + name: "create_studio" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } + PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } + PropertyChanges { target: setupScreen; x: window.width } + PropertyChanges { target: browseScreen; x: -browseScreen.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: 0 } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: changeDevicesScreen; x: window.width } + PropertyChanges { target: failedScreen; x: window.width } + }, + + State { + name: "connected" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } + PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } + PropertyChanges { target: setupScreen; x: 0 } + PropertyChanges { target: browseScreen; x: -browseScreen.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: -createStudioScreen.width } + PropertyChanges { target: connectedScreen; x: 0 } + PropertyChanges { target: changeDevicesScreen; x: window.width } + PropertyChanges { target: failedScreen; x: window.width } + }, + + State { + name: "change_devices" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } + PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } + PropertyChanges { target: setupScreen; x: 0 } + PropertyChanges { target: browseScreen; x: -browseScreen.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: -createStudioScreen.width } + PropertyChanges { target: connectedScreen; x: 0 } + PropertyChanges { target: changeDevicesScreen; x: 0 } + PropertyChanges { target: failedScreen; x: window.width } + }, + + State { + name: "failed" + PropertyChanges { target: loginScreen; x: -loginScreen.width } + PropertyChanges { target: startScreen; x: -startScreen.width } + PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width } + PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width } + PropertyChanges { target: setupScreen; x: -setupScreen.width } + PropertyChanges { target: browseScreen; x: -browseScreen.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: createStudioScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: changeDevicesScreen; x: 2*window.width } + PropertyChanges { target: failedScreen; x: 0 } + } + ] + + transitions: Transition { + NumberAnimation { properties: "x"; duration: 500; easing.type: Easing.InOutQuad } + } + + FirstLaunch { + id: startScreen + x: -startScreen.width + } + + Login { + id: loginScreen + x: window.width + } + + Recommendations { + id: recommendationsScreen + x: window.width + } + + Permissions { + id: permissionsScreen + x: window.width + } + + Browse { + id: browseScreen + x: window.width + } + + Setup { + id: setupScreen + x: window.width + } + + Settings { + id: settingsScreen + x: window.width + } + + Connected { + id: connectedScreen + x: window.width + } + + ChangeDevices { + id: changeDevicesScreen + x: window.width + } + + CreateStudio { + id: createStudioScreen + x: window.width + } + + Failed { + id: failedScreen + x: window.width + } + + onWidthChanged: { + if (virtualstudio.windowState === "start") { + startScreen.x = 0 + } else if (virtualstudio.windowState === "login") { + loginScreen.x = 0 + } else if (virtualstudio.windowState === "recommendations") { + recommendationsScreen.x = 0; + } else if (virtualstudio.windowState === "permissions") { + permissionsScreen.x = 0; + } else if (virtualstudio.windowState === "setup") { + setupScreen.x = 0 + } else if (virtualstudio.windowState === "browse") { + browseScreen.x = 0 + } else if (virtualstudio.windowState === "settings") { + settingsScreen.x = 0 + } else if (virtualstudio.windowState === "create_studio") { + createStudioScreen.x = 0 + } else if (virtualstudio.windowState === "connected") { + connectedScreen.x = 0 + } else if (virtualstudio.windowState === "change_devices") { + changeDevicesScreen.x = 0 + } else if (virtualstudio.windowState === "failed") { + failedScreen.x = 0 + } + } + + onHeightChanged: { + if (virtualstudio.windowState === "start") { + startScreen.x = 0 + } else if (virtualstudio.windowState === "login") { + loginScreen.x = 0 + } else if (virtualstudio.windowState === "recommendations") { + recommendationsScreen.x = 0; + } else if (virtualstudio.windowState === "permissions") { + permissionsScreen.x = 0; + } else if (virtualstudio.windowState === "setup") { + setupScreen.x = 0 + } else if (virtualstudio.windowState === "browse") { + browseScreen.x = 0 + } else if (virtualstudio.windowState === "settings") { + settingsScreen.x = 0 + } else if (virtualstudio.windowState === "create_studio") { + createStudioScreen.x = 0 + } else if (virtualstudio.windowState === "connected") { + connectedScreen.x = 0 + } else if (virtualstudio.windowState === "change_devices") { + changeDevicesScreen.x = 0 + } else if (virtualstudio.windowState === "failed") { + failedScreen.x = 0 + } + } + + Connections { + target: auth + function onAuthSucceeded() { + if (virtualstudio.windowState !== "login") { + // can happen on settings screen when switching between prod and test + return; + } + if (virtualstudio.showWarnings) { + virtualstudio.windowState = "recommendations"; + } else if (virtualstudio.studioToJoin === "") { + virtualstudio.windowState = "browse"; + } else { + virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected"; + virtualstudio.joinStudio(); + } + } + } + Connections { + target: virtualstudio + function onConnected() { + if (virtualstudio.windowState == "change_devices") { + return; + } + virtualstudio.windowState = "connected"; + } + function onFailed() { + virtualstudio.windowState = "failed"; + } + function onDisconnected() { + virtualstudio.windowState = "browse"; + } + } +} diff --git a/src/vs/vs.qrc b/src/vs/vs.qrc new file mode 100644 index 0000000..0b847fe --- /dev/null +++ b/src/vs/vs.qrc @@ -0,0 +1,95 @@ + + + vs.qml + AboutWindow.qml + FirstLaunch.qml + Login.qml + LearnMoreButton.qml + Recommendations.qml + Permissions.qml + ChangeDevices.qml + Studio.qml + Browse.qml + AudioSettings.qml + Settings.qml + Meter.qml + MeterBars.qml + Connected.qml + CreateStudio.qml + Failed.qml + Setup.qml + SectionHeading.qml + Footer.qml + VolumeSlider.qml + DeviceControls.qml + DeviceControlsGroup.qml + DeviceRefreshButton.qml + DeviceWarning.qml + DeviceWarningModal.qml + InfoTooltip.qml + Web.qml + WebView.qml + WebEngine.qml + WebNull.qml + FeedbackSurvey.qml + AppIcon.qml + logo.svg + wedge.svg + wedge_inactive.svg + private.svg + public.svg + join.svg + leave.svg + manage.svg + speed.svg + share.svg + start.svg + star.svg + cog.svg + mic.svg + language.svg + micoff.svg + help.svg + quiet.svg + loud.svg + refresh.svg + ethernet.svg + networkCheck.svg + externalMic.svg + check.svg + warning.svg + expand_less.svg + expand_more.svg + sentiment_very_dissatisfied.svg + headphones.svg + Prompt.svg + network.svg + video.svg + close.svg + jacktrip.png + jacktrip white.png + JTOriginal.png + JTVS.png + flags/AE.svg + flags/AU.svg + flags/BE.svg + flags/BR.svg + flags/CA.svg + flags/CH.svg + flags/DE.svg + flags/FR.svg + flags/GB.svg + flags/HK.svg + flags/ID.svg + flags/IT.svg + flags/JP.svg + flags/RO.svg + flags/SE.svg + flags/SG.svg + flags/TW.svg + flags/US.svg + flags/ZA.svg + Poppins-Bold.ttf + Poppins-Regular.ttf + + diff --git a/src/vs/vsApi.cpp b/src/vs/vsApi.cpp new file mode 100644 index 0000000..513390b --- /dev/null +++ b/src/vs/vsApi.cpp @@ -0,0 +1,162 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsApi.cpp + * \author Dominick Hing + * \date May 2023 + */ + +#include "vsApi.h" + +#include "../jacktrip_globals.h" + +VsApi::VsApi(QNetworkAccessManager* networkAccessManager) +{ + m_networkAccessManager = networkAccessManager; +} + +QNetworkReply* VsApi::getAuth0UserInfo() +{ + return get(QUrl("https://auth.jacktrip.org/userinfo")); +} + +QNetworkReply* VsApi::getUser(const QString& userId) +{ + return get(QUrl(QString("https://%1/api/users/%2").arg(m_apiHost, userId))); +} + +QNetworkReply* VsApi::getServers() +{ + return get(QUrl(QString("https://%1/api/servers").arg(m_apiHost))); +} + +QNetworkReply* VsApi::getSubscriptions(const QString& userId) +{ + return get( + QUrl(QString("https://%1/api/users/%2/subscriptions").arg(m_apiHost, userId))); +} + +QNetworkReply* VsApi::getRegions(const QString& userId) +{ + return get(QUrl(QString("https://%1/api/users/%2/regions").arg(m_apiHost, userId))); +} + +QNetworkReply* VsApi::getDevice(const QString& deviceId) +{ + return get(QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId))); +} + +QNetworkReply* VsApi::postDevice(const QByteArray& data) +{ + return post(QUrl(QString("https://%1/api/devices").arg(m_apiHost)), data); +} + +QNetworkReply* VsApi::postDeviceHeartbeat(const QString& deviceId, const QByteArray& data) +{ + return post( + QUrl(QString("https://%1/api/devices/%2/heartbeat").arg(m_apiHost, deviceId)), + data); +} + +QNetworkReply* VsApi::submitServerFeedback(const QString& serverId, + const QByteArray& data) +{ + return post( + QUrl(QString("https://%1/api/servers/%2/feedback").arg(m_apiHost, serverId)), + data); +} + +QNetworkReply* VsApi::updateServer(const QString& serverId, const QByteArray& data) +{ + return put(QUrl(QString("https://%1/api/servers/%2").arg(m_apiHost, serverId)), data); +} + +QNetworkReply* VsApi::updateDevice(const QString& deviceId, const QByteArray& data) +{ + return put(QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)), data); +} + +QNetworkReply* VsApi::deleteDevice(const QString& deviceId) +{ + return deleteResource( + QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId))); +} + +QNetworkReply* VsApi::get(const QUrl& url) +{ + QNetworkRequest request = QNetworkRequest(url); + request.setRawHeader(QByteArray("User-Agent"), + QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8()); + request.setRawHeader(QByteArray("Authorization"), + QString("Bearer %1").arg(m_accessToken).toUtf8()); + + QNetworkReply* reply = m_networkAccessManager->get(request); + return reply; +} + +QNetworkReply* VsApi::post(const QUrl& url, const QByteArray& data) +{ + QNetworkRequest request = QNetworkRequest(url); + request.setRawHeader(QByteArray("User-Agent"), + QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8()); + request.setRawHeader(QByteArray("Authorization"), + QString("Bearer %1").arg(m_accessToken).toUtf8()); + request.setRawHeader(QByteArray("Content-Type"), + QString("application/json").toUtf8()); + + QNetworkReply* reply = m_networkAccessManager->post(request, data); + return reply; +} + +QNetworkReply* VsApi::put(const QUrl& url, const QByteArray& data) +{ + QNetworkRequest request = QNetworkRequest(url); + request.setRawHeader(QByteArray("User-Agent"), + QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8()); + request.setRawHeader(QByteArray("Authorization"), + QString("Bearer %1").arg(m_accessToken).toUtf8()); + request.setRawHeader(QByteArray("Content-Type"), + QString("application/json").toUtf8()); + QNetworkReply* reply = m_networkAccessManager->put(request, data); + return reply; +} + +QNetworkReply* VsApi::deleteResource(const QUrl& url) +{ + QNetworkRequest request = QNetworkRequest(url); + request.setRawHeader(QByteArray("User-Agent"), + QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8()); + request.setRawHeader(QByteArray("Authorization"), + QString("Bearer %1").arg(m_accessToken).toUtf8()); + + QNetworkReply* reply = m_networkAccessManager->deleteResource(request); + return reply; +} \ No newline at end of file diff --git a/src/vs/vsApi.h b/src/vs/vsApi.h new file mode 100644 index 0000000..9b1d713 --- /dev/null +++ b/src/vs/vsApi.h @@ -0,0 +1,88 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsApi.h + * \author Dominick Hing + * \date May 2023 + */ + +#ifndef VSAPI_H +#define VSAPI_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class VsApi : public QObject +{ + Q_OBJECT + + public: + VsApi(QNetworkAccessManager* networkAccessManager); + void setAccessToken(QString token) { m_accessToken = token; }; + void setApiHost(QString host) { m_apiHost = host; } + QString getApiHost() { return m_apiHost; } + + QNetworkReply* getAuth0UserInfo(); + QNetworkReply* getUser(const QString& userId); + QNetworkReply* getServers(); + QNetworkReply* getSubscriptions(const QString& userId); + QNetworkReply* getRegions(const QString& userId); + QNetworkReply* getDevice(const QString& deviceId); + + QNetworkReply* postDevice(const QByteArray& data); + QNetworkReply* postDeviceHeartbeat(const QString& deviceId, const QByteArray& data); + QNetworkReply* submitServerFeedback(const QString& serverId, const QByteArray& data); + + QNetworkReply* updateServer(const QString& serverId, const QByteArray& data); + QNetworkReply* updateDevice(const QString& deviceId, const QByteArray& data); + + QNetworkReply* deleteDevice(const QString& deviceId); + + private: + QNetworkReply* get(const QUrl& url); + QNetworkReply* put(const QUrl& url, const QByteArray& data); + QNetworkReply* post(const QUrl& url, const QByteArray& data); + QNetworkReply* deleteResource(const QUrl& url); + + QString m_accessToken; + QString m_apiHost; + QNetworkAccessManager* m_networkAccessManager; +}; + +#endif // VSAPI_H \ No newline at end of file diff --git a/src/vs/vsAudio.cpp b/src/vs/vsAudio.cpp new file mode 100644 index 0000000..6964c77 --- /dev/null +++ b/src/vs/vsAudio.cpp @@ -0,0 +1,1429 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsAudio.cpp + * \author Matt Horton + * \date September 2022 + */ + +#include "vsAudio.h" + +#include +#include +#include +#include +#include + +#ifdef USE_WEAK_JACK +#include "weak_libjack.h" +#endif + +#ifndef NO_JACK +#include "../JackAudioInterface.h" +#endif + +#ifdef __APPLE__ +#include "vsMacPermissions.h" +#else +#include "vsPermissions.h" +#endif + +#ifndef NO_FEEDBACK +#include "../Analyzer.h" +#endif + +#include "../JackTrip.h" +#include "../Meter.h" +#include "../Monitor.h" +#include "../Tone.h" +#include "../Volume.h" +#include "AudioInterfaceMode.h" + +// generic function to wait for a signal to be emitted +template +static inline void WaitForSignal(SignalSenderPtr sender, SignalFuncPtr signal) +{ + QTimer timer; + timer.setTimerType(Qt::CoarseTimer); + timer.setSingleShot(true); + + QEventLoop loop; + QObject::connect(sender, signal, &loop, &QEventLoop::quit); + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.start(10000); // wait for max 10 seconds + loop.exec(); +} + +// Constructor +VsAudio::VsAudio(QObject* parent) + : QObject(parent) + , m_inputMeterLevels(2, 0) + , m_outputMeterLevels(2, 0) + , m_inputComboModel(QJsonArray::fromStringList(QStringList(QLatin1String("")))) + , m_outputComboModel(QJsonArray::fromStringList(QStringList(QLatin1String("")))) + , m_inputChannelsComboModel( + QJsonArray::fromStringList(QStringList(QLatin1String("")))) + , m_outputChannelsComboModel( + QJsonArray::fromStringList(QStringList(QLatin1String("")))) + , m_inputMixModeComboModel(QJsonArray::fromStringList(QStringList(QLatin1String("")))) + , m_audioWorkerPtr(new VsAudioWorker(this)) + , m_workerThreadPtr(nullptr) + , m_inputMeterPluginPtr(nullptr) + , m_outputMeterPluginPtr(nullptr) + , m_inputVolumePluginPtr(nullptr) + , m_outputVolumePluginPtr(nullptr) + , m_monitorPluginPtr(nullptr) + , mHasErrors(false) +{ + loadSettings(); + + QJsonObject element; + element.insert(QString::fromStdString("label"), QString::fromStdString("Mono")); + element.insert(QString::fromStdString("value"), + static_cast(AudioInterface::MONO)); + m_inputMixModeComboModel = QJsonArray(); + m_inputMixModeComboModel.push_back(element); + + element = QJsonObject(); + element.insert(QString::fromStdString("label"), QString::fromStdString("1")); + element.insert(QString::fromStdString("baseChannel"), QVariant(0).toInt()); + element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt()); + m_inputChannelsComboModel = QJsonArray(); + m_inputChannelsComboModel.push_back(element); + + element = QJsonObject(); + element.insert(QString::fromStdString("label"), QString::fromStdString("1 & 2")); + element.insert(QString::fromStdString("baseChannel"), QVariant(0).toInt()); + element.insert(QString::fromStdString("numChannels"), QVariant(2).toInt()); + m_outputChannelsComboModel = QJsonArray(); + m_outputChannelsComboModel.push_back(element); + + // Initialize timers needed for clip indicators + m_inputClipTimer.setTimerType(Qt::CoarseTimer); + m_inputClipTimer.setSingleShot(true); + m_inputClipTimer.setInterval(3000); + m_outputClipTimer.setTimerType(Qt::CoarseTimer); + m_outputClipTimer.setSingleShot(true); + m_outputClipTimer.setInterval(3000); + m_inputClipTimer.callOnTimeout([&]() { + if (m_inputClipped) { + m_inputClipped = false; + emit updatedInputClipped(m_inputClipped); + } + }); + m_outputClipTimer.callOnTimeout([&]() { + if (m_outputClipped) { + m_outputClipped = false; + emit updatedOutputClipped(m_outputClipped); + } + }); + + // move audio worker to its own thread + m_workerThreadPtr = new QThread; + m_workerThreadPtr->setObjectName("VsAudioWorker"); + m_workerThreadPtr->start(); + m_audioWorkerPtr->moveToThread(m_workerThreadPtr); + + // connect worker signals to slots + connect(this, &VsAudio::signalStartAudio, m_audioWorkerPtr.get(), + &VsAudioWorker::openAudioInterface, Qt::QueuedConnection); + connect(this, &VsAudio::signalStopAudio, m_audioWorkerPtr.get(), + &VsAudioWorker::closeAudioInterface, Qt::QueuedConnection); +#ifdef RT_AUDIO + connect(this, &VsAudio::signalRefreshDevices, m_audioWorkerPtr.get(), + &VsAudioWorker::refreshDevices, Qt::QueuedConnection); + connect(this, &VsAudio::signalValidateDevices, m_audioWorkerPtr.get(), + &VsAudioWorker::validateDevices, Qt::QueuedConnection); + connect(m_audioWorkerPtr.get(), &VsAudioWorker::signalUpdatedDeviceModels, this, + &VsAudio::setDeviceModels, Qt::QueuedConnection); +#endif + + // Add permissions for Mac +#ifdef __APPLE__ + m_permissionsPtr.reset(new VsMacPermissions()); + if (m_permissionsPtr->micPermissionChecked() + && m_permissionsPtr->micPermission() == "unknown") { + m_permissionsPtr->getMicPermission(); + } +#else + m_permissionsPtr.reset(new VsPermissions()); +#endif +} + +VsAudio::~VsAudio() +{ + if (m_workerThreadPtr == nullptr) + return; + m_workerThreadPtr->quit(); + WaitForSignal(m_workerThreadPtr, &QThread::finished); + m_workerThreadPtr->deleteLater(); +} + +bool VsAudio::backendAvailable() const +{ + if constexpr ((isBackendAvailable() + || isBackendAvailable())) { + return true; + } else { + return false; + } +} + +bool VsAudio::jackIsAvailable() const +{ + if constexpr (isBackendAvailable()) { +#ifdef USE_WEAK_JACK + // Check if Jack is available + return (have_libjack() == 0); +#else + return true; +#endif + } else { + return false; + } +} + +bool VsAudio::asioIsAvailable() const +{ +#if defined(RT_AUDIO) && defined(_WIN32) + return RtAudio::getCompiledApiByName("asio") != RtAudio::UNSPECIFIED; +#else + return false; +#endif +} + +void VsAudio::setAudioReady(bool ready) +{ + if (ready == m_audioReady) + return; + m_audioReady = ready; + emit signalAudioReadyChanged(); + if (m_audioReady) + emit signalAudioIsReady(); + else + emit signalAudioIsNotReady(); +} + +void VsAudio::setScanningDevices(bool b) +{ + if (b == m_scanningDevices) + return; + m_scanningDevices = b; + emit signalScanningDevicesChanged(); +} + +void VsAudio::setAudioBackend(const QString& backend) +{ + bool useRtAudio = (backend == QStringLiteral("RtAudio")); + if (useRtAudio) { + if (getUseRtAudio()) + return; + m_backend = AudioBackendType::RTAUDIO; + } else { + if (!getUseRtAudio()) + return; + m_backend = AudioBackendType::JACK; + } + emit audioBackendChanged(useRtAudio); +} + +void VsAudio::setFeedbackDetectionEnabled(bool enabled) +{ + if (m_feedbackDetectionEnabled == enabled) + return; + m_feedbackDetectionEnabled = enabled; + emit feedbackDetectionEnabledChanged(); +} + +void VsAudio::setSampleRate(int sampleRate) +{ + if (m_audioSampleRate == sampleRate) + return; + m_audioSampleRate = sampleRate; + emit sampleRateChanged(); +} + +void VsAudio::setBufferSize(int bufSize) +{ + if (m_audioBufferSize == bufSize) + return; + m_audioBufferSize = bufSize; + emit bufferSizeChanged(); +} + +void VsAudio::setQueueBuffer(int queueBuffer) +{ + if (m_queueBuffer == queueBuffer) + return; + if (queueBuffer < 0) + queueBuffer = 0; + m_queueBuffer = queueBuffer; + emit queueBufferChanged(); +} + +void VsAudio::setNumInputChannels(int numChannels) +{ + if (numChannels == m_numInputChannels) + return; + m_numInputChannels = numChannels; + emit numInputChannelsChanged(numChannels); +} + +void VsAudio::setNumOutputChannels(int numChannels) +{ + if (numChannels == m_numOutputChannels) + return; + m_numOutputChannels = numChannels; + emit numOutputChannelsChanged(numChannels); +} + +void VsAudio::setBaseInputChannel(int baseChannel) +{ + if (baseChannel == m_baseInputChannel) + return; + m_baseInputChannel = baseChannel; + emit baseInputChannelChanged(baseChannel); + return; +} + +void VsAudio::setBaseOutputChannel(int baseChannel) +{ + if (baseChannel == m_baseOutputChannel) + return; + m_baseOutputChannel = baseChannel; + emit baseOutputChannelChanged(baseChannel); + return; +} + +void VsAudio::setInputMixMode(const int mode) +{ + if (mode == m_inputMixMode) + return; + m_inputMixMode = mode; + emit inputMixModeChanged(mode); + return; +} + +void VsAudio::setInputMuted(bool muted) +{ + if (m_inMuted == muted) + return; + m_inMuted = muted; + emit updatedInputMuted(muted); +} + +void VsAudio::setInputVolume(float multiplier) +{ + if (multiplier == m_inMultiplier) + return; + m_inMultiplier = multiplier; + emit updatedInputVolume(multiplier); +} + +void VsAudio::setOutputVolume(float multiplier) +{ + if (multiplier == m_outMultiplier) + return; + m_outMultiplier = multiplier; + emit updatedOutputVolume(multiplier); +} + +void VsAudio::setMonitorVolume(float multiplier) +{ + if (multiplier == m_monMultiplier) + return; + m_monMultiplier = multiplier; + emit updatedMonitorVolume(multiplier); +} + +void VsAudio::setInputDevice([[maybe_unused]] const QString& device) +{ + if (!getUseRtAudio()) + return; +#ifdef RT_AUDIO + if (device == m_inputDevice) + return; + m_inputDevice = device; + emit inputDeviceChanged(m_inputDevice); +#endif +} + +void VsAudio::setOutputDevice([[maybe_unused]] const QString& device) +{ + if (!getUseRtAudio()) + return; +#ifdef RT_AUDIO + if (device == m_outputDevice) + return; + m_outputDevice = device; + emit outputDeviceChanged(m_outputDevice); +#endif +} + +void VsAudio::setDevicesErrorMsg(const QString& msg) +{ + if (m_devicesErrorMsg == msg) + return; + m_devicesErrorMsg = msg; + emit devicesErrorChanged(); +} + +void VsAudio::setDevicesWarningMsg(const QString& msg) +{ + if (m_devicesWarningMsg == msg) + return; + m_devicesWarningMsg = msg; + emit devicesWarningChanged(); +} + +void VsAudio::setDevicesErrorHelpUrl(const QString& url) +{ + if (m_devicesErrorHelpUrl == url) + return; + m_devicesErrorHelpUrl = url; + emit devicesErrorHelpUrlChanged(); +} + +void VsAudio::setDevicesWarningHelpUrl(const QString& url) +{ + if (m_devicesWarningHelpUrl == url) + return; + m_devicesWarningHelpUrl = url; + emit devicesWarningHelpUrlChanged(); +} + +void VsAudio::setHighLatencyFlag(bool highLatencyFlag) +{ + if (m_highLatencyFlag == highLatencyFlag) + return; + m_highLatencyFlag = highLatencyFlag; + emit highLatencyFlagChanged(highLatencyFlag); +} + +void VsAudio::startAudio(bool block) +{ + // note this is also used for restartAudio() + emit signalStartAudio(); + if (!block) + return; + WaitForSignal(this, &VsAudio::signalAudioIsReady); +} + +void VsAudio::stopAudio(bool block) +{ + if (!getAudioReady()) + return; + emit signalStopAudio(); + if (!block) + return; + WaitForSignal(this, &VsAudio::signalAudioIsNotReady); +} + +void VsAudio::refreshDevices(bool block) +{ + if (!getUseRtAudio()) + return; + emit signalRefreshDevices(); + if (!block) + return; + WaitForSignal(m_audioWorkerPtr.get(), &VsAudioWorker::signalDevicesValidated); +} + +void VsAudio::validateDevices(bool block) +{ + if (!getUseRtAudio()) + return; + emit signalValidateDevices(); + if (!block) + return; + WaitForSignal(m_audioWorkerPtr.get(), &VsAudioWorker::signalDevicesValidated); +} + +void VsAudio::loadSettings() +{ + QSettings settings; + settings.beginGroup(QStringLiteral("Audio")); + setInputVolume(settings.value(QStringLiteral("InMultiplier"), 1).toFloat()); + setOutputVolume(settings.value(QStringLiteral("OutMultiplier"), 1).toFloat()); + setMonitorVolume(settings.value(QStringLiteral("MonMultiplier"), 0).toFloat()); + // note: we should always reset input muted to false; otherwise, bad things + setInputMuted(false); + // setInputMuted(settings.value(QStringLiteral("InMuted"), false).toBool()); + + // load audio backend + AudioBackendType audioBackend; + if constexpr (isBackendAvailable()) { + audioBackend = + (settings.value(QStringLiteral("Backend"), AudioBackendType::RTAUDIO).toInt() + == 1) + ? AudioBackendType::RTAUDIO + : AudioBackendType::JACK; + } else if constexpr (isBackendAvailable()) { + audioBackend = AudioBackendType::RTAUDIO; + } else { + audioBackend = AudioBackendType::JACK; + } + if (audioBackend != m_backend) { + setAudioBackend(audioBackend == AudioBackendType::RTAUDIO + ? QStringLiteral("RtAudio") + : QStringLiteral("JACK")); + } + + // load input and output devices + QString inputDevice = settings.value(QStringLiteral("InputDevice"), "").toString(); + QString outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString(); + if (inputDevice == QStringLiteral("(default)")) { + inputDevice = ""; + } + if (outputDevice == QStringLiteral("(default)")) { + outputDevice = ""; + } + setInputDevice(inputDevice); + setOutputDevice(outputDevice); + + // use default base channel 0, if the setting does not exist + setBaseInputChannel(settings.value(QStringLiteral("BaseInputChannel"), 0).toInt()); + setBaseOutputChannel(settings.value(QStringLiteral("BaseOutputChannel"), 0).toInt()); + + // Handle migration scenarios. Assume this is a new user + // if we have m_inputDevice == "" and m_outputDevice == "" + if (m_inputDevice == "" && m_outputDevice == "") { + // for fresh installs, use mono by default + setNumInputChannels( + settings.value(QStringLiteral("NumInputChannels"), 1).toInt()); + setInputMixMode(settings + .value(QStringLiteral("InputMixMode"), + static_cast(AudioInterface::MONO)) + .toInt()); + + // use 2 channels for output + setNumOutputChannels( + settings.value(QStringLiteral("NumOutputChannels"), 2).toInt()); + } else { + // existing installs - keep using stereo + setNumInputChannels( + settings.value(QStringLiteral("NumInputChannels"), 2).toInt()); + setInputMixMode(settings + .value(QStringLiteral("InputMixMode"), + static_cast(AudioInterface::STEREO)) + .toInt()); + + // use 2 channels for output + setNumOutputChannels( + settings.value(QStringLiteral("NumOutputChannels"), 2).toInt()); + } + + setBufferSize(settings.value(QStringLiteral("BufferSize"), 128).toInt()); + setQueueBuffer(settings.value(QStringLiteral("QueueBuffer"), 0).toInt()); + setFeedbackDetectionEnabled( + settings.value(QStringLiteral("FeedbackDetectionEnabled"), true).toBool()); + settings.endGroup(); +} + +void VsAudio::saveSettings() +{ + QSettings settings; + settings.beginGroup(QStringLiteral("Audio")); + settings.setValue(QStringLiteral("InMultiplier"), m_inMultiplier); + settings.setValue(QStringLiteral("OutMultiplier"), m_outMultiplier); + settings.setValue(QStringLiteral("MonMultiplier"), m_monMultiplier); + // settings.setValue(QStringLiteral("InMuted"), m_inMuted); + settings.setValue(QStringLiteral("Backend"), getUseRtAudio() ? 1 : 0); + settings.setValue(QStringLiteral("InputDevice"), m_inputDevice); + settings.setValue(QStringLiteral("OutputDevice"), m_outputDevice); + settings.setValue(QStringLiteral("BaseInputChannel"), m_baseInputChannel); + settings.setValue(QStringLiteral("NumInputChannels"), m_numInputChannels); + settings.setValue(QStringLiteral("InputMixMode"), m_inputMixMode); + settings.setValue(QStringLiteral("BaseOutputChannel"), m_baseOutputChannel); + settings.setValue(QStringLiteral("NumOutputChannels"), m_numOutputChannels); + settings.setValue(QStringLiteral("BufferSize"), m_audioBufferSize); + settings.setValue(QStringLiteral("QueueBuffer"), m_queueBuffer); + settings.setValue(QStringLiteral("FeedbackDetectionEnabled"), + m_feedbackDetectionEnabled); + settings.endGroup(); +} + +void VsAudio::detectedFeedbackLoop() +{ + setInputMuted(true); + setMonitorVolume(0); + emit feedbackDetected(); +} + +void VsAudio::updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels) +{ + bool detectedClip = false; + + // Always output 2 meter readings to the UI + for (int i = 0; i < 2; i++) { + // Determine decibel reading + float dB = m_meterMin; + if (i < numChannels) { + dB = std::max(m_meterMin, valuesInDecibels[i]); + } + + // Produce a normalized value from 0 to 1 + m_inputMeterLevels[i] = (dB - m_meterMin) / (m_meterMax - m_meterMin); + + // Signal a clip if we haven't done so already + if (dB >= -0.05 && !detectedClip) { + m_inputClipTimer.start(); + m_inputClipped = true; + emit updatedInputClipped(m_inputClipped); + detectedClip = true; + } + } + +#ifdef RT_AUDIO + // For certain specific cases, copy the first channel's value into the second + // channel's value + if (getUseRtAudio() + && ((m_inputMixMode == static_cast(AudioInterface::MONO) + && m_numInputChannels == 1) + || (m_inputMixMode == static_cast(AudioInterface::MIXTOMONO) + && m_numInputChannels == 2))) { + m_inputMeterLevels[1] = m_inputMeterLevels[0]; + } +#endif + + emit updatedInputMeterLevels(m_inputMeterLevels); +} + +void VsAudio::updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels) +{ + bool detectedClip = false; + + // Always output 2 meter readings to the UI + for (int i = 0; i < 2; i++) { + // Determine decibel reading + float dB = m_meterMin; + if (i < numChannels) { + dB = std::max(m_meterMin, valuesInDecibels[i]); + } + + // Produce a normalized value from 0 to 1 + m_outputMeterLevels[i] = (dB - m_meterMin) / (m_meterMax - m_meterMin); + + // Signal a clip if we haven't done so already + if (dB >= -0.05 && !detectedClip) { + m_outputClipTimer.start(); + m_outputClipped = true; + emit updatedOutputClipped(m_outputClipped); + detectedClip = true; + } + } +#ifdef RT_AUDIO + if (m_numOutputChannels == 1) { + m_outputMeterLevels[1] = m_outputMeterLevels[0]; + } +#endif + emit updatedOutputMeterLevels(m_outputMeterLevels); +} + +void VsAudio::appendProcessPlugins(AudioInterface& audioInterface, bool forJackTrip, + int numInputChannels, int numOutputChannels) +{ + // Make sure clip timers are stopped + m_inputClipTimer.stop(); + m_outputClipTimer.stop(); + + // Reset meters + m_inputMeterLevels[0] = m_inputMeterLevels[1] = 0; + m_outputMeterLevels[0] = m_outputMeterLevels[1] = 0; + m_inputClipped = m_outputClipped = false; + emit updatedInputMeterLevels(m_inputMeterLevels); + emit updatedOutputMeterLevels(m_outputMeterLevels); + emit updatedInputClipped(m_inputClipped); + emit updatedOutputClipped(m_outputClipped); + setInputMuted(false); + + // Create plugins + m_inputMeterPluginPtr = new Meter(numInputChannels); + m_outputMeterPluginPtr = new Meter(numOutputChannels); + m_inputVolumePluginPtr = new Volume(numInputChannels); + m_outputVolumePluginPtr = new Volume(numOutputChannels); + + // initialize input and output volumes + m_outputVolumePluginPtr->volumeUpdated(m_outMultiplier); + m_inputVolumePluginPtr->volumeUpdated(m_inMultiplier); + m_inputVolumePluginPtr->muteUpdated(m_inMuted); + + // Connect plugins for communication with UI + connect(m_inputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this, + &VsAudio::updatedInputVuMeasurements); + connect(m_outputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this, + &VsAudio::updatedOutputVuMeasurements); + connect(this, &VsAudio::updatedInputVolume, m_inputVolumePluginPtr, + &Volume::volumeUpdated); + connect(this, &VsAudio::updatedOutputVolume, m_outputVolumePluginPtr, + &Volume::volumeUpdated); + connect(this, &VsAudio::updatedInputMuted, m_inputVolumePluginPtr, + &Volume::muteUpdated); + + // Note that plugin ownership is passed to the JackTrip class + // In particular, the AudioInterface that it uses to connect + audioInterface.appendProcessPluginToNetwork(m_inputVolumePluginPtr); + audioInterface.appendProcessPluginToNetwork(m_inputMeterPluginPtr); + + if (forJackTrip) { + // plugins for stream going to audio interface + audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr); + + // Setup monitor + // Note: Constructor determines how many internal monitor buffers to allocate + m_monitorPluginPtr = new Monitor(std::max(numInputChannels, numOutputChannels)); + m_monitorPluginPtr->volumeUpdated(m_monMultiplier); + audioInterface.appendProcessPluginToMonitor(m_monitorPluginPtr); + connect(this, &VsAudio::updatedMonitorVolume, m_monitorPluginPtr, + &Monitor::volumeUpdated); + +#ifndef NO_FEEDBACK + // Setup output analyzer + if (m_feedbackDetectionEnabled) { + m_outputAnalyzerPluginPtr = new Analyzer(numOutputChannels); + m_outputAnalyzerPluginPtr->setIsMonitoringAnalyzer(true); + audioInterface.appendProcessPluginToMonitor(m_outputAnalyzerPluginPtr); + connect(m_outputAnalyzerPluginPtr, &Analyzer::signalFeedbackDetected, this, + &VsAudio::detectedFeedbackLoop); + } +#endif + + // Setup output meter + // Note: Add this to monitor process to include self-volume + m_outputMeterPluginPtr->setIsMonitoringMeter(true); + audioInterface.appendProcessPluginToMonitor(m_outputMeterPluginPtr); + + } else { + // tone plugin is used to test audio output + Tone* outputTonePluginPtr = new Tone(getNumOutputChannels()); + connect(this, &VsAudio::signalPlayOutputAudio, outputTonePluginPtr, + &Tone::triggerPlayback); + audioInterface.appendProcessPluginFromNetwork(outputTonePluginPtr); + + // plugins for stream going to audio interface + audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr); + audioInterface.appendProcessPluginFromNetwork(m_outputMeterPluginPtr); + } +} + +void VsAudio::setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel) +{ + m_inputComboModel = inputComboModel; + m_outputComboModel = outputComboModel; + emit inputComboModelChanged(); + emit outputComboModelChanged(); + if (!m_deviceModelsInitialized) { + m_deviceModelsInitialized = true; + emit deviceModelsInitializedChanged(true); + } +} + +void VsAudio::setInputChannelsComboModel(QJsonArray& model) +{ + m_inputChannelsComboModel = model; + emit inputChannelsComboModelChanged(); +} + +void VsAudio::setOutputChannelsComboModel(QJsonArray& model) +{ + m_outputChannelsComboModel = model; + emit outputChannelsComboModelChanged(); +} + +void VsAudio::setInputMixModeComboModel(QJsonArray& model) +{ + m_inputMixModeComboModel = model; + emit inputMixModeComboModelChanged(); +} + +void VsAudio::updateDeviceMessages(AudioInterface& audioInterface) +{ + QString devicesWarningMsg = + QString::fromStdString(audioInterface.getDevicesWarningMsg()); + QString devicesErrorMsg = QString::fromStdString(audioInterface.getDevicesErrorMsg()); + QString devicesWarningHelpUrl = + QString::fromStdString(audioInterface.getDevicesWarningHelpUrl()); + QString devicesErrorHelpUrl = + QString::fromStdString(audioInterface.getDevicesErrorHelpUrl()); + + if (devicesWarningMsg != "") { + qDebug() << "Devices Warning: " << devicesWarningMsg; + if (devicesWarningHelpUrl != "") { + qDebug() << "Learn More: " << devicesWarningHelpUrl; + } + } + + if (devicesErrorMsg != "") { + qDebug() << "Devices Error: " << devicesErrorMsg; + if (devicesErrorHelpUrl != "") { + qDebug() << "Learn More: " << devicesErrorHelpUrl; + } + } + + setDevicesWarningMsg(devicesWarningMsg); + setDevicesErrorMsg(devicesErrorMsg); + setDevicesWarningHelpUrl(devicesWarningHelpUrl); + setDevicesErrorHelpUrl(devicesErrorHelpUrl); + setHighLatencyFlag(audioInterface.getHighLatencyFlag()); +} + +AudioInterface* VsAudio::newAudioInterface(JackTrip* jackTripPtr) +{ + // Create AudioInterface Client Object + AudioInterface* ifPtr = nullptr; + if (m_backend == VsAudio::AudioBackendType::JACK) { + if (!isBackendAvailable()) { + throw std::runtime_error( + "JackTrip was not compiled with support for the Jack backend. " + "In order to use Jack, you'll need to " + "rebuild with Jack support."); + } + if (!jackIsAvailable()) { + throw std::runtime_error( + "Unable to load the Jack client library. " + "In order to use Jack, you'll need to first install it."); + } + qDebug() << "Using JACK backend"; + ifPtr = newJackAudioInterface(jackTripPtr); + } else if (m_backend == VsAudio::AudioBackendType::RTAUDIO) { + if (!isBackendAvailable()) { + throw std::runtime_error( + "JackTrip was not compiled with support for the RtAudio backend. " + "In order to use RtAudio, you'll need to " + "rebuild with RtAudio support."); + } + qDebug() << "Using RtAudio backend"; + ifPtr = newRtAudioInterface(jackTripPtr); + } else { + throw std::runtime_error("Unknown audio backend"); + } + + mHasErrors = false; + ifPtr->setErrorCallback([this, jackTripPtr](const std::string& errorText) { + this->errorCallback(errorText, jackTripPtr); + }); + + // AudioInterface::setup() can return a different buffer size + // if the audio interface doesn't support the one that was requested + if (ifPtr->getBufferSizeInSamples() != uint32_t(getBufferSize())) { + setBufferSize(ifPtr->getBufferSizeInSamples()); + } + + std::cout << "The Sampling Rate is: " << ifPtr->getSampleRate() << std::endl; + std::cout << gPrintSeparator << std::endl; + int AudioBufferSizeInBytes = ifPtr->getBufferSizeInSamples() * sizeof(sample_t); + std::cout << "The Audio Buffer Size is: " << ifPtr->getBufferSizeInSamples() + << " samples" << std::endl; + std::cout << " or: " << AudioBufferSizeInBytes << " bytes" + << std::endl; + std::cout << gPrintSeparator << std::endl; + std::cout << "The Number of Channels is: " << ifPtr->getNumInputChannels() + << std::endl; + std::cout << gPrintSeparator << std::endl; + + // setup audio plugins + appendProcessPlugins(*ifPtr, jackTripPtr != nullptr, getNumInputChannels(), + getNumOutputChannels()); + + return ifPtr; +} + +AudioInterface* VsAudio::newJackAudioInterface([[maybe_unused]] JackTrip* jackTripPtr) +{ + AudioInterface* ifPtr = nullptr; +#ifndef NO_JACK + static const int numJackChannels = 2; + if constexpr (isBackendAvailable() + || isBackendAvailable()) { + QVarLengthArray inputChans; + QVarLengthArray outputChans; + inputChans.resize(numJackChannels); + outputChans.resize(numJackChannels); + + for (int i = 0; i < numJackChannels; i++) { + inputChans[i] = 1 + i; + } + for (int i = 0; i < numJackChannels; i++) { + outputChans[i] = 1 + i; + } + + ifPtr = new JackAudioInterface(inputChans, outputChans, m_audioBitResolution, + jackTripPtr != nullptr, jackTripPtr); + ifPtr->setClientName(QStringLiteral("JackTrip")); +#if defined(__unix__) + AudioInterface::setPipewireLatency(getBufferSize(), getSampleRate()); +#endif + ifPtr->setup(true); + } +#endif + return ifPtr; +} + +AudioInterface* VsAudio::newRtAudioInterface([[maybe_unused]] JackTrip* jackTripPtr) +{ + AudioInterface* ifPtr = nullptr; +#ifdef RT_AUDIO + QVarLengthArray inputChans; + QVarLengthArray outputChans; + inputChans.resize(getNumInputChannels()); + outputChans.resize(getNumOutputChannels()); + + for (int i = 0; i < getNumInputChannels(); i++) { + inputChans[i] = getBaseInputChannel() + i; + } + for (int i = 0; i < getNumOutputChannels(); i++) { + outputChans[i] = getBaseOutputChannel() + i; + } + + ifPtr = new RtAudioInterface( + inputChans, outputChans, + static_cast(getInputMixMode()), + m_audioBitResolution, jackTripPtr != nullptr, jackTripPtr); + ifPtr->setSampleRate(getSampleRate()); + ifPtr->setInputDevice(getInputDevice().toStdString()); + ifPtr->setOutputDevice(getOutputDevice().toStdString()); + ifPtr->setBufferSizeInSamples(getBufferSize()); + + QVector devices = m_audioWorkerPtr->getDevices(); + if (!devices.empty()) + static_cast(ifPtr)->setRtAudioDevices(devices); + +#if defined(__unix__) + AudioInterface::setPipewireLatency(getBufferSize(), ifPtr->getSampleRate()); +#endif + + // Note: setup might change the number of channels and/or buffer size + ifPtr->setup(true); + + // TODO: Add check for if base input channel needs to change + if (jackTripPtr != nullptr && getNumInputChannels() == 2 + && getInputMixMode() == AudioInterface::MIXTOMONO) + jackTripPtr->setNumInputChannels(1); + +#endif // RT_AUDIO + return ifPtr; +} + +void VsAudio::errorCallback(const std::string& errorText, + [[maybe_unused]] JackTrip* jackTripPtr) +{ + const QString errorMsg(QString::fromStdString(errorText)); + setDevicesErrorMsg(errorMsg); +#ifdef _WIN32 + // handle special case for Windows ASIO drivers that trigger + // asynchronous errors shortly after you try opening the + // RtAudio stream with a different sample rate (only for audio tester) + if (jackTripPtr == nullptr && getUseRtAudio() + && errorMsg.contains("sample rate changed")) { + // only refresh devices once + if (mHasErrors) + return; + mHasErrors = true; + // asynchronously refresh devices + refreshDevices(false); + } +#else + mHasErrors = true; +#endif +} + +// VsAudioWorker methods + +VsAudioWorker::VsAudioWorker(VsAudio* ptr) : m_parentPtr(ptr) {} + +void VsAudioWorker::openAudioInterface() +{ +#ifdef __APPLE__ + if (m_parentPtr->m_permissionsPtr->micPermission() != "granted") { + return; + } +#endif + + if constexpr (!(isBackendAvailable() + || isBackendAvailable())) { + return; + } + + if (!m_audioInterfacePtr.isNull()) { + std::cout << "Restarting Audio" << std::endl; + closeAudioInterface(); + } else { + std::cout << "Starting Audio" << std::endl; + } + + unsigned int maxTries = 2; +#ifdef RT_AUDIO + // Update devices, if not already initialized + if (getUseRtAudio()) { + if (!m_parentPtr->getDeviceModelsInitialized()) { + updateDeviceModels(); + maxTries = 1; + } + } +#endif + for (unsigned int tryNum = 0; tryNum < maxTries; ++tryNum) { +#ifdef RT_AUDIO + if (tryNum > 0) { + if (getUseRtAudio()) { + updateDeviceModels(); + } else { + m_parentPtr->setAudioBackend("RtAudio"); + updateDeviceModels(); + } + } +#endif + try { + // create and setup a new audio interface + m_audioInterfacePtr.reset(m_parentPtr->newAudioInterface()); + // success if it doesn't throw + break; + } catch (const std::exception& e) { + emit signalError(QString::fromUtf8(e.what())); + } + } + + if (m_audioInterfacePtr.isNull()) { + return; + } + + // initialize plugins and start the audio callback process + m_audioInterfacePtr->initPlugins(false); + m_audioInterfacePtr->startProcess(); + m_audioInterfacePtr->connectDefaultPorts(); + + m_parentPtr->updateDeviceMessages(*m_audioInterfacePtr); + m_parentPtr->setAudioReady(true); +} + +void VsAudioWorker::closeAudioInterface() +{ + if (m_audioInterfacePtr.isNull()) + return; + std::cout << "Stopping Audio" << std::endl; + try { + m_audioInterfacePtr->stopProcess(); + } catch (const std::exception& e) { + emit signalError(QString::fromUtf8(e.what())); + } + m_audioInterfacePtr.clear(); + m_parentPtr->setAudioReady(false); +} + +#ifdef RT_AUDIO + +void VsAudioWorker::refreshDevices() +{ + if (!getUseRtAudio()) + return; + bool restartAudio = !m_audioInterfacePtr.isNull(); + if (restartAudio) + closeAudioInterface(); + updateDeviceModels(); + if (restartAudio) + openAudioInterface(); +} + +void VsAudioWorker::updateDeviceModels() +{ + if (!getUseRtAudio()) + return; + + // note: audio must not be active when scanning devices + m_parentPtr->setScanningDevices(true); + closeAudioInterface(); + RtAudioInterface::scanDevices(m_devices); + + QStringList inputDeviceCategories; + QStringList outputDeviceCategories; + + getDeviceList(m_devices, m_inputDeviceList, inputDeviceCategories, + m_inputDeviceChannels, true); + getDeviceList(m_devices, m_outputDeviceList, outputDeviceCategories, + m_outputDeviceChannels, false); + + QJsonArray inputComboModel = + formatDeviceList(m_inputDeviceList, inputDeviceCategories, m_inputDeviceChannels); + QJsonArray outputComboModel = formatDeviceList( + m_outputDeviceList, outputDeviceCategories, m_outputDeviceChannels); + + validateDevices(); + + // let VsAudio know that things have been updated + m_parentPtr->setScanningDevices(false); + emit signalUpdatedDeviceModels(inputComboModel, outputComboModel); +} + +void VsAudioWorker::getDeviceList(const QVector& devices, + QStringList& list, QStringList& categories, + QList& channels, bool isInput) +{ + categories.clear(); + channels.clear(); + list.clear(); + + // do not include blacklisted audio interfaces + // these are known to be unstable and cause JackTrip to crash + QVector blacklisted_devices = { +#ifdef _WIN32 + // Realtek ASIO: seems to crash any computer that tries to use it + QString::fromUtf8("Realtek ASIO"), + QString::fromUtf8("Generic Low Latency ASIO Driver"), +#endif + // JackRouter: crashes if not running; use Jack backend instead + QString::fromUtf8("JackRouter"), + }; + + for (int n = 0; n < devices.size(); ++n) { +#ifdef _WIN32 + if (devices[n].api == RtAudio::UNIX_JACK) { + continue; + } +#endif + const QString deviceName(QString::fromStdString(devices[n].name)); + + // Don't include duplicate entries + if (list.contains(deviceName)) { + continue; + } + + // Skip if no channels available + if ((isInput && devices[n].inputChannels == 0) + || (!isInput && devices[n].outputChannels == 0)) { + continue; + } + + // Skip blacklisted devices + const bool iPhoneMic = deviceName.startsWith("Apple Inc.:") + && deviceName.endsWith("Phone Microphone"); + if (blacklisted_devices.contains(deviceName) || iPhoneMic) { + std::cout << "RTAudio: blacklisted " << (isInput ? "input" : "output") + << " device: " << devices[n].name << std::endl; + continue; + } + + // Good to go! + if (isInput) { + list.append(deviceName); + channels.append(devices[n].inputChannels); + } else { + list.append(deviceName); + channels.append(devices[n].outputChannels); + } + + switch (devices[n].api) { + case RtAudio::WINDOWS_ASIO: + categories.append("Low-Latency (ASIO)"); + break; + case RtAudio::WINDOWS_WASAPI: + categories.append("High-Latency (WASAPI)"); + break; + case RtAudio::WINDOWS_DS: + categories.append("High-Latency (DirectSound)"); + break; + case RtAudio::LINUX_ALSA: + categories.append("Low-Latency (ALSA)"); + break; + case RtAudio::LINUX_PULSE: + categories.append("High-Latency (Pulse)"); + break; + case RtAudio::LINUX_OSS: + categories.append("High-Latency (OSS)"); + break; + default: + categories.append(""); + break; + } + } +} + +QJsonArray VsAudioWorker::formatDeviceList(const QStringList& devices, + const QStringList& categories, + const QList& channels) +{ + QStringList uniqueCategories = QStringList(categories); + uniqueCategories.removeDuplicates(); + + bool containsCategories = true; + if (uniqueCategories.size() == 0) { + containsCategories = false; + } else if (uniqueCategories.size() == 1 && uniqueCategories.at(0) == "") { + containsCategories = false; + } + + QJsonArray items; + for (int i = 0; i < uniqueCategories.size(); i++) { + QString category = uniqueCategories.at(i); + + if (containsCategories) { + QJsonObject header = QJsonObject(); + header.insert(QString::fromStdString("text"), category); + header.insert(QString::fromStdString("type"), + QString::fromStdString("header")); + header.insert(QString::fromStdString("category"), category); + items.push_back(header); + } + + for (int j = 0; j < devices.size(); j++) { + if (categories.at(j).toStdString() == category.toStdString()) { + QJsonObject element = QJsonObject(); + element.insert(QString::fromStdString("text"), devices.at(j)); + element.insert(QString::fromStdString("type"), + QString::fromStdString("element")); + element.insert(QString::fromStdString("channels"), channels.at(j)); + element.insert(QString::fromStdString("category"), category); + items.push_back(element); + } + } + } + + return items; +} + +void VsAudioWorker::validateDevices() +{ + if (!getUseRtAudio()) + return; + validateInputDevicesState(); + validateOutputDevicesState(); + emit signalDevicesValidated(); +} + +void VsAudioWorker::validateInputDevicesState() +{ + if (!getUseRtAudio()) { + return; + } + if (m_inputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) { + return; + } + + // Given input device list, check that the currently set device + // actually exists + if (getInputDevice() == QStringLiteral("") + || m_inputDeviceList.indexOf(getInputDevice()) == -1) { + m_parentPtr->setInputDevice(m_inputDeviceList[0]); + } + + // Given the currently selected input device, reset the available input channel + // options + int indexOfInput = m_inputDeviceList.indexOf(getInputDevice()); + if (indexOfInput == -1) { + std::cerr << "Invalid state. Input device index should never be -1" << std::endl; + return; + } + + int numDevicesChannelsAvailable = m_inputDeviceChannels.at(indexOfInput); + if (numDevicesChannelsAvailable < 1) { + std::cerr << "Invalid state. Number of channels should never be less than 1" + << std::endl; + return; + } else if (numDevicesChannelsAvailable == 1) { + // Set the input mix mode to just have "Mono" as the option + QJsonObject inputMixModeComboElement = QJsonObject(); + inputMixModeComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("Mono")); + inputMixModeComboElement.insert(QString::fromStdString("value"), + static_cast(AudioInterface::MONO)); + QJsonArray inputMixModeComboModel; + inputMixModeComboModel.push_back(inputMixModeComboElement); + m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel); + + // Set the input channels combo to only have channel 1 as an option + QJsonObject inputChannelsComboElement; + inputChannelsComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("1")); + inputChannelsComboElement.insert(QString::fromStdString("baseChannel"), + QVariant(0).toInt()); + inputChannelsComboElement.insert(QString::fromStdString("numChannels"), + QVariant(1).toInt()); + QJsonArray inputChannelsComboModel; + inputChannelsComboModel.push_back(inputChannelsComboElement); + m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel); + + // Set the only allowed options for these variables automatically + m_parentPtr->setBaseInputChannel(0); + m_parentPtr->setNumInputChannels(1); + m_parentPtr->setInputMixMode(static_cast(AudioInterface::MONO)); + } else { + // set the input channels selector to have the options based on the currently + // selected device + QJsonArray inputChannelsComboModel; + for (int i = 0; i < numDevicesChannelsAvailable; i++) { + QJsonObject element = QJsonObject(); + element.insert(QString::fromStdString("label"), QVariant(i + 1).toString()); + element.insert(QString::fromStdString("baseChannel"), QVariant(i).toInt()); + element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt()); + inputChannelsComboModel.push_back(element); + } + for (int i = 0; i < numDevicesChannelsAvailable; i++) { + if (i % 2 == 0) { + QJsonObject element = QJsonObject(); + element.insert( + QString::fromStdString("label"), + QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString()); + element.insert(QString::fromStdString("baseChannel"), + QVariant(i).toInt()); + element.insert(QString::fromStdString("numChannels"), + QVariant(2).toInt()); + inputChannelsComboModel.push_back(element); + } + } + m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel); + + // if the current m_baseInputChannel or m_numInputChannels is invalid based on + // this device's option, use the first two channels by default + if (getBaseInputChannel() + getNumInputChannels() > numDevicesChannelsAvailable) { + // we're in the case where numDevicesChannelsAvailable >= 2, so always have + // the ability to use the first 2 channels + m_parentPtr->setBaseInputChannel(0); + m_parentPtr->setNumInputChannels(2); + } + if (getNumInputChannels() != 1) { + // Set the input mix mode to have two options: "Stereo" and "Mix to Mono" if + // we're using 2 channels + QJsonObject inputMixModeComboElement1 = QJsonObject(); + inputMixModeComboElement1.insert(QString::fromStdString("label"), + QString::fromStdString("Stereo")); + inputMixModeComboElement1.insert(QString::fromStdString("value"), + static_cast(AudioInterface::STEREO)); + QJsonObject inputMixModeComboElement2 = QJsonObject(); + inputMixModeComboElement2.insert(QString::fromStdString("label"), + QString::fromStdString("Mix to Mono")); + inputMixModeComboElement2.insert(QString::fromStdString("value"), + static_cast(AudioInterface::MIXTOMONO)); + QJsonArray inputMixModeComboModel; + inputMixModeComboModel.push_back(inputMixModeComboElement1); + inputMixModeComboModel.push_back(inputMixModeComboElement2); + m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel); + + // if m_inputMixMode is an invalid value, set it to "stereo" by default + // given that we are using 2 channels + if (getInputMixMode() != static_cast(AudioInterface::STEREO) + && getInputMixMode() != static_cast(AudioInterface::MIXTOMONO)) { + m_parentPtr->setInputMixMode(static_cast(AudioInterface::STEREO)); + } + } else { + // Set the input mix mode to just have "Mono" as the option if we're using 1 + // channel + QJsonObject inputMixModeComboElement = QJsonObject(); + inputMixModeComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("Mono")); + inputMixModeComboElement.insert(QString::fromStdString("value"), + static_cast(AudioInterface::MONO)); + QJsonArray inputMixModeComboModel; + inputMixModeComboModel.push_back(inputMixModeComboElement); + m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel); + + // if m_inputMixMode is an invalid value, set it to AudioInterface::MONO + if (getInputMixMode() != static_cast(AudioInterface::MONO)) { + m_parentPtr->setInputMixMode(static_cast(AudioInterface::MONO)); + } + } + } +} + +void VsAudioWorker::validateOutputDevicesState() +{ + if (!getUseRtAudio()) { + return; + } + if (m_outputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) { + return; + } + + // Given output device list, check that the currently set device + // actually exists + if (getOutputDevice() == QStringLiteral("") + || m_outputDeviceList.indexOf(getOutputDevice()) == -1) { + m_parentPtr->setOutputDevice(m_outputDeviceList[0]); + } + + // Given the currently selected output device, reset the available output channel + // options + int indexOfOutput = m_outputDeviceList.indexOf(getOutputDevice()); + if (indexOfOutput == -1) { + std::cerr << "Invalid state. Output device index should never be -1" << std::endl; + return; + } + + int numDevicesChannelsAvailable = m_outputDeviceChannels.at(indexOfOutput); + if (numDevicesChannelsAvailable < 1) { + std::cerr << "Invalid state. Number of channels should never be less than 1" + << std::endl; + return; + } else if (numDevicesChannelsAvailable == 1) { + // Set the output channels combo to only have channel 1 as an option + QJsonObject outputChannelsComboElement = QJsonObject(); + outputChannelsComboElement.insert(QString::fromStdString("label"), + QString::fromStdString("1")); + outputChannelsComboElement.insert(QString::fromStdString("baseChannel"), + QVariant(0).toInt()); + outputChannelsComboElement.insert(QString::fromStdString("numChannels"), + QVariant(1).toInt()); + QJsonArray outputChannelsComboModel; + outputChannelsComboModel.push_back(outputChannelsComboElement); + m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel); + + // Set the only allowed options for these variables automatically + m_parentPtr->setBaseOutputChannel(0); + m_parentPtr->setNumOutputChannels(1); + } else { + // set the output channels selector to have the options based on the currently + // selected device + QJsonArray outputChannelsComboModel; + for (int i = 0; i < numDevicesChannelsAvailable; i++) { + if (i % 2 == 0) { + QJsonObject element = QJsonObject(); + element.insert( + QString::fromStdString("label"), + QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString()); + element.insert(QString::fromStdString("baseChannel"), + QVariant(i).toInt()); + element.insert(QString::fromStdString("numChannels"), + QVariant(2).toInt()); + outputChannelsComboModel.push_back(element); + } + } + m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel); + + // if the current m_baseOutputChannel or m_numOutputChannels is invalid based on + // this device's option, use the first two channels by default + if (getBaseOutputChannel() + getNumOutputChannels() + > numDevicesChannelsAvailable) { + // we're in the case where numDevicesChannelsAvailable >= 2, so always have + // the ability to use the first 2 channels + m_parentPtr->setBaseOutputChannel(0); + m_parentPtr->setNumOutputChannels(2); + } + } +} + +#endif // RT_AUDIO diff --git a/src/vs/vsAudio.h b/src/vs/vsAudio.h new file mode 100644 index 0000000..c75b9fc --- /dev/null +++ b/src/vs/vsAudio.h @@ -0,0 +1,439 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsAudio.h + * \author Matt Horton + * \date September 2022 + */ + +#ifndef VSDAUDIO_H +#define VSDAUDIO_H + +#include +#include +#include +#include +#include +#include +#include + +#include "../AudioInterface.h" +#include "../jacktrip_globals.h" +#include "vsPermissions.h" + +#ifdef RT_AUDIO +#include "../RtAudioInterface.h" +#endif + +class Analyzer; +class JackTrip; +class Meter; +class Monitor; +class QThread; +class Tone; +class Volume; +class VsAudioWorker; + +class VsAudio : public QObject +{ + Q_OBJECT + + // state shared with QML + Q_PROPERTY(bool audioReady READ getAudioReady NOTIFY signalAudioReadyChanged) + Q_PROPERTY(bool scanningDevices READ getScanningDevices WRITE setScanningDevices + NOTIFY signalScanningDevicesChanged) + Q_PROPERTY(bool feedbackDetectionEnabled READ getFeedbackDetectionEnabled WRITE + setFeedbackDetectionEnabled NOTIFY feedbackDetectionEnabledChanged) + Q_PROPERTY(bool deviceModelsInitialized READ getDeviceModelsInitialized NOTIFY + deviceModelsInitializedChanged) + Q_PROPERTY(bool backendAvailable READ backendAvailable CONSTANT) + Q_PROPERTY(QString audioBackend READ getAudioBackend WRITE setAudioBackend NOTIFY + audioBackendChanged) + Q_PROPERTY( + int sampleRate READ getSampleRate WRITE setSampleRate NOTIFY sampleRateChanged) + Q_PROPERTY( + int bufferSize READ getBufferSize WRITE setBufferSize NOTIFY bufferSizeChanged) + Q_PROPERTY(int queueBuffer READ getQueueBuffer WRITE setQueueBuffer NOTIFY + queueBufferChanged) + Q_PROPERTY(int numInputChannels READ getNumInputChannels WRITE setNumInputChannels + NOTIFY numInputChannelsChanged) + Q_PROPERTY(int numOutputChannels READ getNumOutputChannels WRITE setNumOutputChannels + NOTIFY numOutputChannelsChanged) + Q_PROPERTY(int baseInputChannel READ getBaseInputChannel WRITE setBaseInputChannel + NOTIFY baseInputChannelChanged) + Q_PROPERTY(int baseOutputChannel READ getBaseOutputChannel WRITE setBaseOutputChannel + NOTIFY baseOutputChannelChanged) + Q_PROPERTY(int inputMixMode READ getInputMixMode WRITE setInputMixMode NOTIFY + inputMixModeChanged) + Q_PROPERTY( + bool inputMuted READ getInputMuted WRITE setInputMuted NOTIFY updatedInputMuted) + Q_PROPERTY(bool inputClipped READ getInputClipped NOTIFY updatedInputClipped) + Q_PROPERTY(bool outputClipped READ getOutputClipped NOTIFY updatedOutputClipped) + Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY + updatedInputVolume) + Q_PROPERTY(float outputVolume READ getOutputVolume WRITE setOutputVolume NOTIFY + updatedOutputVolume) + Q_PROPERTY(float monitorVolume READ getMonitorVolume WRITE setMonitorVolume NOTIFY + updatedMonitorVolume) + Q_PROPERTY(QString inputDevice READ getInputDevice WRITE setInputDevice NOTIFY + inputDeviceChanged) + Q_PROPERTY(QString outputDevice READ getOutputDevice WRITE setOutputDevice NOTIFY + outputDeviceChanged) + Q_PROPERTY(QVector inputMeterLevels READ getInputMeterLevels NOTIFY + updatedInputMeterLevels) + Q_PROPERTY(QVector outputMeterLevels READ getOutputMeterLevels NOTIFY + updatedOutputMeterLevels) + Q_PROPERTY( + QJsonArray inputComboModel READ getInputComboModel NOTIFY inputComboModelChanged) + Q_PROPERTY(QJsonArray outputComboModel READ getOutputComboModel NOTIFY + outputComboModelChanged) + Q_PROPERTY(QJsonArray inputChannelsComboModel READ getInputChannelsComboModel NOTIFY + inputChannelsComboModelChanged) + Q_PROPERTY(QJsonArray outputChannelsComboModel READ getOutputChannelsComboModel NOTIFY + outputChannelsComboModelChanged) + Q_PROPERTY(QJsonArray inputMixModeComboModel READ getInputMixModeComboModel NOTIFY + inputMixModeComboModelChanged) + Q_PROPERTY(QStringList bufferSizeComboModel READ getBufferSizeComboModel CONSTANT) + Q_PROPERTY(QStringList audioBackendComboModel READ getAudioBackendComboModel CONSTANT) + Q_PROPERTY( + QString devicesWarning READ getDevicesWarningMsg NOTIFY devicesWarningChanged) + Q_PROPERTY(QString devicesError READ getDevicesErrorMsg NOTIFY devicesErrorChanged) + Q_PROPERTY(QString devicesWarningHelpUrl READ getDevicesWarningHelpUrl NOTIFY + devicesWarningHelpUrlChanged) + Q_PROPERTY(QString devicesErrorHelpUrl READ getDevicesErrorHelpUrl NOTIFY + devicesErrorHelpUrlChanged) + Q_PROPERTY(bool highLatencyFlag READ getHighLatencyFlag NOTIFY highLatencyFlagChanged) + + public: + enum AudioBackendType { + JACK = 0, ///< Jack Mode + RTAUDIO ///< RtAudio Mode + }; + + // Constructor + explicit VsAudio(QObject* parent = nullptr); + virtual ~VsAudio(); + + // allow VirtualStudio to get Permissions to bind to QML view + VsPermissions& getPermissions() { return *m_permissionsPtr; } + VsAudioWorker& getWorker() { return *m_audioWorkerPtr; } + + // allow VirtualStudio to create new audio interfaces + AudioInterface* newAudioInterface(JackTrip* jackTripPtr = nullptr); + + // allow VirtualStudio to load and save settings + void loadSettings(); + void saveSettings(); + + // getters for state shared with QML + bool backendAvailable() const; + bool jackIsAvailable() const; + bool asioIsAvailable() const; + bool getAudioReady() const { return m_audioReady; } + bool getScanningDevices() const { return m_scanningDevices; } + bool getFeedbackDetectionEnabled() const { return m_feedbackDetectionEnabled; } + bool getDeviceModelsInitialized() const { return m_deviceModelsInitialized; } + bool getUseRtAudio() const { return m_backend == AudioBackendType::RTAUDIO; } + QString getAudioBackend() const + { + return getUseRtAudio() ? QStringLiteral("RtAudio") : QStringLiteral("JACK"); + } + int getSampleRate() const { return m_audioSampleRate; } + int getBufferSize() const { return m_audioBufferSize; } + int getQueueBuffer() const { return m_queueBuffer; } + int getNumInputChannels() const { return getUseRtAudio() ? m_numInputChannels : 2; } + int getNumOutputChannels() const { return getUseRtAudio() ? m_numOutputChannels : 2; } + int getBaseInputChannel() const { return getUseRtAudio() ? m_baseInputChannel : 0; } + int getBaseOutputChannel() const { return getUseRtAudio() ? m_baseOutputChannel : 0; } + int getInputMixMode() const { return getUseRtAudio() ? m_inputMixMode : 0; } + bool getInputMuted() const { return m_inMuted; } + bool getInputClipped() const { return m_inputClipped; } + bool getOutputClipped() const { return m_outputClipped; } + float getInputVolume() const { return m_inMultiplier; } + float getOutputVolume() const { return m_outMultiplier; } + float getMonitorVolume() const { return m_monMultiplier; } + const QString& getInputDevice() const { return m_inputDevice; } + const QString& getOutputDevice() const { return m_outputDevice; } + const QVector& getInputMeterLevels() const { return m_inputMeterLevels; } + const QVector& getOutputMeterLevels() const { return m_outputMeterLevels; } + const QJsonArray& getInputComboModel() const { return m_inputComboModel; } + const QJsonArray& getOutputComboModel() const { return m_outputComboModel; } + const QJsonArray& getInputChannelsComboModel() const + { + return m_inputChannelsComboModel; + } + const QJsonArray& getOutputChannelsComboModel() const + { + return m_outputChannelsComboModel; + } + const QJsonArray& getInputMixModeComboModel() const + { + return m_inputMixModeComboModel; + } + const QStringList& getBufferSizeComboModel() const { return m_bufferSizeComboModel; } + const QStringList& getAudioBackendComboModel() const + { + return m_audioBackendComboModel; + } + const QString& getDevicesWarningMsg() const { return m_devicesWarningMsg; } + const QString& getDevicesErrorMsg() const { return m_devicesErrorMsg; } + const QString& getDevicesWarningHelpUrl() const { return m_devicesWarningHelpUrl; } + const QString& getDevicesErrorHelpUrl() const { return m_devicesErrorHelpUrl; } + bool getHighLatencyFlag() const { return m_highLatencyFlag; } + public slots: + + // setters for state shared with QML + void setFeedbackDetectionEnabled(bool enabled); + void setAudioBackend(const QString& backend); + void setSampleRate(int sampleRate); + void setBufferSize(int bufSize); + void setQueueBuffer(int queueBuffer); + void setNumInputChannels(int numChannels); + void setNumOutputChannels(int numChannels); + void setBaseInputChannel(int baseChannel); + void setBaseOutputChannel(int baseChannel); + void setInputMixMode(int mode); + void setInputMuted(bool muted); + void setInputVolume(float multiplier); + void setOutputVolume(float multiplier); + void setMonitorVolume(float multiplier); + void setInputDevice(const QString& device); + void setOutputDevice(const QString& device); + void setDevicesErrorMsg(const QString& msg); + void setDevicesWarningMsg(const QString& msg); + void setDevicesErrorHelpUrl(const QString& url); + void setDevicesWarningHelpUrl(const QString& url); + void setHighLatencyFlag(bool highLatency); + + // public methods accessible by QML + void startAudio(bool block = false); + void stopAudio(bool block = false); + void refreshDevices(bool block = false); + void validateDevices(bool block = false); + void restartAudio(bool block = false) { return startAudio(block); } + void playOutputAudio() { emit signalPlayOutputAudio(); } + + signals: + + // signals for QML state changes + void signalAudioReadyChanged(); + void signalAudioIsReady(); + void signalAudioIsNotReady(); + void signalScanningDevicesChanged(); + void deviceModelsInitializedChanged(bool initialized); + void audioBackendChanged(bool useRtAudio); + void sampleRateChanged(); + void bufferSizeChanged(); + void queueBufferChanged(); + void numInputChannelsChanged(int numChannels); + void numOutputChannelsChanged(int numChannels); + void baseInputChannelChanged(int baseChannel); + void baseOutputChannelChanged(int baseChannel); + void inputMixModeChanged(int mode); + void updatedInputMuted(bool muted); + void updatedInputClipped(bool clip); + void updatedOutputClipped(bool clip); + void updatedInputVolume(float multiplier); + void updatedOutputVolume(float multiplier); + void updatedMonitorVolume(float multiplier); + void inputDeviceChanged(QString device); + void outputDeviceChanged(QString device); + void updatedInputMeterLevels(const QVector& levels); + void updatedOutputMeterLevels(const QVector& levels); + void feedbackDetectionEnabledChanged(); + void feedbackDetected(); + void inputComboModelChanged(); + void outputComboModelChanged(); + void inputChannelsComboModelChanged(); + void outputChannelsComboModelChanged(); + void inputMixModeComboModelChanged(); + void devicesWarningChanged(); + void devicesErrorChanged(); + void devicesWarningHelpUrlChanged(); + void devicesErrorHelpUrlChanged(); + void highLatencyFlagChanged(bool highLatencyFlag); + + // other signals to perform actions + void signalPlayOutputAudio(); + void signalStartAudio(); + void signalStopAudio(); + void signalRefreshDevices(); + void signalValidateDevices(); + void signalDevicesValidated(); + + private slots: + void setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel); + void setInputChannelsComboModel(QJsonArray& model); + void setOutputChannelsComboModel(QJsonArray& model); + void setInputMixModeComboModel(QJsonArray& model); + + private: + // private methods + void setAudioReady(bool ready); + void setScanningDevices(bool b); + void detectedFeedbackLoop(); + void updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels); + void updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels); + void appendProcessPlugins(AudioInterface& audioInterface, bool forJackTrip, + int numInputChannels, int numOutputChannels); + void updateDeviceMessages(AudioInterface& audioInterface); + AudioInterface* newJackAudioInterface(JackTrip* jackTripPtr = nullptr); + AudioInterface* newRtAudioInterface(JackTrip* jackTripPtr = nullptr); + void errorCallback(const std::string& errorText, JackTrip* jackTripPtr = nullptr); + + // range for volume meters + static constexpr float m_meterMax = 0.0; + static constexpr float m_meterMin = -64.0; + + // audio bit resolution + static constexpr AudioInterface::audioBitResolutionT m_audioBitResolution = + AudioInterface::BIT16; + + // state shared with QML + AudioBackendType m_backend = AudioBackendType::JACK; + bool m_audioReady = false; + bool m_scanningDevices = false; + bool m_feedbackDetectionEnabled = true; + bool m_deviceModelsInitialized = false; + int m_audioSampleRate = gDefaultSampleRate; + int m_audioBufferSize = + gDefaultBufferSizeInSamples; ///< Audio buffer size to process on each callback + int m_queueBuffer = 0; + int m_numInputChannels = gDefaultNumInChannels; + int m_numOutputChannels = gDefaultNumOutChannels; + int m_baseInputChannel = 0; + int m_baseOutputChannel = 0; + int m_inputMixMode = 0; + bool m_inMuted = false; + bool m_inputClipped = false; + bool m_outputClipped = false; + float m_inMultiplier = 1.0; + float m_outMultiplier = 1.0; + float m_monMultiplier = 0; + + QString m_inputDevice; + QString m_outputDevice; + QVector m_inputMeterLevels; + QVector m_outputMeterLevels; + QJsonArray m_inputComboModel; + QJsonArray m_outputComboModel; + QJsonArray m_inputChannelsComboModel; + QJsonArray m_outputChannelsComboModel; + QJsonArray m_inputMixModeComboModel; + QString m_devicesWarningMsg = QStringLiteral(""); + QString m_devicesErrorMsg = QStringLiteral(""); + QString m_devicesWarningHelpUrl = QStringLiteral(""); + QString m_devicesErrorHelpUrl = QStringLiteral(""); + bool m_highLatencyFlag = false; + + // other state not shared with QML + QSharedPointer m_permissionsPtr; + QScopedPointer m_audioWorkerPtr; + QThread* m_workerThreadPtr; + QTimer m_inputClipTimer; + QTimer m_outputClipTimer; + Meter* m_inputMeterPluginPtr; + Meter* m_outputMeterPluginPtr; + Volume* m_inputVolumePluginPtr; + Volume* m_outputVolumePluginPtr; + Monitor* m_monitorPluginPtr; + bool mHasErrors; ///< true if one or more error callbacks have been triggered + +#ifndef NO_FEEDBACK + Analyzer* m_outputAnalyzerPluginPtr; +#endif + + QStringList m_audioBackendComboModel = {"JACK", "RtAudio"}; + QStringList m_bufferSizeComboModel = {"16", "32", "64", "128", "256", "512", "1024"}; + + friend class VsAudioWorker; +}; + +/// VsAudioWorker uses a separate thread to help VsAudio +class VsAudioWorker : public QObject +{ + Q_OBJECT + + public: + VsAudioWorker(VsAudio* ptr); + virtual ~VsAudioWorker() {} + + signals: + void signalDevicesValidated(); + void signalUpdatedDeviceModels(QJsonArray inputComboModel, + QJsonArray outputComboModel); + void signalError(const QString& errorMessage); + + public slots: + void openAudioInterface(); + void closeAudioInterface(); +#ifdef RT_AUDIO + void refreshDevices(); + void validateDevices(); + + private: + void updateDeviceModels(); + void validateInputDevicesState(); + void validateOutputDevicesState(); + static void getDeviceList(const QVector& devices, QStringList& list, + QStringList& categories, QList& channels, + bool isInput); + static QJsonArray formatDeviceList(const QStringList& devices, + const QStringList& categories, + const QList& channels); + QVector m_devices; + + public: + QVector getDevices() const { return m_devices; } +#endif + + private: + // parent getter wrappers + bool getUseRtAudio() const { return m_parentPtr->getUseRtAudio(); } + int getNumInputChannels() const { return m_parentPtr->getNumInputChannels(); } + int getNumOutputChannels() const { return m_parentPtr->getNumOutputChannels(); } + int getBaseInputChannel() const { return m_parentPtr->getBaseInputChannel(); } + int getBaseOutputChannel() const { return m_parentPtr->getBaseOutputChannel(); } + int getSampleRate() const { return m_parentPtr->getSampleRate(); } + int getBufferSize() const { return m_parentPtr->getBufferSize(); } + int getInputMixMode() const { return m_parentPtr->getInputMixMode(); } + const QString& getInputDevice() const { return m_parentPtr->getInputDevice(); } + const QString& getOutputDevice() const { return m_parentPtr->getOutputDevice(); } + + VsAudio* m_parentPtr; + QSharedPointer m_audioInterfacePtr; + QList m_inputDeviceChannels; + QList m_outputDeviceChannels; + QStringList m_inputDeviceList; + QStringList m_outputDeviceList; +}; + +#endif // VSDAUDIO_H diff --git a/src/vs/vsAuth.cpp b/src/vs/vsAuth.cpp new file mode 100644 index 0000000..25d8427 --- /dev/null +++ b/src/vs/vsAuth.cpp @@ -0,0 +1,308 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsAuth.cpp + * \author Dominick Hing + * \date May 2023 + */ + +#include "vsAuth.h" + +#include "./vsConstants.h" + +VsAuth::VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api) + : m_clientId(AUTH_CLIENT_ID), m_authorizationServerHost(AUTH_SERVER_HOST) +{ + m_networkAccessManager = networkAccessManager; + m_api = api; + m_deviceCodeFlow.reset(new VsDeviceCodeFlow(networkAccessManager)); + + connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowInitialized, this, + &VsAuth::initializedCodeFlow); + connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowError, this, + &VsAuth::handleAuthFailed); + connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::onCompletedCodeFlow, this, + &VsAuth::codeFlowCompleted); + connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowTimedOut, this, + &VsAuth::codeExpired); + + m_verificationUrl = QStringLiteral("https://auth.jacktrip.org/activate"); +} + +void VsAuth::authenticate(QString currentRefreshToken) +{ + if (currentRefreshToken.isEmpty()) { + // if no refresh token, initialize device flow + m_deviceCodeFlow->grant(); + } else { + m_attemptingRefreshToken = true; + emit updatedAttemptingRefreshToken(m_attemptingRefreshToken); + + // otherwise, use refresh token to gain a new access token + m_refreshToken = currentRefreshToken; + refreshAccessToken(m_refreshToken); + } +} + +void VsAuth::initializedCodeFlow(QString code, QString verificationUrl) +{ + m_verificationCode = code; + m_verificationUrl = verificationUrl; + m_authenticationStage = QStringLiteral("polling"); + + emit updatedAuthenticationStage(m_authenticationStage); + emit updatedVerificationCode(m_verificationCode); + emit updatedVerificationUrl(m_verificationUrl); +} + +void VsAuth::fetchUserInfo(QString accessToken) +{ + QNetworkReply* reply = m_api->getAuth0UserInfo(); + connect(reply, &QNetworkReply::finished, this, [=]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "VsAuth::fetchUserInfo Error: " + << reply->errorString().toStdString() << std::endl; + handleAuthFailed(reply->errorString()); // handle failure + emit fetchUserInfoFailed(); + reply->deleteLater(); + return; + } + + QByteArray response = reply->readAll(); + QJsonDocument userInfo = QJsonDocument::fromJson(response); + QString userId = userInfo.object()[QStringLiteral("sub")].toString(); + + reply->deleteLater(); + + if (userId.isEmpty()) { + std::cout << "VsAuth::fetchUserInfo Error: empty userId" << std::endl; + handleAuthFailed("empty userId"); // handle failure + emit fetchUserInfoFailed(); + return; + } + + handleAuthSucceeded(userId, accessToken); + }); +} + +void VsAuth::refreshAccessToken(QString refreshToken) +{ + qDebug() << "Refreshing access token"; + m_authenticationStage = QStringLiteral("refreshing"); + emit updatedAuthenticationStage(m_authenticationStage); + + QNetworkRequest request = QNetworkRequest( + QUrl(QString("https://%1/oauth/token").arg(m_authorizationServerHost))); + + request.setRawHeader(QByteArray("Content-Type"), + QByteArray("application/x-www-form-urlencoded")); + + QString data = QString("grant_type=refresh_token&client_id=%1&refresh_token=%2") + .arg(m_clientId, refreshToken); + + // send request + QNetworkReply* reply = m_networkAccessManager->post(request, data.toUtf8()); + + connect(reply, &QNetworkReply::finished, this, [=]() { + QByteArray buffer = reply->readAll(); + + // Error: failed to get device code + QNetworkReply::NetworkError err = reply->error(); + if (err != QNetworkReply::NoError) { + std::cout << "Failed to get new access token: " + << reply->errorString().toStdString() << std::endl; + handleAuthFailed(reply->errorString()); // handle failure + emit refreshTokenFailed(); + reply->deleteLater(); + return; + } + + // parse JSON from string response + QJsonParseError parseError; + QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError); + if (parseError.error) { + std::cout << "Error parsing JSON for Access Token: " + << parseError.errorString().toStdString() << std::endl; + handleAuthFailed("error parsing access token"); // handle failure + emit refreshTokenFailed(); + reply->deleteLater(); + return; + } + + // received access token + QJsonObject object = data.object(); + QString accessToken = object.value(QLatin1String("access_token")).toString(); + m_api->setAccessToken(accessToken); // set access token + reply->deleteLater(); + if (m_userId.isEmpty()) { + fetchUserInfo(accessToken); // get user ID from Auth0 + } else { + handleRefreshSucceeded(accessToken); + } + }); +} + +void VsAuth::resetCode() +{ + if (!m_verificationCode.isEmpty()) { + m_deviceCodeFlow->cancelCodeFlow(); + m_deviceCodeFlow->grant(); + } +} + +void VsAuth::codeFlowCompleted(QString accessToken, QString refreshToken) +{ + m_refreshToken = refreshToken; + m_api->setAccessToken(accessToken); + fetchUserInfo(accessToken); +} + +void VsAuth::codeExpired() +{ + emit deviceCodeExpired(); +} + +void VsAuth::handleRefreshSucceeded(QString accessToken) +{ + qDebug() << "Successfully refreshed access token"; + + m_accessToken = accessToken; + m_authenticationStage = QStringLiteral("success"); + m_errorMessage = QStringLiteral(""); + m_attemptingRefreshToken = false; + + emit updatedAuthenticationStage(m_authenticationStage); + emit updatedErrorMessage(m_errorMessage); + emit updatedVerificationCode(m_verificationCode); + emit updatedAttemptingRefreshToken(m_attemptingRefreshToken); +} + +void VsAuth::handleAuthSucceeded(QString userId, QString accessToken) +{ + // Success case: we got our access token (either through the refresh token or device + // code flow), and fetched the user ID + std::cout << "Successfully authenticated Virtual Studio user" << std::endl; + std::cout << "User ID: " << userId.toStdString() << std::endl; + + if (m_authenticationStage == QStringLiteral("polling")) { + m_authenticationMethod = QStringLiteral("code flow"); + } else { + m_authenticationMethod = QStringLiteral("refresh token"); + } + + m_userId = userId; + m_verificationCode = QStringLiteral(""); + m_accessToken = accessToken; + m_authenticationStage = QStringLiteral("success"); + m_errorMessage = QStringLiteral(""); + m_attemptingRefreshToken = false; + m_isAuthenticated = true; + + emit updatedUserId(m_userId); + emit updatedAuthenticationStage(m_authenticationStage); + emit updatedErrorMessage(m_errorMessage); + emit updatedVerificationCode(m_verificationCode); + emit updatedIsAuthenticated(m_isAuthenticated); + emit updatedAttemptingRefreshToken(m_attemptingRefreshToken); + emit updatedAuthenticationMethod(m_authenticationMethod); + + // notify UI and virtual studio class of success + emit authSucceeded(); +} + +void VsAuth::handleAuthFailed(QString errorMessage) +{ + // this might get called because there was an error getting the access token, + // or there was an issue fetching the user ID. We need both to say + // that authentication succeeded + std::cout << "Failed to authenticate user" << std::endl; + + m_userId = QStringLiteral(""); + m_verificationCode = QStringLiteral(""); + m_accessToken = QStringLiteral(""); + m_authenticationStage = QStringLiteral("failed"); + m_errorMessage = errorMessage; + m_authenticationMethod = QStringLiteral(""); + m_attemptingRefreshToken = false; + m_isAuthenticated = false; + + emit updatedUserId(m_userId); + emit updatedAuthenticationStage(m_authenticationStage); + emit updatedErrorMessage(m_errorMessage); + emit updatedVerificationCode(m_verificationCode); + emit updatedIsAuthenticated(m_isAuthenticated); + emit updatedAttemptingRefreshToken(m_attemptingRefreshToken); + emit updatedAuthenticationMethod(m_authenticationMethod); + + // notify UI and virtual studio class of failure + emit authFailed(); +} + +void VsAuth::cancelAuthenticationFlow() +{ + qDebug() << "Canceling authentication flow"; + m_deviceCodeFlow->cancelCodeFlow(); + + m_userId = QStringLiteral(""); + m_verificationCode = QStringLiteral(""); + m_accessToken = QStringLiteral(""); + m_authenticationStage = QStringLiteral("unauthenticated"); + m_errorMessage = QStringLiteral("cancelled"); + m_isAuthenticated = false; + + emit updatedUserId(m_userId); + emit updatedAuthenticationStage(m_authenticationStage); + emit updatedErrorMessage(m_errorMessage); + emit updatedVerificationCode(m_verificationCode); + emit updatedIsAuthenticated(m_isAuthenticated); +} + +void VsAuth::logout() +{ + if (!m_isAuthenticated) { + std::cout << "Warning: attempting to logout while not authenticated" << std::endl; + } + qDebug() << "Logging out"; + + // reset auth state + m_userId = QStringLiteral(""); + m_verificationCode = QStringLiteral(""); + m_accessToken = QStringLiteral(""); + m_authenticationStage = QStringLiteral("unauthenticated"); + m_errorMessage = QStringLiteral(""); + m_isAuthenticated = false; + + emit updatedUserId(m_userId); + emit updatedAuthenticationStage(m_authenticationStage); + emit updatedErrorMessage(m_errorMessage); + emit updatedVerificationCode(m_verificationCode); + emit updatedIsAuthenticated(m_isAuthenticated); +} \ No newline at end of file diff --git a/src/vs/vsAuth.h b/src/vs/vsAuth.h new file mode 100644 index 0000000..e79116a --- /dev/null +++ b/src/vs/vsAuth.h @@ -0,0 +1,136 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsAuth.h + * \author Dominick Hing + * \date May 2023 + */ + +#ifndef VSAUTH_H +#define VSAUTH_H + +#include +#include +#include +#include +#include + +#include "vsApi.h" +#include "vsDeviceCodeFlow.h" + +class VsAuth : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString authenticationStage READ authenticationStage NOTIFY + updatedAuthenticationStage); + Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY updatedErrorMessage); + Q_PROPERTY(QString verificationCode READ deviceCode NOTIFY updatedVerificationCode); + Q_PROPERTY( + QString verificationUrl READ deviceVerificationUrl NOTIFY updatedVerificationUrl); + Q_PROPERTY(bool isAuthenticated READ isAuthenticated NOTIFY updatedIsAuthenticated); + Q_PROPERTY(QString authenticationMethod READ authenticationMethod NOTIFY + updatedAuthenticationMethod); + Q_PROPERTY(bool attemptingRefreshToken READ attemptingRefreshToken NOTIFY + updatedAttemptingRefreshToken); + Q_PROPERTY(QString userId READ userId NOTIFY updatedUserId); + Q_PROPERTY(QString accessToken READ accessToken CONSTANT); + + public: + VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api); + + void authenticate(QString currentRefreshToken); + void refreshAccessToken(QString refreshToken); + Q_INVOKABLE void resetCode(); + void logout(); + + public slots: + void cancelAuthenticationFlow(); + + // getter methods + QString authenticationStage() { return m_authenticationStage; }; + QString errorMessage() { return m_errorMessage; }; + QString deviceCode() { return m_verificationCode; }; + QString deviceVerificationUrl() { return m_verificationUrl; }; + bool isAuthenticated() { return m_isAuthenticated; }; + QString userId() { return m_userId; }; + QString accessToken() { return m_accessToken; }; + QString refreshToken() { return m_refreshToken; }; + QString authenticationMethod() { return m_authenticationMethod; } + bool attemptingRefreshToken() { return m_attemptingRefreshToken; } + + signals: + void updatedAuthenticationStage(QString authenticationStage); + void updatedErrorMessage(QString errorMessage); + void updatedVerificationCode(QString deviceCode); + void updatedVerificationUrl(QUrl verificationUrl); + void updatedIsAuthenticated(bool isAuthenticated); + void updatedUserId(QString userId); + void updatedAuthenticationMethod(QString grant); + void updatedAttemptingRefreshToken(bool attemptingRefreshToken); + void authSucceeded(); + void authFailed(); + void refreshTokenFailed(); + void fetchUserInfoFailed(); + void deviceCodeExpired(); + + private slots: + void handleRefreshSucceeded(QString accessToken); + void handleAuthSucceeded(QString userId, QString accessToken); + void handleAuthFailed(QString errorMessage); + void initializedCodeFlow(QString code, QString verificationUrl); + void codeFlowCompleted(QString accessToken, QString refreshToken); + void codeExpired(); + + private: + void fetchUserInfo(QString accessToken); + + QString m_clientId; + QString m_authorizationServerHost; + + QString m_authenticationStage = QStringLiteral("unauthenticated"); + QString m_errorMessage = QStringLiteral(""); + QString m_verificationCode = QStringLiteral(""); + QString m_verificationUrl; + QString m_authenticationMethod = QStringLiteral(""); + + bool m_attemptingRefreshToken = false; + bool m_isAuthenticated = false; + QString m_userId; + QString m_accessToken; + QString m_refreshToken; + + QNetworkAccessManager* m_networkAccessManager; + VsApi* m_api; + QScopedPointer m_deviceCodeFlow; +}; + +#endif \ No newline at end of file diff --git a/src/vs/vsConstants.h b/src/vs/vsConstants.h new file mode 100644 index 0000000..83bb423 --- /dev/null +++ b/src/vs/vsConstants.h @@ -0,0 +1,50 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsConstants.h + * \author Nelson Wang + * \date Oct 2022 + */ + +#ifndef VSCONSTANTS_H +#define VSCONSTANTS_H + +#include + +const QString AUTH_AUTHORIZE_URI = QStringLiteral("https://auth.jacktrip.org/authorize"); +const QString AUTH_TOKEN_URI = QStringLiteral("https://auth.jacktrip.org/oauth/token"); +const QString AUTH_AUDIENCE = QStringLiteral("https://api.jacktrip.org"); +const QString AUTH_CLIENT_ID = QStringLiteral("cROUJag0UVKDaJ6jRAKRzlVjKVFNU39I"); +const QString PROD_API_HOST = QStringLiteral("app.jacktrip.com"); +const QString TEST_API_HOST = QStringLiteral("test.jacktrip.com"); +const QString AUTH_SERVER_HOST = QStringLiteral("auth.jacktrip.org"); + +#endif // VSCONSTANTS_H diff --git a/src/vs/vsDeeplink.cpp b/src/vs/vsDeeplink.cpp new file mode 100644 index 0000000..a602aba --- /dev/null +++ b/src/vs/vsDeeplink.cpp @@ -0,0 +1,211 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsDeeplink.cpp + * \author Aaron Wyatt, based on code by Matt Horton + * \date February 2023 + */ + +#include "vsDeeplink.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +VsDeeplink::VsDeeplink(const QString& deeplink) : m_deeplink(deeplink) +{ + setUrlScheme(); + checkForInstance(); + QDesktopServices::setUrlHandler(QStringLiteral("jacktrip"), this, "handleUrl"); +} + +VsDeeplink::~VsDeeplink() +{ + QDesktopServices::unsetUrlHandler(QStringLiteral("jacktrip")); +} + +bool VsDeeplink::waitForReady() +{ + while (!m_isReady) { + QTimer timer; + timer.setTimerType(Qt::CoarseTimer); + timer.setSingleShot(true); + + QEventLoop loop; + QObject::connect(this, &VsDeeplink::signalIsReady, &loop, &QEventLoop::quit); + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.start(100); // wait for 100ms + loop.exec(); + } + return m_readyToExit; +} + +void VsDeeplink::readyForSignals() +{ + m_readyForSignals = true; + if (!m_deeplink.isEmpty()) { + emit signalDeeplink(m_deeplink); + m_deeplink.clear(); + } +} + +void VsDeeplink::handleUrl(const QUrl& url) +{ + if (m_readyForSignals) { + emit signalDeeplink(url); + } else { + m_deeplink = url; + } +} + +void VsDeeplink::checkForInstance() +{ + // Create socket + m_instanceCheckSocket.reset(new QLocalSocket(this)); + QObject::connect(m_instanceCheckSocket.data(), &QLocalSocket::connected, this, + &VsDeeplink::connectionReceived, Qt::QueuedConnection); + // Create instanceServer to prevent new instances from being created + void (QLocalSocket::*errorFunc)(QLocalSocket::LocalSocketError); +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + errorFunc = &QLocalSocket::error; +#else + errorFunc = &QLocalSocket::errorOccurred; +#endif + QObject::connect(m_instanceCheckSocket.data(), errorFunc, this, + &VsDeeplink::connectionFailed); + // Check for existing instance + m_instanceCheckSocket->connectToServer("jacktripExists"); +} + +void VsDeeplink::connectionReceived() +{ + // another jacktrip instance is running + if (!m_deeplink.isEmpty()) { + // pass deeplink to existing instance before quitting + QString deeplinkStr = m_deeplink.toString(); + QByteArray baDeeplink = deeplinkStr.toLocal8Bit(); + qint64 writeBytes = m_instanceCheckSocket->write(baDeeplink); + if (writeBytes < 0) { + qDebug() << "sending deeplink failed"; + } else { + qDebug() << "Sent deeplink request to remote instance"; + } + + // make sure it isn't processed again + m_deeplink.clear(); + + // End process if another instance exists + m_readyToExit = true; + } + + m_instanceCheckSocket->waitForBytesWritten(); + m_instanceCheckSocket->disconnectFromServer(); // remove next + + // let main thread know we are finished + m_isReady = true; + emit signalIsReady(); +} + +void VsDeeplink::connectionFailed(QLocalSocket::LocalSocketError socketError) +{ + switch (socketError) { + case QLocalSocket::ServerNotFoundError: + case QLocalSocket::SocketTimeoutError: + case QLocalSocket::ConnectionRefusedError: + // no other jacktrip instance is running, so we will take over handling deep links + qDebug() << "Listening for deep link requests"; + m_instanceServer.reset(new QLocalServer(this)); + m_instanceServer->setSocketOptions(QLocalServer::WorldAccessOption); + m_instanceServer->listen("jacktripExists"); + QObject::connect(m_instanceServer.data(), &QLocalServer::newConnection, this, + &VsDeeplink::handleDeeplinkRequest, Qt::QueuedConnection); + break; + case QLocalSocket::PeerClosedError: + break; + default: + qDebug() << m_instanceCheckSocket->errorString(); + } + + // let main thread know we are finished + m_isReady = true; + emit signalIsReady(); +} + +void VsDeeplink::handleDeeplinkRequest() +{ + while (m_instanceServer->hasPendingConnections()) { + // Receive URL from 2nd instance + QLocalSocket* connectedSocket = m_instanceServer->nextPendingConnection(); + + if (connectedSocket == nullptr || !connectedSocket->waitForConnected()) { + qDebug() << "Deeplink socket: never received connection"; + return; + } + + if (!connectedSocket->waitForReadyRead() + && connectedSocket->bytesAvailable() <= 0) { + qDebug() << "Deeplink socket: not ready and no bytes available: " + << connectedSocket->errorString(); + return; + } + + if (connectedSocket->bytesAvailable() < (int)sizeof(quint16)) { + qDebug() << "Deeplink socket: ready but no bytes available"; + break; + } + + QByteArray in(connectedSocket->readAll()); + QString urlString(in); + handleUrl(urlString); + } +} + +void VsDeeplink::setUrlScheme() +{ +#ifdef _WIN32 + // Set url scheme in registry + QString path = QDir::toNativeSeparators(qApp->applicationFilePath()); + + QSettings set("HKEY_CURRENT_USER\\Software\\Classes", QSettings::NativeFormat); + set.beginGroup("jacktrip"); + set.setValue("Default", "URL:JackTrip Protocol"); + set.setValue("DefaultIcon/Default", path); + set.setValue("URL Protocol", ""); + set.setValue("shell/open/command/Default", + QString("\"%1\"").arg(path) + " --gui --deeplink \"%1\""); + set.endGroup(); +#endif +} diff --git a/src/vs/vsDeeplink.h b/src/vs/vsDeeplink.h new file mode 100644 index 0000000..464073b --- /dev/null +++ b/src/vs/vsDeeplink.h @@ -0,0 +1,112 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsDeeplink.h + * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton + * \date August 2023 + */ + +#ifndef __VSDEEPLINK_H__ +#define __VSDEEPLINK_H__ + +#include +#include +#include +#include +#include + +class VsDeeplink : public QObject +{ + Q_OBJECT + + public: + // construct with an instance of the application, to parse command line args + VsDeeplink(const QString& deeplink); + + // virtual destructor since it inherits from QObject + // this is used to unregister url handler + virtual ~VsDeeplink(); + + // blocks main thread until local socket server is ready + // returns true if a deeplink was handled and we should exit now + bool waitForReady(); + + // used to let us know VirtualStudio is ready to process deeplink signals + void readyForSignals(); + + // returns deeplink extracted from command line, if any + const QUrl& getDeeplink() const { return m_deeplink; } + + signals: + + // signalIsReady is emitted when the local socket server is ready + void signalIsReady(); + + // signalDeeplink is emitted when we want the local instance to process a deeplink + void signalDeeplink(const QUrl& url); + + private slots: + + // handleUrl is called to trigger processing of a deeplink + void handleUrl(const QUrl& url); + + // checks to see if another instance of jacktrip is available to process requests. + // if there is, this will send any command line deeplinks to it and exit. + // if there isn't, this will start listening for requests. + void checkForInstance(); + + // called if a connection was established with another instance of VS + void connectionReceived(); + + // called if unable to connect to another instance of VS + void connectionFailed(QLocalSocket::LocalSocketError socketError); + + // called by local socket server to process deeplink requests + void handleDeeplinkRequest(); + + private: + // sets url scheme for windows machines; does nothing on other platforms + static void setUrlScheme(); + + // used to check if there is a virtual studio instance already running + QScopedPointer m_instanceCheckSocket; + + // used to listen for deeplink requests via local socket connections + QScopedPointer m_instanceServer; + + // used to synchronize with main thread at startup + bool m_isReady = false; + bool m_readyForSignals = false; + bool m_readyToExit = false; + QUrl m_deeplink; +}; + +#endif // __VSDEEPLINK_H__ diff --git a/src/vs/vsDevice.cpp b/src/vs/vsDevice.cpp new file mode 100644 index 0000000..4f036df --- /dev/null +++ b/src/vs/vsDevice.cpp @@ -0,0 +1,602 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsDevice.cpp + * \author Matt Horton + * \date June 2022 + */ + +#include "vsDevice.h" + +#include + +// Constructor +VsDevice::VsDevice(QSharedPointer& auth, QSharedPointer& api, + QSharedPointer& audio, QObject* parent) + : QObject(parent) + , m_auth(auth) + , m_api(api) + , m_audioConfigPtr(audio) + , m_sendVolumeTimer(this) +{ + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + m_apiPrefix = settings.value(QStringLiteral("ApiPrefix"), "").toString(); + m_apiSecret = settings.value(QStringLiteral("ApiSecret"), "").toString(); + m_appUUID = settings.value(QStringLiteral("AppUUID"), "").toString(); + m_appID = settings.value(QStringLiteral("AppID"), "").toString(); + settings.endGroup(); + + if (!m_appID.isEmpty()) { + std::cout << "Device ID: " << m_appID.toStdString() << std::endl; + } + + m_sendVolumeTimer.setSingleShot(true); + connect(&m_sendVolumeTimer, &QTimer::timeout, this, &VsDevice::sendLevels); + + // Set server levels to stored versions + sendLevels(); +} + +VsDevice::~VsDevice() +{ + m_sendVolumeTimer.stop(); + stopJackTrip(false); +} + +// registerApp idempotently registers an emulated device belonging to the current user +void VsDevice::registerApp() +{ + if (m_appUUID == "") { + m_appUUID = QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces); + } + + // check if device exists + QNetworkReply* reply = m_api->getDevice(m_appID); + connect(reply, &QNetworkReply::finished, this, [=]() { + // Got error + if (reply->error() != QNetworkReply::NoError) { + QVariant statusCode = + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + if (!statusCode.isValid()) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + reply->deleteLater(); + return; + } + + int status = statusCode.toInt(); + // Device does not exist + if (status >= 400 && status < 500) { + std::cout << "Device not found. Creating new device." << std::endl; + + if (m_apiPrefix == "" || m_apiSecret == "") { + m_apiPrefix = randomString(7); + m_apiSecret = randomString(22); + } + + registerJTAsDevice(); + } else { + // Other error status. Won't create device. + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + reply->deleteLater(); + return; + } + } else if (m_apiPrefix != "" && m_apiSecret != "") { + sendHeartbeat(); + } + + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("AppUUID"), m_appUUID); + settings.setValue(QStringLiteral("ApiPrefix"), m_apiPrefix); + settings.setValue(QStringLiteral("ApiSecret"), m_apiSecret); + settings.endGroup(); + if (!m_appID.isEmpty()) { + updateState(""); + } + + reply->deleteLater(); + }); +} + +// removeApp deletes the emulated device +void VsDevice::removeApp() +{ + if (m_appID.isEmpty()) { + return; + } + + QNetworkReply* reply = m_api->deleteDevice(m_appID); + QEventLoop loop; + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + } else { + m_appID.clear(); + m_appUUID.clear(); + m_apiPrefix.clear(); + m_apiSecret.clear(); + + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.remove(QStringLiteral("AppID")); + settings.remove(QStringLiteral("AppUUID")); + settings.remove(QStringLiteral("ApiPrefix")); + settings.remove(QStringLiteral("ApiSecret")); + settings.endGroup(); + } + reply->deleteLater(); +} + +// sendHeartbeat is reponsible for sending liveness heartbeats to the API +void VsDevice::sendHeartbeat() +{ + QString now = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + QJsonObject json = { + {QLatin1String("stats_updated_at"), now}, + {QLatin1String("mac"), m_appUUID}, + {QLatin1String("version"), QLatin1String(gVersion)}, + {QLatin1String("type"), "jacktrip_app"}, + {QLatin1String("apiPrefix"), m_apiPrefix}, + {QLatin1String("apiSecret"), m_apiSecret}, + }; + + // Add stats to heartbeat body + if (!m_pinger.isNull() && m_pinger->active()) { + VsPinger::PingStat stats = m_pinger->getPingStats(); + + // API server expects RTTs to be in int64 nanoseconds, so we must convert + // from milliseconds to nanoseconds + int ns_per_ms = 1000000; + + json.insert(QLatin1String("pkts_sent"), (int)stats.packetsSent); + json.insert(QLatin1String("pkts_recv"), (int)stats.packetsReceived); + json.insert(QLatin1String("min_rtt"), (qint64)(stats.minRtt * ns_per_ms)); + json.insert(QLatin1String("max_rtt"), (qint64)(stats.maxRtt * ns_per_ms)); + json.insert(QLatin1String("avg_rtt"), (qint64)(stats.avgRtt * ns_per_ms)); + json.insert(QLatin1String("stddev_rtt"), (qint64)(stats.stdDevRtt * ns_per_ms)); + json.insert(QLatin1String("high_latency"), + m_audioConfigPtr->getHighLatencyFlag()); + json.insert(QLatin1String("network_outage"), m_networkOutage); + + // For the internal application UI, ms will suffice. No conversion needed + QJsonObject pingStats = {}; + pingStats.insert(QLatin1String("packetsSent"), (int)stats.packetsSent); + pingStats.insert(QLatin1String("packetsReceived"), (int)stats.packetsReceived); + pingStats.insert(QLatin1String("minRtt"), ((int)(10 * stats.minRtt)) / 10.0); + pingStats.insert(QLatin1String("maxRtt"), ((int)(10 * stats.maxRtt)) / 10.0); + pingStats.insert(QLatin1String("avgRtt"), ((int)(10 * stats.avgRtt)) / 10.0); + pingStats.insert(QLatin1String("stdDevRtt"), + ((int)(10 * stats.stdDevRtt)) / 10.0); + pingStats.insert(QLatin1String("highLatency"), + m_audioConfigPtr->getHighLatencyFlag()); + emit updateNetworkStats(pingStats); + } + + QJsonDocument request = QJsonDocument(json); + + if (!m_deviceSocketPtr.isNull() && m_deviceSocketPtr->isValid()) { + // Send heartbeat via websocket + m_deviceSocketPtr->sendMessage(request.toJson()); + } else { + if (enabled()) { + if (m_deviceSocketPtr.isNull()) { + qDebug() << "Heartbeat not sent"; + } else { + qDebug() << "Heartbeat not sent; trying to reopen socket"; + m_deviceSocketPtr->openSocket(); + } + } + } +} + +bool VsDevice::hasTerminated() +{ + return m_jackTrip.isNull(); +} + +// updateState updates the emulated device with the provided state +void VsDevice::updateState(const QString& serverId) +{ + m_deviceAgentConfig.insert("serverId", serverId); + m_deviceAgentConfig.insert("enabled", !serverId.isEmpty()); + QJsonObject json = { + {QLatin1String("serverId"), serverId}, + {QLatin1String("enabled"), !serverId.isEmpty()}, + }; + QJsonDocument request = QJsonDocument(json); + QNetworkReply* reply = m_api->updateDevice(m_appID, request.toJson()); + connect(reply, &QNetworkReply::finished, this, [=]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + } + reply->deleteLater(); + }); +} + +void VsDevice::sendLevels() +{ + if (m_appID.isEmpty()) { + return; + } + // Add latest volume and mute values to heartbeat body + QJsonObject json = {{QLatin1String("version"), QLatin1String(gVersion)}, + {QLatin1String("captureVolume"), + (int)(m_audioConfigPtr->getInputVolume() * 100.0)}, + {QLatin1String("captureMute"), m_audioConfigPtr->getInputMuted()}, + {QLatin1String("playbackVolume"), + (int)(m_audioConfigPtr->getOutputVolume() * 100.0)}, + {QLatin1String("playbackMute"), false}, + {QLatin1String("monitorVolume"), + (int)(m_audioConfigPtr->getMonitorVolume() * 100.0)}}; + + QJsonDocument request = QJsonDocument(json); + QNetworkReply* reply = m_api->updateDevice(m_appID, request.toJson()); + connect(reply, &QNetworkReply::finished, this, [=]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + } + reply->deleteLater(); + }); +} + +// initJackTrip spawns a new jacktrip process with the desired settings +JackTrip* VsDevice::initJackTrip( + [[maybe_unused]] bool useRtAudio, [[maybe_unused]] std::string input, + [[maybe_unused]] std::string output, [[maybe_unused]] int baseInputChannel, + [[maybe_unused]] int numChannelsIn, [[maybe_unused]] int baseOutputChannel, + [[maybe_unused]] int numChannelsOut, [[maybe_unused]] int inputMixMode, + [[maybe_unused]] int bufferSize, [[maybe_unused]] int queueBuffer, + VsServerInfo* studioInfo) +{ + m_jackTrip.reset( + new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, baseInputChannel, + numChannelsIn, baseOutputChannel, numChannelsOut, + static_cast(inputMixMode), +#ifdef WAIR // wair + 0, +#endif // endwhere + 4, 1)); + m_jackTrip->setConnectDefaultAudioPorts(true); +#ifdef RT_AUDIO + if (useRtAudio) { + m_jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO); + m_jackTrip->setAudioBufferSizeInSamples(bufferSize); + m_jackTrip->setInputDevice(input); + m_jackTrip->setOutputDevice(output); + } +#endif + m_jackTrip->setSampleRate(studioInfo->sampleRate()); + int bindPort = selectBindPort(); + if (bindPort == 0) { + return 0; + } + m_jackTrip->setBindPorts(bindPort); + m_jackTrip->setRemoteClientName(m_appID); + m_jackTrip->setBufferStrategy(3); // PLC + m_jackTrip->setBufferQueueLength(queueBuffer); + m_jackTrip->setUseRtUdpPriority( + true); // rt udp priority reduces glitches on desktops + m_jackTrip->setPeerAddress(studioInfo->host()); + m_jackTrip->setPeerPorts(studioInfo->port()); + m_jackTrip->setPeerHandshakePort(studioInfo->port()); + + QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this, + &VsDevice::handleJackTripError, Qt::QueuedConnection); + QObject::connect(m_jackTrip.data(), &JackTrip::signalError, this, + &VsDevice::handleJackTripError, Qt::QueuedConnection); + + return m_jackTrip.data(); +} + +// startJackTrip starts the current jacktrip process if applicable +void VsDevice::startJackTrip(const VsServerInfo& studioInfo) +{ + m_stopping = false; + m_networkOutage = false; + updateState(studioInfo.id()); + + // setup websocket listener + m_deviceSocketPtr.reset( + new VsWebSocket(QUrl(QStringLiteral("wss://%1/api/devices/%2/heartbeat") + .arg(m_api->getApiHost(), m_appID)), + m_auth->accessToken(), m_apiPrefix, m_apiSecret)); + connect(m_deviceSocketPtr.get(), &VsWebSocket::textMessageReceived, this, + &VsDevice::onTextMessageReceived); + connect(m_deviceSocketPtr.get(), &VsWebSocket::disconnected, this, + &VsDevice::restartDeviceSocket); + m_deviceSocketPtr->openSocket(); + + if (!m_jackTrip.isNull()) { +#ifdef WAIRTOHUB // WAIR + m_jackTrip->startProcess(0); // for WAIR compatibility, ID in jack client name +#else + m_jackTrip->startProcess(); +#endif // endwhere + } + + // intialize the pinger used to generate network latency statistics for + // Virtual Studio + QString host = studioInfo.host(); + if (studioInfo.isManaged()) { + host = studioInfo.sessionId(); + host.append(QString::fromStdString(".jacktrip.cloud")); + } + if (studioInfo.isManaged() || !studioInfo.sessionId().isEmpty()) { + m_pinger.reset(new VsPinger(QString::fromStdString("wss"), host, + QString::fromStdString("/ping"))); + } +} + +// stopJackTrip stops the current jacktrip process if applicable +void VsDevice::stopJackTrip(bool isReconnecting) +{ + // check if another process has already initiated + QMutexLocker stopLock(&m_stopMutex); + if (m_stopping) + return; + m_stopping = true; + + // only clear state if we are not reconnecting + if (!isReconnecting) + updateState(""); + + // stop the Virtual Studio pinger + if (!m_pinger.isNull()) { + m_pinger->stop(); + m_pinger->unsetToken(); + } + + if (!m_jackTrip.isNull()) { + if (!m_deviceSocketPtr.isNull()) { + m_deviceSocketPtr->closeSocket(); + } + m_jackTrip->stop(); + m_jackTrip.reset(); + } +} + +// reconcileAgentConfig updates the internal DeviceAgentConfig structure +void VsDevice::reconcileAgentConfig(QJsonDocument newState) +{ + // Only sync if the incoming type matches DeviceAgentConfig: + // https://github.com/jacktrip/jacktrip-agent/blob/fd3940c293daf16d8467c62b39a30779d21a0a22/pkg/client/devices.go#L87 + QJsonObject newObject = newState.object(); + if (!newObject.contains("enabled")) { + return; + } + for (auto it = newObject.constBegin(); it != newObject.constEnd(); it++) { + // if currently enabled but new config is not enabled, disconnect immediately + if (enabled() && it.key() == "enabled" && !it.value().toBool() + && !m_jackTrip.isNull()) { + stopJackTrip(false); + } + m_deviceAgentConfig.insert(it.key(), it.value()); + } +} + +// syncDeviceSettings updates volume/mute controls against the API +void VsDevice::syncDeviceSettings() +{ + m_sendVolumeTimer.start(100); +} + +// handleJackTripError is a slot intended to be triggered on jacktrip process signals +void VsDevice::handleJackTripError() +{ + stopJackTrip(false); +} + +// onTextMessageReceived is a slot intended to be triggered by new incoming WSS messages +void VsDevice::onTextMessageReceived(const QString& message) +{ + QJsonDocument newState = QJsonDocument::fromJson(message.toUtf8()); + QJsonObject newObj = newState.object(); + + // We have a heartbeat from which we can read the studio auth token + // Use it to set up and start the pinger connection + QString token = newState["authToken"].toString(); + if (!m_pinger.isNull() && !m_pinger->active() && !token.isEmpty()) { + m_pinger->setToken(token); + m_pinger->start(); + } + + // capture (input) volume + m_audioConfigPtr->setInputVolume( + (float)(newObj[QStringLiteral("captureVolume")].toDouble() / 100.0)); + m_audioConfigPtr->setInputMuted(newObj[QStringLiteral("captureMute")].toBool()); + + // playback (output) volume + m_audioConfigPtr->setOutputVolume( + (float)(newObj[QStringLiteral("playbackVolume")].toDouble() / 100.0)); + + // monitor volume + m_audioConfigPtr->setMonitorVolume( + (float)(newObj[QStringLiteral("monitorVolume")].toDouble() / 100.0)); + + reconcileAgentConfig(newState); +} + +void VsDevice::restartDeviceSocket() +{ + if (m_deviceAgentConfig[QStringLiteral("serverId")].toString() != "") { + if (!m_deviceSocketPtr.isNull()) { + m_deviceSocketPtr->openSocket(); + } + } +} + +// registerJTAsDevice creates the emulated device belonging to the current user +void VsDevice::registerJTAsDevice() +{ + /* + REGISTER JT APP AS A DEVICE ON VIRTUAL STUDIO + + Defaults: + period - 128 - set by studio = buffer size + queueBuffer - 0 - set by studio = net queue + devicePort - 4464 + reverb - 0 - off + limiter - false + compressor - false + quality - 2 - high + captureMute - false - unused right now + captureVolume - 100 - unused right now + playbackMute - false - unused right now + playbackVolume - 100 - unused right now + monitorMute - false - unsure if we should enable + monitorVolume - 0 - unsure if we should enable + name - "JackTrip App" + alsaName - "jacktripapp" + overlay - "jacktrip_app" + mac - UUID tied to app session + version - app version - will need to update in heartbeat + apiPrefix - random 7 character string tied to app session + apiSecret - random 22 character string tied to app session + */ + + QJsonObject json = { + // TODO: Fix me + //{QLatin1String("period"), m_bufferOptions[bufferSize()].toInt()}, + {QLatin1String("period"), 128}, + {QLatin1String("queueBuffer"), 0}, + {QLatin1String("devicePort"), 4464}, + {QLatin1String("reverb"), 0}, + {QLatin1String("limiter"), false}, + {QLatin1String("compressor"), false}, + {QLatin1String("quality"), 2}, + {QLatin1String("captureMute"), false}, + {QLatin1String("captureVolume"), 100}, + {QLatin1String("playbackMute"), false}, + {QLatin1String("playbackVolume"), 100}, + {QLatin1String("monitorMute"), false}, + {QLatin1String("monitorVolume"), 100}, + {QLatin1String("alsaName"), "jacktripapp"}, + {QLatin1String("overlay"), "jacktrip_app"}, + {QLatin1String("mac"), m_appUUID}, + {QLatin1String("version"), QLatin1String(gVersion)}, + {QLatin1String("apiPrefix"), m_apiPrefix}, + {QLatin1String("apiSecret"), m_apiSecret}, +#if defined(Q_OS_MACOS) + {QLatin1String("name"), "JackTrip App (macOS)"}, +#elif defined(Q_OS_WIN) + {QLatin1String("name"), "JackTrip App (Windows)"}, +#else + {QLatin1String("name"), "JackTrip App"}, +#endif // Q_OS_WIN + }; + QJsonDocument request = QJsonDocument(json); + + QNetworkReply* reply = m_api->postDevice(request.toJson()); + connect(reply, &QNetworkReply::finished, this, [=]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + reply->deleteLater(); + return; + } else { + QJsonDocument response = QJsonDocument::fromJson(reply->readAll()); + QJsonObject newObject = response.object(); + + m_appID = newObject[QStringLiteral("id")].toString(); + + // capture (input) volume + m_audioConfigPtr->setInputVolume( + (float)(newObject[QStringLiteral("captureVolume")].toDouble() / 100.0)); + m_audioConfigPtr->setInputMuted( + newObject[QStringLiteral("captureMute")].toBool()); + + // playback (output) volume + m_audioConfigPtr->setOutputVolume( + (float)(newObject[QStringLiteral("playbackVolume")].toDouble() / 100.0)); + + // monitor volume + m_audioConfigPtr->setMonitorVolume( + (float)(newObject[QStringLiteral("monitorVolume")].toDouble() / 100.0)); + + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("AppID"), m_appID); + settings.endGroup(); + + std::cout << "Device ID: " << m_appID.toStdString() << std::endl; + sendHeartbeat(); + } + + reply->deleteLater(); + }); +} + +// enabled returns whether or not the client is connected to a studio +bool VsDevice::enabled() +{ + return m_deviceAgentConfig[QStringLiteral("enabled")].toBool(); +} + +// randomString generates a random sequence of characters +QString VsDevice::randomString(int stringLength) +{ + QString str = ""; + static bool seeded = false; + QString allow_symbols( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); + + if (!seeded) { + m_randomizer.seed((QTime::currentTime().msec())); + seeded = true; + } + + for (int i = 0; i < stringLength; ++i) { + str.append(allow_symbols.at(m_randomizer.generate() % (allow_symbols.length()))); + } + + return str; +} + +// selectBindPort finds the next open bind port to use for jacktrip +int VsDevice::selectBindPort() +{ + int candidate = gDefaultPort; + if (m_jackTrip.isNull()) { + return candidate; + } + int attempt = 0; + while (attempt <= 5000) { + candidate = QRandomGenerator::global()->bounded(gBindPortLow, gBindPortHigh + 1); + attempt++; + if (!m_jackTrip->checkIfPortIsBinded(candidate)) { + return candidate; + } + } + return 0; +} diff --git a/src/vs/vsDevice.h b/src/vs/vsDevice.h new file mode 100644 index 0000000..2ddb7f9 --- /dev/null +++ b/src/vs/vsDevice.h @@ -0,0 +1,121 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsDevice.h + * \author Matt Horton + * \date June 2022 + */ + +#ifndef VSDEVICE_H +#define VSDEVICE_H + +#include +#include +#include +#include +#include +#include + +#include "../JackTrip.h" +#include "../jacktrip_globals.h" +#include "vsApi.h" +#include "vsAudio.h" +#include "vsAuth.h" +#include "vsConstants.h" +#include "vsPinger.h" +#include "vsServerInfo.h" +#include "vsWebSocket.h" + +class VsDevice : public QObject +{ + Q_OBJECT + + public: + // Constructor + explicit VsDevice(QSharedPointer& auth, QSharedPointer& api, + QSharedPointer& audio, QObject* parent = nullptr); + virtual ~VsDevice(); + + // Public functions + void registerApp(); + void removeApp(); + void sendHeartbeat(); + bool hasTerminated(); + JackTrip* initJackTrip(bool useRtAudio, std::string input, std::string output, + int baseInputChannel, int numChannelsIn, int baseOutputChannel, + int numChannelsOut, int inputMixMode, int bufferSize, + int queueBuffer, VsServerInfo* studioInfo); + void startJackTrip(const VsServerInfo& studioInfo); + void stopJackTrip(bool isReconnecting = false); + void reconcileAgentConfig(QJsonDocument newState); + void setNetworkOutage(bool outage = true) { m_networkOutage = outage; } + bool getNetworkOutage() const { return m_networkOutage; } + + signals: + void updateNetworkStats(QJsonObject stats); + + public slots: + void syncDeviceSettings(); + + private slots: + void handleJackTripError(); + void onTextMessageReceived(const QString& message); + void restartDeviceSocket(); + void sendLevels(); + + private: + void updateState(const QString& serverId); + void registerJTAsDevice(); + bool enabled(); + int selectBindPort(); + QString randomString(int stringLength); + + QSharedPointer m_auth; + QSharedPointer m_api; + QSharedPointer m_audioConfigPtr; + QScopedPointer m_pinger; + + QString m_appID; + QString m_appUUID; + QString m_token; + QString m_apiPrefix; + QString m_apiSecret; + QMutex m_stopMutex; + QJsonObject m_deviceAgentConfig; + QScopedPointer m_deviceSocketPtr; + QScopedPointer m_jackTrip; + QRandomGenerator m_randomizer; + QTimer m_sendVolumeTimer; + bool m_networkOutage = false; + bool m_stopping = false; +}; + +#endif // VSDEVICE_H diff --git a/src/vs/vsDeviceCodeFlow.cpp b/src/vs/vsDeviceCodeFlow.cpp new file mode 100644 index 0000000..4becfea --- /dev/null +++ b/src/vs/vsDeviceCodeFlow.cpp @@ -0,0 +1,251 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsDeviceCodeFlow.cpp + * \author Dominick Hing + * \date May 2023 + */ + +#include "./vsDeviceCodeFlow.h" + +#include "./vsConstants.h" + +VsDeviceCodeFlow::VsDeviceCodeFlow(QNetworkAccessManager* networkAccessManager) + : m_clientId(AUTH_CLIENT_ID) + , m_audience(AUTH_AUDIENCE) + , m_authorizationServerHost(AUTH_SERVER_HOST) + , m_authenticationError(false) + , m_netManager(networkAccessManager) +{ + // start polling when the device flow has been initialized + connect(this, &VsDeviceCodeFlow::deviceCodeFlowInitialized, this, + &VsDeviceCodeFlow::startPolling); + connect(&m_tokenPollingTimer, &QTimer::timeout, this, + &VsDeviceCodeFlow::onPollingTimerTick); + connect(&m_deviceFlowExpirationTimer, &QTimer::timeout, this, + &VsDeviceCodeFlow::onDeviceCodeExpired); + + m_tokenPollingTimer.setSingleShot(false); + m_deviceFlowExpirationTimer.setSingleShot(true); +} + +void VsDeviceCodeFlow::grant() +{ + initDeviceAuthorizationCodeFlow(); +} + +void VsDeviceCodeFlow::initDeviceAuthorizationCodeFlow() +{ + m_authenticationError = false; + + // form initial request for device authorization code + QNetworkRequest request = QNetworkRequest( + QUrl(QString("https://%1/oauth/device/code").arg(m_authorizationServerHost))); + + request.setRawHeader(QByteArray("Content-Type"), + QByteArray("application/x-www-form-urlencoded")); + + QString data = + QString("client_id=%1&scope=%2&audience=%3") + .arg(m_clientId, + QLatin1String("openid profile email offline_access read:servers"), + m_audience); + + // send request + QNetworkReply* reply = m_netManager->post(request, data.toUtf8()); + connect(reply, &QNetworkReply::finished, this, [=]() { + bool success = processDeviceCodeNetworkReply(reply); + if (success) { + // notify success along with user code and verification URL + emit deviceCodeFlowInitialized(m_userCode, m_verificationUriComplete); + } else if (m_authenticationError) { + // notify failure + emit deviceCodeFlowError(reply->errorString()); + } + reply->deleteLater(); + }); +} + +void VsDeviceCodeFlow::startPolling() +{ + if (m_pollingInterval <= 0 || m_deviceCodeValidityDuration <= 0) { + std::cout << "Could not start polling. This should not print and indicates a bug." + << std::endl; + return; + } + + // poll on a regular interval, up until the expiration of the code + m_tokenPollingTimer.setInterval(m_pollingInterval * 1000); + m_deviceFlowExpirationTimer.setInterval(m_deviceCodeValidityDuration * 1000); + + m_tokenPollingTimer.start(); + m_deviceFlowExpirationTimer.start(); +} + +void VsDeviceCodeFlow::stopPolling() +{ + if (m_tokenPollingTimer.isActive()) { + m_tokenPollingTimer.stop(); + } + if (m_deviceFlowExpirationTimer.isActive()) { + m_deviceFlowExpirationTimer.stop(); + } +} + +void VsDeviceCodeFlow::onPollingTimerTick() +{ + // form request to /oauth/token + QNetworkRequest request = QNetworkRequest( + QUrl(QString("https://%1/oauth/token").arg(m_authorizationServerHost))); + + request.setRawHeader(QByteArray("Content-Type"), + QByteArray("application/x-www-form-urlencoded")); + + QString data = + QString("client_id=%1&device_code=%2&grant_type=%3") + .arg(m_clientId, m_deviceCode, + QLatin1String("urn:ietf:params:oauth:grant-type:device_code")); + + // send send request for token + QNetworkReply* reply = m_netManager->post(request, data.toUtf8()); + connect(reply, &QNetworkReply::finished, this, [=]() { + bool success = processPollingOAuthTokenNetworkReply(reply); + if (m_authenticationError) { + // shouldn't happen + emit deviceCodeFlowError(reply->errorString()); + } else if (success) { + // flow successfully completed + emit onCompletedCodeFlow(m_accessToken, m_refreshToken); + // cleanup + stopPolling(); + cleanupDeviceCodeFlow(); + } + reply->deleteLater(); + }); +} + +void VsDeviceCodeFlow::onDeviceCodeExpired() +{ + emit deviceCodeFlowTimedOut(); + + std::cout << "Device Code has expired." << std::endl; + stopPolling(); + cleanupDeviceCodeFlow(); +} + +void VsDeviceCodeFlow::cancelCodeFlow() +{ + stopPolling(); + cleanupDeviceCodeFlow(); +} + +bool VsDeviceCodeFlow::processDeviceCodeNetworkReply(QNetworkReply* reply) +{ + QByteArray buffer = reply->readAll(); + + // Error: failed to get device code + if (reply->error()) { + std::cout << "Failed to get device code: " << reply->errorString().toStdString() + << std::endl; + m_authenticationError = true; + return false; + } + + // parse JSON from string response + QJsonParseError parseError; + QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError); + if (parseError.error) { + std::cout << "Error parsing JSON for Device Code: " + << parseError.errorString().toStdString() << std::endl; + m_authenticationError = true; + return false; + } + + // get fields + QJsonObject object = data.object(); + m_deviceCode = object.value(QLatin1String("device_code")).toString(); + m_userCode = object.value(QLatin1String("user_code")).toString(); + m_verificationUri = object.value(QLatin1String("verification_uri")).toString(); + m_verificationUriComplete = + object.value(QLatin1String("verification_uri_complete")).toString(); + m_pollingInterval = + object.value(QLatin1String("interval")).toInt(2); // default to 2s + m_deviceCodeValidityDuration = + object.value(QLatin1String("expires_in")).toInt(900); // default to 900s + + // return true if success + return true; +} + +bool VsDeviceCodeFlow::processPollingOAuthTokenNetworkReply(QNetworkReply* reply) +{ + QByteArray buffer = reply->readAll(); + + // Error: failed to get device code (this is expected) + if (reply->error()) { + return false; + } + + // parse JSON from string response + QJsonParseError parseError; + QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError); + if (parseError.error) { + std::cout << "Error parsing JSON for access token: " + << parseError.errorString().toStdString() << std::endl; + return false; + } + + // get fields + QJsonObject object = data.object(); + m_idToken = object.value(QLatin1String("id_token")).toString(); + m_accessToken = object.value(QLatin1String("access_token")).toString(); + m_refreshToken = object.value(QLatin1String("refresh_token")).toString(); + m_authenticationError = false; + + // return true if success + return true; +} + +void VsDeviceCodeFlow::cleanupDeviceCodeFlow() +{ + m_deviceCode = QStringLiteral(""); + m_userCode = QStringLiteral(""); + m_verificationUri = QStringLiteral("https://auth.jacktrip.org/activate"); + m_verificationUriComplete = QStringLiteral(""); + + m_pollingInterval = -1; + m_deviceCodeValidityDuration = -1; +} + +QString VsDeviceCodeFlow::accessToken() +{ + return m_accessToken; +} \ No newline at end of file diff --git a/src/vs/vsDeviceCodeFlow.h b/src/vs/vsDeviceCodeFlow.h new file mode 100644 index 0000000..0bd91d0 --- /dev/null +++ b/src/vs/vsDeviceCodeFlow.h @@ -0,0 +1,108 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsDeviceCodeFlow.h + * \author Dominick Hing + * \date May 2023 + */ + +#ifndef VSDEVICECODEFLOW_H +#define VSDEVICECODEFLOW_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vsDeviceCodeFlow.h" + +class VsDeviceCodeFlow : public QObject +{ + Q_OBJECT + + public: + explicit VsDeviceCodeFlow(QNetworkAccessManager* networkAccessManager); + virtual ~VsDeviceCodeFlow() { stopPolling(); } + + void grant(); + void refreshAccessToken(){}; + void initDeviceAuthorizationCodeFlow(); + + bool processDeviceCodeNetworkReply(QNetworkReply* reply); + bool processPollingOAuthTokenNetworkReply(QNetworkReply* reply); + void startPolling(); + void stopPolling(); + void onPollingTimerTick(); + void onDeviceCodeExpired(); + void cancelCodeFlow(); + void cleanupDeviceCodeFlow(); + + bool authenticated(); + QString accessToken(); + + signals: + void deviceCodeFlowInitialized(QString code, QString verificationUrl); + void deviceCodeFlowError(QString errorMessage); + void deviceCodeFlowTimedOut(); + void onCompletedCodeFlow(QString accessToken, QString refreshToken); + + private: + QString m_clientId; + QString m_audience; + QString m_authorizationServerHost; + + // state used specifically in the device code flow + QString m_deviceCode; + QString m_userCode; + QString m_verificationUri; + QString m_verificationUriComplete; + int m_pollingInterval = -1; // seconds + int m_deviceCodeValidityDuration = -1; // seconds + + QTimer m_tokenPollingTimer; + QTimer m_deviceFlowExpirationTimer; + + // authentication state variables + bool m_authenticationError; + QString m_refreshToken; + QString m_accessToken; + QString m_idToken; + + QScopedPointer m_netManager; +}; + +#endif // VSDEVICECODEFLOW \ No newline at end of file diff --git a/src/vs/vsMacPermissions.h b/src/vs/vsMacPermissions.h new file mode 100644 index 0000000..05fd873 --- /dev/null +++ b/src/vs/vsMacPermissions.h @@ -0,0 +1,63 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** +/** + * \file vsMacPermissions.h + * \author Matt Horton + * \date Oct 2022 + */ + +#ifndef __VSMACPERMISSIONS_H__ +#define __VSMACPERMISSIONS_H__ + +#include + +#include +#include +#include + +#include "vsPermissions.h" + +class VsMacPermissions : public VsPermissions +{ + Q_OBJECT + + public: + explicit VsMacPermissions(); + + bool micPermissionChecked() override; + Q_INVOKABLE void getMicPermission() override; + Q_INVOKABLE void openSystemPrivacy(); + + private: + QString m_micPermission = "unknown"; + bool m_micPermissionChecked = false; +}; + +#endif // __VSMACPERMISSIONS_H__ diff --git a/src/vs/vsMacPermissions.mm b/src/vs/vsMacPermissions.mm new file mode 100644 index 0000000..cca1300 --- /dev/null +++ b/src/vs/vsMacPermissions.mm @@ -0,0 +1,103 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** +/** + * \file vsMacPermissions.mm + * \author Matt Horton + * \date Oct 2022 + */ + +#include "vsMacPermissions.h" +#include +#include +#include +#include +#include + +VsMacPermissions::VsMacPermissions() +{ + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + m_micPermissionChecked = settings.value(QStringLiteral("MicPermissionChecked"), false).toBool(); + settings.endGroup(); +} + +bool VsMacPermissions::micPermissionChecked() +{ + if (m_micPermissionChecked) { + getMicPermission(); + } + return m_micPermissionChecked; +} + +void VsMacPermissions::getMicPermission() +{ + if (@available(macOS 10.14, *)) { + // Request permission to access. + switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + { + case AVAuthorizationStatusAuthorized: + { + // The user has previously granted access. + setMicPermission(QStringLiteral("granted")); + break; + } + case AVAuthorizationStatusNotDetermined: + { + // The app hasn't yet asked the user for access. + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { + if (granted) { + setMicPermission(QStringLiteral("granted")); + } else { + setMicPermission(QStringLiteral("denied")); + } + }]; + setMicPermission(QStringLiteral("unknown")); + break; + } + case AVAuthorizationStatusDenied: + { + // The user has previously denied access. + setMicPermission(QStringLiteral("denied")); + } + case AVAuthorizationStatusRestricted: + { + // The user can't grant access due to restrictions. + setMicPermission(QStringLiteral("denied")); + } + } + } else { + setMicPermission(QStringLiteral("granted")); + } +} + +void VsMacPermissions::openSystemPrivacy() +{ + QDesktopServices::openUrl(QUrl("x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")); +} diff --git a/src/vs/vsPermissions.cpp b/src/vs/vsPermissions.cpp new file mode 100644 index 0000000..b8c07db --- /dev/null +++ b/src/vs/vsPermissions.cpp @@ -0,0 +1,67 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** +/** + * \file vsPermissions.mm + * \author Matt Horton + * \date Oct 2022 + */ + +#include "vsPermissions.h" + +#include +#include +#include + +QString VsPermissions::micPermission() +{ + return m_micPermission; +} + +bool VsPermissions::micPermissionChecked() +{ + return m_micPermissionChecked; +} + +void VsPermissions::getMicPermission() +{ + setMicPermission("granted"); +} + +void VsPermissions::setMicPermission(QString status) +{ + m_micPermission = status; + m_micPermissionChecked = true; + emit micPermissionUpdated(); + + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("MicPermissionChecked"), m_micPermissionChecked); + settings.endGroup(); +} diff --git a/src/vs/vsPermissions.h b/src/vs/vsPermissions.h new file mode 100644 index 0000000..f95c5b5 --- /dev/null +++ b/src/vs/vsPermissions.h @@ -0,0 +1,69 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** +/** + * \file vsPermissions.h + * \author Matt Horton + * \date Nov 2022 + */ + +#ifndef __VSPERMISSIONS_H__ +#define __VSPERMISSIONS_H__ + +#include +#include +#include + +class VsPermissions : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString micPermission READ micPermission NOTIFY micPermissionUpdated) + + public: + VsPermissions() = default; // define here and there + + QString micPermission(); // define here + virtual bool micPermissionChecked(); // define here and there + Q_INVOKABLE virtual void getMicPermission(); + void setMicPermission(QString status); // define here + + signals: + void micPermissionUpdated(); // leave here + + protected: +#if __APPLE__ + QString m_micPermission = "unknown"; + bool m_micPermissionChecked = false; +#else + QString m_micPermission = "granted"; + bool m_micPermissionChecked = true; +#endif +}; + +#endif // __VSPERMISSIONS_H__ diff --git a/src/vs/vsPing.cpp b/src/vs/vsPing.cpp new file mode 100644 index 0000000..58b6b79 --- /dev/null +++ b/src/vs/vsPing.cpp @@ -0,0 +1,81 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsPinger.cpp + * \author Dominick Hing + * \date July 2022 + */ + +#include "vsPing.h" + +#include + +using std::cout; +using std::endl; + +// NOTE: It's better not to use +// using namespace std; +// because some functions (like exit()) get confused with QT functions + +//******************************************************************************* +VsPing::VsPing(uint32_t pingNum, uint32_t timeout_msec) : mPingNumber(pingNum) +{ + connect(&mTimer, &QTimer::timeout, this, &VsPing::onTimeout); + + mTimer.setTimerType(Qt::PreciseTimer); + mTimer.setSingleShot(true); + mTimer.setInterval(timeout_msec); + mTimer.start(); +} + +void VsPing::send() +{ + QDateTime now = QDateTime::currentDateTime(); + mSent = now; +} + +void VsPing::receive() +{ + QDateTime now = QDateTime::currentDateTime(); + if (!mTimedOut) { + mTimer.stop(); + mReceivedReply = true; + mReceived = now; + } +} + +void VsPing::onTimeout() +{ + if (!mReceivedReply) { + mTimedOut = true; + emit timeout(mPingNumber); + } +} diff --git a/src/vs/vsPing.h b/src/vs/vsPing.h new file mode 100644 index 0000000..fa4d3f8 --- /dev/null +++ b/src/vs/vsPing.h @@ -0,0 +1,83 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsPing.h + * \author Dominick Hing + * \date July 2022 + */ + +#ifndef VSPING_H +#define VSPING_H + +#include +#include +#include +#include +#include +#include + +/** \brief A helper class for VsPinger + * + */ +class VsPing : public QObject +{ + Q_OBJECT; + + public: + explicit VsPing(uint32_t pingNum, uint32_t timeout_msec); + virtual ~VsPing() { mTimer.stop(); } + uint32_t pingNumber() { return mPingNumber; } + + QDateTime sentTimestamp() { return mSent; } + QDateTime receivedTimestamp() { return mReceived; } + bool receivedReply() { return mReceivedReply; } + bool timedOut() { return mTimedOut; } + + void send(); + void receive(); + + private: + uint32_t mPingNumber; + QDateTime mSent; + QDateTime mReceived; + + QTimer mTimer; + bool mTimedOut = false; + bool mReceivedReply = false; + + public slots: + void onTimeout(); + + signals: + void timeout(uint32_t pingNum); +}; + +#endif // VSPING_H \ No newline at end of file diff --git a/src/vs/vsPinger.cpp b/src/vs/vsPinger.cpp new file mode 100644 index 0000000..1034af0 --- /dev/null +++ b/src/vs/vsPinger.cpp @@ -0,0 +1,316 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsPinger.cpp + * \author Dominick Hing + * \date July 2022 + */ + +#include "vsPinger.h" + +#include + +using std::cout; +using std::endl; + +// NOTE: It's better not to use +// using namespace std; +// because some functions (like exit()) get confused with QT functions + +//******************************************************************************* +VsPinger::VsPinger(QString scheme, QString host, QString path) +{ + mURL.setScheme(scheme); + mURL.setHost(host); + mURL.setPath(path); + + mTimer.setTimerType(Qt::PreciseTimer); + + connect(&mSocket, &QWebSocket::binaryMessageReceived, this, + &VsPinger::onReceivePingMessage); + connect(&mSocket, &QWebSocket::connected, this, &VsPinger::onConnected); +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + connect(&mSocket, + QOverload::of(&QWebSocket::errorOccurred), this, + &VsPinger::onError); +#else + connect(&mSocket, QOverload::of(&QWebSocket::error), + this, &VsPinger::onError); +#endif + connect(&mTimer, &QTimer::timeout, this, &VsPinger::onPingTimer); +} + +//******************************************************************************* +void VsPinger::start() +{ + // fail to start if no token is supplied + if (mToken.toStdString() == "") { + std::cout << "Error: auth token is not set" << std::endl; + return; + } + + mTimer.setInterval(mPingInterval); + + QString authVal = "Bearer "; + authVal.append(mToken); + + QNetworkRequest req = QNetworkRequest(QUrl(mURL)); + req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket")); + req.setRawHeader(QByteArray("Connection"), QByteArray("upgrade")); + req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8()); + mSocket.open(req); + + mStarted = true; +} + +//******************************************************************************* +void VsPinger::stop() +{ + mStarted = false; + mError = false; + mTimer.stop(); + mSocket.close(QWebSocketProtocol::CloseCodeNormal, NULL); +} + +//******************************************************************************* +void VsPinger::setToken(QString token) +{ + if (mStarted) { + std::cout << "Error: cannot set token while pinger is active." << std::endl; + return; + } + + mToken = token; + mAuthorized = true; +}; + +//******************************************************************************* +void VsPinger::unsetToken() +{ + if (mStarted) { + std::cout << "Error: cannot unset token while pinger is active." << std::endl; + return; + } + + mToken = QString(); + mAuthorized = false; +} + +//******************************************************************************* +void VsPinger::sendPingMessage(const QByteArray& message) +{ + if (mAuthorized && !mError) { + mSocket.sendBinaryMessage(message); + } +} + +//******************************************************************************* +void VsPinger::updateStats() +{ + PingStat stat; + stat.packetsReceived = 0; + stat.packetsSent = 0; + + uint32_t count = 0; + + std::vector vec_expired; + std::vector vec_rtt; + std::map::reverse_iterator it; + for (it = mPings.rbegin(); it != mPings.rend(); ++it) { + VsPing* ping = it->second; + + // mark this ping as ready to delete, since it will no longer be used in stats + if (count >= mPingNumPerInterval) { + vec_expired.push_back(ping->pingNumber()); + count++; + } else if (ping->timedOut() || ping->receivedReply()) { + // Only include in statistics pings that have timed out or been received. + // All others are pending and are not considered in statistics + stat.packetsSent++; + if (ping->receivedReply()) { + stat.packetsReceived++; + } + + QDateTime sent = ping->sentTimestamp(); + QDateTime received = ping->receivedTimestamp(); + qint64 diff = sent.msecsTo(received); + + // don't include case where dif = 0 in stats, mark as expired instead + if (diff != 0) { + vec_rtt.push_back(diff); + } else { + vec_expired.push_back(ping->pingNumber()); + } + + count++; + } + } + + // Deleted pings marked as expired by freeing the Ping object + // and clearing the map item + for (std::vector::iterator it_expired = vec_expired.begin(); + it_expired != vec_expired.end(); it_expired++) { + uint32_t expiredPingNum = *it_expired; + delete mPings.at(expiredPingNum); + mPings.erase(expiredPingNum); + } + + // Update RTT stats + double min_rtt = 0.0; + double max_rtt = 0.0; + double avg_rtt = 0.0; + double stddev_rtt = 0.0; + + // avoid edge case due to min_rtt and max_rtt being at the numeric limits + // when vector size is 0 + if (vec_rtt.size() == 0) { + stat.maxRtt = 0; + stat.minRtt = 0; + stat.avgRtt = 0; + stat.stdDevRtt = 0; + + // Update mStats + mStats = stat; + return; + } + + for (std::vector::iterator it_rtt = vec_rtt.begin(); it_rtt != vec_rtt.end(); + it_rtt++) { + double rtt = (double)*it_rtt; + if (rtt < min_rtt || min_rtt == 0.0) { + min_rtt = rtt; + } + if (rtt > max_rtt || max_rtt == 0.0) { + max_rtt = rtt; + } + + avg_rtt += rtt / vec_rtt.size(); + } + + for (std::vector::iterator it_rtt = vec_rtt.begin(); it_rtt != vec_rtt.end(); + it_rtt++) { + double rtt = (double)*it_rtt; + stddev_rtt += (rtt - avg_rtt) * (rtt - avg_rtt); + } + stddev_rtt /= vec_rtt.size(); + stddev_rtt = sqrt(stddev_rtt); + + stat.maxRtt = max_rtt; + stat.minRtt = min_rtt; + stat.avgRtt = avg_rtt; + stat.stdDevRtt = stddev_rtt; + + // Update mStats + mStats = stat; + return; +} + +//******************************************************************************* +VsPinger::PingStat VsPinger::getPingStats() +{ + return mStats; +} + +//******************************************************************************* +void VsPinger::onError(QAbstractSocket::SocketError error) +{ + cout << "WebSocket Error: " << error << endl; + mError = true; + mStarted = false; + mTimer.stop(); +} + +//******************************************************************************* +void VsPinger::onConnected() +{ + // start the ping timer after the connection is established + mTimer.start(); +} + +//******************************************************************************* +void VsPinger::onPingTimer() +{ + updateStats(); + + QByteArray bytes = QByteArray::number(mPingCount); + QDateTime now = QDateTime::currentDateTime(); + this->sendPingMessage(bytes); + + VsPing* ping = new VsPing(mPingCount, mPingInterval); + ping->send(); + mPings[mPingCount] = ping; + + connect(ping, &VsPing::timeout, this, &VsPinger::onPingTimeout); + + mLastPacketSent = mPingCount; + mPingCount++; +} + +//******************************************************************************* +void VsPinger::onPingTimeout(uint32_t pingNum) +{ + std::map::iterator it = mPings.find(pingNum); + if (it == mPings.end()) { + return; + } + + updateStats(); +} + +//******************************************************************************* +void VsPinger::onReceivePingMessage(const QByteArray& message) +{ + QDateTime now = QDateTime::currentDateTime(); + uint32_t pingNum = message.toUInt(); + + // locate the appropriate corresponding ping message + std::map::iterator it = mPings.find(pingNum); + if (it == mPings.end()) { + return; + } + + VsPing* ping = (*it).second; + + // do not apply to pings that have timed out + if (!ping->timedOut()) { + // update ping data + ping->receive(); + + // update vsPinger + mHasReceivedPing = true; + mLastPacketReceived = pingNum; + if (pingNum > mLargestPingNumReceived) { + mLargestPingNumReceived = pingNum; + } + } + + updateStats(); +} \ No newline at end of file diff --git a/src/vs/vsPinger.h b/src/vs/vsPinger.h new file mode 100644 index 0000000..3319d14 --- /dev/null +++ b/src/vs/vsPinger.h @@ -0,0 +1,121 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsPinger.h + * \author Dominick Hing + * \date July 2022 + */ + +#ifndef VSPINGER_H +#define VSPINGER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vsPing.h" + +/** \brief VsPinger for generating latency statistics between + * Virtual Studio devices and Virtual Studio Servers + * + */ +class VsPinger : public QObject +{ + Q_OBJECT; + + public: + /** \brief The class constructor + * \param scheme The protocol scheme for the pinger + * \param host The hostname of the server + * \param path The path to ping the server on + */ + explicit VsPinger(QString scheme, QString host, QString path); + virtual ~VsPinger() { stop(); } + void start(); + void stop(); + bool active() { return mStarted; }; + void setToken(QString token); + void unsetToken(); + + struct PingStat { + uint32_t packetsReceived = 0; + uint32_t packetsSent = 0; + double minRtt = 0.0; + double maxRtt = 0.0; + double avgRtt = 0.0; + double stdDevRtt = 0.0; + }; + + PingStat getPingStats(); + + private: + QWebSocket mSocket; + QUrl mURL; + QString mToken; + bool mAuthorized = false; + bool mStarted = false; + bool mError = false; + + QTimer mTimer; + uint32_t mPingCount = 0; + const uint32_t mPingNumPerInterval = 5; + const uint32_t mPingInterval = 1000; + const uint32_t mPingTimeout = 1000; + + std::map mPings; + + uint32_t mLastPacketSent; + uint32_t mLastPacketReceived; + uint32_t mLargestPingNumReceived = + 0; // is 0 if no ping has been received, otherwise, is the largest ping number + // received + bool mHasReceivedPing = false; // used for edge case where we have't received a ping + // yet (mLargestPingNumReceived = 0) + + PingStat mStats; + + void sendPingMessage(const QByteArray& message); + void updateStats(); + + private slots: + void onError(QAbstractSocket::SocketError error); + void onConnected(); + void onPingTimer(); + void onPingTimeout(uint32_t pingNum); + void onReceivePingMessage(const QByteArray& message); +}; + +#endif // VSPINGER_H \ No newline at end of file diff --git a/src/vs/vsQmlClipboard.h b/src/vs/vsQmlClipboard.h new file mode 100644 index 0000000..1bd544e --- /dev/null +++ b/src/vs/vsQmlClipboard.h @@ -0,0 +1,56 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +#ifndef VSQMLCLIPBOARD_H +#define VSQMLCLIPBOARD_H + +#include +#include +#include + +class VsQmlClipboard : public QObject +{ + Q_OBJECT + public: + explicit VsQmlClipboard(QObject* parent = 0) : QObject(parent) + { + clipboard = QApplication::clipboard(); + } + + Q_INVOKABLE void setText(QString text) + { + clipboard->setText(text, QClipboard::Clipboard); + } + + private: + QClipboard* clipboard; +}; + +#endif // VSQMLCLIPBOARD_H \ No newline at end of file diff --git a/src/vs/vsQuickView.cpp b/src/vs/vsQuickView.cpp new file mode 100644 index 0000000..6143cbd --- /dev/null +++ b/src/vs/vsQuickView.cpp @@ -0,0 +1,67 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsQuickView.cpp + * \author Aaron Wyatt + * \date March 2022 + */ + +#include "vsQuickView.h" + +#include +#include + +VsQuickView::VsQuickView(QWindow* parent) : QQuickView(parent) +{ +#ifdef Q_OS_MACOS + auto* quit = new QAction("&Quit", this); + + QMenuBar* menuBar = new QMenuBar(nullptr); + QMenu* appName = menuBar->addMenu("&JackTrip"); + appName->addAction(quit); + + connect(quit, &QAction::triggered, this, &VsQuickView::closeWindow); +#endif +} + +bool VsQuickView::event(QEvent* event) +{ + if (event->type() == QEvent::Close || event->type() == QEvent::Quit) { + emit windowClose(); + event->ignore(); + } + return QQuickView::event(event); +} + +void VsQuickView::closeWindow() +{ + emit windowClose(); +} diff --git a/src/vs/vsQuickView.h b/src/vs/vsQuickView.h new file mode 100644 index 0000000..db7810d --- /dev/null +++ b/src/vs/vsQuickView.h @@ -0,0 +1,63 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsQuickView.h + * \author Aaron Wyatt + * \date March 2022 + */ + +#ifndef VSQUICKVIEW_H +#define VSQUICKVIEW_H + +#include +#ifdef Q_OS_MACOS +#include +#include +#include +#include +#endif + +class VsQuickView : public QQuickView +{ + Q_OBJECT + + public: + VsQuickView(QWindow* parent = nullptr); + bool event(QEvent* event) override; + + signals: + void windowClose(); + + private slots: + void closeWindow(); +}; + +#endif // VSQUICKVIEW_H diff --git a/src/vs/vsServerInfo.cpp b/src/vs/vsServerInfo.cpp new file mode 100644 index 0000000..8d13a77 --- /dev/null +++ b/src/vs/vsServerInfo.cpp @@ -0,0 +1,320 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsServerInfo.cpp + * \author Aaron Wyatt + * \date March 2022 + */ + +#include "vsServerInfo.h" + +VsServerInfo::VsServerInfo(QObject* parent) : QObject(parent) {} + +VsServerInfo& VsServerInfo::operator=(const VsServerInfo& info) +{ + m_section = info.m_section; + m_name = info.m_name; + m_host = info.m_host; + m_port = info.m_port; + m_enabled = info.m_enabled; + m_owner = info.m_owner; + m_admin = info.m_admin; + m_isManaged = info.m_isManaged; + m_isPublic = info.m_isPublic; + m_region = info.m_region; + m_period = info.m_period; + m_sampleRate = info.m_sampleRate; + m_queueBuffer = info.m_queueBuffer; + m_bannerURL = info.m_bannerURL; + m_id = info.m_id; + m_sessionId = info.m_sessionId; + m_streamId = info.m_streamId; + m_status = info.m_status; + m_cloudId = info.m_cloudId; + m_inviteKey = info.m_inviteKey; + return *this; +} + +VsServerInfo::serverSectionT VsServerInfo::section() +{ + return m_section; +} + +QString VsServerInfo::type() const +{ + if (m_section == YOUR_STUDIOS) { + return QStringLiteral("Your Studios"); + } else if (m_section == SUBSCRIBED_STUDIOS) { + return QStringLiteral("Subscribed Studios"); + } else { + return QStringLiteral("Public Studios"); + } +} + +void VsServerInfo::setSection(serverSectionT section) +{ + m_section = section; +} + +QString VsServerInfo::name() const +{ + return m_name; +} + +void VsServerInfo::setName(const QString& name) +{ + m_name = name; +} + +QString VsServerInfo::host() const +{ + return m_host; +} + +QString VsServerInfo::status() const +{ + return m_status; +} + +bool VsServerInfo::canConnect() const +{ + return !m_host.isEmpty() && m_status == "Ready"; +} + +bool VsServerInfo::canStart() const +{ + return m_owner || m_admin; +} + +void VsServerInfo::setHost(const QString& host) +{ + m_host = host; + emit canConnectChanged(); +} + +void VsServerInfo::setStatus(const QString& status) +{ + m_status = status; + emit canConnectChanged(); +} + +quint16 VsServerInfo::port() const +{ + return m_port; +} + +void VsServerInfo::setPort(quint16 port) +{ + m_port = port; +} + +bool VsServerInfo::enabled() const +{ + return m_enabled; +} + +void VsServerInfo::setEnabled(bool enabled) +{ + m_enabled = enabled; +} + +bool VsServerInfo::isOwner() const +{ + return m_owner; +} + +void VsServerInfo::setIsOwner(bool owner) +{ + m_owner = owner; +} + +bool VsServerInfo::isAdmin() const +{ + return m_admin; +} + +void VsServerInfo::setIsAdmin(bool admin) +{ + m_admin = admin; +} + +bool VsServerInfo::isPublic() const +{ + return m_isPublic; +} + +void VsServerInfo::setIsPublic(bool isPublic) +{ + m_isPublic = isPublic; +} + +QString VsServerInfo::region() const +{ + return m_region; +} + +QString VsServerInfo::flag() const +{ + QStringList parts = m_region.split(QStringLiteral("-")); + if (parts.count() > 1) { + QString countryCode = parts.at(1).toUpper(); + if (countryCode == QStringLiteral("TF")) { + countryCode = QStringLiteral("TW"); + } + return QStringLiteral("flags/%1.svg").arg(countryCode); + } + // Have a fallback here + return QStringLiteral("flags/US.svg"); +} + +QString VsServerInfo::location() const +{ + return m_region; +} + +void VsServerInfo::setRegion(const QString& region) +{ + m_region = region; +} + +bool VsServerInfo::isManaged() const +{ + return m_isManaged; +} + +void VsServerInfo::setIsManaged(bool isManaged) +{ + m_isManaged = isManaged; +} + +quint16 VsServerInfo::period() const +{ + return m_period; +} + +void VsServerInfo::setPeriod(quint16 period) +{ + m_period = period; +} + +quint32 VsServerInfo::sampleRate() const +{ + return m_sampleRate; +} + +void VsServerInfo::setSampleRate(quint32 sampleRate) +{ + m_sampleRate = sampleRate; +} + +quint16 VsServerInfo::queueBuffer() const +{ + return m_queueBuffer; +} + +void VsServerInfo::setQueueBuffer(quint16 queueBuffer) +{ + m_queueBuffer = queueBuffer; +} + +QString VsServerInfo::bannerURL() const +{ + return m_bannerURL; +} + +void VsServerInfo::setBannerURL(const QString& bannerURL) +{ + m_bannerURL = bannerURL; +} + +QString VsServerInfo::id() const +{ + return m_id; +} + +void VsServerInfo::setId(const QString& id) +{ + m_id = id; +} + +QString VsServerInfo::sessionId() const +{ + return m_sessionId; +} + +void VsServerInfo::setSessionId(const QString& sessionId) +{ + m_sessionId = (sessionId == "undefined") ? "" : sessionId; +} + +QString VsServerInfo::streamId() const +{ + return m_streamId; +} + +void VsServerInfo::setStreamId(const QString& streamId) +{ + m_streamId = (streamId == "undefined") ? "" : streamId; +} + +QString VsServerInfo::inviteKey() const +{ + return m_inviteKey; +} + +void VsServerInfo::setInviteKey(const QString& inviteKey) +{ + m_inviteKey = inviteKey; +} + +QString VsServerInfo::cloudId() const +{ + return m_cloudId; +} + +void VsServerInfo::setCloudId(const QString& cloudId) +{ + m_cloudId = cloudId; +} + +bool VsServerInfo::operator<(const VsServerInfo& other) const +{ + if (status() == QStringLiteral("Ready")) { + if (other.status() != QStringLiteral("Ready")) { + return true; + } + } else if (other.status() == QStringLiteral("Ready")) { + return false; + } + return name() < other.name(); +} + +VsServerInfo::~VsServerInfo() = default; diff --git a/src/vs/vsServerInfo.h b/src/vs/vsServerInfo.h new file mode 100644 index 0000000..15f9d8f --- /dev/null +++ b/src/vs/vsServerInfo.h @@ -0,0 +1,163 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsServerInfo.h + * \author Aaron Wyatt + * \date March 2022 + */ + +#ifndef VSSERVERINFO_H +#define VSSERVERINFO_H + +#include + +class VsServerInfo : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString type READ type CONSTANT) + Q_PROPERTY(QString name READ name CONSTANT) + // Q_PROPERTY(QString host READ host CONSTANT) + Q_PROPERTY(bool canConnect READ canConnect NOTIFY canConnectChanged) + Q_PROPERTY(bool canStart READ canStart CONSTANT) + // Q_PROPERTY(quint16 port READ port CONSTANT) + Q_PROPERTY(bool isPublic READ isPublic CONSTANT) + Q_PROPERTY(QString flag READ flag CONSTANT) + Q_PROPERTY(QString bannerURL READ bannerURL CONSTANT) + Q_PROPERTY(QString location READ location CONSTANT) + Q_PROPERTY(bool isAdmin READ isAdmin CONSTANT) + Q_PROPERTY(bool isManaged READ isManaged CONSTANT) + Q_PROPERTY(quint16 period READ period CONSTANT) + Q_PROPERTY(quint32 sampleRate READ sampleRate CONSTANT) + Q_PROPERTY(quint16 queueBuffer READ queueBuffer CONSTANT) + Q_PROPERTY(QString sessionId READ sessionId CONSTANT) + Q_PROPERTY(QString streamId READ streamId CONSTANT) + Q_PROPERTY(QString status READ status CONSTANT) + Q_PROPERTY(bool enabled READ enabled CONSTANT) + Q_PROPERTY(QString cloudId READ cloudId CONSTANT) + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QString inviteKey READ inviteKey CONSTANT) + + public: + enum serverSectionT { YOUR_STUDIOS, SUBSCRIBED_STUDIOS, PUBLIC_STUDIOS }; + + explicit VsServerInfo(QObject* parent = nullptr); + VsServerInfo& operator=(const VsServerInfo& info); + ~VsServerInfo() override; + + serverSectionT section(); + QString type() const; + void setSection(serverSectionT section); + QString name() const; + void setName(const QString& name); + QString host() const; + bool canConnect() const; + bool canStart() const; + void setHost(const QString& host); + quint16 port() const; + void setPort(quint16 port); + bool enabled() const; + void setEnabled(bool enabled); + bool isOwner() const; + void setIsOwner(bool owner); + bool isAdmin() const; + void setIsAdmin(bool admin); + bool isPublic() const; + void setIsPublic(bool isPublic); + QString region() const; + QString flag() const; + QString location() const; + void setRegion(const QString& region); + bool isManaged() const; + void setIsManaged(bool isManageable); + quint16 period() const; + void setPeriod(quint16 period); + quint32 sampleRate() const; + void setSampleRate(quint32 sampleRate); + quint16 queueBuffer() const; + void setQueueBuffer(quint16 queueBuffer); + QString bannerURL() const; + void setBannerURL(const QString& bannerURL); + QString id() const; + void setId(const QString& id); + QString sessionId() const; + void setSessionId(const QString& sessionId); + QString streamId() const; + void setStreamId(const QString& streamId); + QString status() const; + void setStatus(const QString& status); + QString inviteKey() const; + void setInviteKey(const QString& inviteKey); + QString cloudId() const; + void setCloudId(const QString& cloudId); + bool operator<(const VsServerInfo& other) const; + + signals: + void canConnectChanged(); + + private: + serverSectionT m_section = PUBLIC_STUDIOS; + QString m_name; + QString m_host; + quint16 m_port; + bool m_enabled; + bool m_owner; + bool m_admin; + bool m_isManaged; + bool m_isPublic; + QString m_region; + quint16 m_period; + quint32 m_sampleRate; + quint16 m_queueBuffer; + QString m_bannerURL; + QString m_id; + QString m_sessionId; + QString m_streamId; + QString m_status; + QString m_cloudId; + QString m_inviteKey; + + /* Remaining JSON fields + "loopback": true, + "stereo": true, + "type": "JackTrip", + "size": "c5.large", + "mixBranch": "main", + "mixCode": "SimpleMix(~maxClients).masterVolume_(1).connect.start;", + "ownerId": "string", + "subStatus": "Active", + "createdAt": "2021-09-07T17:15:38Z", + "expiresAt": "2021-09-07T17:15:38Z", + "updatedAt": "2021-09-07T17:15:38Z" + */ +}; + +#endif // VSSERVERINFO_H diff --git a/src/vs/vsWebSocket.cpp b/src/vs/vsWebSocket.cpp new file mode 100644 index 0000000..62946a0 --- /dev/null +++ b/src/vs/vsWebSocket.cpp @@ -0,0 +1,145 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsWebSocket.cpp + * \author Matt Horton + * \date June 2022 + */ + +#include "vsWebSocket.h" + +#include +#include + +// Constructor +VsWebSocket::VsWebSocket(const QUrl& url, QString token, QString apiPrefix, + QString apiSecret, QObject* parent) + : QObject(parent) + , m_url(url) + , m_token(token) + , m_apiPrefix(apiPrefix) + , m_apiSecret(apiSecret) +{ + m_webSocket.reset(new QWebSocket()); + connect(m_webSocket.get(), &QWebSocket::disconnected, this, + &VsWebSocket::disconnected); + connect(m_webSocket.get(), + QOverload&>::of(&QWebSocket::sslErrors), this, + &VsWebSocket::onSslErrors); +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + connect(m_webSocket.get(), + QOverload::of(&QWebSocket::errorOccurred), this, + &VsWebSocket::onError); +#else + connect(m_webSocket.get(), + QOverload::of(&QWebSocket::error), this, + &VsWebSocket::onError); +#endif + connect(m_webSocket.get(), &QWebSocket::textMessageReceived, this, + &VsWebSocket::textMessageReceived); +} + +VsWebSocket::~VsWebSocket() +{ + if (isValid()) { + closeSocket(); + } + if (!m_webSocket.isNull()) { + m_webSocket->disconnect(); + m_webSocket.reset(); + } +} + +void VsWebSocket::openSocket() +{ + if (isValid()) { + return; + } + + QNetworkRequest req = QNetworkRequest(QUrl(m_url)); + QString authVal = "Bearer "; + authVal.append(m_token); + req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket")); + req.setRawHeader(QByteArray("Connection"), QByteArray("Upgrade")); + req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8()); + req.setRawHeader(QByteArray("Origin"), QByteArray("http://jacktrip.local")); + req.setRawHeader(QByteArray("APIPrefix"), m_apiPrefix.toUtf8()); + req.setRawHeader(QByteArray("APISecret"), m_apiSecret.toUtf8()); + + if (!m_webSocket.isNull()) { + m_webSocket->open(req); + qDebug() << "Opened websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery); + } +} + +void VsWebSocket::closeSocket() +{ + if (!m_webSocket.isNull() + && m_webSocket->state() != QAbstractSocket::UnconnectedState) { + qDebug() << "Closing websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery); + m_webSocket->abort(); + } +} + +void VsWebSocket::onError(QAbstractSocket::SocketError error) +{ + // RemoteHostClosedError may be expected due to finite connection durations + // ConnectionRefusedError may be expected if the server-side endpoint is closed + if (error != QAbstractSocket::RemoteHostClosedError) { + qDebug() << "Websocket error: " << error; + } + if (!m_webSocket.isNull()) { + m_webSocket->abort(); + } +} + +void VsWebSocket::onSslErrors(const QList& errors) +{ + for (int i = 0; i < errors.size(); ++i) { + qDebug() << "SSL error: " << errors.at(i); + } + if (!m_webSocket.isNull()) { + m_webSocket->abort(); + } +} + +void VsWebSocket::sendMessage(const QByteArray& message) +{ + if (isValid()) { + m_webSocket->sendBinaryMessage(message); + } +} + +bool VsWebSocket::isValid() +{ + return !m_webSocket.isNull() + && m_webSocket->state() == QAbstractSocket::ConnectedState; +} diff --git a/src/vs/vsWebSocket.h b/src/vs/vsWebSocket.h new file mode 100644 index 0000000..34da9b4 --- /dev/null +++ b/src/vs/vsWebSocket.h @@ -0,0 +1,80 @@ +//***************************************************************** +/* + JackTrip: A System for High-Quality Audio Network Performance + over the Internet + + Copyright (c) 2022-2024 JackTrip Labs, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ +//***************************************************************** + +/** + * \file vsWebSocket.h + * \author Matt Horton + * \date June 2022 + */ + +#ifndef VSWEBSOCKET_H +#define VSWEBSOCKET_H + +#include +#include +#include +#include +#include +#include +#include + +class VsWebSocket : public QObject +{ + Q_OBJECT + + public: + // Constructor + explicit VsWebSocket(const QUrl& url, QString token, QString apiPrefix, + QString apiSecret, QObject* parent = nullptr); + virtual ~VsWebSocket(); + + // Public functions + void openSocket(); + void closeSocket(); + void sendMessage(const QByteArray& message); + bool isValid(); + + signals: + void textMessageReceived(const QString& message); + void disconnected(); + + private slots: + void onError(QAbstractSocket::SocketError error); + void onSslErrors(const QList& errors); + + private: + QScopedPointer m_webSocket; + QUrl m_url; + QString m_token; + QString m_apiPrefix; + QString m_apiSecret; +}; + +#endif // VSWEBSOCKET_H diff --git a/src/vs/warning.svg b/src/vs/warning.svg new file mode 100644 index 0000000..e532c5f --- /dev/null +++ b/src/vs/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/wedge.svg b/src/vs/wedge.svg new file mode 100644 index 0000000..230cdd2 --- /dev/null +++ b/src/vs/wedge.svg @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/src/vs/wedge_inactive.svg b/src/vs/wedge_inactive.svg new file mode 100644 index 0000000..68ecbcf --- /dev/null +++ b/src/vs/wedge_inactive.svg @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/win/build_installer.bat b/win/build_installer.bat index 52c8229..a21fc61 100755 --- a/win/build_installer.bat +++ b/win/build_installer.bat @@ -55,9 +55,11 @@ if defined DYNAMIC_QT ( echo Including Qt%QTVERSION% Files for /f "tokens=*" %%a in ('objdump -p jacktrip.exe ^| findstr Qt%QTVERSION%Qml.dll') do set VS=%%a if defined VS ( - windeployqt -release --qmldir ..\..\src\gui jacktrip.exe + echo Including QML files + windeployqt -release --qmldir ..\..\src\vs jacktrip.exe set WIXDEFINES=%WIXDEFINES% -dvs ) else ( + echo Not including QML files windeployqt -release jacktrip.exe ) set WIXDEFINES=!WIXDEFINES! -ddynamic -dqt%QTVERSION% diff --git a/win/meson.build b/win/meson.build index 7362bba..b7ed4d4 100644 --- a/win/meson.build +++ b/win/meson.build @@ -31,6 +31,7 @@ if host_machine.system() == 'windows' link_args += 'Winhttp.lib' link_args += 'Dnsapi.lib' link_args += 'Iphlpapi.lib' + link_args += 'Qwave.lib' endif endif