New upstream version 1.6.2+ds0
authorIOhannes m zmölnig <zmoelnig@umlautS.umlaeute.mur.at>
Fri, 19 Aug 2022 10:15:25 +0000 (12:15 +0200)
committerIOhannes m zmölnig <zmoelnig@umlautS.umlaeute.mur.at>
Fri, 19 Aug 2022 10:15:25 +0000 (12:15 +0200)
50 files changed:
.clang-tidy-ignore [new file with mode: 0644]
CMakeLists.txt
docs/changelog.yml
jacktrip.pro
linux/org.jacktrip.JackTrip.desktop.in
macos/Info_novs.plist [new file with mode: 0644]
macos/JackTrip.app_template/Contents/Info.plist
macos/assemble_app.sh
meson.build
releases/edge/mac-manifests.json
releases/edge/win-manifests.json
releases/stable/mac-manifests.json
releases/stable/win-manifests.json
rtaudio.pro
src/JTApplication.h [new file with mode: 0644]
src/JackTrip.cpp
src/RtAudioInterface.cpp
src/UdpDataProtocol.cpp
src/UdpDataProtocol.h
src/gui/Browse.qml
src/gui/Connected.qml
src/gui/Failed.qml [new file with mode: 0644]
src/gui/Settings.qml
src/gui/Setup.qml
src/gui/Studio.qml
src/gui/manage.svg [new file with mode: 0644]
src/gui/network.svg [new file with mode: 0644]
src/gui/ohno.png [new file with mode: 0644]
src/gui/qjacktrip.cpp
src/gui/qjacktrip.h
src/gui/qjacktrip.qrc
src/gui/virtualstudio.cpp
src/gui/virtualstudio.h
src/gui/vs.qml
src/gui/vsDevice.cpp [new file with mode: 0644]
src/gui/vsDevice.h [new file with mode: 0644]
src/gui/vsPing.cpp [new file with mode: 0644]
src/gui/vsPing.h [new file with mode: 0644]
src/gui/vsPinger.cpp [new file with mode: 0644]
src/gui/vsPinger.h [new file with mode: 0644]
src/gui/vsQuickView.cpp
src/gui/vsQuickView.h
src/gui/vsServerInfo.cpp
src/gui/vsServerInfo.h
src/gui/vsUrlHandler.cpp [new file with mode: 0644]
src/gui/vsUrlHandler.h [new file with mode: 0644]
src/gui/vsWebSocket.cpp [new file with mode: 0644]
src/gui/vsWebSocket.h [new file with mode: 0644]
src/jacktrip_globals.h
src/main.cpp

