From c379276694ab899b13a146fbe67b397168789d7c Mon Sep 17 00:00:00 2001 From: =?utf8?q?IOhannes=20m=20zm=C3=B6lnig?= Date: Fri, 19 Aug 2022 12:15:25 +0200 Subject: [PATCH] New upstream version 1.6.2+ds0 --- .clang-tidy-ignore | 4 + CMakeLists.txt | 25 +- docs/changelog.yml | 27 + jacktrip.pro | 36 +- linux/org.jacktrip.JackTrip.desktop.in | 3 +- macos/Info_novs.plist | 54 ++ .../JackTrip.app_template/Contents/Info.plist | 11 + macos/assemble_app.sh | 12 +- meson.build | 19 +- releases/edge/mac-manifests.json | 40 ++ releases/edge/win-manifests.json | 40 ++ releases/stable/mac-manifests.json | 10 + releases/stable/win-manifests.json | 10 + rtaudio.pro | 1 + src/JTApplication.h | 66 ++ src/JackTrip.cpp | 16 +- src/RtAudioInterface.cpp | 2 +- src/UdpDataProtocol.cpp | 49 +- src/UdpDataProtocol.h | 4 +- src/gui/Browse.qml | 31 +- src/gui/Connected.qml | 88 ++- src/gui/Failed.qml | 90 +++ src/gui/Settings.qml | 616 +++++++++++------- src/gui/Setup.qml | 8 +- src/gui/Studio.qml | 10 +- src/gui/manage.svg | 1 + src/gui/network.svg | 4 + src/gui/ohno.png | Bin 0 -> 3200 bytes src/gui/qjacktrip.cpp | 4 +- src/gui/qjacktrip.h | 3 +- src/gui/qjacktrip.qrc | 4 + src/gui/virtualstudio.cpp | 291 +++++++-- src/gui/virtualstudio.h | 42 +- src/gui/vs.qml | 28 +- src/gui/vsDevice.cpp | 487 ++++++++++++++ src/gui/vsDevice.h | 102 +++ src/gui/vsPing.cpp | 82 +++ src/gui/vsPing.h | 83 +++ src/gui/vsPinger.cpp | 311 +++++++++ src/gui/vsPinger.h | 121 ++++ src/gui/vsQuickView.cpp | 23 +- src/gui/vsQuickView.h | 11 +- src/gui/vsServerInfo.cpp | 23 +- src/gui/vsServerInfo.h | 8 +- src/gui/vsUrlHandler.cpp | 46 ++ src/gui/vsUrlHandler.h | 58 ++ src/gui/vsWebSocket.cpp | 123 ++++ src/gui/vsWebSocket.h | 82 +++ src/jacktrip_globals.h | 2 +- src/main.cpp | 196 +++++- 50 files changed, 3047 insertions(+), 360 deletions(-) create mode 100644 .clang-tidy-ignore create mode 100644 macos/Info_novs.plist create mode 100644 src/JTApplication.h create mode 100644 src/gui/Failed.qml create mode 100644 src/gui/manage.svg create mode 100644 src/gui/network.svg create mode 100644 src/gui/ohno.png create mode 100644 src/gui/vsDevice.cpp create mode 100644 src/gui/vsDevice.h create mode 100644 src/gui/vsPing.cpp create mode 100644 src/gui/vsPing.h create mode 100644 src/gui/vsPinger.cpp create mode 100644 src/gui/vsPinger.h create mode 100644 src/gui/vsUrlHandler.cpp create mode 100644 src/gui/vsUrlHandler.h create mode 100644 src/gui/vsWebSocket.cpp create mode 100644 src/gui/vsWebSocket.h diff --git a/.clang-tidy-ignore b/.clang-tidy-ignore new file mode 100644 index 0000000..0cb64dc --- /dev/null +++ b/.clang-tidy-ignore @@ -0,0 +1,4 @@ +subprojects +externals +documentation +src/*dsp.h \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 3fb5738..8673486 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,19 @@ project(QJackTrip) set(nogui FALSE) set(rtaudio TRUE) set(weakjack TRUE) -set(novs FALSE) +set(novs TRUE) + +message(STATUS "Hello Aaron! For anyone else, heed the following warning:") +message(WARNING "The CMake build of JackTrip is currently NOT officially supported. Meson or QMake are recommended for a full featured build." + "https://jacktrip.github.io/jacktrip/Build/Meson_build/") + +add_compile_definitions(PSI) add_compile_definitions(NO_UPDATER) +#add_compile_definitions(BUILD_TYPE="psi-borg.org NO_VS binary") +#string(TIMESTAMP BUILD_DATE "%Y%m%d") +#set(BUILD_NUMBER "00") +#add_compile_definitions(BUILD_ID="${BUILD_DATE}${BUILD_NUMBER}") +#add_compile_definitions(NDEBUG) add_compile_definitions(QT_OPENSOURCE) if (nogui) @@ -58,7 +69,7 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin endif () endif () -set_property(SOURCE src/Regulator.h PROPERTY SKIP_AUTOGEN ON) +#set_property(SOURCE src/Regulator.h PROPERTY SKIP_AUTOGEN ON) # Find includes in corresponding build directories set(CMAKE_INCLUDE_CURRENT_DIR ON) # Instruct CMake to run moc automatically when needed. @@ -74,6 +85,7 @@ if (NOT nogui) if (NOT novs) find_package(Qt5Quick CONFIG REQUIRED) find_package(Qt5NetworkAuth CONFIG REQUIRED) + find_package(Qt5WebSockets CONFIG REQUIRED) endif () endif () find_package(Qt5Network CONFIG REQUIRED) @@ -127,8 +139,13 @@ if (NOT nogui) if (NOT novs) set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/virtualstudio.cpp - src/gui/vsServerInfo.cpp src/gui/vsQuickView.cpp + src/gui/vsServerInfo.cpp + src/gui/vsPing.cpp + src/gui/vsPinger.cpp + src/gui/vsDevice.cpp + src/gui/vsUrlHandler.cpp + src/gui/vsWebSocket.cpp src/gui/qjacktrip.qrc ) else () @@ -154,7 +171,7 @@ set (qjacktrip_LIBS Qt5::Network) if (NOT nogui) set (qjacktrip_LIBS ${qjacktrip_LIBS} Qt5::Widgets) if (NOT novs) - set (qjacktrip_LIBS ${qjacktrip_LIBS} Qt5::Quick Qt5::NetworkAuth) + set (qjacktrip_LIBS ${qjacktrip_LIBS} Qt5::Quick Qt5::NetworkAuth Qt5::WebSockets) endif () endif () diff --git a/docs/changelog.yml b/docs/changelog.yml index be39c91..4a83d87 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -1,3 +1,30 @@ +- Version: "1.6.2" + Date: 2022-08-05 + Description: + - (updated) Static Qt version for Linux builds + - (upated) cleaner, easier to read VS settings + - (updated) icons for 'Manage' and 'Settings' in VS mode + - (added) human-readable locations in VS mode + - (added) warning that cmake is not officially supported + - (added) VS mode is treated as a device by VS web + - (added) Network statistics in Virtual Studio mode + - (added) URL scheme support to join a Studio from the VS web join button + - (added) banner images on Studios in VS mode + - (added) VS mode sets remote client name to app ID + - (fixed) WebSocket connection behavior in Virtual Studio (VS) mode + - (fixed) dblsqd errors in Linux builds + - (fixed) Windows datagramAvailable error + - (fixed) High Sierra compatibility in static builds + - (fixed) Doesn't crash if RtAudio sample rate isn't supported + - (fixed) Fractional UI scaling on Windows +- Version: "1.6.1" + Date: 2022-06-20 + Description: + - (added) ToS IP header to use DSCP Expedited Forwarding + - (fixed) Ubuntu deoendencies + - (fixed) timeout of client restored + - (fixed) bufstrategy 3 history minimum + - (fixed) perpetual logging in screen - Version: "1.6.0" Date: 2022-05-30 Description: diff --git a/jacktrip.pro b/jacktrip.pro index 106e37d..15cda35 100644 --- a/jacktrip.pro +++ b/jacktrip.pro @@ -33,8 +33,9 @@ nogui { QT += qml QT += quick QT += svg + QT += websockets } - noupdater { + noupdater|linux-g++|linux-g++-64 { DEFINES += NO_UPDATER } } @@ -239,14 +240,20 @@ HEADERS += src/DataProtocol.h \ src/gui/textbuf.h !novs { HEADERS += src/gui/virtualstudio.h \ + src/gui/vsDevice.h \ src/gui/vsServerInfo.h \ - src/gui/vsQuickView.h + src/gui/vsQuickView.h \ + src/gui/vsWebSocket.h \ + src/gui/vsPinger.h \ + src/gui/vsPing.h \ + src/gui/vsUrlHandler.h \ + src/JTApplication.h } - !noupdater { + !noupdater:!linux-g++:!linux-g++-64 { HEADERS += src/dblsqd/feed.h \ - src/dblsqd/release.h \ - src/dblsqd/semver.h \ - src/dblsqd/update_dialog.h + src/dblsqd/release.h \ + src/dblsqd/semver.h \ + src/dblsqd/update_dialog.h } } @@ -289,14 +296,19 @@ SOURCES += src/DataProtocol.cpp \ src/gui/textbuf.cpp !novs { SOURCES += src/gui/virtualstudio.cpp \ + src/gui/vsDevice.cpp \ src/gui/vsServerInfo.cpp \ - src/gui/vsQuickView.cpp + src/gui/vsQuickView.cpp \ + src/gui/vsWebSocket.cpp \ + src/gui/vsPinger.cpp \ + src/gui/vsPing.cpp \ + src/gui/vsUrlHandler.cpp } - !noupdater { + !noupdater:!linux-g++:!linux-g++-64 { SOURCES += src/dblsqd/feed.cpp \ - src/dblsqd/release.cpp \ - src/dblsqd/semver.cpp \ - src/dblsqd/update_dialog.cpp + src/dblsqd/release.cpp \ + src/dblsqd/semver.cpp \ + src/dblsqd/update_dialog.cpp } } @@ -311,7 +323,7 @@ SOURCES += src/DataProtocol.cpp \ } else { RESOURCES += src/gui/qjacktrip.qrc } - !noupdater { + !noupdater:!linux-g++:!linux-g++-64 { FORMS += src/dblsqd/update_dialog.ui } } diff --git a/linux/org.jacktrip.JackTrip.desktop.in b/linux/org.jacktrip.JackTrip.desktop.in index 2b77704..c34033c 100644 --- a/linux/org.jacktrip.JackTrip.desktop.in +++ b/linux/org.jacktrip.JackTrip.desktop.in @@ -3,7 +3,8 @@ Type=Application Name=JackTrip@name_suffix@ Comment=Network Music Performance over the Internet Comment[fr]=Performance de musique en réseau sur internet -Exec=jacktrip +Exec=/bin/bash -c 'if [ -z "$1" ]; then jacktrip; else jacktrip --gui --deeplink $1; fi' /bin/bash %u Icon=@icon@ Terminal=false StartupWMClass=@wmclass@ +MimeType=application/jacktrip;x-scheme-handler/jacktrip; diff --git a/macos/Info_novs.plist b/macos/Info_novs.plist new file mode 100644 index 0000000..0ca37fd --- /dev/null +++ b/macos/Info_novs.plist @@ -0,0 +1,54 @@ + + + + + BuildMachineOSBuild + 19E287 + CFBundleDevelopmentRegion + en + CFBundleExecutable + jacktrip + CFBundleIconFile + jacktrip + CFBundleIdentifier + %BUNDLEID% + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + %BUNDLENAME% + CFBundlePackageType + APPL + CFBundleShortVersionString + %VERSION% + CFBundleSignature + ???? + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + %VERSION% + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 11E503a + DTPlatformVersion + GM + DTSDKBuild + 19E258 + DTSDKName + macosx10.15 + DTXcode + 1141 + DTXcodeBuild + 11E503a + LSMinimumSystemVersion + 10.13 + NSHighResolutionCapable + + NSHumanReadableCopyright + Copyright © 2020 Juan-Pablo Caceres, Chris Chafe, Aaron Wyatt, et al. All rights reserved. + NSMicrophoneUsageDescription + This app requires microphone access to allow the jack server to capture audio. + + diff --git a/macos/JackTrip.app_template/Contents/Info.plist b/macos/JackTrip.app_template/Contents/Info.plist index 0ca37fd..99843e7 100644 --- a/macos/JackTrip.app_template/Contents/Info.plist +++ b/macos/JackTrip.app_template/Contents/Info.plist @@ -16,6 +16,17 @@ 6.0 CFBundleName %BUNDLENAME% + CFBundleURLTypes + + + CFBundleURLSchemes + + jacktrip + + CFBundleTypeRole + Editor + + CFBundlePackageType APPL CFBundleShortVersionString diff --git a/macos/assemble_app.sh b/macos/assemble_app.sh index 6318bb7..bf93244 100755 --- a/macos/assemble_app.sh +++ b/macos/assemble_app.sh @@ -96,11 +96,18 @@ cp -f $BINARY "$APPNAME.app/Contents/MacOS/" # copy licenses cp -f ../LICENSE.md "$APPNAME.app/Contents/Resources/" cp -Rf ../LICENSES "$APPNAME.app/Contents/Resources/" + +DYNAMIC_QT=$(otool -L $BINARY | grep QtCore) +DYNAMIC_VS=$(otool -L $BINARY | grep QtQml) + +if [ ! -z "$DYNAMIC_QT" ] && [ -z "$DYNAMIC_VS" ]; then + cp "Info_novs.plist" "$APPNAME.app/Contents/Info.plist" +fi + sed -i '' "s/%VERSION%/$VERSION/" "$APPNAME.app/Contents/Info.plist" sed -i '' "s/%BUNDLENAME%/$APPNAME/" "$APPNAME.app/Contents/Info.plist" sed -i '' "s/%BUNDLEID%/$BUNDLE_ID/" "$APPNAME.app/Contents/Info.plist" -DYNAMIC_QT=$(otool -L ../builddir/jacktrip | grep QtCore) if [ ! -z "$DYNAMIC_QT" ]; then DEPLOY_CMD="$(which macdeployqt)" if [ -z "$DEPLOY_CMD" ]; then @@ -114,9 +121,8 @@ if [ ! -z "$DYNAMIC_QT" ]; then exit 1 fi fi - VS=$(otool -L ../builddir/jacktrip | grep QtQml) QMLDIR="" - if [ ! -z "VS" ]; then + if [ ! -z "$DYNAMIC_VS" ]; then QMLDIR=" -qmldir=../src/gui" fi if [ ! -z "$CERTIFICATE" ]; then diff --git a/meson.build b/meson.build index 4c36da9..af9e535 100644 --- a/meson.build +++ b/meson.build @@ -105,19 +105,30 @@ else else src += [ 'src/gui/virtualstudio.cpp', + 'src/gui/vsDevice.cpp', 'src/gui/vsServerInfo.cpp', - 'src/gui/vsQuickView.cpp' + 'src/gui/vsQuickView.cpp', + 'src/gui/vsWebSocket.cpp', + 'src/gui/vsUrlHandler.cpp', + 'src/gui/vsPinger.cpp', + 'src/gui/vsPing.cpp' ] moc_h += [ 'src/gui/virtualstudio.h', + 'src/gui/vsDevice.h', 'src/gui/vsServerInfo.h', - 'src/gui/vsQuickView.h' + 'src/gui/vsQuickView.h', + 'src/gui/vsWebSocket.h', + 'src/gui/vsPinger.h', + 'src/gui/vsPing.h', + 'src/gui/vsUrlHandler.h', + 'src/JTApplication.h' ] - qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'Qml', 'Svg', 'NetworkAuth'], include_type: 'system') + qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'Qml', 'Svg', 'NetworkAuth', 'WebSockets'], include_type: 'system') qres = ['src/gui/qjacktrip.qrc'] endif - if get_option('noupdater') == true + if get_option('noupdater') == true or host_machine.system() == 'linux' defines += '-DNO_UPDATER' else src += [ diff --git a/releases/edge/mac-manifests.json b/releases/edge/mac-manifests.json index 97de59d..5fa65e2 100644 --- a/releases/edge/mac-manifests.json +++ b/releases/edge/mac-manifests.json @@ -1,6 +1,46 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.6.2-rc.3", + "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc3", + "download": { + "date": "2022-08-15T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc3/JackTrip-v1.6.2-rc3-macOS-x64-installer.pkg", + "downloadSize": 11536485, + "sha256": "accf625c8c797c13bde01fb50fe5bbb87fe4eefd0ae8ef06b74034e1cde6f22b" + } + }, + { + "version": "1.6.2-rc.2", + "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc2", + "download": { + "date": "2022-08-09T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc2/JackTrip-v1.6.2-rc2-macOS-x64-installer.pkg", + "downloadSize": 11531462, + "sha256": "a8b5418992045a5d08bfce1e7a412a1ad8414f9d7ea770564f2bba0caa83297b" + } + }, + { + "version": "1.6.2-rc.1", + "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc1", + "download": { + "date": "2022-08-06T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc1/JackTrip-v1.6.2-rc1-macOS-x64-installer.pkg", + "downloadSize": 11534071, + "sha256": "9a2200d157c4bb308b0b5ba5854ee5af17ae74991f7aa94fa5a0da19282cc571" + } + }, + { + "version": "1.6.1", + "changelog": "Bugfixes around UDP timeout and 'Logging In' screen navigation. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.1", + "download": { + "date": "2022-06-21T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.1/JackTrip-v1.6.1-macOS-x64-installer.pkg", + "downloadSize": 11476305, + "sha256": "eaf05c842d6b3ae799208a40b37da1cdb13e3700dcbbd97443c80cad81f4d2ac" + } + }, { "version": "1.6.0", "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0", diff --git a/releases/edge/win-manifests.json b/releases/edge/win-manifests.json index 59b8186..2471877 100644 --- a/releases/edge/win-manifests.json +++ b/releases/edge/win-manifests.json @@ -1,6 +1,46 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.6.2-rc.3", + "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc3", + "download": { + "date": "2022-08-15T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc3/JackTrip-v1.6.2-rc3-Windows-x64-installer.msi", + "downloadSize": 43606016, + "sha256": "62771ca5efbf2e91fa4cd347214e6e517b76c032a8895ca80bcbc2fa765ab81a" + } + }, + { + "version": "1.6.2-rc.2", + "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc2", + "download": { + "date": "2022-08-09T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc2/JackTrip-v1.6.2-rc2-Windows-x64-installer.msi", + "downloadSize": 43606016, + "sha256": "ff88acd1804362589478366a620d12be302071dba9781ea38ed6a8343c94c16d" + } + }, + { + "version": "1.6.2-rc.1", + "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc1", + "download": { + "date": "2022-08-06T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc1/JackTrip-v1.6.2-rc1-Windows-x64-installer.msi", + "downloadSize": 43601920, + "sha256": "f1412de0b13ff7599353a10aec8f2b69e9831a37103187f8fa68334c8f8f09de" + } + }, + { + "version": "1.6.1", + "changelog": "Bugfixes around UDP timeout and 'Logging In' screen navigation. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.1", + "download": { + "date": "2022-06-21T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.1/JackTrip-v1.6.1-Windows-x64-installer.msi", + "downloadSize": 43368448, + "sha256": "8eac390617488d849c0356e3305c96a59bbe46a8174d02b0321bb1dc86774b87" + } + }, { "version": "1.6.0", "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0", diff --git a/releases/stable/mac-manifests.json b/releases/stable/mac-manifests.json index 0faec7c..2bcad7b 100644 --- a/releases/stable/mac-manifests.json +++ b/releases/stable/mac-manifests.json @@ -1,6 +1,16 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.6.1", + "changelog": "Bugfixes around UDP timeout and 'Logging In' screen navigation. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.1", + "download": { + "date": "2022-06-21T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.1/JackTrip-v1.6.1-macOS-x64-installer.pkg", + "downloadSize": 11476305, + "sha256": "eaf05c842d6b3ae799208a40b37da1cdb13e3700dcbbd97443c80cad81f4d2ac" + } + }, { "version": "1.6.0", "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0", diff --git a/releases/stable/win-manifests.json b/releases/stable/win-manifests.json index cb255c0..42b0d70 100644 --- a/releases/stable/win-manifests.json +++ b/releases/stable/win-manifests.json @@ -1,6 +1,16 @@ { "app_name": "JackTrip", "releases": [ + { + "version": "1.6.1", + "changelog": "Bugfixes around UDP timeout and 'Logging In' screen navigation. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.1", + "download": { + "date": "2022-06-21T00:00:00Z", + "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.1/JackTrip-v1.6.1-Windows-x64-installer.msi", + "downloadSize": 43368448, + "sha256": "8eac390617488d849c0356e3305c96a59bbe46a8174d02b0321bb1dc86774b87" + } + }, { "version": "1.6.0", "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0", diff --git a/rtaudio.pro b/rtaudio.pro index 6934b19..51586be 100644 --- a/rtaudio.pro +++ b/rtaudio.pro @@ -15,6 +15,7 @@ linux-g++ | linux-g++-64 { } macx { QMAKE_CXXFLAGS += -D__MACOSX_CORE__ + QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.9 # the same deployment target as in jacktrip.pro } win32 { QMAKE_CXXFLAGS += -D__WINDOWS_ASIO__ -D__WINDOWS_WASAPI__ diff --git a/src/JTApplication.h b/src/JTApplication.h new file mode 100644 index 0000000..7d27b52 --- /dev/null +++ b/src/JTApplication.h @@ -0,0 +1,66 @@ +//***************************************************************** +/* + 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/JackTrip.cpp b/src/JackTrip.cpp index a5475bb..5031beb 100644 --- a/src/JackTrip.cpp +++ b/src/JackTrip.cpp @@ -1050,13 +1050,17 @@ void JackTrip::stop(const QString& errorMessage) mHasShutdown = true; std::cout << "Stopping JackTrip..." << std::endl; - // Stop The Sender - mDataProtocolSender->stop(); - mDataProtocolSender->wait(); + if (mDataProtocolSender != nullptr) { + // Stop The Sender + mDataProtocolSender->stop(); + mDataProtocolSender->wait(); + } - // Stop The Receiver - mDataProtocolReceiver->stop(); - mDataProtocolReceiver->wait(); + if (mDataProtocolReceiver != nullptr) { + // Stop The Receiver + mDataProtocolReceiver->stop(); + mDataProtocolReceiver->wait(); + } // Stop the audio processes // mAudioInterface->stopProcess(); diff --git a/src/RtAudioInterface.cpp b/src/RtAudioInterface.cpp index a40c1e1..0b221c2 100644 --- a/src/RtAudioInterface.cpp +++ b/src/RtAudioInterface.cpp @@ -163,7 +163,7 @@ void RtAudioInterface::setup() setBufferSize(bufferFrames); } catch (RtAudioError& e) { std::cout << '\n' << e.getMessage() << '\n' << std::endl; - exit(0); + throw std::runtime_error(e.getMessage()); } // Setup parent class diff --git a/src/UdpDataProtocol.cpp b/src/UdpDataProtocol.cpp index 43d5df8..bf1e4c3 100644 --- a/src/UdpDataProtocol.cpp +++ b/src/UdpDataProtocol.cpp @@ -50,7 +50,10 @@ #include "jacktrip_globals.h" #ifdef _WIN32 //#include +#include #include //cc need SD_SEND +#pragma comment(lib, "ws2_32.lib") +#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12) #else #include #include // for POSIX Sockets @@ -259,6 +262,14 @@ int UdpDataProtocol::bindSocket() local_addr.sin_port = htons(mBindPort); // set local port } + // Prevent WSAECONNRESET errors that occur on Windows due to async UDP port setup +#if defined(_WIN32) + BOOL bNewBehavior = FALSE; + DWORD dwBytesReturned = 0; + WSAIoctl(sock_fd, SIO_UDP_CONNRESET, &bNewBehavior, sizeof bNewBehavior, NULL, 0, + &dwBytesReturned, NULL, NULL); +#endif + // Set socket to be reusable, this is platform dependent int one = 1; #if defined(_WIN32) @@ -367,25 +378,25 @@ functions. DWORD n_bytes; WSABUF buffer; int error; buffer.len = n; buffer.buf = } //******************************************************************************* -void UdpDataProtocol::getPeerAddressFromFirstPacket(QHostAddress& peerHostAddress, - uint16_t& port) -{ - while (!datagramAvailable()) { - msleep(100); - } - char buf[1]; - - struct sockaddr_storage addr; - std::memset(&addr, 0, sizeof(addr)); - socklen_t sa_len = sizeof(addr); - ::recvfrom(mSocket, buf, 1, 0, (struct sockaddr*)&addr, &sa_len); - peerHostAddress.setAddress((struct sockaddr*)&addr); - if (mIPv6) { - port = ((struct sockaddr_in6*)&addr)->sin6_port; - } else { - port = ((struct sockaddr_in*)&addr)->sin_port; - } -} +// void UdpDataProtocol::getPeerAddressFromFirstPacket(QHostAddress& peerHostAddress, +// uint16_t& port) +// { +// while (!datagramAvailable()) { +// msleep(100); +// } +// char buf[1]; + +// struct sockaddr_storage addr; +// std::memset(&addr, 0, sizeof(addr)); +// socklen_t sa_len = sizeof(addr); +// ::recvfrom(mSocket, buf, 1, 0, (struct sockaddr*)&addr, &sa_len); +// peerHostAddress.setAddress((struct sockaddr*)&addr); +// if (mIPv6) { +// port = ((struct sockaddr_in6*)&addr)->sin6_port; +// } else { +// port = ((struct sockaddr_in*)&addr)->sin_port; +// } +// } //******************************************************************************* void UdpDataProtocol::run() diff --git a/src/UdpDataProtocol.h b/src/UdpDataProtocol.h index d3ffbe6..a49fda1 100644 --- a/src/UdpDataProtocol.h +++ b/src/UdpDataProtocol.h @@ -122,8 +122,8 @@ class UdpDataProtocol : public DataProtocol * \param peerHostAddress QHostAddress to store the peer address * \param port Receiving port */ - virtual void getPeerAddressFromFirstPacket(QHostAddress& peerHostAddress, - uint16_t& port); + // virtual void getPeerAddressFromFirstPacket(QHostAddress& peerHostAddress, + // uint16_t& port); /** \brief Sets the bind port number */ diff --git a/src/gui/Browse.qml b/src/gui/Browse.qml index 1eea79f..c4558ff 100644 --- a/src/gui/Browse.qml +++ b/src/gui/Browse.qml @@ -15,6 +15,7 @@ Item { property int buttonHeight: 25 property int buttonWidth: 103 + property int extraSettingsButtonWidth: 16 property int fontMedium: 11 property int scrollY: 0 @@ -248,8 +249,8 @@ Item { delegate: Studio { x: 16 * virtualstudio.uiScale width: studioListView.width - (2 * x) - serverLocation: location - flagImage: flag + serverLocation: virtualstudio.regions[location] ? "in " + virtualstudio.regions[location].label : "" + flagImage: bannerURL ? bannerURL : flag studioName: name publicStudio: isPublic manageable: isManageable @@ -320,7 +321,7 @@ Item { } onClicked: { virtualstudio.showAbout() } anchors.verticalCenter: parent.verticalCenter - x: parent.width - (230 * virtualstudio.uiScale) + x: parent.width - ((230 + extraSettingsButtonWidth) * virtualstudio.uiScale) width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale Text { text: "About" @@ -332,6 +333,12 @@ Item { 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) @@ -339,15 +346,17 @@ Item { border.color: settingsButton.down ? buttonPressedStroke : (settingsButton.hovered ? buttonHoverStroke : buttonStroke) } onClicked: window.state = "settings" - anchors.verticalCenter: parent.verticalCenter - x: parent.width - (119 * virtualstudio.uiScale) - width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale - Text { - text: "Settings" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } - color: textColour + 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 } } diff --git a/src/gui/Connected.qml b/src/gui/Connected.qml index b449c46..29ffbe5 100644 --- a/src/gui/Connected.qml +++ b/src/gui/Connected.qml @@ -11,6 +11,9 @@ Item { property int leftMargin: 16 property int fontBig: 28 property int fontMedium: 18 + property int fontSmall: 11 + + property int smallTextPadding: 8 property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D" property real imageLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0 @@ -33,8 +36,8 @@ Item { x: parent.leftMargin * virtualstudio.uiScale; y: 96 * virtualstudio.uiScale width: parent.width - (2 * x) connected: true - serverLocation: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].location : "Germany - Berlin" - flagImage: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].flag : "flags/DE.svg" + serverLocation: virtualstudio.currentStudio >= 0 && virtualstudio.regions[serverModel[virtualstudio.currentStudio].location] ? "in " + virtualstudio.regions[serverModel[virtualstudio.currentStudio].location].label : "" + flagImage: virtualstudio.currentStudio >= 0 ? ( serverModel[virtualstudio.currentStudio].bannerURL ? serverModel[virtualstudio.currentStudio].bannerURL : serverModel[virtualstudio.currentStudio].flag ) : "flags/DE.svg" studioName: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].name : "Test Studio" publicStudio: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isPublic : false manageable: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isManageable : false @@ -64,6 +67,14 @@ Item { width: 24 * virtualstudio.uiScale; height: 26 * virtualstudio.uiScale } + Image { + id: network + source: "network.svg" + anchors.horizontalCenter: mic.horizontalCenter + y: 408 * virtualstudio.uiScale + width: 28 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale + } + Colorize { anchors.fill: headphones source: headphones @@ -71,8 +82,17 @@ Item { saturation: 0 lightness: imageLightnessValue } + + Colorize { + anchors.fill: network + source: network + hue: 0 + saturation: 0 + lightness: imageLightnessValue + } Text { + id: inputDeviceHeader x: 120 * virtualstudio.uiScale text: virtualstudio.audioBackend == "JACK" ? virtualstudio.audioBackend : inputComboModel[virtualstudio.inputDevice] @@ -82,6 +102,7 @@ Item { } Text { + id: outputDeviceHeader x: 120 * virtualstudio.uiScale text: virtualstudio.audioBackend == "JACK" ? virtualstudio.audioBackend : outputComboModel[virtualstudio.outputDevice] @@ -89,6 +110,65 @@ Item { anchors.verticalCenter: headphones.verticalCenter color: textColour } - - //43 822 + + Text { + id: networkStatsHeader + x: 120 * virtualstudio.uiScale + text: "Network" + font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + anchors.verticalCenter: network.verticalCenter + color: textColour + } + + function getNetworkStatsText (networkStats) { + let minRtt = networkStats.minRtt; + let maxRtt = networkStats.maxRtt; + let avgRtt = networkStats.avgRtt; + + let texts = ["Measuring stats ...", "", ""]; + + if (!minRtt || !maxRtt) { + return texts; + } + + texts[0] = "" + minRtt + " ms - " + maxRtt + " ms, avg " + avgRtt + " ms round-trip time"; + + let quality = "poor"; + if (avgRtt <= 25) { + + if (maxRtt <= 30) { + quality = "excellent"; + } else { + quality = "good"; + } + + } else if (avgRtt <= 30) { + quality = "good"; + } else if (avgRtt <= 35) { + quality = "fair"; + } + + texts[1] = "Your connection quality is " + quality + "." + return texts; + } + + Text { + id: netstat0 + text: getNetworkStatsText(virtualstudio.networkStats)[0] + font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + topPadding: smallTextPadding + anchors.left: inputDeviceHeader.left + anchors.top: networkStatsHeader.bottom + color: textColour + } + + Text { + id: netstat1 + text: getNetworkStatsText(virtualstudio.networkStats)[1] + font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + topPadding: smallTextPadding + anchors.left: inputDeviceHeader.left + anchors.top: netstat0.bottom + color: textColour + } } diff --git a/src/gui/Failed.qml b/src/gui/Failed.qml new file mode 100644 index 0000000..8eb4a61 --- /dev/null +++ b/src/gui/Failed.qml @@ -0,0 +1,90 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.12 + +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" + + property real imageLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0 + + Image { + id: ohnoImage + source: "ohno.png" + width: 180 + height: 180 + y: 60 + anchors.horizontalCenter: parent.horizontalCenter + } + + Colorize { + anchors.fill: ohnoImage + source: ohnoImage + hue: 0 + saturation: 0 + lightness: imageLightnessValue + } + + 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: { window.state = "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/Settings.qml b/src/gui/Settings.qml index 0ba2ed8..02a6f3d 100644 --- a/src/gui/Settings.qml +++ b/src/gui/Settings.qml @@ -26,251 +26,427 @@ Item { property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797" property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC" property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC" - - Text { - x: 16 * virtualstudio.uiScale; y: 32 * virtualstudio.uiScale - text: "Settings" - font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - - ComboBox { - id: backendCombo - model: backendComboModel - currentIndex: virtualstudio.audioBackend == "JACK" ? 0 : 1 - onActivated: { virtualstudio.audioBackend = currentText } - x: 234 * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale - width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale - visible: virtualstudio.selectableBackend - } - - Text { - id: backendLabel - anchors.verticalCenter: backendCombo.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Audio Backend" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - visible: virtualstudio.selectableBackend - color: textColour - } - - Text { - id: jackLabel - x: leftMargin * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale - width: parent.width - x - (16 * 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: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - wrapMode: Text.WordWrap - visible: virtualstudio.audioBackend == "JACK" && !virtualstudio.selectableBackend - color: textColour - } - - ComboBox { - id: inputCombo - model: inputComboModel - currentIndex: virtualstudio.inputDevice - onActivated: { virtualstudio.inputDevice = currentIndex } - x: 234 * virtualstudio.uiScale; y: virtualstudio.uiScale * (virtualstudio.selectableBackend ? 148 : 100) - width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale - visible: virtualstudio.audioBackend != "JACK" - } - - ComboBox { - id: outputCombo - model: outputComboModel - currentIndex: virtualstudio.outputDevice - onActivated: { virtualstudio.outputDevice = currentIndex } - x: backendCombo.x; y: inputCombo.y + (48 * virtualstudio.uiScale) - width: backendCombo.width; height: backendCombo.height - visible: virtualstudio.audioBackend != "JACK" - } - - Text { - anchors.verticalCenter: inputCombo.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Input Device" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - visible: virtualstudio.audioBackend != "JACK" - color: textColour - } - - Text { - anchors.verticalCenter: outputCombo.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Output Device" - font { family: "Poppins"; pixelSize: 13 * virtualstudio.fontScale * virtualstudio.uiScale } - visible: virtualstudio.audioBackend != "JACK" - color: textColour - } - Button { - id: refreshButton + property string settingsGroupView: "Audio" + + ToolBar { + id: header + width: parent.width + 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: { virtualstudio.refreshDevices() } - x: parent.width - (232 * virtualstudio.uiScale); y: inputCombo.y + (100 * virtualstudio.uiScale) - width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - visible: virtualstudio.audioBackend != "JACK" - Text { - text: "Refresh Device List" - font { family: "Poppins"; pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + border.color: "#33979797" + color: backgroundColour + width: parent.width + } + + contentItem: Label { + text: "Settings" + elide: Label.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale } color: textColour } } - - Rectangle { - x: leftMargin * virtualstudio.uiScale; y: inputCombo.y + (146 * virtualstudio.uiScale) - width: parent.width - x - (16 * virtualstudio.uiScale); height: 1 * virtualstudio.uiScale - color: textColour - visible: virtualstudio.audioBackend != "JACK" - } - - ComboBox { - id: bufferCombo - x: backendCombo.x; y: inputCombo.y + (162 * virtualstudio.uiScale) - width: backendCombo.width; height: backendCombo.height - model: bufferComboModel - currentIndex: virtualstudio.bufferSize - onActivated: { virtualstudio.bufferSize = currentIndex } - font.family: "Poppins" - visible: virtualstudio.audioBackend != "JACK" - } - Text { - anchors.verticalCenter: bufferCombo.verticalCenter - x: 48 * virtualstudio.uiScale - text: "Buffer Size" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - visible: virtualstudio.audioBackend != "JACK" - color: textColour + Drawer { + id: drawer + width: 0.2 * parent.width + height: parent.height - header.height + y: header.height-1 + modal: false + interactive: false + visible: window.state == "settings" + + background: Rectangle { + border.color: "#33979797" + color: backgroundColour + } + + ButtonGroup { + buttons: viewControls.children + onClicked: { 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: Label { + text: audioBtn.text + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: textColour + } + background: Rectangle { + width: parent.width + color: audioBtn.down ? buttonPressedColour : (audioBtn.hovered || settingsGroupView == "Audio" ? buttonHoverColour : backgroundColour) + } + } + Button { + id: appearanceBtn + text: "Appearance" + width: parent.width + contentItem: Label { + text: appearanceBtn.text + 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: profileBtn + text: "Profile" + width: parent.width + contentItem: Label { + text: profileBtn.text + 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 + } + } } - + Rectangle { - id: separator - x: leftMargin * virtualstudio.uiScale - width: parent.width - x - (16 * virtualstudio.uiScale); height: 1 * virtualstudio.uiScale - y: virtualstudio.audioBackend == "JACK" ? - (virtualstudio.selectableBackend ? backendCombo.y + (48 * virtualstudio.uiScale) : jackLabel.y + (64 * virtualstudio.uiScale)) : bufferCombo.y + (52 * virtualstudio.uiScale) - color: textColour - } - - Slider { - id: scaleSlider - x: backendCombo.x; y: separator.y + (16 * virtualstudio.uiScale) - width: backendCombo.width - from: 1; to: 2; value: virtualstudio.uiScale - onMoved: { virtualstudio.uiScale = value } - } - - 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: 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: { window.state = "login"; virtualstudio.toStandard(); } - x: parent.width - (232 * virtualstudio.uiScale); y: scaleSlider.y + (40 * virtualstudio.uiScale) - width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + id: audioSettingsView + width: 0.8 * parent.width + height: parent.height - header.height + x: 0.2 * window.width + y: header.height + color: backgroundColour + visible: settingsGroupView == "Audio" + + ComboBox { + id: backendCombo + model: backendComboModel + currentIndex: virtualstudio.audioBackend == "JACK" ? 0 : 1 + onActivated: { virtualstudio.audioBackend = currentText } + x: 234 * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale + width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale + visible: virtualstudio.selectableBackend + } + 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 } + id: backendLabel + anchors.verticalCenter: backendCombo.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Audio Backend" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + visible: virtualstudio.selectableBackend 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 -(464 * virtualstudio.uiScale) - width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - anchors.verticalCenter: modeButton.verticalCenter + 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 } + id: jackLabel + x: leftMargin * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale + width: parent.width - x - (16 * 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: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + wrapMode: Text.WordWrap + visible: virtualstudio.audioBackend == "JACK" && !virtualstudio.selectableBackend + color: textColour + } + + ComboBox { + id: inputCombo + model: inputComboModel + currentIndex: virtualstudio.inputDevice + onActivated: { virtualstudio.inputDevice = currentIndex } + x: 234 * virtualstudio.uiScale; y: virtualstudio.uiScale * (virtualstudio.selectableBackend ? 148 : 100) + width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale + visible: virtualstudio.audioBackend != "JACK" + } + + ComboBox { + id: outputCombo + model: outputComboModel + currentIndex: virtualstudio.outputDevice + onActivated: { virtualstudio.outputDevice = currentIndex } + x: backendCombo.x; y: inputCombo.y + (48 * virtualstudio.uiScale) + width: backendCombo.width; height: backendCombo.height + visible: virtualstudio.audioBackend != "JACK" + } + + Text { + anchors.verticalCenter: inputCombo.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Input Device" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + visible: virtualstudio.audioBackend != "JACK" + color: textColour + } + + Text { + anchors.verticalCenter: outputCombo.verticalCenter + x: leftMargin * virtualstudio.uiScale + text: "Output Device" + font { family: "Poppins"; pixelSize: 13 * virtualstudio.fontScale * virtualstudio.uiScale } + visible: virtualstudio.audioBackend != "JACK" color: textColour } - } - - Text { - anchors.verticalCenter: modeButton.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Change Mode" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - } - ComboBox { - id: updateChannelCombo - x: parent.width - (232 * virtualstudio.uiScale); y: modeButton.y + (40 * virtualstudio.uiScale) - width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale - model: updateChannelComboModel - currentIndex: virtualstudio.updateChannel == "stable" ? 0 : 1 - onActivated: { virtualstudio.updateChannel = currentIndex == 0 ? "stable": "edge" } - font.family: "Poppins" - visible: !virtualstudio.noUpdater - } + 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: { virtualstudio.refreshDevices() } + x: parent.width - (232 * virtualstudio.uiScale); y: inputCombo.y + (100 * virtualstudio.uiScale) + width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + visible: virtualstudio.audioBackend != "JACK" + Text { + text: "Refresh Device List" + font { family: "Poppins"; pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale } + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + } + + Rectangle { + x: leftMargin * virtualstudio.uiScale; y: inputCombo.y + (146 * virtualstudio.uiScale) + width: parent.width - x - (16 * virtualstudio.uiScale); height: 1 * virtualstudio.uiScale + color: textColour + visible: virtualstudio.audioBackend != "JACK" + } + + ComboBox { + id: bufferCombo + x: backendCombo.x; y: inputCombo.y + (162 * virtualstudio.uiScale) + width: backendCombo.width; height: backendCombo.height + model: bufferComboModel + currentIndex: virtualstudio.bufferSize + onActivated: { virtualstudio.bufferSize = currentIndex } + font.family: "Poppins" + visible: virtualstudio.audioBackend != "JACK" + } - Text { - anchors.verticalCenter: updateChannelCombo.verticalCenter - x: leftMargin * virtualstudio.uiScale - text: "Update Channel" - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } - color: textColour - visible: !virtualstudio.noUpdater + Text { + anchors.verticalCenter: bufferCombo.verticalCenter + x: 48 * virtualstudio.uiScale + text: "Buffer Size" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + visible: virtualstudio.audioBackend != "JACK" + color: textColour + } } - - Text { - x: leftMargin * virtualstudio.uiScale; y: parent.height - (75 * virtualstudio.uiScale) - text: "JackTrip version " + virtualstudio.versionString - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} - color: textColour + + 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: 234 * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale + width: backendCombo.width + from: 1; to: 2; value: virtualstudio.uiScale + onMoved: { virtualstudio.uiScale = value } + } + + 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: 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: { window.state = "login"; virtualstudio.toStandard(); } + x: parent.width - (232 * virtualstudio.uiScale); y: scaleSlider.y + (56 * 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 + } + + 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 + } + + ComboBox { + id: updateChannelCombo + x: 234 * virtualstudio.uiScale; y: darkButton.y + (56 * virtualstudio.uiScale) + width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale + model: updateChannelComboModel + currentIndex: virtualstudio.updateChannel == "stable" ? 0 : 1 + onActivated: { virtualstudio.updateChannel = currentIndex == 0 ? "stable": "edge" } + font.family: "Poppins" + visible: !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 + visible: !virtualstudio.noUpdater + } } - 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: { window.state = "login"; virtualstudio.logout() } - x: parent.width - ((16 + buttonWidth) * virtualstudio.uiScale) - y: virtualstudio.noUpdater ? modeButton.y + (46 * virtualstudio.uiScale) : updateChannelCombo.y + (46 * virtualstudio.uiScale) - width: buttonWidth * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale + 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 { - text: "Log Out" - font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } - anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + 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: { window.state = "login"; 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 + } + } } Rectangle { - x: 0; y: parent.height - (36 * virtualstudio.uiScale) + x: -1; y: parent.height - (36 * virtualstudio.uiScale) width: parent.width; height: (36 * virtualstudio.uiScale) border.color: "#33979797" color: backgroundColour diff --git a/src/gui/Setup.qml b/src/gui/Setup.qml index d7f53e6..7a27a20 100644 --- a/src/gui/Setup.qml +++ b/src/gui/Setup.qml @@ -392,7 +392,7 @@ Item { anchors.verticalCenter: outputCombo.verticalCenter x: leftMargin * virtualstudio.uiScale text: "Output Device" - font { family: "Poppins"; pixelSize: 13 * virtualstudio.fontScale * virtualstudio.uiScale } + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } visible: virtualstudio.audioBackend != "JACK" color: textColour } @@ -411,7 +411,7 @@ Item { visible: virtualstudio.audioBackend != "JACK" Text { text: "Refresh Device List" - font { family: "Poppins"; pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale } + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } color: textColour } @@ -427,7 +427,7 @@ Item { horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap color: warningText - font { family: "Poppins"; pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale } + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } visible: Qt.platform.os == "windows" && virtualstudio.audioBackend != "JACK" } @@ -456,7 +456,7 @@ Item { Text { text: "Save Settings" font.family: "Poppins" - font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale + font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale font.weight: Font.Bold color: saveButtonText anchors.horizontalCenter: parent.horizontalCenter diff --git a/src/gui/Studio.qml b/src/gui/Studio.qml index f2004f5..b087cc5 100644 --- a/src/gui/Studio.qml +++ b/src/gui/Studio.qml @@ -60,18 +60,18 @@ Rectangle { color: shadowColour source: shadow } - + Rectangle { width: 12 * virtualstudio.uiScale; height: parent.height radius: width / 2 color: available ? "#0C1424" : "#B3B3B3" } - + Image { source: available ? "wedge.svg" : "wedge_inactive.svg" x: 6; y: 0; width: 52 * virtualstudio.uiScale; height: 83 * virtualstudio.uiScale } - + Image { source: "logo.svg" x: 8; y: 11; width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale @@ -133,7 +133,7 @@ Rectangle { anchors.verticalCenter: publicRect.verticalCenter x: (leftMargin + 22) * virtualstudio.uiScale width: manageable ? parent.width - (255 * virtualstudio.uiScale) : parent.width - (178 * virtualstudio.uiScale) - text: publicStudio ? "Public hub studio in " + serverLocation : "Private hub studio in " + serverLocation + text: publicStudio ? "Public hub studio " + serverLocation : "Private hub studio " + serverLocation font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } elide: Text.ElideRight color: textColour @@ -195,7 +195,7 @@ Rectangle { Image { width: 20 * virtualstudio.uiScale; height: width anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } - source: "cog.svg" + source: "manage.svg" } } diff --git a/src/gui/manage.svg b/src/gui/manage.svg new file mode 100644 index 0000000..7d9ae31 --- /dev/null +++ b/src/gui/manage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/network.svg b/src/gui/network.svg new file mode 100644 index 0000000..4aa8827 --- /dev/null +++ b/src/gui/network.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/gui/ohno.png b/src/gui/ohno.png new file mode 100644 index 0000000000000000000000000000000000000000..7d118f3223beb91c936ebf108c082c3147c1dc40 GIT binary patch literal 3200 zcmaJ^`#Tei7bmxovAMt5Xrv;ygfJtwTqgH4gdx1{mo?GCOD2~x7Yql-Nzwnn$?>c}>bS{?aWFULcr^f7J}$WZjjkDSaMTe_vb}gXI0VHl zO^uwQIe)#_%=w`$@}PaP_2>)qmR@ZQS6L!UDMc>@j`HAPUcZ7xd-6yFOB%J1cnQpMnia%*4FQ2^|&~!odYfJPr7N0o;Uk+?+>;2{Bi;cM0E)aM=1y z+RfRYo%AsB7tG1fGu#NEcrd1Rl-!O#H&iyei7pgot}bI5X9QqNeGg9@J|=s-;dYwe z;oDb({hr}@c0X2UY=dv#Ar;8CYJ(L{R5D{fd4oqg{>}Qmjz(blnG*X8Xn5jvdA?R3 zC?#ThLi(yuGg%c@R+|Be`VN52gO~z?Qo9IjYA}DRe3HAqVMDdlcp%rsXk`(1_EH7X zTH)zH8o&rVX`dHu)eKMU9`ta@iwgoHRkT4AY?Pd{P0-4ZJ5ClMy_R!=JsBEOmu6*j z{JIf;Mu)#pvk>OdR+pgPY#Ccxz6vG*uJ7Ah?g7mlvWoC??`=`ZTek;r#H4}~;lQaC0lk96OsIv8;f~|nNrn0X>#gh`)af2E((3_&oKMr!L$AT_*-<$3t zcjkkth(2m4E4E$^G=@I20q`r%=u?ZFn7#`(iroRzw8yQ-&au*Ue|r%;TZ7LM zWdIlR`eoXgyZ9W8c}fc}lhx$dX9Z>#Y(GA&g=0@3Q7|rpfuZq-QXFOGv1?lULk${H zEFHhoWs8w=EUaneQwHc>LH+Xea-Jc)5<%ST;L&w`mz^%C2Ydxb&hG3oy?=@yRAQUrZc2`j_QNM(D#UW2=Wb4V%KH*K8@sM zV(UqOk6{8@=T?cHXXAEm+Pz#5VU*T~flGg5;n8VtsUtn#`;i;9G)kf{lqTdqkKgbR zaZXV zxp`d?;(qqPTeHLFa$VI=;}}Qo9IN5i*|C<{o!jAGOolb;s)9!B)N#R*^q=$VJsNyO z)7bQ)cV-Ck8YZ6qHNjn1AX;XREygb6HOiU8i>fDEg;Kp(GZr%FzN#%U^ zdX&Q+G0tnO+0jvuReYxHu!pxyui^@4`|{9@p1Zi__LC#ykAysvwnpAto4!Ey%M(e?vVefPBxtua&1H`My2QLTUGH_ixxVQOlYXoF)kMFeLm+&_ zYBGyRy?-DpO;Fw%2qHvo^lkW8C$$o# zdYX~+mYk1Ccl|9p7*#?QF_(;Z^HQM=`IjLXgi*CWxm*T;fAx0Jx_J-%HxyK6^~6?j zahN3WRZRd}Vp9A48Y(mS&w3BU+eRS}MyroC*RzTZY)|pU*HQ}$yqOYBnTB=$$^$9I zHgG`{`Ij>6Lk)|BZ801R`XdLCw#z5zuzMg;C16HazqvYEyivVS4D;4A|G~(D=ls)3 z#$tlc6n$^2kVWt{xKy~|0_uCIU{<9`&s^UUzw(nwJ$wSA#*AZ}#jZyy@-DVYuSSFx zqB4oQ;$apaR7h=&1uE8(xq11NPithK~ZYA5B_s8(m`$QDCO3A{ra`Kr|{Is?6S)Pv_(U zM{+Bosv7DA_8#v7I$Y}jdz@U(MryfdrNlm;#Vw^2kEScw9_x@o0+@5Ne7m)`opZ^D zq(q6tChz4cYiYw=L0yM30&kypfLmXvYRgoez5IJVOtfn4@a{l-w z;zT!O|G&`jZm|HHzh9&dh8;6Q*UQb{9R53GvdvS zP;*PAokFmqfKtT}bPMPfp?bZ$8sa81itLXn=i>V$?jJ&usr}xfqb#}-s7rUDLxNFR z3voPCLT<5efs&3WdqfPXH9rr8B~-rx;=RSKOkA7g77Iwt8HKz8J*T3!OH5Gy{rYC= z#(K=Ll~H?jAd?rw9^$0`LkZPN*Xu5Fs~x>|zCh*&PZXM~^!>#-wf|*Wy9xiXXaqqKo1@9f9g*#3uGjr_Z zADiKO6V(e*T@}H_4$LvNxS8f@)0F9$w5bUs4u1+NF3-|A${qaRGZB7aL<3q=5K$cI zdJl3Dd-{@2&TF3MZ{3#s$$wl$vgW!Gb1y%=-QJ6?m3nQf8Ch!dwnRr?If2qlW>kTKrq^1q+!dSp14bxC z71+utJKEjjCHbiivMxokA&~R=wEvlD1uB{TdO*%cT-r~g;xHt;?f%qEBr(k`qNG#U>r1c%Wo^YM?q z&8!X&X+AwWyHaBrqvIPv|E6O0uV72U(#3%Qi@bMD@~o380g&n_YBO$i*qrw@w`71N zeLEq9DF+O240KDZY%IaFJsWU2xk`o|;JjTEEgMTh08JU#<5|HtI3dGDRhFfF#h9nA z(t*3vlix-9NeZ@H6^vm?pH=jRN{pw`s7U@t$})ClEa-AJ@5{ylYO<@ zGiFCT!g~V@Ra#-~DPImi>1PK3eH%;bx@k&H?-!hU9ymTlwqa*3i35q4wLPY(S;-@| zsBckgX;Vi$Vt;?LUqNcB?Jk|~KK5T&X6F6oBq6DVxWE%>ze!Yp{U`i9!mWJ#4ePLfg*h^Y|7 z54-UQ)G33z1w#Ql?Z@13qj!+oA1kk-j%MoUIy{ob(^5dH2d+r9!(;W4eCl~tR`Om~ zHTK}jc*QH13Kd;N$%#HKzWvAbPw<+SONZzq_}Dq;KX>_TjR(I;@w_Um>D@xfHZYvt zJ?p+>3t*WU4{rU1Ti@Km5zD%X@dost+q!AahVk(I{Y$Xv|9_yLj!re?kV~sH64qal RKYkcEEX{08Um2ql{|CNiA;|y$ literal 0 HcmV?d00001 diff --git a/src/gui/qjacktrip.cpp b/src/gui/qjacktrip.cpp index b5a5c3f..c685a7d 100644 --- a/src/gui/qjacktrip.cpp +++ b/src/gui/qjacktrip.cpp @@ -55,7 +55,7 @@ #include "../Limiter.h" #include "../Reverb.h" -QJackTrip::QJackTrip(int argc, QWidget* parent) +QJackTrip::QJackTrip(int argc, bool suppressCommandlineWarning, QWidget* parent) : QMainWindow(parent) #ifdef PSI , m_ui(new Ui::QJackTrip) @@ -281,7 +281,7 @@ QJackTrip::QJackTrip(int argc, QWidget* parent) // One of our arguments will always be --gui, so if that's the only one // then we don't need to show the warning message. - if ((!gVerboseFlag && m_argc > 2) || m_argc > 3) { + if (((!gVerboseFlag && m_argc > 2) || m_argc > 3) && !suppressCommandlineWarning) { QMessageBox msgBox; msgBox.setText( "The GUI version of JackTrip currently ignores any command line " diff --git a/src/gui/qjacktrip.h b/src/gui/qjacktrip.h index 3410d65..3a396e5 100644 --- a/src/gui/qjacktrip.h +++ b/src/gui/qjacktrip.h @@ -67,7 +67,8 @@ class QJackTrip : public QMainWindow Q_OBJECT public: - explicit QJackTrip(int argc = 0, QWidget* parent = nullptr); + explicit QJackTrip(int argc = 0, bool suppressCommandlineWarning = false, + QWidget* parent = nullptr); ~QJackTrip() override; void closeEvent(QCloseEvent* event) override; diff --git a/src/gui/qjacktrip.qrc b/src/gui/qjacktrip.qrc index ad8f864..6a5678c 100644 --- a/src/gui/qjacktrip.qrc +++ b/src/gui/qjacktrip.qrc @@ -12,6 +12,7 @@ Browse.qml Settings.qml Connected.qml + Failed.qml Setup.qml logo.svg wedge.svg @@ -20,10 +21,13 @@ public.svg join.svg leave.svg + manage.svg cog.svg mic.svg ethernet.png + ohno.png headphones.svg + network.svg jacktrip.png jacktrip white.png JTOriginal.png diff --git a/src/gui/virtualstudio.cpp b/src/gui/virtualstudio.cpp index 93df669..edc79d5 100644 --- a/src/gui/virtualstudio.cpp +++ b/src/gui/virtualstudio.cpp @@ -37,6 +37,7 @@ #include "virtualstudio.h" +#include #include #include #include @@ -156,6 +157,17 @@ VirtualStudio::VirtualStudio(bool firstRun, QObject* parent) m_refreshMutex.unlock(); } }); + + connect(&m_heartbeatTimer, &QTimer::timeout, this, [&]() { + sendHeartbeat(); + }); + + // Connect joinStudio callbacks + connect(this, &VirtualStudio::studioToJoinChanged, this, &VirtualStudio::joinStudio); + // QueuedConnection since refreshFinished is sometimes signaled from a network reply + // thread + connect(this, &VirtualStudio::refreshFinished, this, &VirtualStudio::joinStudio, + Qt::QueuedConnection); } void VirtualStudio::setStandardWindow(QSharedPointer window) @@ -186,6 +198,12 @@ void VirtualStudio::show() m_view.show(); } +void VirtualStudio::raiseToTop() +{ + m_view.show(); // Restore from systray + m_view.requestActivate(); // Raise to top +} + bool VirtualStudio::showFirstRun() { return m_showFirstRun; @@ -295,11 +313,26 @@ int VirtualStudio::currentStudio() return m_currentStudio; } +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; @@ -364,6 +397,14 @@ void VirtualStudio::setShowWarnings(bool show) settings.setValue(QStringLiteral("ShowWarnings"), m_showWarnings); settings.endGroup(); emit showWarningsChanged(); + // attempt to join studio if requested + if (!m_studioToJoin.isEmpty()) { + // device setup view proceeds warning view + // if device setup is shown, do not immediately join + if (!m_showDeviceSetup) { + joinStudio(); + } + } } float VirtualStudio::fontScale() @@ -397,6 +438,17 @@ void VirtualStudio::setDarkMode(bool dark) emit darkModeChanged(); } +QUrl VirtualStudio::studioToJoin() +{ + return m_studioToJoin; +} + +void VirtualStudio::setStudioToJoin(const QUrl& url) +{ + m_studioToJoin = url; + emit studioToJoinChanged(); +} + bool VirtualStudio::noUpdater() { #ifdef NO_UPDATER @@ -415,6 +467,49 @@ bool VirtualStudio::psiBuild() #endif } +QString VirtualStudio::failedMessage() +{ + return m_failedMessage; +} + +void VirtualStudio::joinStudio() +{ + if (!m_authenticated || m_studioToJoin.isEmpty() || m_servers.isEmpty()) { + // No servers yet. Making sure we have them. + // getServerList emits refreshFinished which + // will come back to this function. + if (m_authenticated && !m_studioToJoin.isEmpty() && m_servers.isEmpty()) { + getServerList(true); + } + return; + } + + QString scheme = m_studioToJoin.scheme(); + QString path = m_studioToJoin.path(); + QString url = m_studioToJoin.toString(); + m_studioToJoin.clear(); + + m_failedMessage = ""; + if (scheme != "jacktrip" || path.length() <= 1) { + m_failedMessage = "Invalid join request received: " + url; + emit failedMessageChanged(); + emit failed(); + return; + } + QString targetId = path.remove(0, 1); + + int i = 0; + for (i = 0; i < m_servers.count(); i++) { + if (static_cast(m_servers.at(i))->id() == targetId) { + connectToStudio(i); + return; + } + } + m_failedMessage = "Unable to find studio " + targetId; + emit failedMessageChanged(); + emit failed(); +} + void VirtualStudio::toStandard() { if (!m_standardWindow.isNull()) { @@ -424,6 +519,7 @@ void VirtualStudio::toStandard() QSettings settings; settings.setValue(QStringLiteral("UiMode"), QJackTrip::STANDARD); m_refreshTimer.stop(); + m_heartbeatTimer.stop(); if (m_showFirstRun) { m_showFirstRun = false; @@ -466,6 +562,10 @@ void VirtualStudio::login() void VirtualStudio::logout() { + if (m_device != nullptr) { + m_device->removeApp(); + } + m_authenticator->setToken(QLatin1String("")); m_authenticator->setRefreshToken(QLatin1String("")); @@ -476,6 +576,7 @@ void VirtualStudio::logout() settings.endGroup(); m_refreshTimer.stop(); + m_heartbeatTimer.stop(); m_refreshToken.clear(); m_userId.clear(); @@ -552,6 +653,13 @@ void VirtualStudio::applySettings() emit inputDeviceChanged(); emit outputDeviceChanged(); #endif + + // attempt to join studio if requested + // this function is called after the device setup view + // which can display upon opening the app from join link + if (!m_studioToJoin.isEmpty()) { + joinStudio(); + } } void VirtualStudio::connectToStudio(int studioIndex) @@ -562,6 +670,9 @@ void VirtualStudio::connectToStudio(int studioIndex) } m_refreshTimer.stop(); + m_networkStats = QJsonObject(); + emit networkStatsChanged(); + m_currentStudio = studioIndex; VsServerInfo* studioInfo = static_cast(m_servers.at(m_currentStudio)); emit currentStudioChanged(); @@ -624,69 +735,41 @@ void VirtualStudio::completeConnection() emit connectionStateChanged(); VsServerInfo* studioInfo = static_cast(m_servers.at(m_currentStudio)); try { - m_jackTrip.reset(new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, 2, 2, -#ifdef WAIR // wair - 0, -#endif // endwhere - 4, 1)); - m_jackTrip->setConnectDefaultAudioPorts(true); + std::string input = ""; + std::string output = ""; + int buffer_size = 0; #ifdef RT_AUDIO if (m_useRtAudio) { - m_jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO); - m_jackTrip->setSampleRate(studioInfo->sampleRate()); - m_jackTrip->setAudioBufferSizeInSamples(m_bufferSize); + input = m_inputDevice.toStdString(); if (m_inputDevice == QLatin1String("(default)")) { - m_jackTrip->setInputDevice(""); - } else { - m_jackTrip->setInputDevice(m_inputDevice.toStdString()); + input = ""; } + output = m_outputDevice.toStdString(); if (m_outputDevice == QLatin1String("(default)")) { - m_jackTrip->setOutputDevice(""); - } else { - m_jackTrip->setOutputDevice(m_outputDevice.toStdString()); + output = ""; } + buffer_size = m_bufferSize; } #endif - m_jackTrip->setBufferStrategy(1); - m_jackTrip->setBufferQueueLength(-500); - m_jackTrip->setPeerAddress(studioInfo->host()); - m_jackTrip->setPeerPorts(studioInfo->port()); - m_jackTrip->setPeerHandshakePort(studioInfo->port()); + JackTrip* jackTrip = + m_device->initJackTrip(m_useRtAudio, input, output, buffer_size, studioInfo); - QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this, + QObject::connect(jackTrip, &JackTrip::signalProcessesStopped, this, &VirtualStudio::processFinished, Qt::QueuedConnection); - QObject::connect(m_jackTrip.data(), &JackTrip::signalError, this, + QObject::connect(jackTrip, &JackTrip::signalError, this, &VirtualStudio::processError, Qt::QueuedConnection); - QObject::connect(m_jackTrip.data(), &JackTrip::signalReceivedConnectionFromPeer, - this, &VirtualStudio::receivedConnectionFromPeer, + QObject::connect(jackTrip, &JackTrip::signalReceivedConnectionFromPeer, this, + &VirtualStudio::receivedConnectionFromPeer, Qt::QueuedConnection); - // TODO: replace the following: - // m_ui->statusBar->showMessage(QStringLiteral("Waiting for Peer...")); - /* - QObject::connect(m_jackTrip.data(), &JackTrip::signalUdpWaitingTooLong, this, - &QJackTrip::udpWaitingTooLong, Qt::QueuedConnection); - QObject::connect(m_jackTrip.data(), &JackTrip::signalQueueLengthChanged, this, - &QJackTrip::queueLengthChanged, Qt::QueuedConnection);*/ - -#ifdef WAIRTOHUB // WAIR - m_jackTrip->startProcess(0); // for WAIR compatibility, ID in jack client name -#else - m_jackTrip->startProcess(); -#endif // endwhere + m_device->startJackTrip(); + m_device->startPinger(studioInfo); } catch (const std::exception& e) { // Let the user know what our exception was. m_connectionState = QStringLiteral("JackTrip Error"); emit connectionStateChanged(); - QMessageBox msgBox; - msgBox.setText(QStringLiteral("Error: ").append(e.what())); - msgBox.setWindowTitle(QStringLiteral("Doh!")); - msgBox.exec(); - - m_jackTripRunning = false; - emit disconnected(); - m_onConnectedScreen = false; + processError(QString::fromUtf8(e.what())); return; } @@ -717,7 +800,9 @@ void VirtualStudio::disconnect() stopStudio(); } } - m_jackTrip->stop(); + + m_device->stopPinger(); + m_device->stopJackTrip(); } else if (m_startedStudio) { m_startTimer.stop(); stopStudio(); @@ -761,6 +846,12 @@ void VirtualStudio::createStudio() QDesktopServices::openUrl(url); } +void VirtualStudio::editProfile() +{ + QUrl url = QUrl(QStringLiteral("https://app.jacktrip.org/profile")); + QDesktopServices::openUrl(url); +} + void VirtualStudio::showAbout() { About about; @@ -770,8 +861,15 @@ void VirtualStudio::showAbout() void VirtualStudio::exit() { m_refreshTimer.stop(); + m_heartbeatTimer.stop(); if (m_onConnectedScreen) { m_isExiting = true; + + if (m_device != nullptr) { + m_device->stopPinger(); + m_device->stopJackTrip(); + } + disconnect(); } else { emit signalExit(); @@ -780,29 +878,53 @@ void VirtualStudio::exit() void VirtualStudio::slotAuthSucceded() { - m_refreshToken = m_authenticator->refreshToken(); + m_authenticated = true; + m_refreshToken = m_authenticator->refreshToken(); emit hasRefreshTokenChanged(); QSettings settings; + settings.setValue(QStringLiteral("UiMode"), QJackTrip::VIRTUAL_STUDIO); settings.beginGroup(QStringLiteral("VirtualStudio")); settings.setValue(QStringLiteral("RefreshToken"), m_refreshToken); settings.endGroup(); - settings.setValue(QStringLiteral("UiMode"), QJackTrip::VIRTUAL_STUDIO); + m_device = new VsDevice(m_authenticator.data()); + m_device->registerApp(); if (m_userId.isEmpty()) { getUserId(); } else { getSubscriptions(); } + + if (m_regions.isEmpty()) { + getRegions(); + } + if (m_userMetadata.isEmpty()) { + getUserMetadata(); + } + + // attempt to join studio if requested + if (!m_studioToJoin.isEmpty()) { + // FTUX shows warnings and device setup views + // if any of these enabled, do not immediately join + if (!m_showWarnings && !m_showDeviceSetup) { + joinStudio(); + } + } + connect(m_device, &VsDevice::updateNetworkStats, this, &VirtualStudio::updatedStats); } void VirtualStudio::slotAuthFailed() { + m_authenticated = false; emit authFailed(); } void VirtualStudio::processFinished() { + // reset network statistics + m_networkStats = QJsonObject(); + if (m_isExiting) { emit signalExit(); return; @@ -820,7 +942,6 @@ void VirtualStudio::processFinished() m_jackTripRunning = false; m_connectionState = QStringLiteral("Disconnected"); - m_jackTrip.reset(); emit connectionStateChanged(); emit disconnected(); m_onConnectedScreen = false; @@ -835,7 +956,7 @@ void VirtualStudio::processError(const QString& errorMessage) QMessageBox msgBox; if (errorMessage == QLatin1String("Peer Stopped")) { // Report the other end quitting as a regular occurance rather than an error. - msgBox.setText(errorMessage); + msgBox.setText("The Studio has been stopped."); msgBox.setWindowTitle(QStringLiteral("Disconnected")); } else { msgBox.setText(QStringLiteral("Error: ").append(errorMessage)); @@ -848,6 +969,10 @@ void VirtualStudio::processError(const QString& errorMessage) void VirtualStudio::receivedConnectionFromPeer() { + // Connect via API + VsServerInfo* studioInfo = static_cast(m_servers.at(m_currentStudio)); + m_device->setServerId(studioInfo->id()); + m_connectionState = QStringLiteral("Connected"); emit connectionStateChanged(); std::cout << "Received connection" << std::endl; @@ -909,6 +1034,19 @@ void VirtualStudio::launchBrowser(const QUrl& url) } } +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::setupAuthenticator() { if (m_authenticator.isNull()) { @@ -960,6 +1098,13 @@ void VirtualStudio::setupAuthenticator() } } +void VirtualStudio::sendHeartbeat() +{ + if (m_device != nullptr && m_connectionState != "Connecting...") { + m_device->sendHeartbeat(); + } +} + void VirtualStudio::getServerList(bool firstLoad, int index) { { @@ -1037,7 +1182,11 @@ void VirtualStudio::getServerList(bool firstLoad, int index) 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()); if (servers.at(i)[QStringLiteral("owner")].toBool()) { yourServers.append(serverInfo); serverInfo->setSection(VsServerInfo::YOUR_STUDIOS); @@ -1105,11 +1254,15 @@ void VirtualStudio::getServerList(bool firstLoad, int index) } if (firstLoad) { emit authSucceeded(); + emit refreshFinished(index); m_refreshTimer.setInterval(10000); m_refreshTimer.start(); + m_heartbeatTimer.setInterval(5000); + m_heartbeatTimer.start(); } else { emit refreshFinished(index); } + m_refreshInProgress = false; reply->deleteLater(); @@ -1172,6 +1325,42 @@ void VirtualStudio::getSubscriptions() }); } +void VirtualStudio::getRegions() +{ + QNetworkReply* reply = m_authenticator->get( + QStringLiteral("https://app.jacktrip.org/api/users/%1/regions").arg(m_userId)); + connect(reply, &QNetworkReply::finished, this, [&, reply]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + emit authFailed(); + reply->deleteLater(); + return; + } + + m_regions = QJsonDocument::fromJson(reply->readAll()).object(); + emit regionsChanged(); + reply->deleteLater(); + }); +} + +void VirtualStudio::getUserMetadata() +{ + QNetworkReply* reply = m_authenticator->get( + QStringLiteral("https://app.jacktrip.org/api/users/%1").arg(m_userId)); + connect(reply, &QNetworkReply::finished, this, [&, reply]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + emit authFailed(); + reply->deleteLater(); + return; + } + + m_userMetadata = QJsonDocument::fromJson(reply->readAll()).object(); + emit userMetadataChanged(); + reply->deleteLater(); + }); +} + #ifdef RT_AUDIO void VirtualStudio::getDeviceList(QStringList* list, bool isInput) { @@ -1220,4 +1409,6 @@ VirtualStudio::~VirtualStudio() for (int i = 0; i < m_servers.count(); i++) { delete m_servers.at(i); } + + QDesktopServices::unsetUrlHandler("jacktrip"); } diff --git a/src/gui/virtualstudio.h b/src/gui/virtualstudio.h index d6b2446..0d0d401 100644 --- a/src/gui/virtualstudio.h +++ b/src/gui/virtualstudio.h @@ -46,8 +46,11 @@ #include #include "../JackTrip.h" +#include "vsDevice.h" #include "vsQuickView.h" #include "vsServerInfo.h" +#include "vsUrlHandler.h" +#include "vsWebSocket.h" #ifdef __APPLE__ #include "NoNap.h" @@ -72,11 +75,14 @@ class VirtualStudio : public QObject Q_PROPERTY( int bufferSize READ bufferSize WRITE setBufferSize NOTIFY bufferSizeChanged) Q_PROPERTY(int currentStudio READ currentStudio NOTIFY currentStudioChanged) + 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(QString updateChannel READ updateChannel WRITE setUpdateChannel NOTIFY updateChannelChanged) Q_PROPERTY(float fontScale READ fontScale CONSTANT) @@ -88,6 +94,7 @@ class VirtualStudio : public QObject showWarningsChanged) Q_PROPERTY(bool noUpdater READ noUpdater CONSTANT) Q_PROPERTY(bool psiBuild READ psiBuild CONSTANT) + Q_PROPERTY(QString failedMessage READ failedMessage NOTIFY failedMessageChanged) public: explicit VirtualStudio(bool firstRun = false, QObject* parent = nullptr); @@ -95,6 +102,7 @@ class VirtualStudio : public QObject void setStandardWindow(QSharedPointer window); void show(); + void raiseToTop(); bool showFirstRun(); bool hasRefreshToken(); @@ -110,7 +118,10 @@ class VirtualStudio : public QObject int bufferSize(); void setBufferSize(int index); int currentStudio(); + QJsonObject regions(); + QJsonObject userMetadata(); QString connectionState(); + QJsonObject networkStats(); QString updateChannel(); void setUpdateChannel(const QString& channel); bool showInactive(); @@ -122,12 +133,15 @@ class VirtualStudio : public QObject void setUiScale(float scale); bool darkMode(); void setDarkMode(bool dark); + QUrl studioToJoin(); + void setStudioToJoin(const QUrl& url); bool showDeviceSetup(); void setShowDeviceSetup(bool show); bool showWarnings(); void setShowWarnings(bool show); bool noUpdater(); bool psiBuild(); + QString failedMessage(); public slots: void toStandard(); @@ -143,12 +157,14 @@ class VirtualStudio : public QObject void disconnect(); void manageStudio(int studioIndex); void createStudio(); + void editProfile(); void showAbout(); void exit(); signals: void authSucceeded(); void authFailed(); + void failed(); void connected(); void disconnected(); void refreshFinished(int index); @@ -160,17 +176,22 @@ class VirtualStudio : public QObject void outputDeviceChanged(); void bufferSizeChanged(); void currentStudioChanged(); + void regionsChanged(); + void userMetadataChanged(); void showInactiveChanged(); void showSelfHostedChanged(); void connectionStateChanged(); + void networkStatsChanged(); void updateChannelChanged(); void showDeviceSetupChanged(); void showWarningsChanged(); void uiScaleChanged(); void newScale(); void darkModeChanged(); + void studioToJoinChanged(); void signalExit(); void periodicRefresh(); + void failedMessageChanged(); private slots: void slotAuthSucceded(); @@ -181,12 +202,18 @@ class VirtualStudio : public QObject void checkForHostname(); void endRetryPeriod(); void launchBrowser(const QUrl& url); + void joinStudio(); + void updatedStats(const QJsonObject& stats); private: void setupAuthenticator(); + + void sendHeartbeat(); void getServerList(bool firstLoad = false, int index = -1); void getUserId(); void getSubscriptions(); + void getRegions(); + void getUserMetadata(); #ifdef RT_AUDIO void getDeviceList(QStringList* list, bool isInput); #endif @@ -203,11 +230,13 @@ class VirtualStudio : public QObject QList m_servers; QStringList m_subscribedServers; + QJsonObject m_regions; + QJsonObject m_userMetadata; QString m_logoSection = QStringLiteral("Your Studios"); bool m_selectableBackend = true; bool m_useRtAudio = false; int m_currentStudio = -1; - QString m_connectionState = QStringLiteral("Connecting..."); + QString m_connectionState = QStringLiteral("Waiting"); QScopedPointer m_jackTrip; QTimer m_startTimer; QTimer m_retryPeriodTimer; @@ -220,6 +249,12 @@ class VirtualStudio : public QObject bool m_allowRefresh = true; bool m_refreshInProgress = false; + QJsonObject m_networkStats; + + QTimer m_heartbeatTimer; + VsWebSocket* m_heartbeatWebSocket = NULL; + VsDevice* m_device = NULL; + bool m_onConnectedScreen = false; bool m_isExiting = false; bool m_showInactive = false; @@ -229,7 +264,10 @@ class VirtualStudio : public QObject float m_fontScale = 1; float m_uiScale; float m_previousUiScale; - bool m_darkMode = false; + bool m_darkMode = false; + QString m_failedMessage = ""; + QUrl m_studioToJoin; + bool m_authenticated = false; #ifdef RT_AUDIO QStringList m_inputDeviceList; diff --git a/src/gui/vs.qml b/src/gui/vs.qml index 1465189..2185201 100644 --- a/src/gui/vs.qml +++ b/src/gui/vs.qml @@ -21,6 +21,7 @@ Rectangle { PropertyChanges { target: browseScreen; x: window.width } PropertyChanges { target: settingsScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: failedScreen; x: window.width } }, State { @@ -31,6 +32,7 @@ Rectangle { PropertyChanges { target: browseScreen; x: window.width } PropertyChanges { target: settingsScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: failedScreen; x: window.width } }, State { @@ -41,6 +43,7 @@ Rectangle { PropertyChanges { target: browseScreen; x: window.width } PropertyChanges { target: settingsScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: failedScreen; x: window.width } }, State { @@ -51,6 +54,7 @@ Rectangle { PropertyChanges { target: browseScreen; x: 0 } PropertyChanges { target: settingsScreen; x: window.width } PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: failedScreen; x: window.width } }, State { @@ -61,6 +65,7 @@ Rectangle { PropertyChanges { target: browseScreen; x: -browseScreen.width } PropertyChanges { target: settingsScreen; x: 0 } PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: failedScreen; x: window.width } }, State { @@ -71,6 +76,18 @@ Rectangle { PropertyChanges { target: browseScreen; x: -browseScreen.width } PropertyChanges { target: settingsScreen; x: window.width } PropertyChanges { target: connectedScreen; 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: setupScreen; x: -setupScreen.width } + PropertyChanges { target: browseScreen; x: -browseScreen.width } + PropertyChanges { target: settingsScreen; x: window.width } + PropertyChanges { target: connectedScreen; x: window.width } + PropertyChanges { target: failedScreen; x: 0 } } ] @@ -102,6 +119,10 @@ Rectangle { id: connectedScreen } + Failed { + id: failedScreen + } + Connections { target: virtualstudio onAuthSucceeded: { @@ -114,7 +135,12 @@ Rectangle { onAuthFailed: { loginScreen.failTextVisible = true; } - // onConnected: { } + onConnected: { + window.state = "connected"; + } + onFailed: { + window.state = "failed"; + } onDisconnected: { window.state = "browse"; } diff --git a/src/gui/vsDevice.cpp b/src/gui/vsDevice.cpp new file mode 100644 index 0000000..cc4a673 --- /dev/null +++ b/src/gui/vsDevice.cpp @@ -0,0 +1,487 @@ +//***************************************************************** +/* + 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(QOAuth2AuthorizationCodeFlow* authenticator, QObject* parent) + : QObject(parent), m_authenticator(authenticator) +{ + 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(); + + sendHeartbeat(); +} + +// 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_authenticator->get( + QStringLiteral("https://app.jacktrip.org/api/devices/%1").arg(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; + // TODO: Fix me + // emit authFailed(); + 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; + // TODO: Fix me + // emit authFailed(); + reply->deleteLater(); + return; + } + } + + 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(); + + reply->deleteLater(); + }); +} + +// removeApp deletes the emulated device +void VsDevice::removeApp() +{ + if (m_appID == "") { + return; + } + + QNetworkReply* reply = m_authenticator->deleteResource( + QStringLiteral("https://app.jacktrip.org/api/devices/%1").arg(m_appID)); + connect(reply, &QNetworkReply::finished, this, [=]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + // TODO: Fix me + // emit authFailed(); + reply->deleteLater(); + return; + } 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() +{ + if (m_webSocket == nullptr) { + m_webSocket = new VsWebSocket( + QUrl(QStringLiteral("wss://app.jacktrip.org/api/devices/%1/heartbeat") + .arg(m_appID)), + m_authenticator->token(), m_apiPrefix, m_apiSecret); + connect(m_webSocket, &VsWebSocket::textMessageReceived, this, + &VsDevice::onTextMessageReceived); + } + + if (enabled()) { + // When the device is connected to a server, use the underlying wss connection + if (!m_webSocket->isValid()) { + m_webSocket->openSocket(); + } + } else { + // When the device is not connected to a server, use the standard API + m_webSocket->closeSocket(); + } + + 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 != nullptr) { + 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)); + + // 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); + emit updateNetworkStats(pingStats); + } + + QJsonDocument request = QJsonDocument(json); + + if (m_webSocket->isValid()) { + // Send heartbeat via websocket + m_webSocket->sendMessage(request.toJson()); + } else { + // Send heartbeat via POST API + QNetworkReply* reply = m_authenticator->post( + QStringLiteral("https://app.jacktrip.org/api/devices/%1/heartbeat") + .arg(m_appID), + request.toJson()); + connect(reply, &QNetworkReply::finished, this, [=]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + // TODO: Fix me + // emit authFailed(); + reply->deleteLater(); + return; + } else { + QJsonDocument response = QJsonDocument::fromJson(reply->readAll()); + reconcileAgentConfig(response); + } + + reply->deleteLater(); + }); + } +} + +// setServerId updates the emulated device with the provided serverId +void VsDevice::setServerId(QString serverId) +{ + QJsonObject json = { + {QLatin1String("serverId"), serverId}, + }; + QJsonDocument request = QJsonDocument(json); + QNetworkReply* reply = m_authenticator->put( + QStringLiteral("https://app.jacktrip.org/api/devices/%1").arg(m_appID), + request.toJson()); + connect(reply, &QNetworkReply::finished, this, [=]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + // TODO: Fix me + // emit authFailed(); + reply->deleteLater(); + return; + } + 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 bufferSize, + VsServerInfo* studioInfo) +{ + m_jackTrip.reset(new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, 2, 2, +#ifdef WAIR // wair + 0, +#endif // endwhere + 4, 1)); + m_jackTrip->setConnectDefaultAudioPorts(true); +#ifdef RT_AUDIO + if (useRtAudio) { + m_jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO); + m_jackTrip->setSampleRate(studioInfo->sampleRate()); + m_jackTrip->setAudioBufferSizeInSamples(bufferSize); + m_jackTrip->setInputDevice(input); + m_jackTrip->setOutputDevice(output); + } +#endif + m_jackTrip->setRemoteClientName(m_appID); + m_jackTrip->setBufferStrategy(1); + m_jackTrip->setBufferQueueLength(-500); + m_jackTrip->setPeerAddress(studioInfo->host()); + m_jackTrip->setPeerPorts(studioInfo->port()); + m_jackTrip->setPeerHandshakePort(studioInfo->port()); + + QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this, + &VsDevice::terminateJackTrip, Qt::QueuedConnection); + QObject::connect(m_jackTrip.data(), &JackTrip::signalError, this, + &VsDevice::terminateJackTrip, Qt::QueuedConnection); + + return m_jackTrip.data(); +} + +// startJackTrip starts the current jacktrip process if applicable +void VsDevice::startJackTrip() +{ + 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 + } +} + +// stopJackTrip stops the current jacktrip process if applicable +void VsDevice::stopJackTrip() +{ + if (!m_jackTrip.isNull()) { + setServerId(""); + m_jackTrip->stop(); + } +} + +// 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++) { + m_deviceAgentConfig.insert(it.key(), it.value()); + } + if (!enabled() && !m_jackTrip.isNull()) { + stopJackTrip(); + } +} + +// initPinger intializes the pinger used to generate network latency statistics for +// Virtual Studio +VsPinger* VsDevice::startPinger(VsServerInfo* studioInfo) +{ + QString id = studioInfo->id(); + QString host = studioInfo->sessionId(); + host.append(QString::fromStdString(".jacktrip.cloud")); + + m_pinger = new VsPinger(QString::fromStdString("wss"), host, + QString::fromStdString("/ping")); + + return m_pinger; +} + +// stopPinger stops the Virtual Studio pinger +void VsDevice::stopPinger() +{ + if (m_pinger != nullptr) { + m_pinger->stop(); + m_pinger->unsetToken(); + } +} + +// terminateJackTrip is a slot intended to be triggered on jacktrip process signals +void VsDevice::terminateJackTrip() +{ + if (!enabled()) { + setServerId(""); + } + m_jackTrip.reset(); +} + +// 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()); + + // 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 != nullptr && !m_pinger->active()) { + m_pinger->setToken(token); + m_pinger->start(); + } + + reconcileAgentConfig(newState); +} + +// 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_authenticator->post( + QStringLiteral("https://app.jacktrip.org/api/devices"), request.toJson()); + connect(reply, &QNetworkReply::finished, this, [=]() { + if (reply->error() != QNetworkReply::NoError) { + std::cout << "Error: " << reply->errorString().toStdString() << std::endl; + // TODO: Fix me + // emit authFailed(); + reply->deleteLater(); + return; + } else { + QJsonDocument response = QJsonDocument::fromJson(reply->readAll()); + + m_appID = response.object()[QStringLiteral("id")].toString(); + QSettings settings; + settings.beginGroup(QStringLiteral("VirtualStudio")); + settings.setValue(QStringLiteral("AppID"), m_appID); + settings.endGroup(); + } + + 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; +} diff --git a/src/gui/vsDevice.h b/src/gui/vsDevice.h new file mode 100644 index 0000000..bab61c7 --- /dev/null +++ b/src/gui/vsDevice.h @@ -0,0 +1,102 @@ +//***************************************************************** +/* + 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 "../JackTrip.h" +#include "../jacktrip_globals.h" +#include "vsPinger.h" +#include "vsServerInfo.h" +#include "vsWebSocket.h" + +class VsDevice : public QObject +{ + Q_OBJECT + + public: + // Constructor + explicit VsDevice(QOAuth2AuthorizationCodeFlow* authenticator, + QObject* parent = nullptr); + + // Public functions + void registerApp(); + void removeApp(); + void sendHeartbeat(); + void setServerId(QString studioID); + JackTrip* initJackTrip(bool useRtAudio, std::string input, std::string output, + int bufferSize, VsServerInfo* studioInfo); + void startJackTrip(); + void stopJackTrip(); + void reconcileAgentConfig(QJsonDocument newState); + + VsPinger* startPinger(VsServerInfo* studioInfo); + void stopPinger(); + + signals: + void updateNetworkStats(QJsonObject stats); + + private slots: + void terminateJackTrip(); + void onTextMessageReceived(const QString& message); + + private: + void registerJTAsDevice(); + bool enabled(); + QString randomString(int stringLength); + + VsPinger* m_pinger = NULL; + + QString m_appID; + QString m_appUUID; + QString m_token; + QString m_apiPrefix; + QString m_apiSecret; + QJsonObject m_deviceAgentConfig; + VsWebSocket* m_webSocket = NULL; + QScopedPointer m_jackTrip; + QOAuth2AuthorizationCodeFlow* m_authenticator; + QRandomGenerator m_randomizer; +}; + +#endif // VSDEVICE_H diff --git a/src/gui/vsPing.cpp b/src/gui/vsPing.cpp new file mode 100644 index 0000000..2d1ec56 --- /dev/null +++ b/src/gui/vsPing.cpp @@ -0,0 +1,82 @@ +//***************************************************************** +/* + 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 new file mode 100644 index 0000000..8265b50 --- /dev/null +++ b/src/gui/vsPing.h @@ -0,0 +1,83 @@ +//***************************************************************** +/* + 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); + 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 new file mode 100644 index 0000000..8e30df3 --- /dev/null +++ b/src/gui/vsPinger.cpp @@ -0,0 +1,311 @@ +//***************************************************************** +/* + 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); + connect(&mSocket, QOverload::of(&QWebSocket::error), + this, &VsPinger::onError); + 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 new file mode 100644 index 0000000..0f689f2 --- /dev/null +++ b/src/gui/vsPinger.h @@ -0,0 +1,121 @@ +//***************************************************************** +/* + 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); + 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/vsQuickView.cpp b/src/gui/vsQuickView.cpp index 791104f..e0a9281 100644 --- a/src/gui/vsQuickView.cpp +++ b/src/gui/vsQuickView.cpp @@ -37,11 +37,32 @@ #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) { + 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 index bb9ad78..ab80a73 100644 --- a/src/gui/vsQuickView.h +++ b/src/gui/vsQuickView.h @@ -39,17 +39,26 @@ #define VSQUICKVIEW_H #include +#ifdef Q_OS_MACOS +#include +#include +#include +#include +#endif class VsQuickView : public QQuickView { Q_OBJECT public: - VsQuickView(QWindow* parent = nullptr) : QQuickView(parent) {} + 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 index ec9a761..c01a4b8 100644 --- a/src/gui/vsServerInfo.cpp +++ b/src/gui/vsServerInfo.cpp @@ -147,9 +147,6 @@ QString VsServerInfo::flag() QString VsServerInfo::location() { - if (m_region.split(QStringLiteral("-")).count() > 2) { - return m_region.section(QStringLiteral("-"), 2); - } return m_region; } @@ -198,6 +195,16 @@ void VsServerInfo::setQueueBuffer(quint16 queueBuffer) m_queueBuffer = queueBuffer; } +QString VsServerInfo::bannerURL() +{ + return m_bannerURL; +} + +void VsServerInfo::setBannerURL(const QString& bannerURL) +{ + m_bannerURL = bannerURL; +} + QString VsServerInfo::id() { return m_id; @@ -208,4 +215,14 @@ void VsServerInfo::setId(const QString& id) m_id = id; } +QString VsServerInfo::sessionId() +{ + return m_sessionId; +} + +void VsServerInfo::setSessionId(const QString& sessionId) +{ + m_sessionId = sessionId; +} + VsServerInfo::~VsServerInfo() = default; diff --git a/src/gui/vsServerInfo.h b/src/gui/vsServerInfo.h index fecb849..642bc6d 100644 --- a/src/gui/vsServerInfo.h +++ b/src/gui/vsServerInfo.h @@ -52,6 +52,7 @@ class VsServerInfo : public QObject // 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 isManageable READ isManageable CONSTANT) Q_PROPERTY(quint16 period READ period CONSTANT) @@ -90,8 +91,12 @@ class VsServerInfo : public QObject void setSampleRate(quint32 sampleRate); quint16 queueBuffer(); void setQueueBuffer(quint16 queueBuffer); + QString bannerURL(); + void setBannerURL(const QString& bannerURL); QString id(); void setId(const QString& id); + QString sessionId(); + void setSessionId(const QString& sessionId); QString status(); void setStatus(const QString& status); @@ -109,7 +114,9 @@ class VsServerInfo : public QObject quint16 m_period; quint32 m_sampleRate; quint16 m_queueBuffer; + QString m_bannerURL; QString m_id; + QString m_sessionId; QString m_status; /* Remaining JSON fields @@ -126,7 +133,6 @@ class VsServerInfo : public QObject "owner": true, "ownerId": "string", "status": "Ready", - "sessionId": "1636042722abcdefg", "subStatus": "Active", "createdAt": "2021-09-07T17:15:38Z", "expiresAt": "2021-09-07T17:15:38Z", diff --git a/src/gui/vsUrlHandler.cpp b/src/gui/vsUrlHandler.cpp new file mode 100644 index 0000000..8de8a0f --- /dev/null +++ b/src/gui/vsUrlHandler.cpp @@ -0,0 +1,46 @@ +//***************************************************************** +/* + 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 vsUrlHandler.cpp + * \author Matt Horton + * \date June 2022 + */ + +#include "vsUrlHandler.h" + +#include +#include + +void VsUrlHandler::handleUrl(const QUrl& url) +{ + emit joinUrlClicked(url); +} \ No newline at end of file diff --git a/src/gui/vsUrlHandler.h b/src/gui/vsUrlHandler.h new file mode 100644 index 0000000..1e85ff8 --- /dev/null +++ b/src/gui/vsUrlHandler.h @@ -0,0 +1,58 @@ +//***************************************************************** +/* + 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 vsUrlHandler.h + * \author Matt Horton + * \date June 2022 + */ + +#ifndef VSURLHANDLER_H +#define VSURLHANDLER_H + +#include +#include +#include +#include +#include + +class VsUrlHandler : public QObject +{ + Q_OBJECT + + signals: + void joinUrlClicked(const QUrl& url); + + public slots: + void handleUrl(const QUrl& url); +}; + +#endif // VSURLHANDLER_H diff --git a/src/gui/vsWebSocket.cpp b/src/gui/vsWebSocket.cpp new file mode 100644 index 0000000..f099b8f --- /dev/null +++ b/src/gui/vsWebSocket.cpp @@ -0,0 +1,123 @@ +//***************************************************************** +/* + 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) +{ + connect(&m_webSocket, &QWebSocket::connected, this, &VsWebSocket::onConnected); + connect(&m_webSocket, &QWebSocket::disconnected, this, &VsWebSocket::onClosed); + connect(&m_webSocket, QOverload&>::of(&QWebSocket::sslErrors), + this, &VsWebSocket::onSslErrors); + connect(&m_webSocket, QOverload::of(&QWebSocket::error), + this, &VsWebSocket::onError); + connect(&m_webSocket, &QWebSocket::textMessageReceived, this, + &VsWebSocket::textMessageReceived); +} + +void VsWebSocket::openSocket() +{ + if (m_connected) { + 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("https://app.jacktrip.org")); + req.setRawHeader(QByteArray("APIPrefix"), m_apiPrefix.toUtf8()); + req.setRawHeader(QByteArray("APISecret"), m_apiSecret.toUtf8()); + + m_webSocket.open(req); +} + +void VsWebSocket::closeSocket() +{ + if (m_connected) { + m_webSocket.close(); + } +} + +// Fires when connected to websocket +void VsWebSocket::onConnected() +{ + m_connected = true; + m_error = false; +} + +// Fires when disconnected from websocket +void VsWebSocket::onClosed() +{ + m_connected = false; +} + +void VsWebSocket::onError(QAbstractSocket::SocketError error) +{ + // qDebug() << error; + m_error = true; +} + +void VsWebSocket::onSslErrors(const QList& errors) +{ + for (int i = 0; i < errors.size(); ++i) { + // qDebug() << errors.at(i); + } + m_error = true; +} + +void VsWebSocket::sendMessage(const QByteArray& message) +{ + m_webSocket.sendBinaryMessage(message); +} + +bool VsWebSocket::isValid() +{ + return !m_error && m_connected; +} diff --git a/src/gui/vsWebSocket.h b/src/gui/vsWebSocket.h new file mode 100644 index 0000000..4581355 --- /dev/null +++ b/src/gui/vsWebSocket.h @@ -0,0 +1,82 @@ +//***************************************************************** +/* + 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 + +class VsWebSocket : public QObject +{ + Q_OBJECT + + public: + // Constructor + explicit VsWebSocket(const QUrl& url, QString token, QString apiPrefix, + QString apiSecret, QObject* parent = nullptr); + + // Public functions + void openSocket(); + void closeSocket(); + void sendMessage(const QByteArray& message); + bool isValid(); + + signals: + void textMessageReceived(const QString& message); + + private slots: + void onConnected(); + void onClosed(); + void onError(QAbstractSocket::SocketError error); + void onSslErrors(const QList& errors); + + private: + QWebSocket m_webSocket; + QUrl m_url; + bool m_connected = false; + bool m_error = false; + QString m_token; + QString m_apiPrefix; + QString m_apiSecret; +}; + +#endif // VSWEBSOCKET_H diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h index c20c196..eb27960 100644 --- a/src/jacktrip_globals.h +++ b/src/jacktrip_globals.h @@ -40,7 +40,7 @@ #include "AudioInterface.h" -constexpr const char* const gVersion = "1.6.1"; ///< JackTrip version +constexpr const char* const gVersion = "1.6.2"; ///< JackTrip version //******************************************************************************* /// \name Default Values diff --git a/src/main.cpp b/src/main.cpp index ebe553b..2c11e33 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,10 +45,18 @@ #endif #ifndef NO_VS +#include +#include +#include +#include +#include #include #include +#include +#include "JTApplication.h" #include "gui/virtualstudio.h" +#include "gui/vsUrlHandler.h" #endif #include "gui/qjacktrip.h" @@ -70,11 +78,19 @@ #include #endif +#ifndef NO_GUI +#ifndef NO_VS +static QTextStream* ts; +static QFile outFile; +#endif // NO_VS +#endif // NO_GUI + QCoreApplication* createApplication(int& argc, char* argv[]) { // Check for some specific, GUI related command line options. bool forceGui = false; for (int i = 1; i < argc; i++) { + std::cout << argv[i] << std::endl; if (strcmp(argv[i], "--gui") == 0) { forceGui = true; } else if (strcmp(argv[i], "--test-gui") == 0) { @@ -121,9 +137,25 @@ QCoreApplication* createApplication(int& argc, char* argv[]) std::exit(1); } #endif +#if defined(Q_OS_MACOS) && !defined(NO_VS) + // Turn on high DPI support. + JTApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + // 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 + return new JTApplication(argc, argv); +#else // Turn on high DPI support. QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + // 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 return new QApplication(argc, argv); +#endif // Q_OS_MACOS #endif // NO_GUI } else { return new QCoreApplication(argc, argv); @@ -135,6 +167,16 @@ void qtMessageHandler([[maybe_unused]] QtMsgType type, const QString& msg) { std::cerr << msg.toStdString() << std::endl; +#ifndef NO_GUI +#ifndef NO_VS + // Writes to file in order to debug bundles and executables +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + *ts << msg << Qt::endl; +#else + *ts << msg << endl; +#endif // QT_VERSION > 5.14.0 +#endif // NO_VS +#endif // NO_GUI } #ifndef _WIN32 @@ -234,10 +276,19 @@ int main(int argc, char* argv[]) QSharedPointer window; #ifndef NO_VS + QString deeplink = QLatin1String(""); QSharedPointer vs; +#ifdef _WIN32 + QSharedPointer instanceServer; + QSharedPointer instanceCheckSocket; +#endif #endif +#if defined(Q_OS_MACOS) && !defined(NO_VS) + 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 @@ -261,14 +312,128 @@ int main(int argc, char* argv[]) } #ifndef NO_VS + // Parse command line for deep link + QCommandLineOption deeplinkOption(QStringList() << QStringLiteral("deeplink")); + deeplinkOption.setValueName(QStringLiteral("deeplink")); + parser.addOption(deeplinkOption); + parser.parse(app->arguments()); + if (parser.isSet(deeplinkOption)) { + deeplink = parser.value(deeplinkOption); + } + // Check if we need to show our first run window. QSettings settings; int uiMode = settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt(); +#ifndef __unix__ QString updateChannel = settings.value(QStringLiteral("UpdateChannel"), "stable") .toString() .toLower(); -#endif // NO_VS +#endif +#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(); + + // Create socket + instanceCheckSocket = + QSharedPointer::create(new QLocalSocket(app.data())); + // End process if instance exists + QObject::connect( + instanceCheckSocket.data(), &QLocalSocket::connected, app.data(), + [&]() { + // pass deeplink to existing instance before quitting + if (!deeplink.isEmpty()) { + QByteArray baDeeplink = deeplink.toLocal8Bit(); + qint64 writeBytes = instanceCheckSocket->write(baDeeplink); + instanceCheckSocket->flush(); + instanceCheckSocket->disconnectFromServer(); // remove next + + if (writeBytes < 0) { + qDebug() << "sending deeplink failed"; + } + } + emit QCoreApplication::quit(); + }, + Qt::QueuedConnection); + // Create instanceServer to prevent new instances from being created + void (QLocalSocket::*errorFunc)(QLocalSocket::LocalSocketError); +#ifdef Q_OS_LINUX + errorFunc = &QLocalSocket::error; +#else + errorFunc = &QLocalSocket::errorOccurred; +#endif + QObject::connect( + instanceCheckSocket.data(), errorFunc, app.data(), + [&](QLocalSocket::LocalSocketError socketError) { + switch (socketError) { + case QLocalSocket::ServerNotFoundError: + case QLocalSocket::SocketTimeoutError: + case QLocalSocket::ConnectionRefusedError: + instanceServer = QSharedPointer::create( + new QLocalServer(app.data())); + instanceServer->setSocketOptions(QLocalServer::WorldAccessOption); + instanceServer->listen("jacktripExists"); + QObject::connect( + instanceServer.data(), &QLocalServer::newConnection, app.data(), + [&]() { + // This is the first instance. Bring it to the + // top. + vs->raiseToTop(); + while (instanceServer->hasPendingConnections()) { + // Receive URL from 2nd instance + QLocalSocket* connectedSocket = + instanceServer->nextPendingConnection(); + + if (!connectedSocket->waitForConnected()) { + qDebug() << "Never received connection"; + return; + } + + if (!connectedSocket->waitForReadyRead()) { + qDebug() << "Never ready to read"; + return; + } + + if (connectedSocket->bytesAvailable() + < (int)sizeof(quint16)) { + qDebug() << "no bytes available"; + break; + } + + QByteArray in(connectedSocket->readAll()); + QString urlString(in); + QUrl url(urlString); + + // Join studio using received URL + if (url.scheme() == "jacktrip" && url.host() == "join") { + vs->setStudioToJoin(url); + } + } + }, + Qt::QueuedConnection); + break; + case QLocalSocket::PeerClosedError: + break; + default: + qDebug() << instanceCheckSocket->errorString(); + } + }); + // Check for existing instance + instanceCheckSocket->connectToServer("jacktripExists"); + +#endif // _WIN32 + window.reset(new QJackTrip(argc, !deeplink.isEmpty())); +#else window.reset(new QJackTrip(argc)); +#endif // NO_VS QObject::connect(window.data(), &QJackTrip::signalExit, app.data(), &QCoreApplication::quit, Qt::QueuedConnection); #ifndef NO_VS @@ -278,6 +443,19 @@ int main(int argc, char* argv[]) vs->setStandardWindow(window); window->setVs(vs); + VsUrlHandler* m_urlHandler = new VsUrlHandler(); + QDesktopServices::setUrlHandler(QStringLiteral("jacktrip"), m_urlHandler, + "handleUrl"); + QObject::connect(m_urlHandler, &VsUrlHandler::joinUrlClicked, vs.data(), + [&](const QUrl& url) { + if (url.scheme() == QLatin1String("jacktrip") + && url.host() == QLatin1String("join")) { + vs->setStudioToJoin(url); + } + }); + // Open with any command line-passed url + QDesktopServices::openUrl(QUrl(deeplink)); + if (uiMode == QJackTrip::UNSET) { vs->show(); } else if (uiMode == QJackTrip::VIRTUAL_STUDIO) { @@ -285,6 +463,22 @@ int main(int argc, char* argv[]) } 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 -- 2.30.2