--- /dev/null
+subprojects
+externals
+documentation
+src/*dsp.h
\ No newline at end of file
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)
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.
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)
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 ()
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 ()
+- 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:
QT += qml
QT += quick
QT += svg
+ QT += websockets
}
- noupdater {
+ noupdater|linux-g++|linux-g++-64 {
DEFINES += NO_UPDATER
}
}
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
}
}
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
}
}
} else {
RESOURCES += src/gui/qjacktrip.qrc
}
- !noupdater {
+ !noupdater:!linux-g++:!linux-g++-64 {
FORMS += src/dblsqd/update_dialog.ui
}
}
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;
--- /dev/null
+<?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>
<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>
# 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
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
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 += [
{
"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",
{
"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",
{
"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",
{
"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",
}
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__
--- /dev/null
+//*****************************************************************
+/*
+ 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
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();
setBufferSize(bufferFrames);
} catch (RtAudioError& e) {
std::cout << '\n' << e.getMessage() << '\n' << std::endl;
- exit(0);
+ throw std::runtime_error(e.getMessage());
}
// Setup parent class
#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
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)
}
//*******************************************************************************
-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()
* \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
*/
property int buttonHeight: 25
property int buttonWidth: 103
+ property int extraSettingsButtonWidth: 16
property int fontMedium: 11
property int scrollY: 0
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
}
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"
Button {
id: settingsButton
+ text: "Settings"
+ palette.buttonText: textColour
+ icon {
+ source: "cog.svg";
+ color: textColour;
+ }
background: Rectangle {
radius: 6 * virtualstudio.uiScale
color: settingsButton.down ? buttonPressedColour : (settingsButton.hovered ? buttonHoverColour : buttonColour)
border.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
}
}
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
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
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
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]
}
Text {
+ id: outputDeviceHeader
x: 120 * virtualstudio.uiScale
text: virtualstudio.audioBackend == "JACK" ?
virtualstudio.audioBackend : outputComboModel[virtualstudio.outputDevice]
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
+ }
}
--- /dev/null
+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
+ }
+}
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
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
}
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
}
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"
}
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
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
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
Image {
width: 20 * virtualstudio.uiScale; height: width
anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
- source: "cog.svg"
+ source: "manage.svg"
}
}
--- /dev/null
+<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
--- /dev/null
+<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>
#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)
// 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 "
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;
<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>
#include "virtualstudio.h"
+#include <QDebug>
#include <QDesktopServices>
#include <QMessageBox>
#include <QQmlContext>
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)
m_view.show();
}
+void VirtualStudio::raiseToTop()
+{
+ m_view.show(); // Restore from systray
+ m_view.requestActivate(); // Raise to top
+}
+
bool VirtualStudio::showFirstRun()
{
return m_showFirstRun;
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;
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()
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
#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()) {
QSettings settings;
settings.setValue(QStringLiteral("UiMode"), QJackTrip::STANDARD);
m_refreshTimer.stop();
+ m_heartbeatTimer.stop();
if (m_showFirstRun) {
m_showFirstRun = false;
void VirtualStudio::logout()
{
+ if (m_device != nullptr) {
+ m_device->removeApp();
+ }
+
m_authenticator->setToken(QLatin1String(""));
m_authenticator->setRefreshToken(QLatin1String(""));
settings.endGroup();
m_refreshTimer.stop();
+ m_heartbeatTimer.stop();
m_refreshToken.clear();
m_userId.clear();
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)
}
m_refreshTimer.stop();
+ m_networkStats = QJsonObject();
+ emit networkStatsChanged();
+
m_currentStudio = studioIndex;
VsServerInfo* studioInfo = static_cast<VsServerInfo*>(m_servers.at(m_currentStudio));
emit currentStudioChanged();
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;
}
stopStudio();
}
}
- m_jackTrip->stop();
+
+ m_device->stopPinger();
+ m_device->stopJackTrip();
} else if (m_startedStudio) {
m_startTimer.stop();
stopStudio();
QDesktopServices::openUrl(url);
}
+void VirtualStudio::editProfile()
+{
+ QUrl url = QUrl(QStringLiteral("https://app.jacktrip.org/profile"));
+ QDesktopServices::openUrl(url);
+}
+
void VirtualStudio::showAbout()
{
About about;
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();
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;
m_jackTripRunning = false;
m_connectionState = QStringLiteral("Disconnected");
- m_jackTrip.reset();
emit connectionStateChanged();
emit disconnected();
m_onConnectedScreen = false;
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));
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;
}
}
+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()) {
}
}
+void VirtualStudio::sendHeartbeat()
+{
+ if (m_device != nullptr && m_connectionState != "Connecting...") {
+ m_device->sendHeartbeat();
+ }
+}
+
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);
}
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();
});
}
+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)
{
for (int i = 0; i < m_servers.count(); i++) {
delete m_servers.at(i);
}
+
+ QDesktopServices::unsetUrlHandler("jacktrip");
}
#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"
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)
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);
void setStandardWindow(QSharedPointer<QJackTrip> window);
void show();
+ void raiseToTop();
bool showFirstRun();
bool hasRefreshToken();
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();
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();
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);
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();
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
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;
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;
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;
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 {
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 {
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 {
PropertyChanges { target: browseScreen; x: 0 }
PropertyChanges { target: settingsScreen; x: window.width }
PropertyChanges { target: connectedScreen; x: window.width }
+ PropertyChanges { target: failedScreen; x: window.width }
},
State {
PropertyChanges { target: browseScreen; x: -browseScreen.width }
PropertyChanges { target: settingsScreen; x: 0 }
PropertyChanges { target: connectedScreen; x: window.width }
+ PropertyChanges { target: failedScreen; x: window.width }
},
State {
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 }
}
]
id: connectedScreen
}
+ Failed {
+ id: failedScreen
+ }
+
Connections {
target: virtualstudio
onAuthSucceeded: {
onAuthFailed: {
loginScreen.failTextVisible = true;
}
- // onConnected: { }
+ onConnected: {
+ window.state = "connected";
+ }
+ onFailed: {
+ window.state = "failed";
+ }
onDisconnected: {
window.state = "browse";
}
--- /dev/null
+//*****************************************************************
+/*
+ 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;
+}
--- /dev/null
+//*****************************************************************
+/*
+ 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
--- /dev/null
+//*****************************************************************
+/*
+ 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);
+ }
+}
--- /dev/null
+//*****************************************************************
+/*
+ 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
--- /dev/null
+//*****************************************************************
+/*
+ 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
--- /dev/null
+//*****************************************************************
+/*
+ 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
#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();
+}
#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
QString VsServerInfo::location()
{
- if (m_region.split(QStringLiteral("-")).count() > 2) {
- return m_region.section(QStringLiteral("-"), 2);
- }
return m_region;
}
m_queueBuffer = queueBuffer;
}
+QString VsServerInfo::bannerURL()
+{
+ return m_bannerURL;
+}
+
+void VsServerInfo::setBannerURL(const QString& bannerURL)
+{
+ m_bannerURL = bannerURL;
+}
+
QString VsServerInfo::id()
{
return m_id;
m_id = id;
}
+QString VsServerInfo::sessionId()
+{
+ return m_sessionId;
+}
+
+void VsServerInfo::setSessionId(const QString& sessionId)
+{
+ m_sessionId = sessionId;
+}
+
VsServerInfo::~VsServerInfo() = default;
// 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)
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);
quint16 m_period;
quint32 m_sampleRate;
quint16 m_queueBuffer;
+ QString m_bannerURL;
QString m_id;
+ QString m_sessionId;
QString m_status;
/* Remaining JSON fields
"owner": true,
"ownerId": "string",
"status": "Ready",
- "sessionId": "1636042722abcdefg",
"subStatus": "Active",
"createdAt": "2021-09-07T17:15:38Z",
"expiresAt": "2021-09-07T17:15:38Z",
--- /dev/null
+//*****************************************************************
+/*
+ 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
--- /dev/null
+//*****************************************************************
+/*
+ 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
--- /dev/null
+//*****************************************************************
+/*
+ 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;
+}
--- /dev/null
+//*****************************************************************
+/*
+ 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
#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
#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) {
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);
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
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
}
#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
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) {
} 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