diff --git a/.clang-tidy-ignore b/.clang-tidy-ignore
new file mode 100644 (file)
index 0000000..0cb64dc
--- /dev/null
@@ -0,0 +1,4 @@
+subprojects
+externals
+documentation
+src/*dsp.h
\ No newline at end of file
index 3fb57389eabfe6e8933075812b6e2fa1a0f681b1..8673486e972e741ac2f59f88451c03f1a43caaca 100644 (file)
@@ -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 ()
 
index be39c91a3f2e5b0743a9de341433492c572579db..4a83d87b176a421240c421c90234e6afc2a2ed9a 100644 (file)
@@ -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:
index 106e37d4e8836091700a0014ab2ff7b1f2c74d14..15cda35dff56bcd693b126e35387fb7b545e663d 100644 (file)
@@ -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
   }
 }
index 2b77704821d25af482e5a8b4b7006f85997371df..c34033c0d3d1befb212e6f9f96bd6b2361a5515e 100644 (file)
@@ -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 (file)
index 0000000..0ca37fd
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>BuildMachineOSBuild</key>
+       <string>19E287</string>
+       <key>CFBundleDevelopmentRegion</key>
+       <string>en</string>
+       <key>CFBundleExecutable</key>
+       <string>jacktrip</string>
+       <key>CFBundleIconFile</key>
+       <string>jacktrip</string>
+       <key>CFBundleIdentifier</key>
+       <string>%BUNDLEID%</string>
+       <key>CFBundleInfoDictionaryVersion</key>
+       <string>6.0</string>
+       <key>CFBundleName</key>
+       <string>%BUNDLENAME%</string>
+       <key>CFBundlePackageType</key>
+       <string>APPL</string>
+       <key>CFBundleShortVersionString</key>
+       <string>%VERSION%</string>
+       <key>CFBundleSignature</key>
+       <string>????</string>
+       <key>CFBundleSupportedPlatforms</key>
+       <array>
+               <string>MacOSX</string>
+       </array>
+       <key>CFBundleVersion</key>
+       <string>%VERSION%</string>
+       <key>DTCompiler</key>
+       <string>com.apple.compilers.llvm.clang.1_0</string>
+       <key>DTPlatformBuild</key>
+       <string>11E503a</string>
+       <key>DTPlatformVersion</key>
+       <string>GM</string>
+       <key>DTSDKBuild</key>
+       <string>19E258</string>
+       <key>DTSDKName</key>
+       <string>macosx10.15</string>
+       <key>DTXcode</key>
+       <string>1141</string>
+       <key>DTXcodeBuild</key>
+       <string>11E503a</string>
+       <key>LSMinimumSystemVersion</key>
+       <string>10.13</string>
+       <key>NSHighResolutionCapable</key>
+       <true/>
+       <key>NSHumanReadableCopyright</key>
+       <string>Copyright © 2020 Juan-Pablo Caceres, Chris Chafe, Aaron Wyatt, et al. All rights reserved.</string>
+       <key>NSMicrophoneUsageDescription</key>
+       <string>This app requires microphone access to allow the jack server to capture audio.</string>
+</dict>
+</plist>
index 0ca37fd1b90682f086d02836e29389ac1237f821..99843e7cd945b87b440122b0ec54148c61cd1306 100644 (file)
        <string>6.0</string>
        <key>CFBundleName</key>
        <string>%BUNDLENAME%</string>
+       <key>CFBundleURLTypes</key>
+               <array>
+                       <dict>
+                               <key>CFBundleURLSchemes</key>
+                                       <array>
+                                               <string>jacktrip</string>
+                                       </array>
+                               <key>CFBundleTypeRole</key>
+                               <string>Editor</string>
+                       </dict>
+               </array>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleShortVersionString</key>
index 6318bb7a04e8b902e84db40116bb570ce95563e9..bf9324489513cf5589df3f81e915beda16c5be0f 100755 (executable)
@@ -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
index 4c36da9ec333c1caa9a3f0711acdd4064c842dd1..af9e535dfe98a4fcc250a28aa65ddc48d04a519a 100644 (file)
@@ -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 += [
index 97de59d0b34e3211396b4ea237cd0a4eb08e8a75..5fa65e2c50654427937c66c3d69ff472a599af68 100644 (file)
@@ -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",
index 59b8186e7920dabc8bf4e404a58626619deb838f..24718775113eec99f24fd05948892e5870e3cd29 100644 (file)
@@ -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",
index 0faec7cbda961abcf6cb3a5214913484cca4a051..2bcad7b929ac14cb568ac681c172de3abb10a433 100644 (file)
@@ -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",
index cb255c03956b073456448bb208e7fd53d7d8cf73..42b0d70ff7caa3381b85188b6e899c838a6b4a60 100644 (file)
@@ -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",
index 6934b19bd36425b5f0d9a74b4e0517465850fa58..51586bebc1cebcae0854c7f0698f57eff59a10b6 100644 (file)
@@ -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 (file)
index 0000000..7d27b52
--- /dev/null
@@ -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 <QApplication>
+#include <QDebug>
+#include <QDesktopServices>
+#include <QEvent>
+#include <QFileOpenEvent>
+#include <QObject>
+
+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<QFileOpenEvent*>(event);
+
+            QDesktopServices::openUrl(openEvent->url());
+        }
+        return QApplication::event(event);
+    }
+};
+
+#endif  // JTAPPLICATION_H
index a5475bb8f84f7eb91a9dd040173f08d5e59847a3..5031bebd27b02577f08ca5b5991670c558d5a822 100644 (file)
@@ -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();
index a40c1e1f9ee0ebaf9f81680ee9427645f03718c5..0b221c237311ec1e7f402ffa559322e62d4c15e7 100644 (file)
@@ -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
index 43d5df8e1faa9ff4b2e47975fb08069b0ddab68b..bf1e4c3dde2d50ae27f309195f1ab38230ad75a4 100644 (file)
 #include "jacktrip_globals.h"
 #ifdef _WIN32
 //#include <winsock.h>
+#include <stdio.h>
 #include <winsock2.h>  //cc need SD_SEND
+#pragma comment(lib, "ws2_32.lib")
+#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
 #else
 #include <fcntl.h>
 #include <sys/socket.h>  // 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()
index d3ffbe66bc4314f3b2aea94d80387855399abdc3..a49fda12cf5d28d9edb532db724c93ed8c658267 100644 (file)
@@ -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
      */
index 1eea79f4ea27974ab18afb333e69852bca2e14fa..c4558ff954285c1a33acadb7a9fe346e0cadd76f 100644 (file)
@@ -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
         }
     }
     
index b449c46215c0d6a94b525bcced2beda1148cde1a..29ffbe51a79e37394ceccdec5a94437dcf63367d 100644 (file)
@@ -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] = "<b>" + minRtt + " ms - " + maxRtt + " ms</b>, 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 <b>" + quality + "</b>."
+        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 (file)
index 0000000..8eb4a61
--- /dev/null
@@ -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
+    }
+}
index 0ba2ed852a4d0b8ecd6c028c14fb363572a782dc..02a6f3d05625b80ab0cd9d54d980ae345df2760a 100644 (file)
@@ -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
index d7f53e6bc479cd2ac3369c77e3d5e25c0e0000a2..7a27a20689204d963b2fac2f142e04d23af1a94e 100644 (file)
@@ -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
index f2004f55ef20e409a5554a68499a6c658d6554c9..b087cc58e0ba34def8a5cffe22912ceee8d74f3f 100644 (file)
@@ -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 (file)
index 0000000..7d9ae31
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
\ No newline at end of file
diff --git a/src/gui/network.svg b/src/gui/network.svg
new file mode 100644 (file)
index 0000000..4aa8827
--- /dev/null
@@ -0,0 +1,4 @@
+<svg width="624" height="750" viewBox="0 0 624 750" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M614.44 570.333C601.997 557.891 583.331 557.891 570.883 570.333L499.331 641.891V32.104C499.331 14.9947 485.331 0.99469 468.221 0.99469C451.112 0.99469 437.112 14.9947 437.112 32.104V641.877L365.555 570.32C353.112 557.877 334.445 557.877 321.997 570.32C309.555 582.763 309.555 601.429 321.997 613.877L446.44 738.32C447.997 739.877 449.549 741.429 451.107 742.987L452.664 744.544C454.221 744.544 454.221 746.101 455.773 746.101C457.331 746.101 457.331 746.101 458.883 747.659C460.44 747.659 460.44 747.659 461.992 749.216H468.216H474.44C475.997 749.216 475.997 749.216 477.549 747.659C479.107 747.659 479.107 747.659 480.659 746.101C482.216 746.101 482.216 744.544 483.768 744.544C483.768 744.544 485.325 744.544 485.325 742.987C486.883 741.429 488.435 739.877 489.992 738.32L614.435 613.877C626.888 601.435 626.888 582.768 614.44 570.325L614.44 570.333Z" fill="black"/>
+<path d="M303.333 134.773L174.224 5.664C172.667 5.664 172.667 4.10667 171.115 4.10667C169.557 4.10667 169.557 2.54934 168.005 2.54934C166.448 2.54934 166.448 2.54934 164.896 0.992004H161.787C158.667 0.997213 155.557 0.997213 150.891 0.997213H147.781C146.224 0.997213 146.224 0.997213 144.672 2.55455C143.115 2.55455 143.115 4.11188 141.563 4.11188C140.005 4.11188 140.005 5.66921 138.453 5.66921L9.34413 134.779C-3.09854 147.221 -3.09854 165.888 9.34413 178.336C21.7868 190.779 40.4535 190.779 52.9015 178.336L126 106.773V716.56C126 733.669 140 747.669 157.109 747.669C174.219 747.669 188.219 733.669 188.219 716.56L188.224 106.773L259.781 178.331C266 184.555 273.776 187.664 281.557 187.664C289.333 187.664 297.115 184.555 303.333 178.331C315.776 165.888 315.776 147.221 303.333 134.773V134.773Z" fill="black"/>
+</svg>
diff --git a/src/gui/ohno.png b/src/gui/ohno.png
new file mode 100644 (file)
index 0000000..7d118f3
Binary files /dev/null and b/src/gui/ohno.png differ
index b5a5c3f6850ea36039f73ca3fcb58ed3274ce850..c685a7d6b0b63be83f580310a0411a4804522d64 100644 (file)
@@ -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 "
index 3410d651eb04e11231160adb69c5a0eb31515a1a..3a396e54d5356a943a2a2cb3e345749f8673a2f8 100644 (file)
@@ -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;
index ad8f86496f8b397f464e96c302b7ca7b0a425ed9..6a5678c7d143cbe03cf09522c603d6a1072ccd4a 100644 (file)
@@ -12,6 +12,7 @@
     <file>Browse.qml</file>
     <file>Settings.qml</file>
     <file>Connected.qml</file>
+    <file>Failed.qml</file>
     <file>Setup.qml</file>
     <file>logo.svg</file>
     <file>wedge.svg</file>
     <file>public.svg</file>
     <file>join.svg</file>
     <file>leave.svg</file>
+    <file>manage.svg</file>
     <file>cog.svg</file>
     <file>mic.svg</file>
     <file>ethernet.png</file>
+    <file>ohno.png</file>
     <file>headphones.svg</file>
+    <file>network.svg</file>
     <file>jacktrip.png</file>
     <file>jacktrip white.png</file>
     <file>JTOriginal.png</file>
index 93df66998ab3edaddb55921ff2c860aae5e23185..edc79d5f4dfbabcf82827ae16f00edd7b10b661a 100644 (file)
@@ -37,6 +37,7 @@
 
 #include "virtualstudio.h"
 
+#include <QDebug>
 #include <QDesktopServices>
 #include <QMessageBox>
 #include <QQmlContext>
@@ -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<QJackTrip> 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<VsServerInfo*>(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<VsServerInfo*>(m_servers.at(m_currentStudio));
     emit currentStudioChanged();
@@ -624,69 +735,41 @@ void VirtualStudio::completeConnection()
     emit connectionStateChanged();
     VsServerInfo* studioInfo = static_cast<VsServerInfo*>(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<VsServerInfo*>(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");
 }
index d6b2446267d4bf113c0f1bd88aa9daed993e13cf..0d0d401701e680dcee65cff8b8ec61c7d4acc4ae 100644 (file)
 #include <QtNetworkAuth>
 
 #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<QJackTrip> 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<QObject*> 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<JackTrip> 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;
index 1465189e474d461c1efd7a03b5092b1fcad95b2a..218520182307bd7c800e45cffbfd067a22398034 100644 (file)
@@ -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 (file)
index 0000000..cc4a673
--- /dev/null
@@ -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 <QDebug>
+
+// 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 (file)
index 0000000..bab61c7
--- /dev/null
@@ -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 <QObject>
+#include <QString>
+#include <QUuid>
+#include <QtNetworkAuth>
+#include <QtWebSockets>
+
+#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<JackTrip> 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 (file)
index 0000000..2d1ec56
--- /dev/null
@@ -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 <iostream>
+
+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 (file)
index 0000000..8265b50
--- /dev/null
@@ -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 <QAbstractSocket>
+#include <QDateTime>
+#include <QObject>
+#include <QTimer>
+#include <QtWebSockets>
+#include <stdexcept>
+
+/** \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 (file)
index 0000000..8e30df3
--- /dev/null
@@ -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 <iostream>
+
+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<QAbstractSocket::SocketError>::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<uint32_t> vec_expired;
+    std::vector<qint64> vec_rtt;
+    std::map<uint32_t, VsPing*>::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<uint32_t>::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<qint64>::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<qint64>::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<uint32_t, VsPing*>::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<uint32_t, VsPing*>::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 (file)
index 0000000..0f689f2
--- /dev/null
@@ -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 <QAbstractSocket>
+#include <QDateTime>
+#include <QObject>
+#include <QTimer>
+#include <QUrl>
+#include <QtWebSockets>
+#include <stdexcept>
+#include <vector>
+
+#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<uint32_t, VsPing*> 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
index 791104f7f757195c76c4923a8904d6a413303f7b..e0a9281c619f988e89cb90caff8fb9a6bb9188df 100644 (file)
 
 #include "vsQuickView.h"
 
+#include <QDesktopServices>
+#include <iostream>
+
+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();
+}
index bb9ad7840b29db02412eafb776d5169c860d4167..ab80a73383310f3da54d2783113b6956f67ceadc 100644 (file)
 #define VSQUICKVIEW_H
 
 #include <QQuickView>
+#ifdef Q_OS_MACOS
+#include <QAction>
+#include <QMenu>
+#include <QMenuBar>
+#include <QObject>
+#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
index ec9a761cb9f03b5776d8fa9754e4c89acff00e61..c01a4b87f2fc0255f0c33c47f39ee88e6fca5383 100644 (file)
@@ -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;
index fecb8492891af10ca0bc3c57886b8605533421de..642bc6dbb440f3fc6b9c2833540ebeeea2f12990 100644 (file)
@@ -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 (file)
index 0000000..8de8a0f
--- /dev/null
@@ -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 <QDebug>
+#include <iostream>
+
+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 (file)
index 0000000..1e85ff8
--- /dev/null
@@ -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 <QDesktopServices>
+#include <QObject>
+#include <QSslError>
+#include <QString>
+#include <QUrl>
+
+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 (file)
index 0000000..f099b8f
--- /dev/null
@@ -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 <QDebug>
+#include <iostream>
+
+// 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<const QList<QSslError>&>::of(&QWebSocket::sslErrors),
+            this, &VsWebSocket::onSslErrors);
+    connect(&m_webSocket, QOverload<QAbstractSocket::SocketError>::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<QSslError>& 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 (file)
index 0000000..4581355
--- /dev/null
@@ -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 <QList>
+#include <QObject>
+#include <QSslError>
+#include <QString>
+#include <QUrl>
+#include <QtWebSockets>
+
+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<QSslError>& 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
index c20c196a9169a87653fd05bbe7fcbfe8868a484b..eb2796002a3e5f92de8ba027687c60dd656f09d3 100644 (file)
@@ -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
index ebe553b14f8c4c9b1d0a86f1b6da16c4ccc992ab..2c11e333b8aef3694d5b38c068958a379e854463 100644 (file)
 #endif
 
 #ifndef NO_VS
+#include <QDebug>
+#include <QFile>
+#include <QLocalServer>
+#include <QLocalSocket>
+#include <QQmlEngine>
 #include <QQuickView>
 #include <QSettings>
+#include <QTextStream>
 
+#include "JTApplication.h"
 #include "gui/virtualstudio.h"
+#include "gui/vsUrlHandler.h"
 #endif
 
 #include "gui/qjacktrip.h"
 #include <windows.h>
 #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<QJackTrip> window;
 
 #ifndef NO_VS
+    QString deeplink = QLatin1String("");
     QSharedPointer<VirtualStudio> vs;
+#ifdef _WIN32
+    QSharedPointer<QLocalServer> instanceServer;
+    QSharedPointer<QLocalSocket> instanceCheckSocket;
+#endif
 #endif
 
+#if defined(Q_OS_MACOS) && !defined(NO_VS)
+    if (qobject_cast<JTApplication*>(app.data())) {
+#else
     if (qobject_cast<QApplication*>(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<QLocalSocket>::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<QLocalServer>::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