New upstream version 3.0.0+ds
authorIOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
Tue, 28 Apr 2026 09:48:31 +0000 (11:48 +0200)
committerIOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
Tue, 28 Apr 2026 09:48:31 +0000 (11:48 +0200)
88 files changed:
AGENTS.md [new file with mode: 0644]
CLAUDE.md [new file with mode: 0644]
Dockerfile
docs/Documentation/ClientConnectionTypes.md [new file with mode: 0644]
docs/Documentation/NetworkProtocol.md
docs/changelog.yml
linux/Dockerfile.build
meson.build
meson_options.txt
src/AudioInterface.cpp
src/AudioTester.cpp
src/Effects.h
src/JackTrip.cpp
src/JackTrip.h
src/JackTripWorker.cpp
src/JackTripWorker.h
src/JitterBuffer.cpp
src/Limiter.h
src/LoopBack.h
src/PacketHeader.cpp
src/PacketHeader.h
src/ProcessPlugin.h
src/Regulator.cpp
src/Regulator.h
src/Settings.cpp
src/Settings.h
src/SocketClient.cpp
src/SocketServer.cpp
src/SslServer.cpp
src/SslServer.h
src/UdpDataProtocol.cpp
src/UdpHubListener.cpp
src/UdpHubListener.h
src/gui/about.cpp
src/http3/Http3Protocol.cpp [new file with mode: 0644]
src/http3/Http3Protocol.h [new file with mode: 0644]
src/http3/Http3Server.cpp [new file with mode: 0644]
src/http3/Http3Server.h [new file with mode: 0644]
src/jacktrip_globals.cpp
src/jacktrip_globals.h
src/main.cpp
src/vs/AboutWindow.qml
src/vs/Browse.qml
src/vs/ChangeDevices.qml
src/vs/Connected.qml
src/vs/CreateStudio.qml
src/vs/DeviceControlsGroup.qml
src/vs/DeviceRefreshButton.qml
src/vs/DeviceWarningModal.qml
src/vs/Failed.qml
src/vs/FeedbackSurvey.qml
src/vs/LearnMoreButton.qml
src/vs/Login.qml
src/vs/Permissions.qml
src/vs/Recommendations.qml
src/vs/Settings.qml
src/vs/Setup.qml
src/vs/StyledButton.qml [new file with mode: 0644]
src/vs/arrow-left.svg [new file with mode: 0644]
src/vs/arrow-right.svg [new file with mode: 0644]
src/vs/arrow-top-right-on-square.svg [new file with mode: 0644]
src/vs/home.svg [new file with mode: 0644]
src/vs/question-mark-circle.svg [new file with mode: 0644]
src/vs/squares-2x2.svg [new file with mode: 0644]
src/vs/virtualstudio.cpp
src/vs/virtualstudio.h
src/vs/vs.qrc
src/vs/vsAudio.cpp
src/vs/vsConstants.h
src/vs/vsDeviceCodeFlow.h
src/webrtc/WebRtcDataProtocol.cpp [new file with mode: 0644]
src/webrtc/WebRtcDataProtocol.h [new file with mode: 0644]
src/webrtc/WebRtcPeerConnection.cpp [new file with mode: 0644]
src/webrtc/WebRtcPeerConnection.h [new file with mode: 0644]
src/webrtc/WebRtcSignalingProtocol.cpp [new file with mode: 0644]
src/webrtc/WebRtcSignalingProtocol.h [new file with mode: 0644]
src/webrtc/WebSocketSignalingConnection.cpp [new file with mode: 0644]
src/webrtc/WebSocketSignalingConnection.h [new file with mode: 0644]
src/webtransport/WebTransportDataProtocol.cpp [new file with mode: 0644]
src/webtransport/WebTransportDataProtocol.h [new file with mode: 0644]
src/webtransport/WebTransportSession.cpp [new file with mode: 0644]
src/webtransport/WebTransportSession.h [new file with mode: 0644]
subprojects/libdatachannel.wrap [new file with mode: 0644]
subprojects/msquic.wrap [new file with mode: 0644]
subprojects/packagefiles/msquic/CMakeLists.txt [new file with mode: 0644]
subprojects/packagefiles/msquic/meson.build [new file with mode: 0644]
subprojects/packagefiles/msquic/meson_options.txt [new file with mode: 0644]
subprojects/packagefiles/msquic/src/bin/CMakeLists.txt [new file with mode: 0644]

diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644 (file)
index 0000000..60f9091
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,185 @@
+## Project Overview
+
+JackTrip is a high-quality audio network performance system for bidirectional, uncompressed audio streaming over the Internet. It's a C++20 desktop application using Qt 6, supporting Linux, macOS and Windows.
+
+## Branching
+
+Always create new branches from the latest commit on the `dev` branch:
+
+```bash
+git fetch origin
+git checkout -b your-branch-name origin/dev
+```
+
+## Build Commands
+
+**Build system**: Meson (primary). CMake exists but is legacy/unsupported.
+
+```bash
+# Configure (first time)
+meson setup builddir
+
+# Build
+meson compile -C builddir
+
+# Reconfigure with options
+meson configure builddir -Dnogui=true   # CLI-only build
+meson configure builddir -Dnovs=true    # without Virtual Studio
+meson configure builddir -Dnoclassic=true  # without classic Qt Widgets GUI
+
+# Clean rebuild
+meson setup builddir --wipe
+```
+
+See `meson_options.txt` for setup options.
+
+## Code Formatting & Linting
+
+- **clang-format** (version 13) enforced in CI on all `src/**/*.{h,cpp}` files
+- Style: Google-based, 90-column limit, 4-space indent, Linux brace style
+- Run manually: `clang-format -i src/MyFile.cpp`
+- **clang-tidy** runs as a separate CI check on PRs
+- Pre-commit hook enforces clang-format
+
+## Testing
+
+Tests use Qt Test framework. Test files are in `tests/`:
+- `tests/jacktrip_tests.cpp` — threading tests
+- `tests/audio_socket_test.cpp` — AudioSocket tests
+
+Test coverage is minimal; the project relies primarily on manual/integration testing.
+
+## Architecture
+
+### Core Design (Mediator Pattern)
+
+**`JackTrip`** (`src/JackTrip.cpp`, ~3000 lines) is the central orchestrator coordinating all subsystems.
+
+**Data flow**:
+- Sender: AudioInterface → RingBuffer → PacketHeader → UdpDataProtocol → Network
+- Receiver: Network → UdpDataProtocol → JitterBuffer → RingBuffer → Effects → AudioInterface
+
+### Threading Model
+
+Four threads in operation:
+1. **Audio thread** — real-time priority, driven by audio backend callbacks
+2. **Network sender thread** — `DataProtocol` subclass
+3. **Network receiver thread** — `DataProtocol` subclass
+4. **GUI thread** — Qt event loop
+
+### Key Components
+
+| Component | Files | Purpose |
+|-----------|-------|---------|
+| Audio backends | `AudioInterface.cpp`, `JackAudioInterface.cpp`, `RtAudioInterface.cpp` | Abstract audio I/O (JACK or RtAudio) |
+| Networking | `DataProtocol.cpp`, `UdpDataProtocol.cpp` | UDP-based audio transport |
+| Buffering | `RingBuffer.cpp`, `JitterBuffer.cpp`, `Regulator.cpp` | Lock-free audio buffering and jitter management |
+| Effects | `Compressor.cpp`, `Limiter.cpp`, `Reverb.cpp`, `Volume.cpp`, etc. | Audio processing via `ProcessPlugin` base class |
+| Hub server | `UdpHubListener.cpp`, `JackTripWorker.cpp` | Multi-client server mode |
+| Settings | `Settings.cpp` | Command-line parsing and configuration |
+| Entry point | `main.cpp` | GUI/CLI dispatch |
+
+### GUI Modes
+
+- **Virtual Studio** (`src/vs/`) — Modern Qt QML + WebEngine GUI, requires Qt 6.2+. Main class: `VirtualStudio`
+- **Classic** (`src/gui/`) — Traditional Qt Widgets GUI. Main class: `QJackTrip`
+- **CLI** — Headless mode when built with `-Dnogui=true`
+
+### Extending Audio Effects
+
+Subclass `ProcessPlugin` and implement the `compute` method. Effects are chained in the audio processing pipeline by `JackTrip`.
+
+### Platform-Specific Code
+
+- macOS: `NoNap.mm`, `vsMacPermissions.mm`, CoreAudio integration, DYLD injection protection
+- Windows: RtAudio primary backend, DLL management in `win/`
+- Linux: Strong JACK support, Flatpak packaging in `linux/`
+
+## Adding Icons to the UI
+
+Prefer sourcing icons from [heroicons.com](https://heroicons.com). Download the SVG and follow the steps below.
+
+### Steps
+
+1. **Add the SVG file** to `src/vs/` (or `src/vs/flags/` for country flags)
+2. **Register it in `src/vs/vs.qrc`** — add a `<file>youricon.svg</file>` entry inside the `<qresource prefix="vs">` block
+3. **Reference in QML** — use `icon.source: "youricon.svg"` or the `AppIcon` wrapper component for theme-aware rendering:
+   ```qml
+   AppIcon {
+       width: 24 * virtualstudio.uiScale
+       height: 24 * virtualstudio.uiScale
+       icon.source: "youricon.svg"
+       color: textColour
+   }
+   ```
+4. **Rebuild** — Meson recompiles the QRC, embedding the icon into the executable
+
+### QRC files
+
+- `src/vs/vs.qrc` — Virtual Studio icons, QML files, fonts (prefix: `vs`)
+- `src/images/images.qrc` — application window icons (prefix: `images`)
+
+### Icon conventions
+
+- **Format**: SVG (all UI icons are SVG; PNG only for app icons and branding)
+- **Color**: icons are colored at runtime via `icon.color` in QML, so use a single fill color in the SVG (the `AppIcon` component in `src/vs/AppIcon.qml` handles dark/light theme defaults automatically)
+- **Sizing**: controlled by the parent QML element and `virtualstudio.uiScale`, not baked into the SVG
+
+## Code Style Conventions
+
+- **Classes**: PascalCase (`JackTrip`, `AudioInterface`)
+- **Methods**: camelCase (`startProcess`, `computeProcessFromNetwork`)
+- **Member variables**: `m` prefix (`mAudioInterface`, `mDataProtocol`)
+- Heavy use of Qt signals/slots, `QObject`, `Q_PROPERTY`
+- Pointers: left-aligned (`int* ptr`, not `int *ptr`)
+
+## Cursor Cloud specific instructions
+
+### Building in the cloud VM
+
+The default C compiler on the VM is clang, which fails meson's C++ compiler check due to missing libstdc++ headers. Always set `CC=gcc CXX=g++` when running `meson setup`:
+
+```bash
+CC=gcc CXX=g++ meson setup -Dnogui=true -Drtaudio=enabled \
+  -Drtaudio:jack=disabled -Drtaudio:default_library=static \
+  -Drtaudio:alsa=enabled -Drtaudio:pulse=disabled -Drtaudio:werror=false \
+  -Dnofeedback=true -Dlibsamplerate=enabled -Ddefault_library=shared builddir
+
+meson compile -C builddir
+```
+
+- Use `-Dnogui=true` for headless/CLI builds (avoids X11/GUI dependencies).
+- Use `-Dnovs=true` instead if you want the classic GUI but not Virtual Studio (requires additional Qt6 GUI/Widgets packages).
+- Git submodules must be initialized: `git submodule update --init --recursive`.
+
+### Running the hub server
+
+The hub server **requires a running JACK daemon**. Start JACK with a dummy audio driver first:
+
+```bash
+jackd -d dummy -r 48000 -p 1024 &
+```
+
+Then start the hub server: `./builddir/jacktrip -S`
+
+To test a client connecting to the local hub server on the same host, use separate bind and peer ports (the server listens on TCP 4464, so the client must keep peer port at 4464 but use a different bind port):
+  ```bash
+  ./builddir/jacktrip -C 127.0.0.1 -B 4465 -P 4464
+  ```
+  Note: `-o` offsets **both** bind and peer ports, which breaks same-host testing since the client would try to connect to the wrong TCP port.
+
+Use `jack_lsp` to verify JACK ports are registered after a client connects.
+
+### Linting
+
+The CI runs clang-format version 13; the cloud VM has a newer version which may flag pre-existing style differences. Run on changed files only to match CI behavior:
+
+```bash
+clang-format --dry-run --Werror src/YourFile.cpp
+```
+
+### Gotchas
+
+- `meson setup` downloads RtAudio as a subproject automatically if not found on the system; this requires network access.
+- Rebuilding after source changes: `meson compile -C builddir` (incremental).
+- To reconfigure: `meson configure builddir -Doption=value` or wipe with `rm -rf builddir`.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644 (file)
index 0000000..43c994c
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+@AGENTS.md
index 3a6d3df0736d6c89f64152db21df4388f785e048..07e23718382c4663aef3749782ce62cf511d8166 100644 (file)
@@ -17,7 +17,7 @@ ARG JACK_VERSION=latest
 FROM registry.fedoraproject.org/fedora:${FEDORA_VERSION} AS builder
 
 # install tools require to build jacktrip
-RUN dnf install -y --nodocs cmake gcc gcc-c++ meson git python3-pyyaml python3-jinja2 glib2-devel jack-audio-connection-kit-devel dbus-devel
+RUN dnf install -y --nodocs cmake gcc gcc-c++ meson git perl python3-pyyaml python3-jinja2 glib2-devel jack-audio-connection-kit-devel dbus-devel libatomic-static
 
 ENV QT_VERSION=6.8.3
 RUN if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; else export ARCH=arm64; fi \
@@ -33,7 +33,7 @@ RUN cd /root \
        && export QT_PATH=/opt/qt-${QT_VERSION}-static \
        && export PATH=${QT_PATH}/bin:${PATH} \
        && export LDFLAGS="-L${QT_PATH}/lib -L${QT_PATH}/plugins/tls" \
-       && meson setup -Ddefault_library=static -Dnogui=true --buildtype release builddir \
+       && meson setup -Dpkg_config_path=/opt/qt-${QT_VERSION}-static/lib/pkgconfig -Dlibdatachannel=enabled -Dmsquic=enabled -Ddefault_library=static -Dnogui=true --buildtype release builddir \
        && meson compile -C builddir
 
 # stage files in INSTALLDIR
@@ -65,4 +65,5 @@ COPY --from=builder /artifacts /
 
 # jacktrip hub server listens on 4464 and uses 61000+ for clients
 EXPOSE 4464/tcp
+EXPOSE 4464/udp
 EXPOSE 61000-61100/udp
diff --git a/docs/Documentation/ClientConnectionTypes.md b/docs/Documentation/ClientConnectionTypes.md
new file mode 100644 (file)
index 0000000..349edd2
--- /dev/null
@@ -0,0 +1,387 @@
+# JackTrip Hub Server Connection Types
+
+## Overview
+
+The JackTrip Hub Server supports three different connection types for clients, each with different characteristics suited to different deployment scenarios:
+
+1. **UDP** - Traditional low-latency UDP transport with TCP signaling
+2. **WebRTC** - Browser-compatible connection with NAT traversal using WebRTC data channels
+3. **WebTransport** - Modern HTTP/3 transport using QUIC with built-in encryption
+
+All three connection types use the same audio packet format (see [NetworkProtocol.md](NetworkProtocol.md)) and share the same worker pool allocation mechanism.
+
+## Connection Type Comparison
+
+| Feature | UDP | WebRTC | WebTransport |
+|---------|-----|--------|--------------|
+| **Transport** | UDP datagrams | WebRTC data channels over UDP | QUIC datagrams over UDP |
+| **Signaling** | TCP port 4464 | WebSocket over TCP 4464 | HTTP/3 over UDP 4464 |
+| **NAT Traversal** | No | Yes (ICE/STUN/TURN) | Yes (QUIC connection migration) |
+| **Browser Support** | No | Yes (all modern browsers) | Yes (Chrome 97+, Edge 97+) |
+| **Encryption** | Optional (TLS) | Mandatory (DTLS) | Mandatory (TLS 1.3) |
+| **Setup Complexity** | Simple | Complex (ICE negotiation) | Medium (HTTP/3 CONNECT) |
+| **Connection Time** | Fastest | Medium (ICE gathering) | Fast (0-RTT after first) |
+| **Audio Transport Port** | UDP (61002 + worker_id) | ICE-negotiated UDP ports | UDP 4464 (QUIC) |
+| **Library Required** | None (Qt Network) | libdatachannel | msquic |
+| **Build Option** | Always available | `-Dlibdatachannel=enabled` | `-Dmsquic=enabled` |
+
+## 1. UDP Connections
+
+### Overview
+
+Traditional UDP connections use a simple TCP-based signaling handshake followed by direct UDP audio transport. This is the lowest-latency option but requires open firewall ports and doesn't work behind NAT without port forwarding.
+
+### Connection Handshake
+
+1. Client connects to TCP port 4464
+2. Client sends UDP port number (4 bytes, little-endian) and optional client name (64 bytes)
+3. Server allocates a worker slot and responds with server UDP port (`mBasePort + worker_id`)
+4. TCP connection closes
+5. Audio exchange begins over UDP using the negotiated ports
+
+For complete details on the UDP handshake protocol, packet format, and authentication flow, see [NetworkProtocol.md](NetworkProtocol.md).
+
+### Port Requirements
+
+- **TCP 4464**: Signaling handshake
+- **UDP 61002 + worker_id**: Audio transport (base port configurable with `--udpbaseport`)
+  - Example: First client uses 61002, second uses 61003, etc.
+
+### Authentication
+
+Optional TLS authentication is supported. When enabled:
+- Client sends special value (`65536`) instead of port to initiate SSL handshake
+- Credentials (username/password) are exchanged over encrypted connection
+- Server validates and responds with port assignment or error code
+
+## 2. WebRTC Connections
+
+### Overview
+
+WebRTC connections use WebSocket-based signaling followed by ICE-negotiated data channels. This provides excellent NAT traversal and works from web browsers, making it ideal for browser-based clients.
+
+### Connection Handshake
+
+1. **WebSocket Upgrade**: Client sends HTTP upgrade request to TCP port 4464
+   ```http
+   GET / HTTP/1.1
+   Upgrade: websocket
+   Connection: Upgrade
+   Sec-WebSocket-Key: <key>
+   ```
+
+2. **SDP Exchange**: Client sends SDP offer, server responds with answer
+   ```json
+   {
+       "type": "offer",
+       "sdp": "v=0\r\no=- 4611731400430051336 2 IN IP4 127.0.0.1\r\n..."
+   }
+   ```
+
+3. **ICE Candidates**: Both sides exchange ICE candidates for connectivity
+   ```json
+   {
+       "type": "ice",
+       "candidate": "candidate:1 1 UDP 2130706431 192.168.1.100 54321 typ host",
+       "sdpMid": "data",
+       "sdpMLineIndex": 0
+   }
+   ```
+
+4. **Connection Establishment**: ICE performs connectivity checks, DTLS establishes encryption, SCTP creates data channel association
+
+5. **Audio Exchange**: Audio packets are sent over the data channel using the same packet format as UDP
+
+### Data Channel Configuration
+
+The data channel is configured for low-latency, unreliable delivery similar to UDP:
+
+```cpp
+rtc::DataChannelInit config;
+config.ordered = false;       // Don't wait for in-order delivery
+config.maxRetransmits = 0;    // No retransmissions (like UDP)
+```
+
+### ICE Server Configuration
+
+The server can be configured with STUN/TURN servers for NAT traversal:
+
+```bash
+jacktrip -S --iceservers "stun:stun.l.google.com:19302"
+```
+
+### Implementation
+
+- **Library**: libdatachannel
+- **Classes**: `WebRtcPeerConnection`, `WebRtcDataProtocol`, `WebRtcSignalingProtocol`
+- **Detection**: Server detects WebSocket upgrade by checking for "GET" in initial TCP data
+
+## 3. WebTransport Connections
+
+### Overview
+
+WebTransport provides modern, low-latency transport using HTTP/3 over QUIC. Unlike WebRTC, it requires no ICE negotiation and provides a simpler connection model with built-in NAT traversal. All connections use unreliable QUIC datagrams for audio transport.
+
+**Important**: WebTransport uses UDP (not TCP) for the entire connection, including signaling.
+
+### Connection Handshake
+
+1. **QUIC Connection**: Client initiates QUIC connection to UDP port 4464
+   - TLS 1.3 handshake (mandatory, built into QUIC)
+   - 0-RTT capable after first connection
+
+2. **HTTP/3 CONNECT**: Client sends HTTP/3 CONNECT request over QUIC
+   ```
+   :method = CONNECT
+   :protocol = webtransport
+   :path = /webtransport
+   :authority = server.example.com:4464
+   ```
+
+3. **Session Established**: Server responds with 200 OK
+   ```
+   :status = 200
+   sec-webtransport-http3-draft = draft02
+   ```
+
+4. **Audio Exchange**: Audio packets are sent as QUIC DATAGRAM frames (RFC 9221)
+
+### QUIC Datagram Transport
+
+Audio packets are sent as unreliable QUIC datagrams:
+
+```
+┌──────────┬────────────────────┐
+│ JackTrip │   Audio Samples    │
+│ Header   │                    │
+│ (16B)    │   (variable)       │
+└──────────┴────────────────────┘
+```
+
+**Key properties:**
+- Unreliable (no retransmissions, like UDP)
+- Unordered (can arrive out of sequence)
+- Encrypted (TLS 1.3 via QUIC)
+- Preserve datagram boundaries
+- Typical size limit: 1200 bytes (path MTU)
+
+### TLS Certificate Requirements
+
+WebTransport requires TLS 1.3 certificates - encryption is mandatory and cannot be disabled.
+
+**Development (self-signed):**
+```bash
+openssl genpkey -algorithm RSA -out webtransport.key -pkeyopt rsa_keygen_bits:2048
+openssl req -new -x509 -key webtransport.key -out webtransport.crt -days 365 \
+  -subj "/CN=jacktrip.example.com"
+
+jacktrip -S --certfile webtransport.crt --keyfile webtransport.key
+```
+
+**Production (Let's Encrypt):**
+```bash
+sudo certbot certonly --standalone -d jacktrip.example.com
+
+jacktrip -S \
+  --certfile /etc/letsencrypt/live/jacktrip.example.com/fullchain.pem \
+  --keyfile /etc/letsencrypt/live/jacktrip.example.com/privkey.pem
+```
+
+**Note**: Browsers will reject self-signed certificates unless explicitly trusted.
+
+### Implementation
+
+- **Library**: msquic (only supported QUIC library)
+- **Classes**: `WebTransportSession`, `WebTransportDataProtocol`
+- **Detection**: Server detects QUIC packets on UDP 4464 by examining packet header flags
+- **Port**: Single UDP port (4464) for both signaling and audio
+
+### Why QUIC?
+
+QUIC provides several advantages over TCP for real-time audio:
+
+- **True unreliable datagrams**: Native support for unreliable delivery (no head-of-line blocking)
+- **Lower latency**: 1-RTT connection setup (0-RTT after first connection)
+- **Connection migration**: Survives IP address changes (WiFi ↔ Cellular)
+- **Single port operation**: All communication over one UDP port
+- **No framing overhead**: QUIC datagrams preserve packet boundaries
+
+## Connection Type Detection
+
+The server automatically detects the connection type:
+
+```
+┌─────────────────────┐
+│ Incoming Connection │
+└──────────┬──────────┘
+           │
+    ┌──────┴──────┐
+    │             │
+TCP 4464      UDP 4464
+    │             │
+    │             └──> QUIC packet → WebTransport
+    │
+    └──> Peek first bytes
+          │
+          ├──> "GET" → WebRTC (WebSocket)
+          │
+          └──> Binary (4 bytes) → UDP
+```
+
+### Detection Logic
+
+**TCP port 4464** (UDP and WebRTC):
+```cpp
+QByteArray peekData = clientConnection->peek(512);
+
+if (peekData.startsWith("GET")) {
+    // WebRTC connection (WebSocket signaling)
+    createWebRtcWorker(clientConnection, "webrtc");
+} else {
+    // Binary data - legacy UDP client
+    readClientUdpPort(clientConnection, clientName);
+}
+```
+
+**UDP port 4464** (WebTransport):
+```cpp
+// QUIC packets have distinctive header format
+uint8_t first_byte = datagram[0];
+bool is_long_header = (first_byte & 0x80) != 0;
+
+if (is_long_header) {
+    // QUIC Initial or Handshake packet
+    handleQuicConnection(datagram, sender, senderPort);
+}
+```
+
+## Audio Packet Format
+
+All three connection types use the same audio packet format. See [NetworkProtocol.md](NetworkProtocol.md) for complete details on:
+
+- Packet header structure (16 bytes)
+- Audio payload format (planar/non-interleaved)
+- Sample encoding (8/16/24/32-bit)
+- Special field encodings
+
+## Worker Pool Management
+
+All connection types share the same worker pool:
+
+- **Slot allocation**: First available slot from 0 to `gMaxThreads-1`
+- **Audio ports**: All create identical JACK/RtAudio ports (`receive_N`, `send_N` where N = worker_id + 1)
+- **Port assignment for UDP**: Each UDP client is assigned `mBasePort + worker_id` (typically 61002 + worker_id) for audio transport
+
+## Building with Connection Type Support
+
+### WebRTC Support
+
+```bash
+# Auto-detect libdatachannel (default)
+meson setup build
+
+# Explicitly enable (error if not available)
+meson setup build -Dlibdatachannel=enabled
+
+# Explicitly disable
+meson setup build -Dlibdatachannel=disabled
+```
+
+When enabled, defines `WEBRTC_SUPPORT` macro.
+
+### WebTransport Support
+
+```bash
+# Auto-detect msquic (default)
+meson setup build
+
+# Explicitly enable (error if not available)
+meson setup build -Dmsquic=enabled
+
+# Explicitly disable
+meson setup build -Dmsquic=disabled
+```
+
+When enabled, defines `WEBTRANSPORT_SUPPORT` macro.
+
+## Server Configuration
+
+### Starting the Server
+
+```bash
+# Start hub server (binds to both TCP and UDP port 4464)
+jacktrip -S
+
+# Specify custom server port
+jacktrip -S -p 4464
+
+# Specify custom UDP base port for legacy UDP audio
+jacktrip -S --udpbaseport 61002
+
+# Configure ICE servers for WebRTC
+jacktrip -S --iceservers "stun:stun.l.google.com:19302"
+
+# Enable TLS for WebTransport (and optionally UDP auth)
+jacktrip -S --certfile server.crt --keyfile server.key
+```
+
+### Firewall Configuration
+
+```bash
+# TCP for UDP and WebRTC signaling
+sudo ufw allow 4464/tcp
+
+# UDP for WebTransport and legacy audio
+sudo ufw allow 4464/udp
+
+# UDP port range for legacy UDP audio streams
+sudo ufw allow 61002:62000/udp
+```
+
+### Port Summary
+
+| Connection Type | Port | Protocol | Purpose |
+|----------------|------|----------|---------|
+| UDP | 4464 | TCP | Signaling handshake |
+| UDP | 61002 + worker_id | UDP | Audio transport |
+| WebRTC | 4464 | TCP | WebSocket signaling |
+| WebRTC | ICE-negotiated | UDP | Audio transport (data channels) |
+| WebTransport | 4464 | UDP | QUIC (signaling + audio) |
+
+## Error Handling
+
+### UDP Errors
+
+- **Port already bound**: Worker slot exhausted or port conflict
+- **Authentication failed**: Invalid credentials (if auth enabled)
+- **Timeout**: Client doesn't send UDP packets after handshake
+
+### WebRTC Errors
+
+- **ICE failed**: No connectivity path found
+- **DTLS handshake failed**: Certificate or crypto mismatch
+- **Data channel failed**: SCTP association error
+
+### WebTransport Errors
+
+- **Handshake failed**: Invalid HTTP/3 CONNECT request
+- **Certificate verification failed**: Invalid or untrusted TLS certificate
+- **Session closed**: QUIC connection terminated
+
+## References
+
+### Source Files
+
+- **UDP**: `src/UdpHubListener.cpp`, `src/UdpDataProtocol.cpp`
+- **WebRTC**: `src/webrtc/WebRtcPeerConnection.cpp`, `src/webrtc/WebRtcDataProtocol.cpp`, `src/webrtc/WebRtcSignalingProtocol.cpp`
+- **WebTransport**: `src/webtransport/WebTransportSession.cpp`, `src/webtransport/WebTransportDataProtocol.cpp`
+- **Worker**: `src/JackTripWorker.cpp`
+
+### External Documentation
+
+- [NetworkProtocol.md](NetworkProtocol.md) - Detailed packet format and UDP protocol
+- [WebRTC Specification](https://www.w3.org/TR/webrtc/)
+- [WebTransport Specification](https://www.w3.org/TR/webtransport/)
+- [RFC 9221 - QUIC Datagrams](https://www.rfc-editor.org/rfc/rfc9221.html)
+- [libdatachannel](https://github.com/paullouisageneau/libdatachannel)
+- [MsQuic](https://github.com/microsoft/msquic)
index fefba86c4dbd0871baf67af6cb5be6ae2818c6b1..5ec2fdb008ffdb37046e157a76fae3ce5e6af45d 100644 (file)
@@ -4,7 +4,7 @@ This document describes JackTrip’s **on-the-wire protocol** as implemented in
 
 ### Scope and non-goals
 
-- **In scope**: the real-time **UDP audio stream**, its headers and payload layout, the optional **UDP redundancy** framing, the small **UDP “stop” control packet**, and the **TCP handshake** used by hub/ping-server style deployments (including the authentication variant).
+- **In scope**: the real-time **UDP audio stream**, its headers and payload layout, the optional **UDP redundancy** framing, the small **UDP “stop” control packet**, the **TCP handshake** used by hub/ping-server style deployments (including the authentication variant), the **WebRTC data channel transport** (used by the hub server’s WebRTC path), and the **WebTransport transport** (HTTP/3 over QUIC datagrams).
 - **Out of scope**: local-only IPC (e.g. `QLocalSocket` “AudioSocket”), OSC control, and any higher-level application semantics outside packet exchange.
 
 ### Transports at a glance
@@ -12,6 +12,8 @@ This document describes JackTrip’s **on-the-wire protocol** as implemented in
 - **UDP (audio)**: real-time audio is sent as UDP datagrams containing `PacketHeader` + raw audio payload.
 - **UDP (control)**: a small fixed-size “stop” datagram is used to signal shutdown.
 - **TCP (hub/ping-server handshake)**: a short-lived TCP connection is used to exchange ephemeral UDP port information (and optionally do TLS + credentials). The client sends 4 bytes representing the port number it is binding to, and the server responds by sending 4 bytes representing its own port number.
+- **WebRTC data channel (audio)**: JackTrip hub server’s WebRTC path uses a WebRTC data channel to carry the same packet format (header + planar audio payload) as the UDP stream. Signaling uses an encrypted WebSocket (`wss://`) on the hub TCP port; plain `ws://` is not accepted. The same interleaving conversion applies. See `WebRtcDataProtocol.cpp`.
+- **WebTransport / QUIC datagrams (audio)**: the hub server’s WebTransport path uses HTTP/3 over QUIC (via msquic) with unreliable QUIC datagrams (RFC 9221) to carry the same packet format as the UDP stream. The WebTransport session is established with an HTTP/3 CONNECT request before audio flows. See `src/webtransport/` and `src/http3/`.
 
 ---
 
@@ -108,6 +110,8 @@ This is explicit in `UdpDataProtocol` which converts between:
 - **Internal**: interleaved layout \([n][c]\)
 - **Network**: planar layout \([c][n]\)
 
+For **mono** (\(C = 1\)) there is no difference between planar and interleaved layouts, so no conversion is needed. Multi-channel conversion also applies on the WebRTC data channel path (see `WebRtcDataProtocol.cpp`) and the WebTransport path.
+
 See `UdpDataProtocol::sendPacketRedundancy()` and `UdpDataProtocol::receivePacketRedundancy()` in `src/UdpDataProtocol.cpp`.
 
 ### Sample encoding (bit resolution)
@@ -239,6 +243,126 @@ See `src/UdpDataProtocol.cpp`.
 
 ---
 
+## WebRTC data channel transport
+
+JackTrip's WebRTC path carries the same audio packet format as the UDP stream but over a WebRTC data channel. It enables NAT traversal through ICE and is implemented in `src/webrtc/` using the libdatachannel library.
+
+### Why an unordered, unreliable data channel
+
+The data channel is configured for **unordered, unreliable** delivery — equivalent to UDP semantics — to minimise latency. Retransmissions and head-of-line blocking are explicitly disabled.
+
+### Transport requirement: encrypted WebSocket (WSS)
+
+WebRTC clients **must** connect using an encrypted WebSocket (`wss://`). Plain-text WebSocket (`ws://`) is not accepted. This requirement exists because browsers only allow `wss://` from HTTPS pages, and because the TLS layer is what allows the server to multiplex WebRTC and legacy binary clients on the same port (see "Protocol detection" below).
+
+The server must be started with `--certfile` and `--keyfile` for WebRTC connections to succeed. Without a loaded TLS certificate, any TLS ClientHello is rejected with a logged error and the connection is closed.
+
+### Protocol detection on the hub TCP port
+
+The hub server multiplexes three connection types on a single TCP listen port. The server inspects the first three bytes of each new connection to route it correctly:
+
+| First 3 bytes (hex)             | Interpretation                                                                    |
+| ------------------------------- | --------------------------------------------------------------------------------- |
+| `16 03 01` through `16 03 04`   | TLS ClientHello (browser `wss://`) — start TLS handshake; re-detect after decrypt |
+| Anything else                   | Legacy binary hub protocol — read 32-bit LE port number                           |
+
+After the TLS handshake completes the server inspects the first decrypted bytes:
+
+| Decrypted content              | Interpretation                                               |
+| ------------------------------ | ------------------------------------------------------------ |
+| `GET /ping …`                  | Health-check endpoint — responds `{"status":"OK"}` and closes |
+| `GET /webrtc …`                | HTTP WebSocket upgrade → WebRTC signaling path               |
+| Other `GET …`                  | Unsupported path — responds HTTP 404 and closes              |
+| Other binary data              | Authenticated binary hub protocol (credentials follow)       |
+
+**Why 3 bytes are needed for unambiguous TLS detection**
+
+The binary protocol sends a 32-bit little-endian port number (≤ 65535) as its first 4 bytes, which means bytes 2 and 3 are always `0x00`. TLS record headers always have byte 2 set to `0x01`–`0x04` (the TLS minor version). Checking only the first byte would produce false positives: port 22 (`{0x16, 0x00, …}`) and port 790 (`{0x16, 0x03, 0x00, …}`) both start with byte sequences that overlap with TLS. Requiring all three bytes `{0x16, 0x03, 0x01–0x04}` is provably collision-free with any valid port number.
+
+See `UdpHubListener::readyRead()` in `src/UdpHubListener.cpp`.
+
+### Health-check endpoint (`GET /ping`)
+
+The hub server exposes a simple health-check endpoint on the same TLS port as WebRTC signaling. It is useful for diagnosing TLS and HTTP connectivity issues before attempting a WebSocket upgrade.
+
+**Request:**
+```
+GET /ping HTTP/1.1
+```
+
+**Response:**
+```
+HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 15
+Connection: close
+
+{"status":"OK"}
+```
+
+The connection is closed immediately after the response is sent. The endpoint is only available when the binary is built with `WEBRTC_SUPPORT` and when TLS is configured (`--certfile` / `--keyfile`). A plain `curl` command can verify connectivity:
+
+```bash
+curl -k https://<host>:<port>/ping
+```
+
+### Signaling message framing
+
+All signaling messages are JSON objects framed with a **4-byte big-endian length prefix** over the TCP socket:
+
+```
+[4-byte length (BE)] [JSON payload]
+```
+
+### Signaling flow
+
+1. Client opens a TLS connection to the hub TCP port (`wss://`). The server detects the TLS ClientHello by its 3-byte record header and performs the TLS handshake.
+2. Client sends an HTTP `GET /webrtc` request with `Upgrade: websocket` headers. The server upgrades the connection to a WebSocket.
+3. Client sends `PROTOCOL_DETECT` message: `{"type": "protocol_detect", "protocol": 2, "clientName": "…", "version": 1}`.
+4. Server responds with its own `PROTOCOL_DETECT` acknowledgement.
+5. Client sends an `OFFER` message containing its SDP.
+6. Server sets the remote description, generates an answer, and sends an `ANSWER` message.
+7. Both sides exchange `ICE_CANDIDATE` messages as ICE candidates are gathered.
+8. ICE + DTLS handshake completes; the data channel (label `"audio"`) opens.
+9. Audio datagrams flow bidirectionally over the data channel.
+
+Either side can send a `HANGUP` message to terminate the session.
+
+### Packet format
+
+Identical to UDP: the standard JackTrip packet header followed by the planar audio payload. The same non-interleaved channel layout and interleaving conversion apply. No additional framing is added inside the data channel message.
+
+See `src/webrtc/WebRtcDataProtocol.cpp`, `src/webrtc/WebRtcPeerConnection.cpp`, and `src/webrtc/WebRtcSignalingProtocol.cpp`.
+
+---
+
+## WebTransport transport (HTTP/3 / QUIC datagrams)
+
+JackTrip's WebTransport path carries the same audio packet format as the UDP stream but over HTTP/3 using unreliable QUIC datagrams (RFC 9221). It is implemented in `src/webtransport/` and `src/http3/`, using Microsoft's msquic library.
+
+### Why QUIC datagrams
+
+QUIC datagrams provide UDP-like unreliable, unordered delivery without head-of-line blocking, which makes them well suited for low-latency audio. The QUIC transport layer still handles path MTU discovery and congestion signalling.
+
+### Connection setup
+
+1. The server starts an `Http3Server` (backed by msquic) listening on a configured UDP port with a TLS certificate.
+2. The client opens a QUIC connection and performs a TLS handshake.
+3. Both sides exchange HTTP/3 `SETTINGS` frames on their respective control streams to advertise WebTransport support and datagram receipt capability.
+4. The client sends an HTTP/3 `CONNECT` request with `:protocol: webtransport` and a path such as `/webtransport?name=MyClient`. The optional `name` query parameter identifies the client (equivalent to the remote name in the TCP handshake).
+5. The server replies with HTTP/3 status `200`, accepting the session.
+6. Audio datagrams flow bidirectionally once the session is accepted.
+
+Each QUIC datagram payload is prefixed with a **quarter stream ID** (a QUIC varint encoding of `stream_id / 4`) per the WebTransport-over-HTTP/3 framing spec, followed immediately by the standard JackTrip packet (header + planar audio payload). Receivers strip the quarter stream ID prefix before processing.
+
+### Packet format
+
+Identical to UDP: `DefaultHeaderStruct` (or the selected header type) followed by the planar audio payload. Redundancy and all other payload conventions apply unchanged.
+
+See `src/webtransport/WebTransportSession.cpp`, `src/http3/Http3Server.cpp`, and `src/http3/Http3Protocol.cpp`.
+
+---
+
 ## References
 
 For additional context on JackTrip's network behavior and interpretation of debug output (`-V` flag):
index ee60b87d655071518908f6b2db3d94ddd2c71ea7..1686df66f3da0098fe143c7f4e0e3786fe288f5d 100644 (file)
@@ -1,3 +1,19 @@
+- Version: "3.0.0"
+  Date: 2026-04-25
+  Description:
+  - (added) VS Mode now uses jacktrip.com for studios list
+  - (added) Hub server support for WebTransport connections
+  - (added) Hub server support for WebRTC datachannel connections
+  - (updated) Default OSC port is now hub server port + 1
+  - (updated) Re-enabling classic mode for signed Windows builds
+  - (updated) VS Mode prefer default audio devices on first run
+  - (updated) VS Mode restore previously used window dimensions
+  - (fixed) Potential hub server crash if invalid peer settings
+  - (fixed) Use DSCP value of 46 instead of 56 for UDP packets
+  - (fixed) Added guards to protect against buffer overflows
+  - (fixed) Deep links were sometimes being ignored on Windows
+  - (fixed) Sending deep links caused segmentation fault on OSX
+  - (fixed) Stats timer lifecycle issues
 - Version: "2.7.2"
   Date: 2026-02-06
   Description:
index 9d30e16a6235b9e3ce5d5f5cb839d31034979bbb..6a59b2481366fd49ddef5bfd89f94aff6b67ac12 100644 (file)
@@ -27,7 +27,7 @@ RUN apt-get update \
   && apt-get install -yq --no-install-recommends help2man clang-tidy desktop-file-utils
 RUN python3 -m pip install --upgrade pip \
   && python3 -m pip install --upgrade certifi \
-  && python3 -m pip install meson pyyaml Jinja2
+  && python3 -m pip install meson==1.10.2 pyyaml Jinja2
 
 WORKDIR /opt/jacktrip
 
index 277dd02269a137729d88449d25d790cbe169be2a..63fcc99911d8e0798dc385cc2008738bf13fae68 100644 (file)
@@ -159,6 +159,9 @@ else
 endif
 deps += qt_core_deps
 
+# Get Qt installation prefix for cmake subprojects
+qt_prefix = run_command(qmake, '-query', 'QT_INSTALL_PREFIX', check : true).stdout().strip()
+
 if get_option('nogui') == true or (get_option('noclassic') == true and get_option('novs') == true)
        # command line only
        defines += '-DNO_GUI'
@@ -384,6 +387,96 @@ if found_libsamplerate
        deps += libsamplerate_dep
 endif
 
+# WebRTC Data Channel Support (requires libdatachannel)
+found_libdatachannel = false
+libdatachannel_extra_deps = []
+if get_option('libdatachannel').allowed()
+       # First try to find libdatachannel as a system dependency
+       libdatachannel_dep = dependency('libdatachannel', required: false)
+       if libdatachannel_dep.found()
+               found_libdatachannel = true
+       else
+               # Try to build libdatachannel as a subproject
+               opt_var = cmake.subproject_options()
+               if get_option('buildtype') == 'release'
+                       opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Release'})
+               else
+                       opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Debug'})
+               endif
+               opt_var.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'})
+               # Build static library only
+               opt_var.add_cmake_defines({'BUILD_SHARED_LIBS': 'OFF'})
+               # Disable features we don't need to speed up build
+               opt_var.add_cmake_defines({'NO_WEBSOCKET': 'ON'})
+               opt_var.add_cmake_defines({'NO_MEDIA': 'ON'})
+               opt_var.add_cmake_defines({'NO_EXAMPLES': 'ON'})
+               opt_var.add_cmake_defines({'NO_TESTS': 'ON'})
+               opt_var.add_cmake_defines({'USE_GNUTLS': '0'})
+               opt_var.add_cmake_defines({'USE_NICE': '0'})
+               opt_var.add_cmake_defines({'CMAKE_PREFIX_PATH': qt_prefix})
+               libdatachannel_subproject = cmake.subproject('libdatachannel', options: opt_var, required: false)
+               if libdatachannel_subproject.found()
+                       libdatachannel_dep = libdatachannel_subproject.dependency('datachannel')
+                       found_libdatachannel = libdatachannel_dep.found()
+                       # When building as a static library, we need to also link the private
+                       # dependencies (libjuice and usrsctp) that libdatachannel uses internally
+                       if found_libdatachannel
+                               libdatachannel_extra_deps += libdatachannel_subproject.dependency('juice')
+                               libdatachannel_extra_deps += libdatachannel_subproject.dependency('usrsctp')
+                       endif
+               endif
+               if not found_libdatachannel and not get_option('libdatachannel').auto()
+                       error('libdatachannel requested but could not be configured')
+               endif
+       endif
+endif
+if found_libdatachannel
+       defines += '-DWEBRTC_SUPPORT'
+       deps += libdatachannel_dep
+       deps += libdatachannel_extra_deps
+       src += [
+               'src/webrtc/WebRtcDataProtocol.cpp',
+               'src/webrtc/WebRtcPeerConnection.cpp',
+               'src/webrtc/WebRtcSignalingProtocol.cpp',
+               'src/webrtc/WebSocketSignalingConnection.cpp'
+       ]
+       moc_h += [
+               'src/webrtc/WebRtcDataProtocol.h',
+               'src/webrtc/WebRtcPeerConnection.h',
+               'src/webrtc/WebRtcSignalingProtocol.h',
+               'src/webrtc/WebSocketSignalingConnection.h'
+       ]
+endif
+
+# WebTransport support (requires msquic)
+found_msquic = false
+if get_option('msquic').allowed()
+       # Try to get msquic dependency (from system or subproject with native meson.build)
+       msquic_dep = dependency('msquic', required: false)
+       if not msquic_dep.found()
+               # Try the subproject with native meson.build overlay
+               msquic_subproject = subproject('msquic', required: get_option('msquic').enabled(), default_options: ['qt_prefix=' + qt_prefix])
+               if msquic_subproject.found()
+                       msquic_dep = msquic_subproject.get_variable('msquic_dep')
+               endif
+       endif
+       found_msquic = msquic_dep.found()
+endif
+if found_msquic
+       defines += '-DWEBTRANSPORT_SUPPORT'
+       deps += msquic_dep
+       src += [
+               'src/http3/Http3Protocol.cpp',
+               'src/http3/Http3Server.cpp',
+               'src/webtransport/WebTransportSession.cpp',
+               'src/webtransport/WebTransportDataProtocol.cpp'
+       ]
+       moc_h += [
+               'src/webtransport/WebTransportSession.h',
+               'src/webtransport/WebTransportDataProtocol.h'
+       ]
+endif
+
 if host_machine.system() == 'darwin'
        src += ['src/NoNap.mm']
        # Adding CoreAudio here is a workaround and should be removed
@@ -463,4 +556,6 @@ summary({'Application ID': application_id,
        'GUI': not get_option('nogui'),
        'WAIR': get_option('wair'),
        'Sample rate conversions': found_libsamplerate,
+       'WebRTC Data Channels': found_libdatachannel,
+       'WebTransport API': found_msquic,
        'Manpage': help2man.found()}, bool_yn: true, section: 'Configuration')
index a1850ae43692c3384c727cd8a5729b72b92b5b9a..4d478d9eec704f95a4d59ae06edc0b3c2075e599 100644 (file)
@@ -17,4 +17,8 @@ option('buildinfo', type : 'string', value : '', yield : true, description: 'Add
 option('vst-libdir', type : 'string', value : '', yield : true,
   description : 'Directory with VST SDK3 libraries (e.g. libsdk.a, libbase.a)')
 option('vst-sdkdir', type : 'string', value : '', yield : true,
-  description : 'Directory with VST SDK3 headers (e.g. public.sdk/source/vst/hosting/module.h)')
\ No newline at end of file
+  description : 'Directory with VST SDK3 headers (e.g. public.sdk/source/vst/hosting/module.h)')
+option('libdatachannel', type : 'feature', value : 'disabled',
+  description : 'Build with WebRTC data channel support (requires libdatachannel)')
+option('msquic', type : 'feature', value : 'disabled',
+  description : 'Build with WebTransport support (requires msquic)')
\ No newline at end of file
index 044cdbb2a747c487ec5d8476f4afd6c0975a2993..8078ea3755ed1a3fe66f7e39589126b8c021897a 100644 (file)
@@ -487,6 +487,12 @@ void AudioInterface::computeProcessToNetwork(QVarLengthArray<sample_t*>& in_buff
     // Concatenate  all the channels from jack to form packet
 
 #ifdef WAIR  // WAIR
+
+#define INGAIN \
+    (0.9999)  // 0.9999 because 1.0 can saturate the fixed pt rounding on output
+
+#define COMBGAIN (1.0)
+
     if (mNumNetRevChans)
         for (int i = 0; i < mNumNetRevChans; i++) {
             sample_t* tmp_sample =
@@ -498,9 +504,6 @@ void AudioInterface::computeProcessToNetwork(QVarLengthArray<sample_t*>& in_buff
                 // Change the bit resolution on each sample
                 // Add the input jack buffer to the buffer resulting from the output
                 // process
-#define INGAIN \
-    (0.9999)  // 0.9999 because 1.0 can saturate the fixed pt rounding on output
-#define COMBGAIN (1.0)
                 tmp_result = INGAIN * tmp_sample[j] + COMBGAIN * tmp_process_sample[j];
                 fromSampleToBitConversion(
                     &tmp_result,
index 64e141ea73ae566dda67d8bb6eb67ba54713f12b..a20ccecf35f301754601202ff96547db4dd4b06e 100644 (file)
@@ -165,10 +165,10 @@ void AudioTester::lookForReturnPulse(QVarLengthArray<sample_t*>& out_buffer,
                 }  // found our impulse
                 // remain pending until timeout, hoping to find our return pulse
             }  // got something
-        }      // loop over samples
+        }  // loop over samples
         sampleCountSinceImpulse +=
             n_frames;  // gets reset to 1 when impulse is found, counts freely until then
-    }                  // ImpulsePending
+    }  // ImpulsePending
 }
 
 // Called 2nd in Audiointerface.cpp
index 0332c4e9dc5f0043e97392f680fe599533e7694a..7a5cb8edab2905611df1a5072393a3f8ba36194e 100644 (file)
@@ -462,11 +462,11 @@ class Effects
                                     << "*** Effects.h: parseCompressorArgs: lastParam "
                                     << lastParam << " invalid\n";
                                 returnCode = 3;  // "reality failure"
-                            }                    // switch(lastParam)
-                        }                        // have valid parameter from atof
+                            }  // switch(lastParam)
+                        }  // have valid parameter from atof
                     }  // have valid non-alpha char for parameter
-                }      // switch(ch)
-            }          // for (ulong i=0; i<argLen; i++) {
+                }  // switch(ch)
+            }  // for (ulong i=0; i<argLen; i++) {
             if (inOrOut == IO_IN) {
                 inCompressorPreset = newPreset;
             } else if (inOrOut == IO_OUT) {
@@ -612,7 +612,7 @@ class Effects
                     break;
                 default:
                     break;  // ignore
-                }           // switch(optarg[i])
+                }  // switch(optarg[i])
             }
         }
         return returnCode;
@@ -658,7 +658,7 @@ class Effects
                               << ch << "\n";
                     returnCode = 2;
                 }  // switch(ch)
-            }      // process optarg char ch
+            }  // process optarg char ch
             mLimit =
                 (haveIncoming && haveOutgoing
                      ? LIMITER_BOTH
@@ -685,7 +685,7 @@ class Effects
                     std::cout << "Set up NO Overflow Limiters\n";
                 }
             }  // gVerboseFlag
-        }      // optarg cases
+        }  // optarg cases
         return returnCode;
     }  // parseLimiterOptArg()
 
index 2813da17e3489469916c2db2bb46ae37d85ccb05..c74ebe15bdc7c47274b336c0f68f9bbf441b3b38 100644 (file)
 #ifdef RT_AUDIO
 #include "RtAudioInterface.h"
 #endif
+#ifdef WEBRTC_SUPPORT
+#include "webrtc/WebRtcDataProtocol.h"
+#endif
+#ifdef WEBTRANSPORT_SUPPORT
+#include "webtransport/WebTransportDataProtocol.h"
+#endif
 
 #include <QDateTime>
 #include <QHostAddress>
@@ -64,6 +70,7 @@
 #include <stdexcept>
 using std::setw;
 
+using std::cerr;
 using std::cout;
 using std::endl;
 
@@ -146,6 +153,7 @@ JackTrip::JackTrip(jacktripModeT JacktripMode, dataProtocolT DataProtocolType,
     , mConnectDefaultAudioPorts(true)
     , mIOStatTimeout(0)
     , mIOStatLogStream(std::cout.rdbuf())
+    , mStatTimer(nullptr)
     , mSimulatedLossRate(0.0)
     , mSimulatedJitterRate(0.0)
     , mSimulatedDelayRel(0.0)
@@ -380,6 +388,55 @@ void JackTrip::setupDataProtocol()
     case SCTP:
         throw std::invalid_argument("SCTP Protocol is not implemented");
         break;
+    case WEBRTC:
+#ifdef WEBRTC_SUPPORT
+        cout << "JackTrip::setupDataProtocol: Using WebRTC Data Channel Protocol" << endl;
+        QThread::usleep(100);
+        // Note: WebRTC data protocol requires a data channel to be set externally
+        // via setWebRtcDataChannel before calling setupDataProtocol
+        if (!mWebRtcDataChannel) {
+            cerr << "JackTrip::setupDataProtocol: ERROR - WebRTC data channel not set!"
+                 << endl;
+            throw std::invalid_argument(
+                "WebRTC Protocol requires data channel to be set first");
+        }
+        mDataProtocolSender =
+            new WebRtcDataProtocol(this, DataProtocol::SENDER, mWebRtcDataChannel);
+        mDataProtocolReceiver =
+            new WebRtcDataProtocol(this, DataProtocol::RECEIVER, mWebRtcDataChannel);
+        mDataProtocolSender->setUseRtPriority(mUseRtUdpPriority);
+        mDataProtocolReceiver->setUseRtPriority(mUseRtUdpPriority);
+        std::cout << gPrintSeparator << std::endl;
+        break;
+#else
+        throw std::invalid_argument("WebRTC Protocol support not compiled in");
+        break;
+#endif
+    case WEBTRANSPORT:
+#ifdef WEBTRANSPORT_SUPPORT
+        std::cout << "JackTrip::setupDataProtocol: Using WebTransport Protocol"
+                  << std::endl;
+        QThread::usleep(100);
+        // Note: WebTransport protocol requires a session to be set externally
+        // via setWebTransportSession before calling setupDataProtocol
+        if (!mWebTransportSession) {
+            cerr << "JackTrip::setupDataProtocol: ERROR - WebTransport session not set!"
+                 << endl;
+            throw std::invalid_argument(
+                "WebTransport Protocol requires session to be set first");
+        }
+        mDataProtocolSender   = new WebTransportDataProtocol(this, DataProtocol::SENDER,
+                                                             mWebTransportSession);
+        mDataProtocolReceiver = new WebTransportDataProtocol(this, DataProtocol::RECEIVER,
+                                                             mWebTransportSession);
+        mDataProtocolSender->setUseRtPriority(mUseRtUdpPriority);
+        mDataProtocolReceiver->setUseRtPriority(mUseRtUdpPriority);
+        std::cout << gPrintSeparator << std::endl;
+        break;
+#else
+        throw std::invalid_argument("WebTransport Protocol support not compiled in");
+        break;
+#endif
     default:
         throw std::invalid_argument("Protocol not defined or unimplemented");
         break;
@@ -422,8 +479,7 @@ void JackTrip::setupRingBuffers()
                 new RingBuffer(audio_output_slot_size, mBufferQueueLength);
             mPacketHeader->setBufferRequiresSameSettings(true);
         } else if ((mBufferStrategy == 3) || (mBufferStrategy == 4)) {
-            cout << "Using experimental buffer strategy " << mBufferStrategy
-                 << "-- Regulator with PLC" << endl;
+            cout << "Using Regulator buffer strategy " << mBufferStrategy << endl;
             Regulator* regulator_ptr =
                 new Regulator(mNumAudioChansOut, mAudioBitResolution, mAudioBufferSize,
                               mBufferQueueLength, mBroadcastQueueLength, mSampleRate);
@@ -500,35 +556,41 @@ void JackTrip::startProcess(
     }
 #endif*/
     // Check if ports are already binded by another process on this machine
+    // Skip this check for WebRTC and WebTransport which don't use UDP sockets
     // ------------------------------------------------------------------
     if (gVerboseFlag)
         std::cout << "step 1" << std::endl;
 
-    if (gVerboseFlag)
-        std::cout
-            << "  JackTrip:startProcess before checkIfPortIsBinded(mReceiverBindPort)"
-            << std::endl;
-#if defined _WIN32
-        // cc fixed windows crash with this print statement!
-        // qDebug() << "before mJackTrip->startProcess" << mReceiverBindPort<<
-        // mSenderBindPort;
-#endif
-    if (checkIfPortIsBinded(mReceiverBindPort)) {
-        stop(QStringLiteral("Could not bind %1 UDP socket. It may already be binded by "
-                            "another process on "
-                            "your machine. Try using a different port number")
-                 .arg(mReceiverBindPort));
-        return;
-    }
-    if (gVerboseFlag)
-        std::cout << "  JackTrip:startProcess before checkIfPortIsBinded(mSenderBindPort)"
-                  << std::endl;
-    if (checkIfPortIsBinded(mSenderBindPort)) {
-        stop(QStringLiteral("Could not bind %1 UDP socket. It may already be binded by "
-                            "another process on "
-                            "your machine. Try using a different port number")
-                 .arg(mSenderBindPort));
-        return;
+    if (mDataProtocol != WEBRTC && mDataProtocol != WEBTRANSPORT) {
+        if (gVerboseFlag)
+            std::cout
+                << "  JackTrip:startProcess before checkIfPortIsBinded(mReceiverBindPort)"
+                << std::endl;
+        if (checkIfPortIsBinded(mReceiverBindPort)) {
+            stop(QStringLiteral(
+                     "Could not bind %1 UDP socket. It may already be binded by "
+                     "another process on "
+                     "your machine. Try using a different port number")
+                     .arg(mReceiverBindPort));
+            return;
+        }
+        if (gVerboseFlag)
+            std::cout
+                << "  JackTrip:startProcess before checkIfPortIsBinded(mSenderBindPort)"
+                << std::endl;
+        if (checkIfPortIsBinded(mSenderBindPort)) {
+            stop(QStringLiteral(
+                     "Could not bind %1 UDP socket. It may already be binded by "
+                     "another process on "
+                     "your machine. Try using a different port number")
+                     .arg(mSenderBindPort));
+            return;
+        }
+    } else {
+        if (gVerboseFlag)
+            std::cout << "  JackTrip:startProcess skipping port bind check for "
+                      << (mDataProtocol == WEBRTC ? "WebRTC" : "WebTransport")
+                      << std::endl;
     }
     // Set all classes and parameters
     // ------------------------------
@@ -567,9 +629,28 @@ void JackTrip::startProcess(
                      &JackTrip::slotReceivedConnectionFromPeer, Qt::QueuedConnection);
     // QObject::connect(this, SIGNAL(signalUdpTimeOut()),
     //                 this, SLOT(slotStopProcesses()), Qt::QueuedConnection);
-    QObject::connect(static_cast<UdpDataProtocol*>(mDataProtocolReceiver),
-                     &UdpDataProtocol::signalUdpWaitingTooLong, this,
-                     &JackTrip::slotUdpWaitingTooLong, Qt::QueuedConnection);
+
+    // Connect protocol-specific signals
+    if (mDataProtocol == UDP) {
+        QObject::connect(static_cast<UdpDataProtocol*>(mDataProtocolReceiver),
+                         &UdpDataProtocol::signalUdpWaitingTooLong, this,
+                         &JackTrip::slotUdpWaitingTooLong, Qt::QueuedConnection);
+    }
+#ifdef WEBRTC_SUPPORT
+    else if (mDataProtocol == WEBRTC) {
+        QObject::connect(static_cast<WebRtcDataProtocol*>(mDataProtocolReceiver),
+                         &WebRtcDataProtocol::signalWaitingTooLong, this,
+                         &JackTrip::slotUdpWaitingTooLong, Qt::QueuedConnection);
+    }
+#endif
+#ifdef WEBTRANSPORT_SUPPORT
+    else if (mDataProtocol == WEBTRANSPORT) {
+        QObject::connect(static_cast<WebTransportDataProtocol*>(mDataProtocolReceiver),
+                         &WebTransportDataProtocol::signalWaitingTooLong, this,
+                         &JackTrip::slotUdpWaitingTooLong, Qt::QueuedConnection);
+    }
+#endif
+
     QObject::connect(mDataProtocolSender, &DataProtocol::signalCeaseTransmission, this,
                      &JackTrip::slotStopProcessesDueToError, Qt::QueuedConnection);
     QObject::connect(mDataProtocolReceiver, &DataProtocol::signalCeaseTransmission, this,
@@ -616,14 +697,25 @@ void JackTrip::startProcess(
     case SERVERPINGSERVER:
         if (gVerboseFlag)
             std::cout << "step 2S server only (same as 2s)" << std::endl;
-        if (gVerboseFlag)
-            std::cout
-                << "  JackTrip:startProcess case SERVERPINGSERVER before serverStart"
-                << std::endl;
-        if (serverStart(true)
-            == -1) {  // if error on server start (-1) we return immediately
-            stop();
-            return;
+        // For WebRTC and WebTransport, skip UDP server start and go directly to
+        // connection completion
+        if (mDataProtocol == WEBRTC || mDataProtocol == WEBTRANSPORT) {
+            if (gVerboseFlag)
+                std::cout << "  JackTrip:startProcess "
+                          << (mDataProtocol == WEBRTC ? "WebRTC" : "WebTransport")
+                          << " mode, skipping serverStart" << std::endl;
+            // Data channel/session is already open, proceed directly to start threads
+            completeConnection();
+        } else {
+            if (gVerboseFlag)
+                std::cout
+                    << "  JackTrip:startProcess case SERVERPINGSERVER before serverStart"
+                    << std::endl;
+            if (serverStart(true) == -1) {
+                // if error on server start (-1) we return immediately
+                stop();
+                return;
+            }
         }
         break;
     default:
@@ -645,13 +737,16 @@ void JackTrip::completeConnection()
 
     // Start Threads
     if (gVerboseFlag)
-        std::cout << "  JackTrip:startProcess before mDataProtocolReceiver->start"
-                  << std::endl;
+        std::cout
+            << "  JackTrip::completeConnection: Starting data protocol receiver thread..."
+            << std::endl;
     mDataProtocolReceiver->start();
     mDataProtocolReceiver->waitForStart();
+
     if (gVerboseFlag)
-        std::cout << "  JackTrip:startProcess before mDataProtocolSender->start"
-                  << std::endl;
+        std::cout
+            << "  JackTrip::completeConnection: Starting data protocol sender thread..."
+            << std::endl;
     mDataProtocolSender->start();
     mDataProtocolSender->waitForStart();
     /*
@@ -686,13 +781,14 @@ void JackTrip::completeConnection()
 
     // Start our IO stat timer
     if (mIOStatTimeout > 0) {
-        cout << "STATS" << mIOStatTimeout << endl;
+        cout << "Starting stat timer with interval " << mIOStatTimeout << " seconds"
+             << endl;
         if (!mIOStatStream.isNull()) {
             mIOStatLogStream.rdbuf((mIOStatStream.data()->rdbuf()));
         }
-        QTimer* timer = new QTimer(this);
-        connect(timer, &QTimer::timeout, this, &JackTrip::onStatTimer);
-        timer->start(mIOStatTimeout * 1000);
+        mStatTimer = new QTimer(this);
+        connect(mStatTimer, &QTimer::timeout, this, &JackTrip::onStatTimer);
+        mStatTimer->start(mIOStatTimeout * 1000);
     }
 }
 
@@ -1018,7 +1114,7 @@ void JackTrip::connectionSecured()
              << endl;
 }
 
-void JackTrip::receivedDataUDP()
+void JackTrip::receivedFirstPacketUDP()
 {
     // Stop our timer.
     {
@@ -1178,6 +1274,11 @@ void JackTrip::stop(const QString& errorMessage)
     mHasShutdown = true;
     std::cout << "Stopping JackTrip..." << std::endl;
 
+    // Stop the stats timer if it's running
+    if (mStatTimer != nullptr) {
+        mStatTimer->stop();
+    }
+
     if (mDataProtocolSender != nullptr) {
         // Stop The Sender
         mDataProtocolSender->stop();
@@ -1277,18 +1378,19 @@ int JackTrip::serverStart(bool timeout, int udpTimeout)  // udpTimeout unused
             mAwaitingUdp = false;
             mTimeoutTimer.stop();
         }
-        std::cerr << "in JackTrip: Could not bind UDP socket. It may be already binded."
-                  << endl;
+        cerr << "in JackTrip: Could not bind UDP socket. It may be already binded."
+             << endl;
         throw std::runtime_error("Could not bind UDP socket. It may be already binded.");
     }
-    connect(&mUdpSockTemp, &QUdpSocket::readyRead, this, &JackTrip::receivedDataUDP);
+    connect(&mUdpSockTemp, &QUdpSocket::readyRead, this,
+            &JackTrip::receivedFirstPacketUDP);
 
     if (gVerboseFlag)
         std::cout << "JackTrip:serverStart before !UdpSockTemp.hasPendingDatagrams()"
                   << std::endl;
     cout << "Waiting for Connection From a Client..." << endl;
     return 0;
-    // Continued in the receivedDataUDP slot.
+    // Continued in the receivedFirstPacketUDP slot.
 
     //    char buf[1];
     //    // set client address
@@ -1317,14 +1419,14 @@ int JackTrip::clientPingToServerStart()
             QString error_message =
                 "SSL not supported. Make sure you have the appropriate SSL "
                 "libraries\ninstalled to enable authentication.";
-            std::cerr << "ERROR: " << error_message.toStdString() << std::endl;
+            cerr << "ERROR: " << error_message.toStdString() << endl;
             stop(error_message);
             return -1;
         } else if (mUsername.isEmpty() || mPassword.isEmpty()) {
             QString error_message =
                 "You must supply a username and password to authenticate with a hub "
                 "server.";
-            std::cerr << "ERROR: " << error_message.toStdString() << std::endl;
+            cerr << "ERROR: " << error_message.toStdString() << endl;
             stop(error_message);
             return -1;
         } else {
@@ -1516,6 +1618,8 @@ void JackTrip::putHeaderInIncomingPacket(int8_t* full_packet, int8_t* audio_pack
 {
     mPacketHeader->fillHeaderCommonFromAudio();
     mPacketHeader->putHeaderInPacket(full_packet);
+    if (audio_packet == nullptr)
+        return;
 
     int8_t* audio_part;
     audio_part = full_packet + mPacketHeader->getHeaderSizeInBytes();
@@ -1529,6 +1633,8 @@ void JackTrip::putHeaderInOutgoingPacket(int8_t* full_packet, int8_t* audio_pack
 {
     mPacketHeader->fillHeaderCommonFromAudio();
     mPacketHeader->putHeaderInPacket(full_packet);
+    if (audio_packet == nullptr)
+        return;
 
     int8_t* audio_part;
     audio_part = full_packet + mPacketHeader->getHeaderSizeInBytes();
index 5a1142f6aef1189d607c50ffe8ba1718e66b69ef..288251ec4108aafde657cac4a9509094f8149184 100644 (file)
@@ -45,6 +45,7 @@
 #include <QString>
 #include <QTimer>
 #include <QUdpSocket>
+#include <functional>
 #include <stdexcept>
 
 #include "AudioInterface.h"
 #include "PacketHeader.h"
 #include "RingBuffer.h"
 
+#ifdef WEBRTC_SUPPORT
+#include <memory>
+namespace rtc
+{
+class DataChannel;
+}
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+class WebTransportSession;
+#endif
+
 // #include <signal.h>
 /** \brief Main class to creates a SERVER (to listen) or a CLIENT (to connect
  * to a listening server) to send audio streams in the network.
@@ -75,9 +88,12 @@ class JackTrip : public QObject
     //----------ENUMS------------------------------------------
     /// \brief Enum for the data Protocol. At this time only UDP is implemented
     enum dataProtocolT {
-        UDP,  ///< Use UDP (User Datagram Protocol)
-        TCP,  ///< <B>NOT IMPLEMENTED</B>: Use TCP (Transmission Control Protocol)
-        SCTP  ///< <B>NOT IMPLEMENTED</B>: Use SCTP (Stream Control Transmission Protocol)
+        UDP,          ///< Use UDP (User Datagram Protocol)
+        TCP,          ///< <B>NOT IMPLEMENTED</B>: Use TCP (Transmission Control Protocol)
+        SCTP,         ///< <B>NOT IMPLEMENTED</B>: Use SCTP (Stream Control Transmission
+                      ///< Protocol)
+        WEBRTC,       ///< Use WebRTC Data Channels (requires libdatachannel)
+        WEBTRANSPORT  ///< Use WebTransport (HTTP/3 based transport)
     };
 
     /// \brief Enum for the JackTrip mode
@@ -391,9 +407,26 @@ class JackTrip : public QObject
     void putHeaderInOutgoingPacket(int8_t* full_packet, int8_t* audio_packet);
     int getSendPacketSizeInBytes() const;
     int getReceivePacketSizeInBytes() const;
+
+    /// \brief Callback type for direct packet sending (bypasses ring buffer)
+    using DirectSendCallback = std::function<void(const int8_t*, int)>;
+
+    /// \brief Set a direct send callback (used by WebTransport for real-time sending)
+    void setDirectSendCallback(DirectSendCallback callback)
+    {
+        mDirectSendCallback = callback;
+    }
+
     virtual void sendNetworkPacket(const int8_t* ptrToSlot)
     {
-        mSendRingBuffer->insertSlotNonBlocking(ptrToSlot, 0, 0, 0);
+        if (mDirectSendCallback) {
+            // Use direct callback for real-time protocols (WebTransport)
+            // ptrToSlot points to audio data only (no header), so pass audio size only
+            mDirectSendCallback(ptrToSlot, getTotalAudioInputPacketSizeInBytes());
+        } else {
+            // Use ring buffer for traditional protocols (UDP, WebRTC)
+            mSendRingBuffer->insertSlotNonBlocking(ptrToSlot, 0, 0, 0);
+        }
     }
     virtual void receiveBroadcastPacket(int8_t* ptrToReadSlot)
     {
@@ -456,6 +489,10 @@ class JackTrip : public QObject
 #endif
     }
     virtual bool checkPeerSettings(int8_t* full_packet);
+    bool validatePeerHeader(const int8_t* full_packet, int received_bytes) const
+    {
+        return mPacketHeader->validatePeerHeader(full_packet, received_bytes);
+    }
     void increaseSequenceNumber() { mPacketHeader->increaseSequenceNumber(); }
     int getSequenceNumber() const { return mPacketHeader->getSequenceNumber(); }
 
@@ -489,8 +526,11 @@ class JackTrip : public QObject
         return mPacketHeader->getPeerNumIncomingChannels(full_packet);
     }
 
+    /// \brief Get the number of outgoing channels from peer packet
+    /// Decodes the space-optimized encoding where 0 means symmetric (same as incoming)
     uint8_t getPeerNumOutgoingChannels(int8_t* full_packet) const
     {
+        // If value is 0, it means symmetric configuration (outgoing == incoming)
         if (0 == mPacketHeader->getPeerNumOutgoingChannels(full_packet)) {
             return mPacketHeader->getPeerNumIncomingChannels(full_packet);
         } else {
@@ -577,6 +617,23 @@ class JackTrip : public QObject
         mSimulatedDelayRel   = delay_rel;
     }
     void setBroadcast(int broadcast_queue) { mBroadcastQueueLength = broadcast_queue; }
+
+#ifdef WEBRTC_SUPPORT
+    /// \brief Set the WebRTC data channel for transport
+    void setWebRtcDataChannel(std::shared_ptr<rtc::DataChannel> dataChannel)
+    {
+        mWebRtcDataChannel = dataChannel;
+    }
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+    /// \brief Set the WebTransport session for transport
+    void setWebTransportSession(WebTransportSession* session)
+    {
+        mWebTransportSession = session;
+    }
+#endif
+
     void queueLengthChanged(int queueLength)
     {
         emit signalQueueLengthChanged(queueLength);
@@ -598,8 +655,7 @@ class JackTrip : public QObject
      */
     void slotUdpWaitingTooLongClientGoneProbably(int wait_msec)
     {
-        int wait_time = 10000;  // msec
-        if (!(wait_msec % wait_time)) {
+        if (!(wait_msec % gClientGoneTimeoutMs)) {
             std::cerr << "UDP WAITED MORE THAN 10 seconds." << std::endl;
             if (mStopOnTimeout) {
                 stop(QStringLiteral("No network data received for 10 seconds"));
@@ -621,7 +677,7 @@ class JackTrip : public QObject
     void receivedDataTCP();
     void receivedErrorTCP(QAbstractSocket::SocketError socketError);
     void connectionSecured();
-    void receivedDataUDP();
+    void receivedFirstPacketUDP();
     void udpTimerTick();
     void tcpTimerTick();
 
@@ -708,6 +764,8 @@ class JackTrip : public QObject
     RingBuffer* mSendRingBuffer;
     /// Pointer for the Receive RingBuffer
     RingBuffer* mReceiveRingBuffer;
+    /// Direct send callback (bypasses ring buffer for real-time protocols)
+    DirectSendCallback mDirectSendCallback;
 
     int mReceiverBindPort;  ///< Incoming (receiving) port for local machine
     int mSenderPeerPort;    ///< Incoming (receiving) port for peer machine
@@ -754,12 +812,22 @@ class JackTrip : public QObject
     QSharedPointer<std::ostream> mIOStatStream;
     int mIOStatTimeout;
     std::ostream mIOStatLogStream;
+    QTimer* mStatTimer;  ///< Timer for stats reporting
     double mSimulatedLossRate;
     double mSimulatedJitterRate;
     double mSimulatedDelayRel;
     bool mUseRtUdpPriority;
 
     QSharedPointer<AudioTester> mAudioTesterP;
+
+#ifdef WEBRTC_SUPPORT
+    std::shared_ptr<rtc::DataChannel> mWebRtcDataChannel;  ///< WebRTC data channel
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+    WebTransportSession* mWebTransportSession =
+        nullptr;  ///< WebTransport session (not owned)
+#endif
 };
 
 #endif
index 2c59173678ba78ee8521109e3255254c0ebaf21e..73acff4302c7b9b0f6650c947ee60b597caa4ecc 100644 (file)
@@ -38,6 +38,7 @@
 #include "JackTripWorker.h"
 
 #include <QMutexLocker>
+#include <QScopedArrayPointer>
 #include <QTimer>
 #include <QWaitCondition>
 #include <iostream>
 #include "dcblock2gain.dsp.h"
 #endif  // endwhere
 
+#ifdef WEBRTC_SUPPORT
+#include <rtc/rtc.hpp>
+
+#include "webrtc/WebRtcDataProtocol.h"
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+#include "webtransport/WebTransportDataProtocol.h"
+#endif
+
+using std::cerr;
 using std::cout;
 using std::endl;
 
@@ -71,7 +83,7 @@ JackTripWorker::JackTripWorker(UdpHubListener* udphublistener, int BufferQueueLe
     // mNetks = new NetKS;
     // mNetks->play();
     connect(&mUdpSockTemp, &QUdpSocket::readyRead, this,
-            &JackTripWorker::receivedDataUDP);
+            &JackTripWorker::receivedFirstPacketUDP);
 }
 
 //*******************************************************************************
@@ -207,9 +219,8 @@ void JackTripWorker::start()
         cout << "---> JackTripWorker: setJackTripFromClientHeader..." << endl;
     if (!mUdpSockTemp.bind(QHostAddress::Any, mServerPort,
                            QUdpSocket::DefaultForPlatform)) {
-        std::cerr
-            << "in JackTripWorker: Could not bind UDP socket. It may already be bound."
-            << endl;
+        cerr << "in JackTripWorker: Could not bind UDP socket. It may already be bound."
+             << endl;
         throw std::runtime_error("Could not bind UDP socket. It may already be bound.");
     }
 }
@@ -232,9 +243,27 @@ void JackTripWorker::stopThread()
         mUdpSockTemp.close();
         mTimeoutTimer.stop();
     }
+
+#ifdef WEBRTC_SUPPORT
+    // Clean up WebRTC connection
+    if (mWebRtcPeerConnection) {
+        mWebRtcPeerConnection->close();
+        mWebRtcPeerConnection->deleteLater();
+        mWebRtcPeerConnection = nullptr;
+    }
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+    // Clean up WebTransport session
+    if (mWebTransportSession) {
+        mWebTransportSession->close();
+        mWebTransportSession->deleteLater();
+        mWebTransportSession = nullptr;
+    }
+#endif
 }
 
-void JackTripWorker::receivedDataUDP()
+void JackTripWorker::receivedFirstPacketUDP()
 {
     QMutexLocker lock(&mMutex);
 
@@ -246,10 +275,18 @@ void JackTripWorker::receivedDataUDP()
 
     // Set our jacktrip parameters from the received header data.
     quint16 port;
-    int packet_size     = mUdpSockTemp.pendingDatagramSize();
-    int8_t* full_packet = new int8_t[packet_size];
-    mUdpSockTemp.readDatagram(reinterpret_cast<char*>(full_packet), packet_size, nullptr,
-                              &port);
+    int packet_size = mUdpSockTemp.pendingDatagramSize();
+    if (packet_size < static_cast<int>(sizeof(DefaultHeaderStruct))) {
+        cerr << "JackTripWorker: first UDP packet too small (" << packet_size
+             << " bytes); ignoring." << endl;
+        mUdpSockTemp.close();
+        mSpawning = false;
+        mUdpHubListener->releaseThread(mID);
+        return;
+    }
+    QScopedArrayPointer<int8_t> full_packet(new int8_t[packet_size]);
+    mUdpSockTemp.readDatagram(reinterpret_cast<char*>(full_packet.get()), packet_size,
+                              nullptr, &port);
     mUdpSockTemp.close();  // close the socket
 
     // Alert the hub listener of the actual client port for incoming packets.
@@ -257,12 +294,19 @@ void JackTripWorker::receivedDataUDP()
     // variable on this object.
     mUdpHubListener->releaseDuplicateThreads(this, port);
 
+    // Process peer settings and configure channels
+    processPeerSettings(full_packet.get());
+}
+
+//*******************************************************************************
+void JackTripWorker::processPeerSettings(int8_t* full_packet)
+{
+    // Extract peer settings from the packet header
     int PeerBufferSize          = mJackTrip->getPeerBufferSize(full_packet);
     int PeerSamplingRate        = mJackTrip->getPeerSamplingRate(full_packet);
     int PeerBitResolution       = mJackTrip->getPeerBitResolution(full_packet);
     int PeerNumIncomingChannels = mJackTrip->getPeerNumIncomingChannels(full_packet);
     int PeerNumOutgoingChannels = mJackTrip->getPeerNumOutgoingChannels(full_packet);
-    delete[] full_packet;
 
     if (gVerboseFlag) {
         cout << "JackTripWorker: getPeerBufferSize       = " << PeerBufferSize << "\n"
@@ -274,6 +318,37 @@ void JackTripWorker::receivedDataUDP()
              << "\n";
     }
 
+    // Validate peer buffer size before using it for any arithmetic.
+    if (PeerBufferSize == 0 || PeerBufferSize > gMaxBufferSizeInSamples) {
+        cerr << "JackTripWorker: peer BufferSize out of range (" << PeerBufferSize
+             << "); shutting down." << endl;
+        mSpawning = false;
+        mUdpHubListener->releaseThread(mID);
+        return;
+    }
+
+    // Clamp channel counts to sane bounds before calling set methods.
+    if (PeerNumIncomingChannels < 1
+        || PeerNumIncomingChannels > static_cast<int>(gMaxAudioChannels)) {
+        cerr << "JackTripWorker: peer NumIncomingChannels out of range ("
+             << PeerNumIncomingChannels << "); shutting down." << endl;
+        mSpawning = false;
+        mUdpHubListener->releaseThread(mID);
+        return;
+    }
+    // PeerNumOutgoingChannels: 0 == NORMAL sentinel, 0xff == no-input sentinel; others
+    // clamped.
+    if (PeerNumOutgoingChannels != JackTrip::NORMAL
+        && PeerNumOutgoingChannels
+               != static_cast<int>((std::numeric_limits<uint8_t>::max)())
+        && PeerNumOutgoingChannels > static_cast<int>(gMaxAudioChannels)) {
+        cerr << "JackTripWorker: peer NumOutgoingChannels out of range ("
+             << PeerNumOutgoingChannels << "); shutting down." << endl;
+        mSpawning = false;
+        mUdpHubListener->releaseThread(mID);
+        return;
+    }
+
     // The header field for NumOutgoingChannels was used for the ConnectionMode.
     // Only the first Mode was used (NORMAL == 0). If this field is set to 0, we
     // can assume the peer is using an old version, and the last field doesn't reflect the
@@ -290,10 +365,12 @@ void JackTripWorker::receivedDataUDP()
         mJackTrip->setNumOutputChannels(PeerNumOutgoingChannels);
     }
 
-    if (PeerNumOutgoingChannels == -1) {
+    if (PeerNumOutgoingChannels < 0 || PeerNumIncomingChannels < 0) {
         // Shut it down
+        cerr << "JackTripWorker: invalid peer settings, shutting down" << endl;
         mSpawning = false;
         mUdpHubListener->releaseThread(mID);
+        return;
     }
 
     // Connect signals and slots
@@ -315,6 +392,7 @@ void JackTripWorker::receivedDataUDP()
     connect(this, &JackTripWorker::signalRemoveThread, mJackTrip.data(),
             &JackTrip::slotStopProcesses, Qt::QueuedConnection);
 
+    // Start the process - this works for UDP, WebRTC, and WebTransport
     if (gVerboseFlag)
         cout << "---> JackTripWorker: startProcess..." << endl;
     mJackTrip->startProcess(
@@ -340,7 +418,7 @@ void JackTripWorker::udpTimerTick()
         cout << "---------> ELAPSED TIME: " << mElapsedTime << endl;
     // Check if we've timed out.
     if (gTimeOutMultiThreadedServer > 0 && mElapsedTime >= gTimeOutMultiThreadedServer) {
-        std::cerr << "--->JackTripWorker: is not receiving Datagrams (timeout)" << endl;
+        cerr << "--->JackTripWorker: is not receiving Datagrams (timeout)" << endl;
         mTimeoutTimer.stop();
         mUdpSockTemp.close();
         mSpawning = false;
@@ -376,3 +454,291 @@ void JackTripWorker::alertPatcher()
     }
 #endif
 }
+
+#ifdef WEBRTC_SUPPORT
+//*******************************************************************************
+void JackTripWorker::receivedFirstPacketWebRtc(const std::vector<std::byte>& packet)
+{
+    QMutexLocker lock(&mMutex);
+
+    if (!mSpawning) {
+        // Already processed or cancelled
+        return;
+    }
+
+    // Extract peer settings from the packet and configure channels
+    const int8_t* full_packet = reinterpret_cast<const int8_t*>(packet.data());
+    processPeerSettings(const_cast<int8_t*>(full_packet));
+}
+
+//*******************************************************************************
+void JackTripWorker::createWebRtcPeerConnection(QSslSocket* signalingSocket,
+                                                const QStringList& iceServers,
+                                                uint16_t portRangeBegin,
+                                                uint16_t portRangeEnd)
+{
+    // Clean up old connection if exists
+    if (mWebRtcPeerConnection) {
+        cout << "JackTripWorker: Warning - replacing existing WebRTC connection" << endl;
+        mWebRtcPeerConnection->close();
+        mWebRtcPeerConnection->deleteLater();
+    }
+
+    // Create the WebRTC peer connection with signaling socket
+    // The peer connection will manage the signaling internally
+    mWebRtcPeerConnection = new WebRtcPeerConnection(signalingSocket, iceServers,
+                                                     portRangeBegin, portRangeEnd, this);
+
+    // Connect signals from peer connection to our slots
+    connect(mWebRtcPeerConnection, &WebRtcPeerConnection::dataChannelOpen, this,
+            &JackTripWorker::onWebRtcDataChannelOpen);
+    connect(mWebRtcPeerConnection, &WebRtcPeerConnection::dataChannelClosed, this,
+            &JackTripWorker::onWebRtcDataChannelClosed);
+    connect(mWebRtcPeerConnection, &WebRtcPeerConnection::connectionFailed, this,
+            &JackTripWorker::onWebRtcConnectionFailed);
+}
+
+//*******************************************************************************
+void JackTripWorker::onWebRtcDataChannelOpen()
+{
+    if (!mWebRtcPeerConnection) {
+        cerr << "JackTripWorker: ERROR - No peer connection" << endl;
+        return;
+    }
+
+    // Get the data channel from the peer connection
+    auto dataChannel = mWebRtcPeerConnection->getDataChannel();
+    if (!dataChannel) {
+        cerr << "JackTripWorker: ERROR - No data channel available" << endl;
+        return;
+    }
+
+    // Get peer address & client name
+    QString peerAddress = mWebRtcPeerConnection->getPeerAddress();
+    mClientName         = mWebRtcPeerConnection->getClientName();
+
+    // Get the base port from the hub listener and calculate the server port
+    int basePort             = mUdpHubListener->getBasePort();
+    uint16_t serverPort      = static_cast<uint16_t>(basePort + mID);
+    bool connectDefaultPorts = mUdpHubListener->getConnectDefaultAudioPorts();
+
+    setJackTrip(mID, peerAddress, serverPort, 0, connectDefaultPorts);
+
+    // Set protocol to WebRTC
+    setDataProtocol(JackTrip::WEBRTC);
+
+    // Start the worker with WebRTC transport
+    if (!mSpawning || !dataChannel) {
+        cerr << "JackTripWorker:: ERROR - Cannot start WebRTC - not ready"
+             << " (mSpawning=" << mSpawning
+             << ", dataChannel=" << (dataChannel ? "valid" : "null") << ")" << endl;
+        return;
+    }
+
+    mJackTrip->setConnectDefaultAudioPorts(m_connectDefaultAudioPorts);
+    mJackTrip->setUnderRunMode(mUnderRunMode);
+    mJackTrip->setAudioBitResolution(mAudioBitResolution);
+
+    if (mIOStatTimeout > 0) {
+        mJackTrip->setIOStatTimeout(mIOStatTimeout);
+        mJackTrip->setIOStatStream(mIOStatStream);
+    }
+
+    if (mAppendThreadID) {
+        mJackTrip->setID(mID + 1);
+    }
+
+    mJackTrip->setClientName(mClientName);
+    mJackTrip->setPeerAddress(mClientAddress);
+    mJackTrip->setBindPorts(mServerPort);
+    mJackTrip->setBufferStrategy(mBufferStrategy);
+    mJackTrip->setNetIssuesSimulation(mSimulatedLossRate, mSimulatedJitterRate,
+                                      mSimulatedDelayRel);
+    mJackTrip->setBroadcast(mBroadcastQueue);
+    mJackTrip->setUseRtUdpPriority(mUseRtUdpPriority);
+
+    // Set the data protocol to WebRTC and provide the data channel
+    mJackTrip->setDataProtocoType(JackTrip::WEBRTC);
+    mJackTrip->setWebRtcDataChannel(dataChannel);
+
+    // Set up a lambda to capture the first message and forward to our handler
+    dataChannel->onMessage([this](rtc::message_variant data) {
+        if (std::holds_alternative<rtc::binary>(data)) {
+            auto& binary = std::get<rtc::binary>(data);
+
+            // Forward to our handler (needs to be thread-safe)
+            // Use QMetaObject::invokeMethod to call in the worker's thread
+            QMetaObject::invokeMethod(
+                this,
+                [this, binary]() {
+                    receivedFirstPacketWebRtc(binary);
+                },
+                Qt::QueuedConnection);
+        }
+    });
+}
+
+//*******************************************************************************
+void JackTripWorker::onWebRtcDataChannelClosed()
+{
+    // Stop the JackTrip process
+    stopThread();
+
+    // Signal the hub listener to remove this thread
+    emit signalRemoveThread();
+}
+
+//*******************************************************************************
+void JackTripWorker::onWebRtcConnectionFailed(const QString& reason)
+{
+    cerr << "JackTripWorker: WebRTC connection failed for worker " << mID << ": "
+         << reason.toStdString() << endl;
+
+    // Stop the thread and signal removal
+    stopThread();
+    emit signalRemoveThread();
+}
+
+#endif  // WEBRTC_SUPPORT
+
+#ifdef WEBTRANSPORT_SUPPORT
+
+//*******************************************************************************
+void JackTripWorker::createWebTransportSession(WebTransportSession* session)
+{
+    // Clean up old session if exists
+    if (mWebTransportSession) {
+        cout << "JackTripWorker: Warning - replacing existing WebTransport session"
+             << endl;
+        mWebTransportSession->close();
+        mWebTransportSession->deleteLater();
+    }
+
+    // Take ownership of the session
+    mWebTransportSession = session;
+    mWebTransportSession->setParent(this);
+
+    // Connect signals from session to our slots
+    connect(mWebTransportSession, &WebTransportSession::sessionEstablished, this,
+            &JackTripWorker::onWebTransportSessionEstablished);
+    connect(mWebTransportSession, &WebTransportSession::sessionClosed, this,
+            &JackTripWorker::onWebTransportSessionClosed);
+    connect(mWebTransportSession, &WebTransportSession::sessionFailed, this,
+            &JackTripWorker::onWebTransportSessionFailed);
+
+    // Set up handler for first datagram (to extract peer settings)
+    // Use QMetaObject::invokeMethod to call in the worker's thread
+    mWebTransportSession->setDatagramCallback([this](const uint8_t* data, size_t len) {
+        // Copy the data since the callback may be from another thread
+        std::vector<uint8_t> dataCopy(data, data + len);
+
+        // Forward to our handler (needs to be thread-safe)
+        QMetaObject::invokeMethod(
+            this,
+            [this, dataCopy]() {
+                this->receivedFirstPacketWebTransport(dataCopy.data(), dataCopy.size());
+            },
+            Qt::QueuedConnection);
+    });
+
+    // If session is already connected (msquic handles handshake), start immediately
+    if (mWebTransportSession->isConnected()) {
+        onWebTransportSessionEstablished();
+    }
+}
+
+//*******************************************************************************
+void JackTripWorker::receivedFirstPacketWebTransport(const uint8_t* data, size_t len)
+{
+    QMutexLocker lock(&mMutex);
+
+    if (!mSpawning) {
+        // Already processed or cancelled
+        return;
+    }
+
+    // Unregister callback so we only process the first packet here
+    // The DataProtocol will register its own callback after spawning
+    mWebTransportSession->setDatagramCallback(nullptr);
+
+    cout << "JackTripWorker: Received first WebTransport packet, size=" << len << endl;
+
+    // Extract peer settings from the packet and configure channels (zero-copy)
+    const int8_t* full_packet = reinterpret_cast<const int8_t*>(data);
+    processPeerSettings(const_cast<int8_t*>(full_packet));
+}
+
+//*******************************************************************************
+void JackTripWorker::onWebTransportSessionEstablished()
+{
+    if (!mWebTransportSession) {
+        cerr << "JackTripWorker: ERROR - No WebTransport session" << endl;
+        return;
+    }
+
+    // Get peer address & client name
+    QString peerAddress = mWebTransportSession->getPeerAddress();
+    mClientName         = mWebTransportSession->getClientName();
+
+    // Get the base port from the hub listener and calculate the server port
+    int basePort             = mUdpHubListener->getBasePort();
+    uint16_t serverPort      = static_cast<uint16_t>(basePort + mID);
+    bool connectDefaultPorts = mUdpHubListener->getConnectDefaultAudioPorts();
+
+    setJackTrip(mID, peerAddress, serverPort, 0, connectDefaultPorts);
+
+    // Configure JackTrip settings (must be done before startProcess is called)
+    mJackTrip->setConnectDefaultAudioPorts(connectDefaultPorts);
+    mJackTrip->setUnderRunMode(mUnderRunMode);
+    mJackTrip->setAudioBitResolution(mAudioBitResolution);
+
+    if (mIOStatTimeout > 0) {
+        mJackTrip->setIOStatTimeout(mIOStatTimeout);
+        mJackTrip->setIOStatStream(mIOStatStream);
+    }
+
+    if (mAppendThreadID) {
+        mJackTrip->setID(mID + 1);
+    }
+
+    mJackTrip->setClientName(mClientName);
+    mJackTrip->setPeerAddress(peerAddress);
+    mJackTrip->setBindPorts(serverPort);
+    mJackTrip->setBufferStrategy(mBufferStrategy);
+    mJackTrip->setNetIssuesSimulation(mSimulatedLossRate, mSimulatedJitterRate,
+                                      mSimulatedDelayRel);
+    mJackTrip->setBroadcast(mBroadcastQueue);
+    mJackTrip->setUseRtUdpPriority(mUseRtUdpPriority);
+
+    // Set protocol to WebTransport and provide the session
+    setDataProtocol(JackTrip::WEBTRANSPORT);
+    mJackTrip->setDataProtocoType(JackTrip::WEBTRANSPORT);
+    mJackTrip->setWebTransportSession(mWebTransportSession);
+
+    // Note: We don't start the process here!
+    // We wait for the first packet which will call processPeerSettings()
+    // and then start the audio from there (via startProcess)
+}
+
+//*******************************************************************************
+void JackTripWorker::onWebTransportSessionClosed()
+{
+    // Stop the JackTrip process
+    stopThread();
+
+    // Signal the hub listener to remove this thread
+    emit signalRemoveThread();
+}
+
+//*******************************************************************************
+void JackTripWorker::onWebTransportSessionFailed(const QString& reason)
+{
+    cerr << "JackTripWorker: WebTransport session failed for worker " << mID << ": "
+         << reason.toStdString() << endl;
+
+    // Stop the thread and signal removal
+    stopThread();
+    emit signalRemoveThread();
+}
+
+#endif  // WEBTRANSPORT_SUPPORT
index 867586c2a84fce27b284f117167329b27ce4b1ee..6507e56bc99e459607a104f49c0e2d7bbfc75040 100644 (file)
@@ -41,6 +41,7 @@
 #include <QHostAddress>
 #include <QMutex>
 #include <QObject>
+#include <QScopedPointer>
 #include <QUdpSocket>
 #include <iostream>
 
 #include "JamTest.h"
 #endif
 
+#ifdef WEBRTC_SUPPORT
+#include "webrtc/WebRtcPeerConnection.h"
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+#include "webtransport/WebTransportSession.h"
+#endif
+
 // class JackTrip; // forward declaration
 class UdpHubListener;  // forward declaration
 
@@ -136,6 +145,38 @@ class JackTripWorker : public QObject
     uint16_t getClientPort() { return mClientPort; }
     QString getClientAddress() { return mClientAddress; }
 
+    /// \brief Set the data protocol type (UDP or WEBRTC)
+    void setDataProtocol(JackTrip::dataProtocolT protocol) { mDataProtocol = protocol; }
+
+#ifdef WEBRTC_SUPPORT
+    /// \brief Create and initialize WebRTC peer connection
+    /// \param signalingSocket The SSL socket for WebSocket signaling (ownership
+    /// transferred to connection)
+    /// \param iceServers List of STUN/TURN server URLs
+    /// \param portRangeBegin First UDP port for ICE candidates (0 = system default)
+    /// \param portRangeEnd Last UDP port for ICE candidates (0 = system default)
+    void createWebRtcPeerConnection(QSslSocket* signalingSocket,
+                                    const QStringList& iceServers,
+                                    uint16_t portRangeBegin = 0,
+                                    uint16_t portRangeEnd   = 0);
+
+    /// \brief Called when first packet is received on WebRTC data channel
+    /// Similar to receivedDataUDP() for UDP mode
+    void receivedFirstPacketWebRtc(const std::vector<std::byte>& packet);
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+    /// \brief Set the WebTransport session for this worker
+    /// \param session The WebTransport session (ownership transferred to this worker)
+    void createWebTransportSession(WebTransportSession* session);
+
+    /// \brief Called when first packet is received on WebTransport
+    /// Similar to receivedFirstPacketWebRtc() for WebRTC mode
+    /// \param data Pointer to packet data (zero-copy)
+    /// \param len Length of packet in bytes
+    void receivedFirstPacketWebTransport(const uint8_t* data, size_t len);
+#endif
+
     double getLatency()
     {
         QMutexLocker lock(&mMutex);
@@ -144,17 +185,32 @@ class JackTripWorker : public QObject
 
    private slots:
     void slotTest() { std::cout << "--- JackTripWorker TEST SLOT ---" << std::endl; }
-    void receivedDataUDP();
+    void receivedFirstPacketUDP();
     void udpTimerTick();
     void jacktripStopped();
     void alertPatcher();
 
+#ifdef WEBRTC_SUPPORT
+    void onWebRtcDataChannelOpen();
+    void onWebRtcDataChannelClosed();
+    void onWebRtcConnectionFailed(const QString& reason);
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+    void onWebTransportSessionEstablished();
+    void onWebTransportSessionClosed();
+    void onWebTransportSessionFailed(const QString& reason);
+#endif
+
    signals:
     void signalRemoveThread();
 
    private:
     JackTrip::connectionModeT getConnectionModeFromHeader();
 
+    /// \brief Process peer settings from packet header and configure channels
+    void processPeerSettings(int8_t* packet);
+
     QUdpSocket mUdpSockTemp;
     QTimer mTimeoutTimer;
     int mSleepTime;
@@ -190,6 +246,18 @@ class JackTripWorker : public QObject
 
     int mID = 0;  ///< ID thread number
 
+    JackTrip::dataProtocolT mDataProtocol = JackTrip::UDP;  ///< Data protocol type
+
+#ifdef WEBRTC_SUPPORT
+    WebRtcPeerConnection* mWebRtcPeerConnection =
+        nullptr;  ///< WebRTC peer connection (owned)
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+    WebTransportSession* mWebTransportSession =
+        nullptr;  ///< WebTransport session (owned)
+#endif
+
     int mBufferStrategy         = 1;
     int mBroadcastQueue         = 0;
     double mSimulatedLossRate   = 0.0;
index 99b1121e27667aa12e408ffbaa61a149809e27e5..d76d953a00abc0059bcfae4e1bd7c7399c404017 100644 (file)
@@ -123,6 +123,9 @@ bool JitterBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int l
     if (0 == len) {
         len = mSlotSize;
     }
+    if (len > mTotalSize) {
+        return false;
+    }
     QMutexLocker locker(&mMutex);
     mInSlotSize = len;
     if (!mActive) {
index e55a92d1f8107485e073fefbbce2706fc5de42b6..13d03384000addef4433f0d5cbbb9bbdf83dfe53 100644 (file)
@@ -127,9 +127,9 @@ class Limiter : public ProcessPlugin
                         warnCount = 0;
                     }
                 }  // warnCount==nextWarning
-            }      // above warningAmp
-        }          // loop over frames
-    }              // checkAmplitudes()
+            }  // above warningAmp
+        }  // loop over frames
+    }  // checkAmplitudes()
 
    private:
     float fs;
index 763da69df1575c319ac74df026155e3e16c1d3ac..2827f12be310ebf3e283d6a8597231a8b775b2d0 100644 (file)
@@ -55,7 +55,7 @@ class LoopBack : public ProcessPlugin
     /// \brief The class constructor sets the number of channels to connect as loopback
     LoopBack(int numchans) { mNumChannels = numchans; };
     /// \brief The class destructor
-    virtual ~LoopBack(){};
+    virtual ~LoopBack() {};
 
     virtual int getNumInputs() { return (mNumChannels); };
     virtual int getNumOutputs() { return (mNumChannels); };
index 5792456925073bfff1e11b7d541f0334bb8733e2..ca0175e33a80efa6ede6e1965c0d3d0bb6ba3eca 100644 (file)
@@ -201,6 +201,74 @@ bool DefaultHeader::checkPeerSettings(int8_t* full_packet)
     /// \todo Check number of channels and other parameters
 }
 
+//***********************************************************************
+bool DefaultHeader::validatePeerHeader(const int8_t* full_packet,
+                                       int received_bytes) const
+{
+    if (received_bytes < static_cast<int>(sizeof(DefaultHeaderStruct))) {
+        std::cerr << "ERROR: Received packet too small to contain header ("
+                  << received_bytes << " < " << sizeof(DefaultHeaderStruct) << ")"
+                  << endl;
+        return false;
+    }
+
+    const DefaultHeaderStruct* h =
+        reinterpret_cast<const DefaultHeaderStruct*>(full_packet);
+
+    if (h->BufferSize == 0 || h->BufferSize > gMaxBufferSizeInSamples) {
+        std::cerr << "ERROR: Peer BufferSize out of range: " << h->BufferSize << endl;
+        return false;
+    }
+
+    if (h->NumIncomingChannelsFromNet == 0
+        || h->NumIncomingChannelsFromNet > gMaxAudioChannels) {
+        std::cerr << "ERROR: Peer NumIncomingChannelsFromNet out of range: "
+                  << static_cast<int>(h->NumIncomingChannelsFromNet) << endl;
+        return false;
+    }
+
+    // 0 is a valid sentinel (symmetric config); 0xff is the "no input" sentinel
+    if (h->NumOutgoingChannelsToNet != 0
+        && h->NumOutgoingChannelsToNet != (std::numeric_limits<uint8_t>::max)()
+        && h->NumOutgoingChannelsToNet > gMaxAudioChannels) {
+        std::cerr << "ERROR: Peer NumOutgoingChannelsToNet out of range: "
+                  << static_cast<int>(h->NumOutgoingChannelsToNet) << endl;
+        return false;
+    }
+
+    const uint8_t br = h->BitResolution;
+    if (br != 8 && br != 16 && br != 24 && br != 32) {
+        std::cerr << "ERROR: Peer BitResolution invalid: " << static_cast<int>(br)
+                  << endl;
+        return false;
+    }
+
+    // Use the effective outgoing channel count to compute expected audio payload.
+    // When NumOutgoingChannelsToNet == 0 the peer sends NumIncomingChannelsFromNet worth
+    // of audio; the 0xff sentinel means no input (0 audio bytes from that direction).
+    uint8_t effective_chans;
+    if (h->NumOutgoingChannelsToNet == 0) {
+        effective_chans = h->NumIncomingChannelsFromNet;
+    } else if (h->NumOutgoingChannelsToNet == (std::numeric_limits<uint8_t>::max)()) {
+        effective_chans = 0;
+    } else {
+        effective_chans = h->NumOutgoingChannelsToNet;
+    }
+
+    int expected_audio_bytes =
+        static_cast<int>(h->BufferSize) * effective_chans * (br / 8);
+    int min_packet_size =
+        static_cast<int>(sizeof(DefaultHeaderStruct)) + expected_audio_bytes;
+
+    if (received_bytes < min_packet_size) {
+        std::cerr << "ERROR: Received packet too small for declared audio payload ("
+                  << received_bytes << " < " << min_packet_size << ")" << endl;
+        return false;
+    }
+
+    return true;
+}
+
 //***********************************************************************
 void DefaultHeader::printHeader() const
 {
index 84c0351170f9e6a5aa3b75fb97c5b19f8e5b3e7b..9d69a49f8739af7cca19ba6a025174e1f81c92e5 100644 (file)
@@ -129,6 +129,10 @@ class PacketHeader : public QObject
     /// \brief Check that the settings in the supplied packet header match the server's
     /// settings \return True if settings match, false otherwise
     virtual bool checkPeerSettings(int8_t* full_packet) = 0;
+    /// \brief Validate peer-supplied header fields against sane bounds and the actual
+    /// received packet length. Returns false (and logs) on any violation.
+    virtual bool validatePeerHeader(const int8_t* full_packet,
+                                    int received_bytes) const = 0;
 
     virtual uint64_t getPeerTimeStamp(int8_t* full_packet) const          = 0;
     virtual uint16_t getPeerSequenceNumber(int8_t* full_packet) const     = 0;
@@ -187,6 +191,8 @@ class DefaultHeader : public PacketHeader
     virtual void fillHeaderCommonFromAudio() override;
     virtual void parseHeader() override {}
     virtual bool checkPeerSettings(int8_t* full_packet) override;
+    virtual bool validatePeerHeader(const int8_t* full_packet,
+                                    int received_bytes) const override;
     virtual void increaseSequenceNumber() override { mHeader.SeqNumber++; }
     virtual uint16_t getSequenceNumber() const override { return mHeader.SeqNumber; }
     virtual int getHeaderSizeInBytes() const override { return sizeof(mHeader); }
@@ -232,6 +238,11 @@ class JamLinkHeader : public PacketHeader
     virtual void fillHeaderCommonFromAudio() override;
     virtual void parseHeader() override {}
     virtual bool checkPeerSettings(int8_t* /*full_packet*/) override { return true; }
+    virtual bool validatePeerHeader(const int8_t* /*full_packet*/,
+                                    int /*received_bytes*/) const override
+    {
+        return true;
+    }
 
     virtual uint64_t getPeerTimeStamp(int8_t* /*full_packet*/) const override
     {
@@ -290,6 +301,11 @@ class EmptyHeader : public PacketHeader
     virtual void fillHeaderCommonFromAudio() override {}
     virtual void parseHeader() override {}
     virtual bool checkPeerSettings(int8_t* /*full_packet*/) override { return true; }
+    virtual bool validatePeerHeader(const int8_t* /*full_packet*/,
+                                    int /*received_bytes*/) const override
+    {
+        return true;
+    }
     virtual void increaseSequenceNumber() override {}
     virtual int getHeaderSizeInBytes() const override { return 0; }
 
index f8fb64af996c23f4aae48c2464c728efdc8b59c3..19d8d066664b21617443f0dae79d5b397292cbdb 100644 (file)
@@ -54,10 +54,10 @@ class ProcessPlugin : public QObject
 
    public:
     /// \brief The Class Constructor
-    ProcessPlugin(){};
+    ProcessPlugin() {};
 
     /// \brief The Class Destructor
-    virtual ~ProcessPlugin(){};
+    virtual ~ProcessPlugin() {};
 
     /// \brief Return Number of Input Channels
     virtual int getNumInputs() = 0;
index 798240836ee5a438c4e36e6784651a5ab66a82b1..5fb990bb89e1fdff533189f427f939549baecdb5 100644 (file)
@@ -86,6 +86,7 @@
 
 #include "JitterBuffer.h"  // for broadcast
 
+using std::cerr;
 using std::cout;
 using std::endl;
 using std::setw;
@@ -97,7 +98,7 @@ constexpr int HISTFPP      = 128;    // default FPP when calibrating burg window
 
 constexpr int NumSlots   = 4096;   // NumSlots looped for recent arrivals
 constexpr double AutoMax = 250.0;  // msec bounds on insane IPI, like ethernet unplugged
-constexpr double AutoInitDur = 2000.0;  // kick in auto after this many msec
+constexpr double AutoInitDur = 3000.0;  // kick in auto after this many msec
 constexpr double AutoInitValFactor =
     0.5;  // scale for initial mMsecTolerance during init phase if unspecified
 
@@ -339,6 +340,14 @@ void Regulator::setFPPratio(int len)
         return;
     }
 
+    const int kMaxPeerBytes =
+        static_cast<int>(gMaxBufferSizeInSamples) * gMaxAudioChannels * 4;
+    if (len <= 0 || len > kMaxPeerBytes) {
+        cerr << "Regulator::setFPPratio: peer packet len out of range (" << len
+             << "); ignoring." << endl;
+        return;
+    }
+
     mPeerBytes           = len;
     mPeerFPP             = len / (mNumChannels * mBitResolutionMode);
     mPeerFPPdurMsec      = 1000.0 * mPeerFPP / mSampleRate;
@@ -494,11 +503,12 @@ void Regulator::updateTolerance(int glitches, int skipped)
             } else {
                 // don't increase headroom two intervals in a row
                 mSkipAutoHeadroom = true;
-                if (mLastMaxLatency > mMsecTolerance + 1) {
-                    // increase headroom enough to cover any skipped packets
+                if (mLastMaxLatency > mMsecTolerance + 3.0) {
+                    // special case to grow headroom faster to catch up
                     mCurrentHeadroom = std::min<double>(
                         maxHeadroom,
-                        mCurrentHeadroom + std::ceil(mLastMaxLatency - mMsecTolerance));
+                        mCurrentHeadroom
+                            + std::ceil((mLastMaxLatency - mMsecTolerance) / 2.0));
                 } else {
                     ++mCurrentHeadroom;
                 }
@@ -563,7 +573,8 @@ void Regulator::updatePushStats(int seq_num)
                 // a calculated tolerance. Otherwise, the switch can
                 // sometimes cause it to bump headroom prematurely even
                 // though there are no real audio glitches.
-                mStatsMaxLatency = 0;  // ignore during warmup
+                mLastMaxLatency  = 0;  // ignore during warmup
+                mStatsMaxLatency = 0;
                 updateTolerance(0, 0);
             } else {
                 mLastMaxLatency  = mStatsMaxLatency;  // only set after warmup
@@ -590,7 +601,7 @@ void Regulator::setQueueBufferLength(int queueBuffer)
         mAutoHeadroom          = -1;
         mCurrentHeadroom       = 0;
         mSkipAutoHeadroom      = true;
-        mAutoHeadroomStartTime = pushStat ? (pushStat->lastTime + AutoInitDur) : 4000.0;
+        mAutoHeadroomStartTime = pushStat ? (pushStat->lastTime + AutoInitDur) : 6000.0;
     } else {
         mAutoHeadroom    = std::abs(queueBuffer);
         mCurrentHeadroom = mAutoHeadroom;
index 6041cdd4a58136e1f9f88bc26a29429c09559570..343f033e6f6e43839ef6f3448ac09f3acd98150e 100644 (file)
@@ -306,7 +306,7 @@ class Regulator : public RingBuffer
     double mStatsMaxLatency       = 0;
     double mStatsMaxPLCdspElapsed = 0;
     double mCurrentHeadroom       = 0;
-    double mAutoHeadroomStartTime = 4000.0;
+    double mAutoHeadroomStartTime = 6000.0;
     double mAutoHeadroom          = -1;
     Time* mTime                   = nullptr;
 
index 9ee0a4332b5bf1e82e24d3ab977211845305bb23..bf4ed391f73abf2e3853f34ec909885602f6e48e 100644 (file)
@@ -100,7 +100,8 @@ enum JTLongOptIDS {
     OPT_AUDIOOUTPUTDEVICE,
     OPT_GUI,
     OPT_CLASSIC_GUI,
-    OPT_DEEPLINK
+    OPT_DEEPLINK,
+    OPT_ICESERVERS
 };
 
 //*******************************************************************************
@@ -212,6 +213,8 @@ void Settings::parseInput(int argc, char** argv)
         {"classic-gui", no_argument, NULL, OPT_CLASSIC_GUI},  // Force Classic Mode GUI
         {"deeplink", optional_argument, NULL,
          OPT_DEEPLINK},  // Deeplink URL (should be in the form jacktrip://...)
+        {"iceservers", required_argument, NULL,
+         OPT_ICESERVERS},  // ICE servers for WebRTC NAT traversal
         {NULL, 0, NULL, 0}};
 
     // Parse Command Line Arguments
@@ -695,6 +698,9 @@ void Settings::parseInput(int argc, char** argv)
             }
             mDeeplink = optarg;
             break;
+        case OPT_ICESERVERS:
+            mIceServers = optarg;
+            break;
         case ':': {
             printUsage();
             printf("*** Missing option argument *** see above for usage\n\n");
@@ -961,6 +967,7 @@ void Settings::printUsage()
     cout << " --credsfile                              The file containing the stored usernames and passwords" << endl;
     cout << " --username                               The username to use when connecting as a hub client (if not supplied here, this is read from standard input)" << endl;
     cout << " --password                               The password to use when connecting as a hub client (if not supplied here, this is read from standard input)" << endl;
+    cout << " --iceservers                             Comma-separated list of ICE servers for WebRTC NAT traversal (e.g., \"stun:stun.l.google.com:19302\")" << endl;
     cout << endl;
     cout << "ARGUMENTS FOR THE GUI:" << endl;
     cout << " --gui                                    Force JackTrip to run with the GUI. If not using VirtualStudio mode, command line switches in the required arguments, optional arguments (except -l, -j, -L, --appendthreadid), audio patching, and authentication sections will be honoured, and default settings will be used where arguments aren't supplied. Options from other sections will be ignored (and the last used settings will be loaded), except for -V, and the --version and --help switches which will override this." << endl;
@@ -1089,10 +1096,24 @@ UdpHubListener* Settings::getConfiguredHubServer()
         //(We don't need to check the validity of these files because it's done by the
         // UdpHubListener.)
         udpHub->setRequireAuth(mAuth);
+        udpHub->setCredsFile(mCredsFile);
+    }
+
+    if (!mCertFile.isEmpty()) {
         udpHub->setCertFile(mCertFile);
+    }
+
+    if (!mKeyFile.isEmpty()) {
         udpHub->setKeyFile(mKeyFile);
-        udpHub->setCredsFile(mCredsFile);
     }
+
+#ifdef WEBRTC_SUPPORT
+    if (!mIceServers.isEmpty()) {
+        QStringList iceServerList = mIceServers.split(',', Qt::SkipEmptyParts);
+        udpHub->setIceServers(iceServerList);
+    }
+#endif
+
     return udpHub;
 }
 
index e30f7ca6e43502a3078c267d539e2520c42b27cf..585e49275e995d8b1833b9d98620e9a25833141f 100644 (file)
@@ -118,6 +118,7 @@ class Settings : public QObject
     QString getUsername() { return mUsername; }
     QString getPassword() { return mPassword; }
     const QString& getDeeplink() const { return mDeeplink; }
+    QString getIceServers() { return mIceServers; }
 
    private:
     void disableEcho(bool disabled);
@@ -193,6 +194,7 @@ class Settings : public QObject
     QString mUsername;
     QString mPassword;
     QString mDeeplink;
+    QString mIceServers;
 
     QSharedPointer<AudioTester> mAudioTester;
 };
index a9ef0020e8dad53185aa857067a7843d43dbf68e..f9967452fe7baffc7b8a88d1ec5f9806492fe611 100644 (file)
@@ -52,7 +52,11 @@ SocketClient::~SocketClient()
 {
     if (isConnected() && m_owns_socket) {
         m_socket->close();
-        m_socket->waitForDisconnected(1000);  // wait for up to 1 second
+        // Windows can reach UnconnectedState synchronously; waitForDisconnected
+        // is not valid in that state.
+        if (m_socket->state() != QLocalSocket::UnconnectedState) {
+            m_socket->waitForDisconnected(1000);
+        }
     }
 }
 
@@ -69,7 +73,11 @@ void SocketClient::close()
 {
     if (isConnected()) {
         m_socket->close();
-        m_socket->waitForDisconnected(1000);  // wait for up to 1 second
+        // Windows can reach UnconnectedState synchronously; waitForDisconnected
+        // is not valid in that state.
+        if (m_socket->state() != QLocalSocket::UnconnectedState) {
+            m_socket->waitForDisconnected(1000);
+        }
     }
 }
 
index bd32862b059b241f1e201bc80cbed7ebc84c3efc..4ddbb35bbc5a4c654926354a694f68e396202830 100644 (file)
@@ -96,9 +96,10 @@ void SocketServer::handlePendingConnections()
             continue;
         }
 
-        // wait for 1 second for ready read, or else give up
-        bool readyToRead = connectedSocket->waitForReadyRead(timeout);
-        if (!readyToRead) {
+        // On Windows, QLocalSocket::waitForReadyRead can return false once the
+        // peer disconnects even if data is still queued in the read buffer.
+        if (!connectedSocket->waitForReadyRead(timeout)
+            && connectedSocket->bytesAvailable() <= 0) {
             qDebug() << "Socket server: timed out waiting for bytes available";
             continue;
         }
index 5b2908d7eca27d2e117166e60bcbe07c0d7963a2..520db4635ad16ef398f29f8ddad8ff0c4dabf42b 100644 (file)
@@ -46,14 +46,23 @@ void SslServer::incomingConnection(qintptr socketDescriptor)
     // an SslSocket rather than a regular socket.
     QSslSocket* sslSocket = new QSslSocket(this);
     sslSocket->setSocketDescriptor(socketDescriptor);
-    sslSocket->setLocalCertificate(m_certificate);
+    // Send the full certificate chain (leaf + intermediates) so browsers can
+    // verify trust all the way to a root CA. setLocalCertificate sends only the
+    // leaf cert, which causes browsers to reject the connection when intermediate
+    // CA certs are not already in their trust store.
+    sslSocket->setLocalCertificateChain(m_certificateChain);
     sslSocket->setPrivateKey(m_privateKey);
     this->addPendingConnection(sslSocket);
 }
 
 void SslServer::setCertificate(const QSslCertificate& certificate)
 {
-    m_certificate = certificate;
+    m_certificateChain = {certificate};
+}
+
+void SslServer::setCertificateChain(const QList<QSslCertificate>& chain)
+{
+    m_certificateChain = chain;
 }
 
 void SslServer::setPrivateKey(const QSslKey& key)
index f3fd1fa813760f8f5ab9541244c82701d99ddbe4..2f336ed134d1630cb71691ce2d10598a08e70f99 100644 (file)
@@ -37,6 +37,7 @@
 #ifndef __SSLSERVER_H__
 #define __SSLSERVER_H__
 
+#include <QList>
 #include <QSslCertificate>
 #include <QSslKey>
 #include <QTcpServer>
@@ -51,10 +52,11 @@ class SslServer : public QTcpServer
 
     void incomingConnection(qintptr socketDescriptor) override;
     void setCertificate(const QSslCertificate& certificate);
+    void setCertificateChain(const QList<QSslCertificate>& chain);
     void setPrivateKey(const QSslKey& key);
 
    private:
-    QSslCertificate m_certificate;
+    QList<QSslCertificate> m_certificateChain;
     QSslKey m_privateKey;
 };
 
index cc499a920243b9210e5bf3c5f5437dc3c23f701a..086bba2027652170eb3f844f53d06a3f70ca0efd 100644 (file)
@@ -375,7 +375,11 @@ bool UdpDataProtocol::setSocketQos(socket_type& sock_fd)
     // See RFC2474 https://datatracker.ietf.org/doc/html/rfc2474
     // See also
     // https://www.slashroot.in/understanding-differentiated-services-tos-field-internet-protocol-header
-    const char tos = 0xE0;  // 11100000 (56 << 2)
+    // Network Control (CS7)
+    // const char tos = 0xE0;  // 11100000 (56 << 2)
+    // but let's use expedited forwarding "EF"
+    // Interactive Audio (EF)
+    const char tos = 0xB8;  // 10111000 (46 << 2)
     int result;
     if (mIPv6) {
         result = ::setsockopt(sock_fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos));
@@ -663,6 +667,14 @@ void UdpDataProtocol::run()
         full_redundant_packet      = new int8_t[full_redundant_packet_size];
         full_redundant_packet_size = receivePacket(
             reinterpret_cast<char*>(full_redundant_packet), full_redundant_packet_size);
+        // Validate peer-supplied header fields before trusting any derived sizes.
+        if (!mJackTrip->validatePeerHeader(full_redundant_packet, full_redundant_packet_size)) {
+            std::cerr << "ERROR: Peer header validation failed; dropping connection." << endl;
+            delete[] full_redundant_packet;
+            full_redundant_packet = nullptr;
+            return;
+        }
+
         // Check that peer has the same audio settings
         if (gVerboseFlag)
             std::cout << std::endl
@@ -680,6 +692,15 @@ void UdpDataProtocol::run()
         full_packet_size = mJackTrip->getHeaderSizeInBytes()
                            + mJackTrip->getPeerBufferSize(full_redundant_packet)
                                  * peer_chans * mSmplSize;
+
+        if (full_packet_size <= 0 || full_packet_size > full_redundant_packet_size) {
+            std::cerr << "ERROR: Computed full_packet_size (" << full_packet_size
+                      << ") out of range for received datagram (" << full_redundant_packet_size
+                      << "); dropping connection." << endl;
+            delete[] full_redundant_packet;
+            full_redundant_packet = nullptr;
+            return;
+        }
         /*
         cout << "peer sizes: " << mJackTrip->getHeaderSizeInBytes()
              << " + " << mJackTrip->getPeerBufferSize(full_redundant_packet)
@@ -957,8 +978,23 @@ void UdpDataProtocol::receivePacketRedundancy(
 
     int peer_chans    = mJackTrip->getPeerNumOutgoingChannels(full_redundant_packet);
     int N             = mJackTrip->getPeerBufferSize(full_redundant_packet);
-    int host_buf_size = N * mChans * mSmplSize;
     int hdr_size      = mJackTrip->getHeaderSizeInBytes();
+
+    // Guard: the stride computed from peer-supplied fields must fit within the datagram.
+    int stride_bytes = hdr_size + N * peer_chans * mSmplSize;
+    if (stride_bytes <= hdr_size || stride_bytes > full_redundant_packet_size) {
+        std::cerr << "ERROR: receivePacketRedundancy stride (" << stride_bytes
+                  << ") out of range; dropping packet." << endl;
+        return;
+    }
+    // Clamp redundancy loop so no index overruns the datagram.
+    int max_redun_index =
+        (full_redundant_packet_size - stride_bytes) / full_packet_size;
+    if (redun_last_index > max_redun_index) {
+        redun_last_index = max_redun_index;
+    }
+
+    int host_buf_size = N * mChans * mSmplSize;
     int gap_size      = mInitialState ? 0 : (lost - redun_last_index) * host_buf_size;
 
     last_seq_num = newer_seq_num;  // Save last read packet
index 9006d764878de50dd2a090e88ca4813fa7dd92c9..6e9fa0dc3d8df058a37f85467173a95c2053abf2 100644 (file)
 #include "JackTripWorker.h"
 #include "jacktrip_globals.h"
 
+#ifdef WEBRTC_SUPPORT
+#include "webrtc/WebRtcPeerConnection.h"
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+#include "webtransport/WebTransportSession.h"
+#endif
+
+using std::cerr;
 using std::cout;
 using std::endl;
 
@@ -116,12 +125,23 @@ UdpHubListener::UdpHubListener(int server_port, int server_udp_port, QObject* pa
     mSimulatedDelayRel   = 0.0;
 
     mUseRtUdpPriority = false;
+
+#ifdef WEBTRANSPORT_SUPPORT
+    mHttp3Server = nullptr;
+#endif
 }
 
 //*******************************************************************************
 UdpHubListener::~UdpHubListener()
 {
     mStopCheckTimer.stop();
+#ifdef WEBTRANSPORT_SUPPORT
+    if (mHttp3Server) {
+        mHttp3Server->stop();
+        delete mHttp3Server;
+        mHttp3Server = nullptr;
+    }
+#endif
     QMutexLocker lock(&mMutex);
     // delete mJTWorker;
     for (int i = 0; i < gMaxThreads; i++) {
@@ -151,9 +171,9 @@ void UdpHubListener::start()
         return;
     }
 
-    if (mRequireAuth) {
-        cout << "JackTrip HUB SERVER: Enabling authentication" << endl;
-        // Check that SSL is available
+    // Load TLS cert+key whenever both files are provided — needed for mRequireAuth and
+    // for accepting WebRTC WSS (wss://) connections via TLS sniffing on port 4464.
+    if (!mCertFile.isEmpty() && !mKeyFile.isEmpty()) {
         bool error = false;
         QString error_message;
         if (!QSslSocket::supportsSsl()) {
@@ -163,21 +183,21 @@ void UdpHubListener::start()
                 "libraries\ninstalled to enable authentication.";
         }
 
-        if (mCertFile.isEmpty()) {
-            error         = true;
-            error_message = QStringLiteral("No certificate file specified.");
-        } else if (mKeyFile.isEmpty()) {
-            error         = true;
-            error_message = QStringLiteral("No private key file specified.");
-        }
-
-        // Load our certificate and private key
         if (!error) {
             QFile certFile(mCertFile);
             if (certFile.open(QIODevice::ReadOnly)) {
-                QSslCertificate cert(certFile.readAll());
-                if (!cert.isNull()) {
-                    mTcpServer.setCertificate(cert);
+                // Read all PEM blocks from the file so that the full certificate
+                // chain (leaf + any intermediate CA certs) is sent to clients.
+                // Using QSslCertificate(data) would silently read only the first
+                // block, leaving out intermediates that browsers need to verify trust.
+                QList<QSslCertificate> chain =
+                    QSslCertificate::fromData(certFile.readAll());
+                if (!chain.isEmpty() && !chain.first().isNull()) {
+                    mTcpServer.setCertificateChain(chain);
+                    if (chain.size() > 1) {
+                        cout << "JackTrip HUB SERVER: Loaded certificate chain ("
+                             << chain.size() << " certificates)" << endl;
+                    }
                 } else {
                     error         = true;
                     error_message = QStringLiteral("Unable to load certificate file.");
@@ -191,20 +211,52 @@ void UdpHubListener::start()
         if (!error) {
             QFile keyFile(mKeyFile);
             if (keyFile.open(QIODevice::ReadOnly)) {
-                QSslKey key(&keyFile, QSsl::Rsa);
+                const QByteArray keyPem = keyFile.readAll();
+                // Certbot / Let's Encrypt may issue ECDSA keys; PEM must be matched to
+                // QSsl::Rsa or QSsl::Ec — a wrong algorithm yields a null QSslKey.
+                QSslKey key(keyPem, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
+                if (key.isNull()) {
+                    key = QSslKey(keyPem, QSsl::Ec, QSsl::Pem, QSsl::PrivateKey);
+                }
                 if (!key.isNull()) {
                     mTcpServer.setPrivateKey(key);
                 } else {
-                    error = true;
-                    error_message =
-                        QStringLiteral("Unable to read RSA private key file.");
+                    error         = true;
+                    error_message = QStringLiteral(
+                        "Unable to read private key file (unsupported "
+                        "algorithm or invalid PEM).");
                 }
             } else {
                 error         = true;
-                error_message = QStringLiteral("Could not find RSA private key file.");
+                error_message = QStringLiteral("Could not find private key file.");
             }
         }
 
+        if (error) {
+            if (mRequireAuth) {
+                emit signalError(error_message);
+                return;
+            }
+            cerr << "JackTrip HUB SERVER: TLS certificate loading failed: "
+                 << error_message.toStdString() << endl;
+        } else {
+            cout << "JackTrip HUB SERVER: Loaded TLS certificate" << endl;
+            mTlsConfigured = true;
+        }
+    }
+
+    if (mRequireAuth) {
+        cout << "JackTrip HUB SERVER: Enabling authentication" << endl;
+        bool error = false;
+        QString error_message;
+        if (mCertFile.isEmpty()) {
+            error         = true;
+            error_message = QStringLiteral("No certificate file specified.");
+        } else if (mKeyFile.isEmpty()) {
+            error         = true;
+            error_message = QStringLiteral("No private key file specified.");
+        }
+
         if (!error) {
             QFileInfo credsInfo(mCredsFile);
             if (!credsInfo.exists() || !credsInfo.isFile()) {
@@ -222,6 +274,39 @@ void UdpHubListener::start()
 
     startOscServer();
 
+#ifdef WEBRTC_SUPPORT
+    if (mTlsConfigured) {
+        cout << "JackTrip HUB SERVER: WebRTC data channels enabled" << endl;
+    } else {
+        cout << "JackTrip HUB SERVER: WebRTC data channels disabled (no TLS certificate)"
+             << endl;
+    }
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+    if (mTlsConfigured) {
+        mHttp3Server = new Http3Server(mCertFile, mKeyFile, mServerPort);
+        mHttp3Server->setConnectionCallback(
+            [this](HQUIC connection, const QHostAddress& addr, quint16 port) {
+                int workerId = createWebTransportWorker(connection, addr, port);
+                if (workerId < 0) {
+                    cerr << "JackTrip HUB SERVER: No available slots for WebTransport "
+                            "client"
+                         << endl;
+                }
+            });
+        if (mHttp3Server->start()) {
+            cout << "JackTrip HUB SERVER: WebTransport enabled" << endl;
+        } else {
+            cerr << "JackTrip HUB SERVER: Failed to initialize WebTransport" << endl;
+            delete mHttp3Server;
+            mHttp3Server = nullptr;
+        }
+    } else {
+        cout << "JackTrip HUB SERVER: WebTransport disabled (no TLS certificate)" << endl;
+    }
+#endif
+
     cout << "JackTrip HUB SERVER: Waiting for client connections..." << endl;
     cout << "JackTrip HUB SERVER: Hub auto audio patch setting = " << mHubPatch << " ("
          << mHubPatchDescriptions.at(mHubPatch).toStdString() << ")" << endl;
@@ -239,13 +324,44 @@ void UdpHubListener::receivedNewConnection()
     QSslSocket* clientSocket =
         static_cast<QSslSocket*>(mTcpServer.nextPendingConnection());
     connect(clientSocket, &QAbstractSocket::readyRead, this, [this, clientSocket] {
-        receivedClientInfo(clientSocket);
+        readyRead(clientSocket);
     });
     cout << "JackTrip HUB SERVER: Client Connection Received!" << endl;
 }
 
-void UdpHubListener::receivedClientInfo(QSslSocket* clientConnection)
+void UdpHubListener::readyRead(QSslSocket* clientConnection)
 {
+    if (!clientConnection->isEncrypted()) {
+        // Detect TLS ClientHello by inspecting the 3-byte TLS record header:
+        //   { 0x16, 0x03, minor } where minor is 0x01–0x04 (TLS 1.0–1.3).
+        // Checking 3 bytes is unambiguous: a valid port number sent by the binary hub
+        // protocol (a 32-bit LE integer ≤ 65535) requires bytes[2] == 0x00, so no valid
+        // port can collide with the TLS prefix. Port 22 (byte[0] == 0x16) and port 790
+        // (bytes[0,1] == {0x16, 0x03}) are the nearest false-positives with a shorter
+        // check; both are ruled out by the third byte. Wait for 3 bytes if they have
+        // not yet arrived rather than making a premature decision.
+        QByteArray header = clientConnection->peek(3);
+        if (header.size() < 3) {
+            return;
+        }
+        if (static_cast<unsigned char>(header[0]) == 0x16
+            && static_cast<unsigned char>(header[1]) == 0x03
+            && static_cast<unsigned char>(header[2]) >= 0x01
+            && static_cast<unsigned char>(header[2]) <= 0x04) {
+            cout << "JackTrip HUB SERVER: TLS ClientHello detected" << endl;
+            if (!mTlsConfigured) {
+                cerr << "JackTrip HUB SERVER: Received TLS ClientHello but no "
+                        "certificate is configured. Closing connection."
+                     << endl;
+                clientConnection->close();
+                clientConnection->deleteLater();
+                return;
+            }
+            clientConnection->startServerEncryption();
+            return;
+        }
+    }
+
     QHostAddress PeerAddress = clientConnection->peerAddress();
     cout << "JackTrip HUB SERVER: Client Connect Received from Address : "
          << PeerAddress.toString().toStdString() << endl;
@@ -253,7 +369,6 @@ void UdpHubListener::receivedClientInfo(QSslSocket* clientConnection)
     // Get UDP port from client
     // ------------------------
     QString clientName = QString();
-    cout << "JackTrip HUB SERVER: Reading UDP port from Client..." << endl;
     int peer_udp_port;
     if (!clientConnection->isEncrypted()) {
         if (clientConnection->bytesAvailable() < (int)sizeof(qint32)) {
@@ -293,6 +408,91 @@ void UdpHubListener::receivedClientInfo(QSslSocket* clientConnection)
             return;
         }
     } else {
+        // Socket is in SSL mode. Check for a WebSocket upgrade before falling through to
+        // the binary authentication flow — browsers send HTTP GET after the TLS
+        // handshake.
+#ifdef WEBRTC_SUPPORT
+        QByteArray peekData = clientConnection->peek(512);
+        if (peekData.startsWith("GET") || peekData.startsWith("OPTIONS")) {
+            // Extract the Origin header (case-insensitive) so we can echo it back in
+            // Access-Control-Allow-Origin, enabling cross-origin browser clients.
+            QByteArray origin;
+            int originIdx = peekData.toLower().indexOf("\r\norigin: ");
+            if (originIdx != -1) {
+                int valueStart = originIdx + 10;  // len("\r\norigin: ") == 10
+                int valueEnd   = peekData.indexOf('\r', valueStart);
+                if (valueEnd != -1) {
+                    origin = peekData.mid(valueStart, valueEnd - valueStart).trimmed();
+                }
+            }
+
+            // Build CORS response headers only when the request carries an Origin.
+            QByteArray corsHeaders;
+            if (!origin.isEmpty()) {
+                corsHeaders = "Access-Control-Allow-Origin: " + origin + "\r\n"
+                              "Access-Control-Allow-Methods: GET, OPTIONS\r\n"
+                              "Access-Control-Allow-Headers: Content-Type\r\n";
+            }
+
+            // Respond to CORS preflight (OPTIONS) immediately.
+            if (peekData.startsWith("OPTIONS")) {
+                QByteArray response =
+                    "HTTP/1.1 204 No Content\r\n"
+                    "Connection: close\r\n"
+                    + corsHeaders + "\r\n";
+                clientConnection->write(response);
+                clientConnection->flush();
+                clientConnection->disconnectFromHost();
+                return;
+            }
+
+            // Extract the request line to check for the /ping health-check endpoint.
+            // This lets browser clients verify TLS + HTTP connectivity before
+            // attempting a WebSocket upgrade, which is useful for diagnosing
+            // connection issues.
+            QByteArray requestLine         = peekData.left(peekData.indexOf('\r'));
+            QList<QByteArray> requestParts = requestLine.split(' ');
+            QByteArray requestPath =
+                requestParts.size() > 1 ? requestParts[1] : QByteArray();
+            if (requestPath == "/ping") {
+                cout << "JackTrip HUB SERVER: Responding to /ping health check" << endl;
+                QByteArray body = "{\"status\":\"OK\"}";
+                QByteArray response =
+                    "HTTP/1.1 200 OK\r\n"
+                    "Content-Type: application/json\r\n"
+                    "Content-Length: "
+                    + QByteArray::number(body.size()) + "\r\n" + "Connection: close\r\n"
+                    + corsHeaders + "\r\n" + body;
+                clientConnection->write(response);
+                clientConnection->flush();
+                clientConnection->disconnectFromHost();
+                return;
+            } else if (requestPath == "/webrtc" || requestPath.startsWith("/webrtc?")) {
+                cout << "JackTrip HUB SERVER: WebRTC connection detected" << endl;
+                disconnect(clientConnection, nullptr, this, nullptr);
+                int workerId = createWebRtcWorker(clientConnection);
+                if (workerId < 0) {
+                    cerr << "JackTrip HUB SERVER: No available slots for WebRTC client"
+                         << endl;
+                    clientConnection->close();
+                    clientConnection->deleteLater();
+                }
+                return;
+            } else {
+                QByteArray body = "{\"status\":\"Not Found\"}";
+                QByteArray response =
+                    "HTTP/1.1 404 Not Found\r\n"
+                    "Content-Type: application/json\r\n"
+                    "Content-Length: "
+                    + QByteArray::number(body.size()) + "\r\n" + "Connection: close\r\n"
+                    + corsHeaders + "\r\n" + body;
+                clientConnection->write(response);
+                clientConnection->flush();
+                clientConnection->disconnectFromHost();
+                return;
+            }
+        }
+#endif  // WEBRTC_SUPPORT
         // This branch executes when our socket is already in SSL mode and we're expecting
         // to read our authentication data.
         peer_udp_port = checkAuthAndReadPort(clientConnection, clientName);
@@ -320,38 +520,37 @@ void UdpHubListener::receivedClientInfo(QSslSocket* clientConnection)
     // or port yet. We need to wait until we receive the port value from the UDP header to
     // accommodate NAT.
     // -----------------------------
-    int id = getJackTripWorker(PeerAddress.toString(), peer_udp_port, clientName);
-
-    // Assign server port and send it to Client
-    if (id != -1) {
-        cout << "JackTrip HUB SERVER: Sending Final UDP Port to Client: "
-             << clientName.toStdString() << " = " << mJTWorkers->at(id)->getServerPort()
-             << endl;
-    }
-
-    if (id == -1
-        || sendUdpPort(clientConnection, mJTWorkers->at(id)->getServerPort()) == 0) {
+    int id = createWorker(clientName);
+    if (id < 0) {
+        cerr << "JackTrip HUB SERVER: No available slots for new client" << endl;
         clientConnection->close();
         clientConnection->deleteLater();
-        releaseThread(id);
         return;
     }
 
+    mJTWorkers->at(id)->setJackTrip(
+        id, PeerAddress.toString(), mBasePort + id,
+        0,  // Set client port to 0 initially until we receive a UDP packet.
+        m_connectDefaultAudioPorts);
+
+    // Assign server port and send it to Client
+    cout << "JackTrip HUB SERVER: Sending Final UDP Port to Client: "
+         << clientName.toStdString() << " = " << mJTWorkers->at(id)->getServerPort()
+         << endl;
+
+    int send_port_result =
+        sendUdpPort(clientConnection, mJTWorkers->at(id)->getServerPort());
+
     // Close and mark socket for deletion
     // ----------------------------------
     clientConnection->close();
     clientConnection->deleteLater();
-    cout << "JackTrip HUB SERVER: Client TCP Connection Closed!" << endl;
 
-    if (mIOStatTimeout > 0) {
-        mJTWorkers->at(id)->setIOStatTimeout(mIOStatTimeout);
-        mJTWorkers->at(id)->setIOStatStream(mIOStatStream);
-    }
-    mJTWorkers->at(id)->setBufferStrategy(mBufferStrategy);
-    mJTWorkers->at(id)->setNetIssuesSimulation(mSimulatedLossRate, mSimulatedJitterRate,
-                                               mSimulatedDelayRel);
-    mJTWorkers->at(id)->setBroadcast(mBroadcastQueue);
-    mJTWorkers->at(id)->setUseRtUdpPriority(mUseRtUdpPriority);
+    if (send_port_result == 0) {
+        releaseThread(id);
+        return;
+    }
+
     cout << "JackTrip HUB SERVER: Starting JackTripWorker..." << endl;
     mJTWorkers->at(id)->start();
 }
@@ -423,7 +622,7 @@ int UdpHubListener::readClientUdpPort(QSslSocket* clientConnection, QString& cli
     if (clientConnection->bytesAvailable() == gMaxRemoteNameLength) {
         char name_buf[gMaxRemoteNameLength];
         clientConnection->read(name_buf, gMaxRemoteNameLength);
-        clientName = QString::fromUtf8((const char*)name_buf);
+        clientName = QString::fromUtf8((const char*)name_buf, gMaxRemoteNameLength);
     }
 
     return udp_port;
@@ -458,8 +657,16 @@ int UdpHubListener::checkAuthAndReadPort(QSslSocket* clientConnection,
         qFromLittleEndian<qint32>(buf + gMaxRemoteNameLength + (2 * sizeof(qint32)));
     delete[] buf;
 
-    // Check if we have enough data.
-    if (clientConnection->bytesAvailable() < size + usernameLength + passwordLength + 2) {
+    // Reject negative or oversized lengths before doing any arithmetic with them.
+    if (usernameLength < 0 || usernameLength > gMaxCredentialLength || passwordLength < 0
+        || passwordLength > gMaxCredentialLength) {
+        return Auth::WRONGCREDS;
+    }
+
+    // Widen to qint64 before summing to avoid signed-integer overflow with
+    // attacker-supplied lengths.
+    if (clientConnection->bytesAvailable()
+        < static_cast<qint64>(size) + usernameLength + passwordLength + 2) {
         return 0;
     }
 
@@ -473,24 +680,23 @@ int UdpHubListener::checkAuthAndReadPort(QSslSocket* clientConnection,
     // Then our jack client name.
     char name_buf[gMaxRemoteNameLength];
     clientConnection->read(name_buf, gMaxRemoteNameLength);
-    clientName = QString::fromUtf8((const char*)name_buf);
+    clientName = QString::fromUtf8((const char*)name_buf, gMaxRemoteNameLength);
 
     // We can discard our username and password length since we already have them.
     clientConnection->read(port_buf, size);
     clientConnection->read(port_buf, size);
     delete[] port_buf;
 
-    // And then get our username and password.
+    // And then get our username and password. Read exactly usernameLength/passwordLength
+    // bytes (the +1 null terminator byte is consumed but not used as a length hint).
     QString username, password;
-    char* username_buf = new char[usernameLength + 1];
-    clientConnection->read(username_buf, usernameLength + 1);
-    username = QString::fromUtf8((const char*)username_buf);
-    delete[] username_buf;
+    QByteArray username_buf = clientConnection->read(usernameLength);
+    clientConnection->read(1);  // consume null terminator
+    username = QString::fromUtf8(username_buf.constData(), username_buf.size());
 
-    char* password_buf = new char[passwordLength + 1];
-    clientConnection->read(password_buf, passwordLength + 1);
-    password = QString::fromUtf8((const char*)password_buf);
-    delete[] password_buf;
+    QByteArray password_buf = clientConnection->read(passwordLength);
+    clientConnection->read(1);  // consume null terminator
+    password = QString::fromUtf8(password_buf.constData(), password_buf.size());
 
     // Check if our credentials are valid, and return either an error code or our port.
     Auth::AuthResponseT response = mAuth->checkCredentials(username, password);
@@ -540,35 +746,44 @@ void UdpHubListener::bindUdpSocket(QUdpSocket& udpsocket, int port)
 }
 
 //*******************************************************************************
-int UdpHubListener::getJackTripWorker(const QString& address,
-                                      [[maybe_unused]] uint16_t port, QString& clientName)
+int UdpHubListener::createWorker(QString& clientName)
 {
-    // Find our first empty slot in our vector of worker object pointers.
-    // Return -1 if we have no space left for additional threads, or the index of the new
-    // JackTripWorker.
     QMutexLocker lock(&mMutex);
+
+    // Find first empty slot
     int id = -1;
     for (int i = 0; i < gMaxThreads; i++) {
         if (mJTWorkers->at(i) == nullptr) {
             id = i;
-            i  = gMaxThreads;
+            break;
         }
     }
 
-    if (id >= 0) {
-        mTotalRunningThreads++;
-        if (mAppendThreadID) {
-            clientName = clientName + QStringLiteral("_%1").arg(id + 1);
-        }
-        mJTWorkers->replace(id,
-                            new JackTripWorker(this, mBufferQueueLength, mUnderRunMode,
-                                               mAudioBitResolution, clientName));
-        mJTWorkers->at(id)->setJackTrip(
-            id, address, mBasePort + id,
-            0,  // Set client port to 0 initially until we receive a UDP packet.
-            m_connectDefaultAudioPorts);  //
+    if (id < 0) {
+        return -1;  // No available slots
+    }
+
+    mTotalRunningThreads++;
+    if (mAppendThreadID) {
+        clientName = clientName + QStringLiteral("_%1").arg(id + 1);
     }
 
+    // Create a JackTripWorker
+    JackTripWorker* worker = new JackTripWorker(this, mBufferQueueLength, mUnderRunMode,
+                                                mAudioBitResolution, clientName);
+
+    if (mIOStatTimeout > 0) {
+        worker->setIOStatTimeout(mIOStatTimeout);
+        worker->setIOStatStream(mIOStatStream);
+    }
+    worker->setBufferStrategy(mBufferStrategy);
+    worker->setNetIssuesSimulation(mSimulatedLossRate, mSimulatedJitterRate,
+                                   mSimulatedDelayRel);
+    worker->setBroadcast(mBroadcastQueue);
+    worker->setUseRtUdpPriority(mUseRtUdpPriority);
+
+    mJTWorkers->replace(id, worker);
+
     return id;
 }
 
@@ -612,6 +827,26 @@ void UdpHubListener::unregisterClientWithPatcher(QString& clientName)
 }
 #endif  // NO_JACK
 
+//*******************************************************************************
+void UdpHubListener::handleWorkerRemoval()
+{
+    // Get the worker that emitted the signal
+    JackTripWorker* worker = qobject_cast<JackTripWorker*>(sender());
+    if (!worker) {
+        cerr << "UdpHubListener::handleWorkerRemoval: ERROR - sender is not a "
+                "JackTripWorker"
+             << endl;
+        return;
+    }
+
+    int id = worker->getID();
+    if (gVerboseFlag) {
+        cout << "UdpHubListener: Removing worker " << id << endl;
+    }
+
+    releaseThread(id);
+}
+
 //*******************************************************************************
 int UdpHubListener::releaseThread(int id)
 {
@@ -707,6 +942,85 @@ void UdpHubListener::stopAllThreads()
         }
     }
 }
+
+#ifdef WEBRTC_SUPPORT
+//*******************************************************************************
+int UdpHubListener::createWebRtcWorker(QSslSocket* signalingSocket)
+{
+    // Create worker with a temporary placeholder name
+    // It will be updated with the actual client name after WebSocket upgrade
+    QString tempName = QStringLiteral("");
+    int id           = createWorker(tempName);
+    if (id < 0) {
+        return -1;  // No available slots
+    }
+    JackTripWorker* worker = mJTWorkers->at(id);
+
+    // Ensure worker runs in UdpHubListener's thread for proper signal/slot handling
+    worker->moveToThread(this->thread());
+
+    // Connect worker's removal signal to our handler
+    connect(worker, &JackTripWorker::signalRemoveThread, this,
+            &UdpHubListener::handleWorkerRemoval, Qt::QueuedConnection);
+
+    // Derive the ICE UDP port from this worker's pre-assigned JackTrip port.
+    // In WebRTC mode JackTrip never binds that port itself (the data travels over
+    // the WebRTC data channel), so libjuice is free to use it.  This keeps all
+    // WebRTC traffic on the same port range that firewall rules already allow.
+    uint16_t workerPort = static_cast<uint16_t>(mBasePort + id);
+
+    // Have the worker create its own WebRTC peer connection
+    // The worker will handle connection lifecycle and start when ready
+    worker->createWebRtcPeerConnection(signalingSocket, mIceServers, workerPort,
+                                       workerPort);
+
+    // Note: We don't call setJackTrip yet because we don't have the data channel.
+    // The worker will be started when the data channel opens (handled by worker).
+    return id;
+}
+#endif  // WEBRTC_SUPPORT
+
+#ifdef WEBTRANSPORT_SUPPORT
+//*******************************************************************************
+int UdpHubListener::createWebTransportWorker(HQUIC connection,
+                                             const QHostAddress& peerAddress,
+                                             quint16 peerPort)
+{
+    QString tempName = QStringLiteral("");
+    int id           = createWorker(tempName);
+    if (id < 0) {
+        cerr << "UdpHubListener: createWorker failed - no available slots" << endl;
+        return -1;  // No available slots
+    }
+
+    JackTripWorker* worker = mJTWorkers->at(id);
+
+    // Move worker to UdpHubListener's thread since we're being called from msquic thread
+    // This ensures signals/slots use the correct event loop
+    worker->moveToThread(this->thread());
+
+    // Connect worker's removal signal to our handler
+    connect(worker, &JackTripWorker::signalRemoveThread, this,
+            &UdpHubListener::handleWorkerRemoval, Qt::QueuedConnection);
+
+    // Create WebTransport session with no parent initially (we're on msquic thread)
+    // The session takes ownership of the connection handle
+    WebTransportSession* session =
+        new WebTransportSession(mHttp3Server ? mHttp3Server->quicApi() : nullptr,
+                                connection, peerAddress, peerPort, nullptr);
+
+    // Move session to the same thread as the worker
+    session->moveToThread(this->thread());
+
+    // Have the worker use this session (will setParent to worker)
+    worker->createWebTransportSession(session);
+
+    // Note: Worker will be started when the session is established
+    return id;
+}
+
+#endif  // WEBTRANSPORT_SUPPORT
+
 // TODO:
 // USE bool QAbstractSocket::isValid () const to check if socket is connect. if not, exit
 // loop
index 84e53a71553332cbbc7ff92d765fc8a89bfc40fc..c1ee56195dfdd343cb4c2eb59b91041e10cc83c0 100644 (file)
 #include "JackTrip.h"
 #include "jacktrip_globals.h"
 #include "jacktrip_types.h"
+
+#ifdef WEBRTC_SUPPORT
+class WebRtcPeerConnection;
+#endif
+#ifdef WEBTRANSPORT_SUPPORT
+#include "http3/Http3Server.h"
+#endif
 #ifndef NO_JACK
 #include "Patcher.h"
 #endif
@@ -89,6 +96,8 @@ class UdpHubListener : public QObject
     int releaseThread(int id);
     void releaseDuplicateThreads(JackTripWorker* worker, uint16_t actual_peer_port);
     void getClientLatencies(QVector<QString>& clientNames, QVector<double>& latencies);
+    int getBasePort() const { return mBasePort; }
+    bool getConnectDefaultAudioPorts() const { return m_connectDefaultAudioPorts; }
 
     void setConnectDefaultAudioPorts(bool connectDefaultAudioPorts)
     {
@@ -115,6 +124,7 @@ class UdpHubListener : public QObject
     void stopCheck();
     void queueBufferChanged(int queueBufferSize);
     void handleLatencyRequest(const QHostAddress& sender, quint16 senderPort);
+    void handleWorkerRemoval();
 
    signals:
     void signalStarted();
@@ -123,12 +133,13 @@ class UdpHubListener : public QObject
     void signalError(const QString& errorMessage);
 
    private:
+    // new bytes are ready to read from the client connection
+    void readyRead(QSslSocket* clientConnection);
+
     /** \brief Binds a QUdpSocket. It chooses the available (active) interface.
      * \param udpsocket a QUdpSocket
      * \param port Port number
      */
-    void receivedClientInfo(QSslSocket* clientConnection);
-
     static void bindUdpSocket(QUdpSocket& udpsocket, int port);
 
     int readClientUdpPort(QSslSocket* clientConnection, QString& clientName);
@@ -138,7 +149,7 @@ class UdpHubListener : public QObject
     void startOscServer()
     {
         // start osc server to listen to config updates
-        mOscServer = new OscServer(mServerPort, this);
+        mOscServer = new OscServer(mServerPort + 1, this);
         mOscServer->start();
 
         QObject::connect(mOscServer, &OscServer::signalQueueBufferChanged, this,
@@ -154,13 +165,21 @@ class UdpHubListener : public QObject
      */
     // void sendToPoolPrototype(int id);
 
-    /**
-     * \brief Check if address is already handled and reuse or create
-     * a JackTripWorker as appropriate
-     * \param address as string (IPv4 or IPv6)
-     * \return id number of JackTripWorker
-     */
-    int getJackTripWorker(const QString& address, uint16_t port, QString& clientName);
+    /// \brief Create a new JackTripWorker and allocate it a slot
+    /// \param clientName The client name (will be modified if mAppendThreadID is set)
+    /// \return The slot id, or -1 if no slots available
+    int createWorker(QString& clientName);
+
+#ifdef WEBRTC_SUPPORT
+    /// \brief Create a WebRTC worker for a new connection
+    int createWebRtcWorker(QSslSocket* signalingSocket);
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+    /// \brief Create a WebTransport worker for a new QUIC connection
+    int createWebTransportWorker(HQUIC connection, const QHostAddress& peerAddress,
+                                 quint16 peerPort);
+#endif
 
     /** \brief Returns the ID of the client in the pool. If the client
      * is not in the pool yet, returns -1.
@@ -184,7 +203,8 @@ class UdpHubListener : public QObject
     // addressPortNameTriple mActiveAddress[gMaxThreads]; ///< Active address pool
     // addresses QHash<QString, uint16_t> mActiveAddressPortPair;
 
-    bool mRequireAuth;
+    bool mRequireAuth   = false;
+    bool mTlsConfigured = false;
     QString mCertFile;
     QString mKeyFile;
     QString mCredsFile;
@@ -217,6 +237,16 @@ class UdpHubListener : public QObject
     double mSimulatedDelayRel;
     bool mUseRtUdpPriority;
 
+#ifdef WEBRTC_SUPPORT
+    /// \brief ICE servers for WebRTC connections
+    QStringList mIceServers;
+#endif
+
+#ifdef WEBTRANSPORT_SUPPORT
+    /// \brief QUIC/HTTP3 server lifecycle (msquic wrapper)
+    Http3Server* mHttp3Server;
+#endif
+
 #ifdef WAIR  // wair
     bool mWAIR;
     void connectMesh(bool spawn);
@@ -289,6 +319,11 @@ class UdpHubListener : public QObject
     void setBroadcast(int broadcast_queue) { mBroadcastQueue = broadcast_queue; }
     void setUseRtUdpPriority(bool use) { mUseRtUdpPriority = use; }
     bool mAppendThreadID = false;
+
+#ifdef WEBRTC_SUPPORT
+    /// \brief Set ICE servers for WebRTC connections
+    void setIceServers(const QStringList& servers) { mIceServers = servers; }
+#endif
 };
 
 #endif  //__UDPHUBLISTENER_H__
index faf15f0822fc503d51b649cfb5436607f0d09ea3..f56540570d5998be448ce99cb63394bc23144160 100644 (file)
@@ -38,7 +38,7 @@ const QString About::s_buildID = QStringLiteral(BUILD_ID);
 #elif defined(JACKTRIP_BUILD_INFO)
 #define STR(s)       #s
 #define TO_STRING(s) STR(s)
-const QString About::s_buildID   = QLatin1String(TO_STRING(JACKTRIP_BUILD_INFO));
+const QString About::s_buildID = QLatin1String(TO_STRING(JACKTRIP_BUILD_INFO));
 #else
 const QString About::s_buildID = QLatin1String("");
 #endif
diff --git a/src/http3/Http3Protocol.cpp b/src/http3/Http3Protocol.cpp
new file mode 100644 (file)
index 0000000..e7d0928
--- /dev/null
@@ -0,0 +1,876 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 Http3Protocol.cpp
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#include "Http3Protocol.h"
+
+#include <iostream>
+
+using std::cerr;
+using std::endl;
+
+namespace Http3
+{
+
+//*******************************************************************************
+// QPACK static table (RFC 9204 Appendix A)
+// Each entry is (name, value); access with name = TABLE[index*2], value =
+// TABLE[index*2+1]
+const char* QPACK_STATIC_TABLE[] = {
+    ":authority",
+    "",  // 0
+    ":path",
+    "/",  // 1
+    "age",
+    "0",  // 2
+    "content-disposition",
+    "",  // 3
+    "content-length",
+    "0",  // 4
+    "cookie",
+    "",  // 5
+    "date",
+    "",  // 6
+    "etag",
+    "",  // 7
+    "if-modified-since",
+    "",  // 8
+    "if-none-match",
+    "",  // 9
+    "last-modified",
+    "",  // 10
+    "link",
+    "",  // 11
+    "location",
+    "",  // 12
+    "referer",
+    "",  // 13
+    "set-cookie",
+    "",  // 14
+    ":method",
+    "CONNECT",  // 15
+    ":method",
+    "DELETE",  // 16
+    ":method",
+    "GET",  // 17
+    ":method",
+    "HEAD",  // 18
+    ":method",
+    "OPTIONS",  // 19
+    ":method",
+    "POST",  // 20
+    ":method",
+    "PUT",  // 21
+    ":scheme",
+    "http",  // 22
+    ":scheme",
+    "https",  // 23
+    ":status",
+    "103",  // 24
+    ":status",
+    "200",  // 25
+    ":status",
+    "304",  // 26
+    ":status",
+    "404",  // 27
+    ":status",
+    "503",  // 28
+    "accept",
+    "*/*",  // 29
+    "accept",
+    "application/dns-message",  // 30
+    "accept-encoding",
+    "gzip, deflate, br",  // 31
+    "accept-ranges",
+    "bytes",  // 32
+    "access-control-allow-headers",
+    "cache-control",  // 33
+    "access-control-allow-headers",
+    "content-type",  // 34
+    "access-control-allow-origin",
+    "*",  // 35
+    "cache-control",
+    "max-age=0",  // 36
+    "cache-control",
+    "max-age=2592000",  // 37
+    "cache-control",
+    "max-age=604800",  // 38
+    "cache-control",
+    "no-cache",  // 39
+    "cache-control",
+    "no-store",  // 40
+    "cache-control",
+    "public, max-age=31536000",  // 41
+    "content-encoding",
+    "br",  // 42
+    "content-encoding",
+    "gzip",  // 43
+    "content-type",
+    "application/dns-message",  // 44
+    "content-type",
+    "application/javascript",  // 45
+    "content-type",
+    "application/json",  // 46
+    "content-type",
+    "application/x-www-form-urlencoded",  // 47
+    "content-type",
+    "image/gif",  // 48
+    "content-type",
+    "image/jpeg",  // 49
+    "content-type",
+    "image/png",  // 50
+    "content-type",
+    "text/css",  // 51
+    "content-type",
+    "text/html; charset=utf-8",  // 52
+    "content-type",
+    "text/plain",  // 53
+    "content-type",
+    "text/plain;charset=utf-8",  // 54
+    "range",
+    "bytes=0-",  // 55
+    "strict-transport-security",
+    "max-age=31536000",  // 56
+    "strict-transport-security",
+    "max-age=31536000; includesubdomains",  // 57
+    "strict-transport-security",
+    "max-age=31536000; includesubdomains; preload",  // 58
+    "vary",
+    "accept-encoding",  // 59
+    "vary",
+    "origin",  // 60
+    "x-content-type-options",
+    "nosniff",  // 61
+    "x-xss-protection",
+    "1; mode=block",  // 62
+    ":status",
+    "100",  // 63
+    ":status",
+    "204",  // 64
+    ":status",
+    "206",  // 65
+    ":status",
+    "302",  // 66
+    ":status",
+    "400",  // 67
+    ":status",
+    "403",  // 68
+    ":status",
+    "421",  // 69
+    ":status",
+    "425",  // 70
+    ":status",
+    "500",  // 71
+    "accept-language",
+    "",  // 72
+    "access-control-allow-credentials",
+    "FALSE",  // 73
+    "access-control-allow-credentials",
+    "TRUE",  // 74
+    "access-control-allow-headers",
+    "*",  // 75
+    "access-control-allow-methods",
+    "get",  // 76
+    "access-control-allow-methods",
+    "get, post, options",  // 77
+    "access-control-allow-methods",
+    "options",  // 78
+    "access-control-expose-headers",
+    "content-length",  // 79
+    "access-control-request-headers",
+    "content-type",  // 80
+    "access-control-request-method",
+    "get",  // 81
+    "access-control-request-method",
+    "post",  // 82
+    "alt-svc",
+    "clear",  // 83
+    "authorization",
+    "",  // 84
+    "content-security-policy",
+    "script-src 'none'; object-src 'none'; base-uri 'none'",  // 85
+    "early-data",
+    "1",  // 86
+    "expect-ct",
+    "",  // 87
+    "forwarded",
+    "",  // 88
+    "if-range",
+    "",  // 89
+    "origin",
+    "",  // 90
+    "purpose",
+    "prefetch",  // 91
+    "server",
+    "",  // 92
+    "timing-allow-origin",
+    "*",  // 93
+    "upgrade-insecure-requests",
+    "1",  // 94
+    "user-agent",
+    "",  // 95
+    "x-forwarded-for",
+    "",  // 96
+    "x-frame-options",
+    "deny",  // 97
+    "x-frame-options",
+    "sameorigin",  // 98
+};
+
+//*******************************************************************************
+// Complete HPACK Huffman table (RFC 7541 Appendix B), sorted by code
+const HuffmanCode HUFFMAN_CODES[] = {
+    // 5-bit codes (0x00-0x09)
+    {0x00, 5, '0'},
+    {0x01, 5, '1'},
+    {0x02, 5, '2'},
+    {0x03, 5, 'a'},
+    {0x04, 5, 'c'},
+    {0x05, 5, 'e'},
+    {0x06, 5, 'i'},
+    {0x07, 5, 'o'},
+    {0x08, 5, 's'},
+    {0x09, 5, 't'},
+    // 6-bit codes (0x14-0x2d)
+    {0x14, 6, ' '},
+    {0x15, 6, '%'},
+    {0x16, 6, '-'},
+    {0x17, 6, '.'},
+    {0x18, 6, '/'},
+    {0x19, 6, '3'},
+    {0x1a, 6, '4'},
+    {0x1b, 6, '5'},
+    {0x1c, 6, '6'},
+    {0x1d, 6, '7'},
+    {0x1e, 6, '8'},
+    {0x1f, 6, '9'},
+    {0x20, 6, '='},
+    {0x21, 6, 'A'},
+    {0x22, 6, '_'},
+    {0x23, 6, 'b'},
+    {0x24, 6, 'd'},
+    {0x25, 6, 'f'},
+    {0x26, 6, 'g'},
+    {0x27, 6, 'h'},
+    {0x28, 6, 'l'},
+    {0x29, 6, 'm'},
+    {0x2a, 6, 'n'},
+    {0x2b, 6, 'p'},
+    {0x2c, 6, 'r'},
+    {0x2d, 6, 'u'},
+    // 7-bit codes (0x5c-0x7b)
+    {0x5c, 7, ':'},
+    {0x5d, 7, 'B'},
+    {0x5e, 7, 'C'},
+    {0x5f, 7, 'D'},
+    {0x60, 7, 'E'},
+    {0x61, 7, 'F'},
+    {0x62, 7, 'G'},
+    {0x63, 7, 'H'},
+    {0x64, 7, 'I'},
+    {0x65, 7, 'J'},
+    {0x66, 7, 'K'},
+    {0x67, 7, 'L'},
+    {0x68, 7, 'M'},
+    {0x69, 7, 'N'},
+    {0x6a, 7, 'O'},
+    {0x6b, 7, 'P'},
+    {0x6c, 7, 'Q'},
+    {0x6d, 7, 'R'},
+    {0x6e, 7, 'S'},
+    {0x6f, 7, 'T'},
+    {0x70, 7, 'U'},
+    {0x71, 7, 'V'},
+    {0x72, 7, 'W'},
+    {0x73, 7, 'Y'},
+    {0x74, 7, 'j'},
+    {0x75, 7, 'k'},
+    {0x76, 7, 'q'},
+    {0x77, 7, 'v'},
+    {0x78, 7, 'w'},
+    {0x79, 7, 'x'},
+    {0x7a, 7, 'y'},
+    {0x7b, 7, 'z'},
+    // 8-bit codes (0xf8-0xff)
+    {0xf8, 8, '&'},
+    {0xf9, 8, '*'},
+    {0xfa, 8, ','},
+    {0xfb, 8, ';'},
+    {0xfc, 8, 'X'},
+    {0xfd, 8, 'Z'},
+    // 10-bit codes
+    {0x3f8, 10, '!'},
+    {0x3f9, 10, '"'},
+    {0x3fa, 10, '('},
+    {0x3fb, 10, ')'},
+    {0x3fc, 10, '?'},
+    // 11-bit codes
+    {0x7fa, 11, '\''},
+    {0x7fb, 11, '+'},
+    {0x7fc, 11, '|'},
+    // 12-bit codes
+    {0xffa, 12, '#'},
+    {0xffb, 12, '>'},
+    // 13-bit codes
+    {0x1ff8, 13, '\x00'},
+    {0x1ff9, 13, '$'},
+    {0x1ffa, 13, '@'},
+    {0x1ffb, 13, '['},
+    {0x1ffc, 13, ']'},
+    {0x1ffd, 13, '~'},
+    // 14-bit codes
+    {0x3ffc, 14, '^'},
+    {0x3ffd, 14, '}'},
+    // 15-bit codes
+    {0x7ffc, 15, '<'},
+    {0x7ffd, 15, '`'},
+    {0x7ffe, 15, '{'},
+    // 19-bit codes
+    {0x7fff0, 19, '\\'},
+    // 20-bit codes
+    {0xfffe6, 20, 0xc3},
+    {0xfffe7, 20, 0xd0},
+    {0xfffe8, 20, 0x80},
+    {0xfffe9, 20, 0x82},
+    {0xfffea, 20, 0x83},
+    {0xfffeb, 20, 0xa2},
+    {0xfffec, 20, 0xb8},
+    {0xfffed, 20, 0xc2},
+    {0xfffee, 20, 0xe0},
+    {0xfffef, 20, 0xe2},
+    // 21-bit codes
+    {0x1fffdc, 21, 0x99},
+    {0x1fffdd, 21, 0xa1},
+    {0x1fffde, 21, 0xa7},
+    {0x1fffdf, 21, 0xac},
+    {0x1fffe0, 21, 0xb0},
+    {0x1fffe1, 21, 0xb1},
+    {0x1fffe2, 21, 0xb3},
+    {0x1fffe3, 21, 0xd1},
+    {0x1fffe4, 21, 0xd8},
+    {0x1fffe5, 21, 0xd9},
+    {0x1fffe6, 21, 0xe3},
+    {0x1fffe7, 21, 0xe5},
+    {0x1fffe8, 21, 0xe6},
+    // 22-bit codes
+    {0x3fffd2, 22, 0x81},
+    {0x3fffd3, 22, 0x84},
+    {0x3fffd4, 22, 0x85},
+    {0x3fffd5, 22, 0x86},
+    {0x3fffd6, 22, 0x88},
+    {0x3fffd7, 22, 0x92},
+    {0x3fffd8, 22, 0x9a},
+    {0x3fffd9, 22, 0x9c},
+    {0x3fffda, 22, 0xa0},
+    {0x3fffdb, 22, 0xa3},
+    {0x3fffdc, 22, 0xa4},
+    {0x3fffdd, 22, 0xa9},
+    {0x3fffde, 22, 0xaa},
+    {0x3fffdf, 22, 0xad},
+    {0x3fffe0, 22, 0xb2},
+    {0x3fffe1, 22, 0xb5},
+    {0x3fffe2, 22, 0xb9},
+    {0x3fffe3, 22, 0xba},
+    {0x3fffe4, 22, 0xbb},
+    {0x3fffe5, 22, 0xbd},
+    {0x3fffe6, 22, 0xbe},
+    {0x3fffe7, 22, 0xc4},
+    {0x3fffe8, 22, 0xc6},
+    {0x3fffe9, 22, 0xe4},
+    {0x3fffea, 22, 0xe8},
+    {0x3fffeb, 22, 0xe9},
+    // 23-bit codes
+    {0x7fffd8, 23, 0x01},
+    {0x7fffd9, 23, 0x87},
+    {0x7fffda, 23, 0x89},
+    {0x7fffdb, 23, 0x8a},
+    {0x7fffdc, 23, 0x8b},
+    {0x7fffdd, 23, 0x8c},
+    {0x7fffde, 23, 0x8d},
+    {0x7fffdf, 23, 0x8f},
+    {0x7fffe0, 23, 0x93},
+    {0x7fffe1, 23, 0x95},
+    {0x7fffe2, 23, 0x96},
+    {0x7fffe3, 23, 0x97},
+    {0x7fffe4, 23, 0x98},
+    {0x7fffe5, 23, 0x9b},
+    {0x7fffe6, 23, 0x9d},
+    {0x7fffe7, 23, 0x9e},
+    {0x7fffe8, 23, 0xa5},
+    {0x7fffe9, 23, 0xa6},
+    {0x7fffea, 23, 0xa8},
+    {0x7fffeb, 23, 0xae},
+    {0x7fffec, 23, 0xaf},
+    {0x7fffed, 23, 0xb4},
+    {0x7fffee, 23, 0xb6},
+    {0x7fffef, 23, 0xb7},
+    {0x7ffff0, 23, 0xbc},
+    {0x7ffff1, 23, 0xbf},
+    {0x7ffff2, 23, 0xc5},
+    {0x7ffff3, 23, 0xe7},
+    {0x7ffff4, 23, 0xef},
+    // 24-bit codes
+    {0xffffea, 24, 0x09},
+    {0xffffeb, 24, 0x8e},
+    {0xffffec, 24, 0x90},
+    {0xffffed, 24, 0x91},
+    {0xffffee, 24, 0x94},
+    {0xffffef, 24, 0x9f},
+    {0xfffff0, 24, 0xab},
+    {0xfffff1, 24, 0xce},
+    {0xfffff2, 24, 0xd7},
+    {0xfffff3, 24, 0xe1},
+    {0xfffff4, 24, 0xec},
+    {0xfffff5, 24, 0xed},
+    // 25-bit codes
+    {0x1ffffec, 25, 0xc7},
+    {0x1ffffed, 25, 0xcf},
+    {0x1ffffee, 25, 0xea},
+    {0x1ffffef, 25, 0xeb},
+    // 26-bit codes
+    {0x3ffffe0, 26, 0xc0},
+    {0x3ffffe1, 26, 0xc1},
+    {0x3ffffe2, 26, 0xc8},
+    {0x3ffffe3, 26, 0xc9},
+    {0x3ffffe4, 26, 0xca},
+    {0x3ffffe5, 26, 0xcd},
+    {0x3ffffe6, 26, 0xd2},
+    {0x3ffffe7, 26, 0xd5},
+    {0x3ffffe8, 26, 0xda},
+    {0x3ffffe9, 26, 0xdb},
+    {0x3ffffea, 26, 0xee},
+    {0x3ffffeb, 26, 0xf0},
+    {0x3ffffec, 26, 0xf2},
+    {0x3ffffed, 26, 0xf3},
+    {0x3ffffee, 26, 0xff},
+    // 27-bit codes
+    {0x7ffffde, 27, 0xcb},
+    {0x7ffffdf, 27, 0xcc},
+    {0x7ffffe0, 27, 0xd3},
+    {0x7ffffe1, 27, 0xd4},
+    {0x7ffffe2, 27, 0xd6},
+    {0x7ffffe3, 27, 0xdd},
+    {0x7ffffe4, 27, 0xde},
+    {0x7ffffe5, 27, 0xdf},
+    {0x7ffffe6, 27, 0xf1},
+    {0x7ffffe7, 27, 0xf4},
+    {0x7ffffe8, 27, 0xf5},
+    {0x7ffffe9, 27, 0xf6},
+    {0x7ffffea, 27, 0xf7},
+    {0x7ffffeb, 27, 0xf8},
+    {0x7ffffec, 27, 0xfa},
+    {0x7ffffed, 27, 0xfb},
+    {0x7ffffee, 27, 0xfc},
+    {0x7ffffef, 27, 0xfd},
+    {0x7fffff0, 27, 0xfe},
+    // 28-bit codes
+    {0xfffffe2, 28, 0x02},
+    {0xfffffe3, 28, 0x03},
+    {0xfffffe4, 28, 0x04},
+    {0xfffffe5, 28, 0x05},
+    {0xfffffe6, 28, 0x06},
+    {0xfffffe7, 28, 0x07},
+    {0xfffffe8, 28, 0x08},
+    {0xfffffe9, 28, 0x0b},
+    {0xfffffea, 28, 0x0c},
+    {0xfffffeb, 28, 0x0e},
+    {0xfffffec, 28, 0x0f},
+    {0xfffffed, 28, 0x10},
+    {0xfffffee, 28, 0x11},
+    {0xfffffef, 28, 0x12},
+    {0xffffff0, 28, 0x13},
+    {0xffffff1, 28, 0x14},
+    {0xffffff2, 28, 0x15},
+    {0xffffff3, 28, 0x17},
+    {0xffffff4, 28, 0x18},
+    {0xffffff5, 28, 0x19},
+    {0xffffff6, 28, 0x1a},
+    {0xffffff7, 28, 0x1b},
+    {0xffffff8, 28, 0x1c},
+    {0xffffff9, 28, 0x1d},
+    {0xffffffa, 28, 0x1e},
+    {0xffffffb, 28, 0x1f},
+    {0xffffffc, 28, 0x7f},
+    {0xffffffd, 28, 0xdc},
+    {0xffffffe, 28, 0xf9},
+    // Note: 30-bit EOS marker (0x3fffffff) is handled via padding check
+};
+
+const size_t HUFFMAN_CODES_SIZE = sizeof(HUFFMAN_CODES) / sizeof(HUFFMAN_CODES[0]);
+
+//*******************************************************************************
+int64_t readVarint(const uint8_t* data, size_t len, size_t& pos)
+{
+    if (pos >= len) {
+        return -1;
+    }
+
+    uint8_t firstByte = data[pos];
+    uint8_t prefix    = firstByte >> 6;    // Top 2 bits indicate length
+    int64_t value     = firstByte & 0x3F;  // Bottom 6 bits are part of value
+
+    size_t numBytes = 1 << prefix;  // 1, 2, 4, or 8 bytes
+
+    if (pos + numBytes > len) {
+        return -1;
+    }
+
+    pos++;  // Move past first byte
+
+    for (size_t i = 1; i < numBytes; i++) {
+        value = (value << 8) | data[pos++];
+    }
+
+    return value;
+}
+
+//*******************************************************************************
+size_t encodeQuicVarint(uint64_t value, uint8_t* output)
+{
+    if (value <= 63) {
+        output[0] = static_cast<uint8_t>(value);
+        return 1;
+    } else if (value <= 16383) {
+        output[0] = static_cast<uint8_t>(0x40 | (value >> 8));
+        output[1] = static_cast<uint8_t>(value & 0xFF);
+        return 2;
+    } else if (value <= 1073741823) {
+        output[0] = static_cast<uint8_t>(0x80 | (value >> 24));
+        output[1] = static_cast<uint8_t>((value >> 16) & 0xFF);
+        output[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
+        output[3] = static_cast<uint8_t>(value & 0xFF);
+        return 4;
+    } else {
+        output[0] = static_cast<uint8_t>(0xC0 | (value >> 56));
+        output[1] = static_cast<uint8_t>((value >> 48) & 0xFF);
+        output[2] = static_cast<uint8_t>((value >> 40) & 0xFF);
+        output[3] = static_cast<uint8_t>((value >> 32) & 0xFF);
+        output[4] = static_cast<uint8_t>((value >> 24) & 0xFF);
+        output[5] = static_cast<uint8_t>((value >> 16) & 0xFF);
+        output[6] = static_cast<uint8_t>((value >> 8) & 0xFF);
+        output[7] = static_cast<uint8_t>(value & 0xFF);
+        return 8;
+    }
+}
+
+//*******************************************************************************
+int64_t readQpackInt(const uint8_t* data, size_t len, size_t& pos, int prefixBits)
+{
+    if (pos >= len || prefixBits < 1 || prefixBits > 8) {
+        return -1;
+    }
+
+    uint8_t mask  = (1 << prefixBits) - 1;
+    int64_t value = data[pos++] & mask;
+
+    if (value == mask) {
+        int m = 0;
+        uint8_t b;
+        do {
+            if (pos >= len) {
+                return -1;
+            }
+            b = data[pos++];
+            value += (b & 0x7F) * (1 << m);
+            m += 7;
+        } while ((b & 0x80) != 0);
+    }
+
+    return value;
+}
+
+//*******************************************************************************
+bool parseHttp3Frame(const uint8_t* data, size_t len, const uint8_t*& payloadData,
+                     size_t& payloadLen)
+{
+    size_t pos = 0;
+
+    while (pos < len) {
+        int64_t frameType = readVarint(data, len, pos);
+        if (frameType < 0) {
+            cerr << "Http3Protocol: Failed to read frame type at pos " << pos << endl;
+            return false;
+        }
+
+        int64_t frameLength = readVarint(data, len, pos);
+        if (frameLength < 0) {
+            cerr << "Http3Protocol: Failed to read frame length at pos " << pos << endl;
+            return false;
+        }
+
+        if (pos + frameLength > len) {
+            cerr << "Http3Protocol: Frame length exceeds buffer" << endl;
+            return false;
+        }
+
+        if (frameType == FRAME_HEADERS) {
+            payloadData = &data[pos];
+            payloadLen  = static_cast<size_t>(frameLength);
+            return true;
+        }
+
+        pos += static_cast<size_t>(frameLength);
+    }
+
+    cerr << "Http3Protocol: No HEADERS frame found in stream data" << endl;
+    return false;
+}
+
+//*******************************************************************************
+QString decodeHuffman(const uint8_t* data, size_t len)
+{
+    QString result;
+    uint64_t accum = 0;
+    int bits       = 0;
+
+    size_t bytePos = 0;
+    while (bytePos < len || bits >= 5) {
+        while (bits < 32 && bytePos < len) {
+            accum = (accum << 8) | data[bytePos++];
+            bits += 8;
+        }
+
+        if (bits < 5)
+            break;
+
+        bool found = false;
+        for (size_t i = 0; i < HUFFMAN_CODES_SIZE && !found; i++) {
+            int codeBits = HUFFMAN_CODES[i].bits;
+            if (codeBits > bits)
+                continue;
+
+            uint32_t code = static_cast<uint32_t>(accum >> (bits - codeBits));
+            uint32_t mask = (1u << codeBits) - 1;
+            code &= mask;
+
+            if (code == HUFFMAN_CODES[i].code) {
+                result += QChar(HUFFMAN_CODES[i].sym);
+                bits -= codeBits;
+                accum &= (1ULL << bits) - 1;
+                found = true;
+            }
+        }
+
+        if (!found) {
+            if (bits <= 7) {
+                uint32_t remaining  = static_cast<uint32_t>(accum & ((1ULL << bits) - 1));
+                uint32_t eosPadding = (1u << bits) - 1;
+                if (remaining == eosPadding) {
+                    break;
+                }
+            }
+            bits--;
+            if (bits > 0) {
+                accum &= (1ULL << bits) - 1;
+            }
+        }
+    }
+
+    return result;
+}
+
+//*******************************************************************************
+bool decodeQPackHeaders(const uint8_t* data, size_t len, QMap<QString, QString>& headers)
+{
+    if (len < 2) {
+        return false;
+    }
+
+    size_t pos = 0;
+
+    int64_t requiredInsertCount = readQpackInt(data, len, pos, 8);
+    if (requiredInsertCount < 0) {
+        cerr << "Http3Protocol: Failed to read Required Insert Count" << endl;
+        return false;
+    }
+
+    if (pos >= len) {
+        cerr << "Http3Protocol: Buffer too short for Delta Base" << endl;
+        return false;
+    }
+
+    int64_t deltaBase = readQpackInt(data, len, pos, 7);
+    if (deltaBase < 0) {
+        cerr << "Http3Protocol: Failed to read Delta Base" << endl;
+        return false;
+    }
+
+    while (pos < len) {
+        uint8_t byte = data[pos];
+
+        // Indexed Header Field (pattern: 1T)
+        if (byte & 0x80) {
+            bool isStatic = (byte & 0x40) != 0;
+            int64_t index = readQpackInt(data, len, pos, 6);
+
+            if (index < 0) {
+                cerr << "    Failed to read index" << endl;
+                return false;
+            }
+
+            if (isStatic
+                && static_cast<size_t>(index) * 2 + 1
+                       < sizeof(QPACK_STATIC_TABLE) / sizeof(char*)) {
+                const char* name  = QPACK_STATIC_TABLE[index * 2];
+                const char* value = QPACK_STATIC_TABLE[index * 2 + 1];
+                if (name && value) {
+                    headers[QString::fromUtf8(name)] = QString::fromUtf8(value);
+                }
+            }
+        }
+        // Literal Header Field with Name Reference (pattern: 01NT)
+        else if (byte & 0x40) {
+            bool isStatic     = (byte & 0x10) != 0;
+            int64_t nameIndex = readQpackInt(data, len, pos, 4);
+
+            if (nameIndex < 0) {
+                cerr << "    Failed to read name index" << endl;
+                return false;
+            }
+
+            QString name;
+            if (isStatic
+                && static_cast<size_t>(nameIndex) * 2
+                       < sizeof(QPACK_STATIC_TABLE) / sizeof(char*)) {
+                const char* nameStr = QPACK_STATIC_TABLE[nameIndex * 2];
+                if (nameStr) {
+                    name = QString::fromUtf8(nameStr);
+                }
+            }
+
+            if (pos >= len) {
+                cerr << "    Buffer too short for value" << endl;
+                return false;
+            }
+
+            bool huffman     = (data[pos] & 0x80) != 0;
+            int64_t valueLen = readQpackInt(data, len, pos, 7);
+
+            if (valueLen < 0) {
+                cerr << "    Failed to read value length" << endl;
+                return false;
+            }
+
+            QString value;
+            if (huffman) {
+                if (pos + valueLen > len) {
+                    cerr << "    Huffman value length exceeds buffer" << endl;
+                    return false;
+                }
+                value = decodeHuffman(&data[pos], static_cast<size_t>(valueLen));
+                pos += static_cast<size_t>(valueLen);
+            } else if (pos + valueLen <= len) {
+                value = QString::fromUtf8(reinterpret_cast<const char*>(&data[pos]),
+                                          static_cast<int>(valueLen));
+                pos += static_cast<size_t>(valueLen);
+            } else {
+                cerr << "    Value length exceeds buffer" << endl;
+                return false;
+            }
+
+            if (!name.isEmpty() && !value.isEmpty()) {
+                headers[name] = value;
+            }
+        }
+        // Literal Header Field with Literal Name (pattern: 001N)
+        else if (byte & 0x20) {
+            bool huffmanName = (data[pos] & 0x08) != 0;
+            int64_t nameLen  = readQpackInt(data, len, pos, 3);
+
+            if (nameLen < 0) {
+                cerr << "    Failed to read name length" << endl;
+                return false;
+            }
+
+            QString name;
+            if (huffmanName) {
+                if (pos + nameLen > len) {
+                    cerr << "    Huffman name length exceeds buffer" << endl;
+                    return false;
+                }
+                name = decodeHuffman(&data[pos], static_cast<size_t>(nameLen));
+                pos += static_cast<size_t>(nameLen);
+            } else if (pos + nameLen <= len) {
+                name = QString::fromUtf8(reinterpret_cast<const char*>(&data[pos]),
+                                         static_cast<int>(nameLen));
+                pos += static_cast<size_t>(nameLen);
+            } else {
+                cerr << "    Name length exceeds buffer" << endl;
+                return false;
+            }
+
+            if (pos >= len) {
+                cerr << "    Buffer too short for value" << endl;
+                return false;
+            }
+
+            bool huffmanValue = (data[pos] & 0x80) != 0;
+            int64_t valueLen  = readQpackInt(data, len, pos, 7);
+
+            if (valueLen < 0) {
+                cerr << "    Failed to read value length" << endl;
+                return false;
+            }
+
+            QString value;
+            if (huffmanValue) {
+                if (pos + valueLen > len) {
+                    cerr << "    Huffman value length exceeds buffer" << endl;
+                    return false;
+                }
+                value = decodeHuffman(&data[pos], static_cast<size_t>(valueLen));
+                pos += static_cast<size_t>(valueLen);
+            } else if (pos + valueLen <= len) {
+                value = QString::fromUtf8(reinterpret_cast<const char*>(&data[pos]),
+                                          static_cast<int>(valueLen));
+                pos += static_cast<size_t>(valueLen);
+            } else {
+                cerr << "    Value length exceeds buffer" << endl;
+                return false;
+            }
+
+            if (!name.isEmpty()) {
+                headers[name] = value;
+            }
+        } else {
+            pos++;
+        }
+    }
+
+    return !headers.isEmpty();
+}
+
+}  // namespace Http3
diff --git a/src/http3/Http3Protocol.h b/src/http3/Http3Protocol.h
new file mode 100644 (file)
index 0000000..bc0b363
--- /dev/null
@@ -0,0 +1,126 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 Http3Protocol.h
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ *
+ * Reusable HTTP/3 protocol utilities: QUIC varints, QPACK decoding,
+ * Huffman decoding, and HTTP/3 frame parsing.
+ */
+
+#ifndef __HTTP3PROTOCOL_H__
+#define __HTTP3PROTOCOL_H__
+
+#include <QMap>
+#include <QString>
+#include <cstddef>
+#include <cstdint>
+
+namespace Http3
+{
+
+// HTTP/3 frame types (RFC 9114)
+enum FrameType {
+    FRAME_DATA         = 0x00,
+    FRAME_HEADERS      = 0x01,
+    FRAME_CANCEL_PUSH  = 0x03,
+    FRAME_SETTINGS     = 0x04,
+    FRAME_PUSH_PROMISE = 0x05,
+    FRAME_GOAWAY       = 0x07,
+    FRAME_MAX_PUSH_ID  = 0x0D,
+};
+
+/** \brief HPACK/QPACK Huffman code entry (RFC 7541 Appendix B) */
+struct HuffmanCode {
+    uint32_t code;
+    uint8_t bits;
+    uint8_t sym;
+};
+
+/** \brief Read a QUIC variable-length integer (RFC 9000 §16)
+ *
+ * Returns the value and advances pos past the integer.
+ * Returns -1 on error (insufficient data).
+ */
+int64_t readVarint(const uint8_t* data, size_t len, size_t& pos);
+
+/** \brief Encode a QUIC variable-length integer
+ *
+ * Writes the encoded value into output (must have room for up to 8 bytes).
+ * Returns the number of bytes written.
+ */
+size_t encodeQuicVarint(uint64_t value, uint8_t* output);
+
+/** \brief Read a QPACK integer with specified prefix bit count (RFC 9204)
+ *
+ * prefixBits: number of bits in the first byte used for the value (1–8).
+ * Returns the value and advances pos. Returns -1 on error.
+ */
+int64_t readQpackInt(const uint8_t* data, size_t len, size_t& pos, int prefixBits);
+
+/** \brief Parse an HTTP/3 frame and extract the first HEADERS frame payload
+ *
+ * Returns true if a HEADERS frame was found.
+ * Sets payloadData/payloadLen to point into data (no copy).
+ */
+bool parseHttp3Frame(const uint8_t* data, size_t len, const uint8_t*& payloadData,
+                     size_t& payloadLen);
+
+/** \brief Decode a Huffman-encoded string (RFC 7541 Appendix B)
+ *
+ * Returns the decoded QString, or an empty string on error.
+ */
+QString decodeHuffman(const uint8_t* data, size_t len);
+
+/** \brief Decode QPACK-encoded HTTP/3 headers (RFC 9204)
+ *
+ * Minimal decoder sufficient for WebTransport CONNECT requests.
+ * Handles static-table indexed fields and literal fields (with/without Huffman).
+ * Returns true if at least one header was decoded.
+ */
+bool decodeQPackHeaders(const uint8_t* data, size_t len, QMap<QString, QString>& headers);
+
+/** \brief QPACK static table (RFC 9204 Appendix A)
+ *
+ * Pairs of (name, value) strings; access as QPACK_STATIC_TABLE[index*2] (name)
+ * and QPACK_STATIC_TABLE[index*2+1] (value).
+ */
+extern const char* QPACK_STATIC_TABLE[];
+
+/** \brief Complete HPACK Huffman table (RFC 7541 Appendix B) */
+extern const HuffmanCode HUFFMAN_CODES[];
+extern const size_t HUFFMAN_CODES_SIZE;
+
+}  // namespace Http3
+
+#endif  // __HTTP3PROTOCOL_H__
diff --git a/src/http3/Http3Server.cpp b/src/http3/Http3Server.cpp
new file mode 100644 (file)
index 0000000..4b88ecc
--- /dev/null
@@ -0,0 +1,318 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 Http3Server.cpp
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#include "Http3Server.h"
+
+#include <msquic.h>
+
+#include <QHostAddress>
+#include <cstring>
+#include <iostream>
+
+using std::cerr;
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+// Static msquic callbacks
+//*******************************************************************************
+
+static QUIC_STATUS QUIC_API ListenerCallback(HQUIC listener, void* context,
+                                             QUIC_LISTENER_EVENT* event)
+{
+    Http3Server* server = static_cast<Http3Server*>(context);
+    if (server) {
+        return server->handleListenerEvent(listener, event);
+    }
+    return QUIC_STATUS_INVALID_STATE;
+}
+
+static QUIC_STATUS QUIC_API ServerConnectionCallback(HQUIC connection, void* context,
+                                                     QUIC_CONNECTION_EVENT* event)
+{
+    Http3Server* server = static_cast<Http3Server*>(context);
+    if (server) {
+        return server->handleQuicConnection(connection, event);
+    }
+    return QUIC_STATUS_INVALID_STATE;
+}
+
+//*******************************************************************************
+Http3Server::Http3Server(const QString& certFile, const QString& keyFile, int port)
+    : mCertFile(certFile)
+    , mKeyFile(keyFile)
+    , mPort(port)
+    , mQuicApi(nullptr)
+    , mQuicRegistration(nullptr)
+    , mQuicConfiguration(nullptr)
+    , mQuicListener(nullptr)
+{
+}
+
+//*******************************************************************************
+Http3Server::~Http3Server()
+{
+    stop();
+}
+
+//*******************************************************************************
+bool Http3Server::start()
+{
+    // Open the msquic library
+    QUIC_STATUS status = MsQuicOpen2(&mQuicApi);
+    if (QUIC_FAILED(status)) {
+        cerr << "Http3Server: Failed to open msquic, status: 0x" << std::hex << status
+             << std::dec << endl;
+        return false;
+    }
+
+    // Create a registration for "JackTrip"
+    const QUIC_REGISTRATION_CONFIG regConfig = {"JackTrip",
+                                                QUIC_EXECUTION_PROFILE_LOW_LATENCY};
+    status = mQuicApi->RegistrationOpen(&regConfig, &mQuicRegistration);
+    if (QUIC_FAILED(status)) {
+        cerr << "Http3Server: Failed to create msquic registration, status: 0x"
+             << std::hex << status << std::dec << endl;
+        MsQuicClose(mQuicApi);
+        mQuicApi = nullptr;
+        return false;
+    }
+
+    // Configure ALPN for HTTP/3 (WebTransport)
+    const char* alpn       = "h3";
+    QUIC_BUFFER alpnBuffer = {
+        static_cast<uint32_t>(strlen(alpn)),
+        const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(alpn))};
+
+    QUIC_SETTINGS settings{};
+    std::memset(&settings, 0, sizeof(settings));
+    settings.IdleTimeoutMs                = 30000;
+    settings.IsSet.IdleTimeoutMs          = TRUE;
+    settings.DatagramReceiveEnabled       = TRUE;
+    settings.IsSet.DatagramReceiveEnabled = TRUE;
+    settings.PeerBidiStreamCount          = 10;
+    settings.IsSet.PeerBidiStreamCount    = TRUE;
+    settings.PeerUnidiStreamCount         = 10;
+    settings.IsSet.PeerUnidiStreamCount   = TRUE;
+
+    status = mQuicApi->ConfigurationOpen(mQuicRegistration, &alpnBuffer, 1, &settings,
+                                         sizeof(settings), nullptr, &mQuicConfiguration);
+    if (QUIC_FAILED(status)) {
+        cerr << "Http3Server: Failed to create msquic configuration, status: 0x"
+             << std::hex << status << std::dec << " (" << status << ")" << endl;
+        mQuicApi->RegistrationClose(mQuicRegistration);
+        MsQuicClose(mQuicApi);
+        mQuicApi          = nullptr;
+        mQuicRegistration = nullptr;
+        return false;
+    }
+
+    // Load TLS credentials
+    QUIC_CREDENTIAL_CONFIG credConfig{};
+    std::memset(&credConfig, 0, sizeof(credConfig));
+    credConfig.Type  = QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE;
+    credConfig.Flags = QUIC_CREDENTIAL_FLAG_NONE;
+
+    QUIC_CERTIFICATE_FILE certFile{};
+    std::memset(&certFile, 0, sizeof(certFile));
+    QByteArray certPath        = mCertFile.toUtf8();
+    QByteArray keyPath         = mKeyFile.toUtf8();
+    certFile.CertificateFile   = certPath.constData();
+    certFile.PrivateKeyFile    = keyPath.constData();
+    credConfig.CertificateFile = &certFile;
+
+    status = mQuicApi->ConfigurationLoadCredential(mQuicConfiguration, &credConfig);
+    if (QUIC_FAILED(status)) {
+        cerr << "Http3Server: Failed to load TLS certificate, status: 0x" << std::hex
+             << status << std::dec << " (" << status << ")" << endl;
+        cerr << "  Certificate: " << mCertFile.toStdString() << endl;
+        cerr << "  Private key: " << mKeyFile.toStdString() << endl;
+        mQuicApi->ConfigurationClose(mQuicConfiguration);
+        mQuicApi->RegistrationClose(mQuicRegistration);
+        MsQuicClose(mQuicApi);
+        mQuicApi           = nullptr;
+        mQuicRegistration  = nullptr;
+        mQuicConfiguration = nullptr;
+        return false;
+    }
+
+    // Create listener on UDP port
+    QUIC_ADDR address{};
+    std::memset(&address, 0, sizeof(address));
+    QuicAddrSetFamily(&address, QUIC_ADDRESS_FAMILY_UNSPEC);
+    QuicAddrSetPort(&address, static_cast<uint16_t>(mPort));
+
+    status =
+        mQuicApi->ListenerOpen(mQuicRegistration, ListenerCallback, this, &mQuicListener);
+    if (QUIC_FAILED(status)) {
+        cerr << "Http3Server: Failed to create QUIC listener, status: 0x" << std::hex
+             << status << std::dec << endl;
+        mQuicApi->ConfigurationClose(mQuicConfiguration);
+        mQuicApi->RegistrationClose(mQuicRegistration);
+        MsQuicClose(mQuicApi);
+        mQuicApi           = nullptr;
+        mQuicRegistration  = nullptr;
+        mQuicConfiguration = nullptr;
+        return false;
+    }
+
+    status = mQuicApi->ListenerStart(mQuicListener, &alpnBuffer, 1, &address);
+    if (QUIC_FAILED(status)) {
+        cerr << "Http3Server: Failed to start QUIC listener, status: 0x" << std::hex
+             << status << std::dec << endl;
+        mQuicApi->ListenerClose(mQuicListener);
+        mQuicApi->ConfigurationClose(mQuicConfiguration);
+        mQuicApi->RegistrationClose(mQuicRegistration);
+        MsQuicClose(mQuicApi);
+        mQuicApi           = nullptr;
+        mQuicRegistration  = nullptr;
+        mQuicConfiguration = nullptr;
+        mQuicListener      = nullptr;
+        return false;
+    }
+
+    return true;
+}
+
+//*******************************************************************************
+void Http3Server::stop()
+{
+    if (mQuicListener && mQuicApi) {
+        mQuicApi->ListenerClose(mQuicListener);
+        mQuicListener = nullptr;
+    }
+    if (mQuicConfiguration && mQuicApi) {
+        mQuicApi->ConfigurationClose(mQuicConfiguration);
+        mQuicConfiguration = nullptr;
+    }
+    if (mQuicRegistration && mQuicApi) {
+        mQuicApi->RegistrationClose(mQuicRegistration);
+        mQuicRegistration = nullptr;
+    }
+    if (mQuicApi) {
+        MsQuicClose(mQuicApi);
+        mQuicApi = nullptr;
+    }
+}
+
+//*******************************************************************************
+unsigned int Http3Server::handleListenerEvent(HQUIC listener, void* eventPtr)
+{
+    Q_UNUSED(listener)
+    QUIC_LISTENER_EVENT* event = static_cast<QUIC_LISTENER_EVENT*>(eventPtr);
+
+    switch (event->Type) {
+    case QUIC_LISTENER_EVENT_NEW_CONNECTION: {
+        HQUIC connection = event->NEW_CONNECTION.Connection;
+
+        // Get peer address
+        QUIC_ADDR peerAddr;
+        uint32_t addrLen = sizeof(peerAddr);
+        mQuicApi->GetParam(connection, QUIC_PARAM_CONN_REMOTE_ADDRESS, &addrLen,
+                           &peerAddr);
+
+        QHostAddress peerAddress;
+        quint16 peerPort = 0;
+
+        if (QuicAddrGetFamily(&peerAddr) == QUIC_ADDRESS_FAMILY_INET) {
+            peerAddress.setAddress(ntohl(peerAddr.Ipv4.sin_addr.s_addr));
+            peerPort = ntohs(peerAddr.Ipv4.sin_port);
+        } else if (QuicAddrGetFamily(&peerAddr) == QUIC_ADDRESS_FAMILY_INET6) {
+            peerAddress.setAddress(reinterpret_cast<quint8*>(&peerAddr.Ipv6.sin6_addr));
+            peerPort = ntohs(peerAddr.Ipv6.sin6_port);
+        }
+
+        // Set the connection callback before accepting
+        mQuicApi->SetCallbackHandler(connection, (void*)ServerConnectionCallback, this);
+
+        // Accept the connection with our configuration
+        QUIC_STATUS status =
+            mQuicApi->ConnectionSetConfiguration(connection, mQuicConfiguration);
+        if (QUIC_FAILED(status)) {
+            cerr << "Http3Server: Failed to set connection configuration, status: 0x"
+                 << std::hex << status << std::dec << " (" << status << ")" << endl;
+            return QUIC_STATUS_CONNECTION_REFUSED;
+        }
+
+        // Notify the delegate (UdpHubListener) about the new connection
+        if (mConnectionCallback) {
+            mConnectionCallback(connection, peerAddress, peerPort);
+        } else {
+            cerr << "Http3Server: No connection callback set - refusing connection"
+                 << endl;
+            return QUIC_STATUS_CONNECTION_REFUSED;
+        }
+
+        return QUIC_STATUS_SUCCESS;
+    }
+
+    case QUIC_LISTENER_EVENT_STOP_COMPLETE:
+        break;
+
+    default:
+        break;
+    }
+
+    return QUIC_STATUS_SUCCESS;
+}
+
+//*******************************************************************************
+unsigned int Http3Server::handleQuicConnection(HQUIC /* connection */, void* eventPtr)
+{
+    QUIC_CONNECTION_EVENT* event = static_cast<QUIC_CONNECTION_EVENT*>(eventPtr);
+
+    switch (event->Type) {
+    case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
+        cerr << "Http3Server: Connection shutdown by transport (before session created)"
+             << endl;
+        cerr << "  Status: 0x" << std::hex
+             << event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status << std::dec << " ("
+             << event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status << ")" << endl;
+        break;
+
+    case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
+        break;
+
+    case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
+        break;
+
+    default:
+        break;
+    }
+
+    return QUIC_STATUS_SUCCESS;
+}
diff --git a/src/http3/Http3Server.h b/src/http3/Http3Server.h
new file mode 100644 (file)
index 0000000..f6b7877
--- /dev/null
@@ -0,0 +1,120 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 Http3Server.h
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ *
+ * QUIC/HTTP3 server lifecycle: msquic registration, configuration, TLS, and
+ * listener management extracted from UdpHubListener.
+ */
+
+#ifndef __HTTP3SERVER_H__
+#define __HTTP3SERVER_H__
+
+#include <QHostAddress>
+#include <QString>
+#include <functional>
+
+// Forward-declare msquic types so callers don't need to include msquic.h
+struct QUIC_API_TABLE;
+struct QUIC_HANDLE;
+typedef QUIC_HANDLE* HQUIC;
+
+/** \brief QUIC/HTTP3 server lifecycle manager.
+ *
+ * Owns the msquic registration, configuration, and listener.  Callers supply
+ * a new-connection callback that is invoked (from a msquic thread) whenever a
+ * new QUIC connection is accepted.
+ *
+ * Usage:
+ * \code
+ *   Http3Server server(certFile, keyFile, port);
+ *   server.setConnectionCallback([](HQUIC conn, const QHostAddress& addr, quint16 port) {
+ *       // create a WebTransportSession for this connection
+ *   });
+ *   server.start();
+ *   // ...
+ *   server.stop();  // or just let the destructor run
+ * \endcode
+ */
+class Http3Server
+{
+   public:
+    using ConnectionCallback =
+        std::function<void(HQUIC connection, const QHostAddress& addr, quint16 port)>;
+
+    /** \brief Construct the server.
+     *
+     * \param certFile  Path to the PEM certificate file.
+     * \param keyFile   Path to the PEM private key file.
+     * \param port      UDP port to listen on.
+     */
+    Http3Server(const QString& certFile, const QString& keyFile, int port);
+    ~Http3Server();
+
+    /** \brief Set the callback invoked for each new QUIC connection. */
+    void setConnectionCallback(ConnectionCallback cb) { mConnectionCallback = cb; }
+
+    /** \brief Initialize msquic and start listening.  Returns true on success. */
+    bool start();
+
+    /** \brief Stop listening and release all msquic resources. */
+    void stop();
+
+    /** \brief Returns true if the server is currently listening. */
+    bool isRunning() const { return mQuicListener != nullptr; }
+
+    /** \brief Expose the API table so sessions can use it. */
+    const QUIC_API_TABLE* quicApi() const { return mQuicApi; }
+
+    /** \brief Expose the configuration handle so connections can be accepted. */
+    HQUIC quicConfiguration() const { return mQuicConfiguration; }
+
+    // These must be public so the static msquic callbacks can reach them.
+    unsigned int handleListenerEvent(HQUIC listener, void* event);
+    unsigned int handleQuicConnection(HQUIC connection, void* event);
+
+   private:
+    QString mCertFile;
+    QString mKeyFile;
+    int mPort;
+
+    const QUIC_API_TABLE* mQuicApi;
+    HQUIC mQuicRegistration;
+    HQUIC mQuicConfiguration;
+    HQUIC mQuicListener;
+
+    ConnectionCallback mConnectionCallback;
+};
+
+#endif  // __HTTP3SERVER_H__
index abd30178881aa3bd5148dac3bdd407c2fcde84e2..ce3341592abb11ad1662c59fe15a49c5b231289e 100644 (file)
@@ -169,7 +169,7 @@ void setRealtimeProcessPriority()
             priority = "idle";
             break;
         case NORMAL_PRIORITY_CLASS:
-            priority = "high";
+            priority = "normal";
             break;
         case REALTIME_PRIORITY_CLASS:
             priority = "realtime";
@@ -224,9 +224,9 @@ void setRealtimeProcessPriority()
 {
     int priority = sched_get_priority_max(SCHED_FIFO);  // 99 is the highest possible
 #ifdef __UBUNTU__
-    priority     = 95;  // anything higher is silently ignored by Ubuntu 18.04
+    priority = 95;  // anything higher is silently ignored by Ubuntu 18.04
 #endif
-    priority     = 3;
+    priority = 3;
 
     struct sched_param sp = {.sched_priority = priority};
 
index 3a56e5d2d48d784f8816b11b7c537cb1f7ef89c1..d2a158b3b2e588309639488b2ffda4d5e0c467f7 100644 (file)
@@ -40,7 +40,7 @@
 
 #include "jacktrip_types.h"
 
-constexpr const char* const gVersion = "2.7.2";  ///< JackTrip version
+constexpr const char* const gVersion = "3.0.0";  ///< JackTrip version
 
 //*******************************************************************************
 /// \name Default Values
@@ -80,10 +80,16 @@ constexpr int gDefaultOutputQueueLength        = 4;
 constexpr uint32_t gDefaultSampleRate          = 48000;
 constexpr int gDefaultDeviceID                 = -1;
 constexpr uint32_t gDefaultBufferSizeInSamples = 128;
-constexpr const char* gDefaultLocalAddress     = "";
-constexpr int gDefaultRedundancy               = 1;
-constexpr int gTimeOutMultiThreadedServer      = 10000;  // seconds
-constexpr int gUdpWaitTimeout                  = 512;    // milliseconds
+constexpr uint16_t gMaxBufferSizeInSamples =
+    4096;  ///< Sane upper bound for peer-supplied buffer size
+constexpr uint8_t gMaxAudioChannels =
+    128;  ///< Sane upper bound for peer-supplied channel count
+constexpr const char* gDefaultLocalAddress = "";
+constexpr int gDefaultRedundancy           = 1;
+constexpr int gTimeOutMultiThreadedServer  = 10000;  // seconds
+constexpr int gUdpWaitTimeout              = 512;    // milliseconds
+constexpr int gClientGoneTimeoutMs =
+    10000;  ///< Close session after this many ms with no received packets
 //@}
 
 //*******************************************************************************
@@ -116,6 +122,7 @@ extern int gVerboseFlag;  ///< Verbose mode flag declaration
 constexpr int gJackBitResolution = 32;  ///< Audio Bit Resolution of the Jack Server
 constexpr const char* gJackDefaultClientName = "JackTrip";
 constexpr int gMaxRemoteNameLength           = 64;
+constexpr int gMaxCredentialLength           = 255;  ///< Max username or password length
 //@}
 
 //*******************************************************************************
index 1310259ef1ecbdce1722d8bff08d34192b183500..ce406db5a79c819c965ea90426453460ba5024b4 100644 (file)
@@ -46,6 +46,7 @@
 #include "jacktrip_globals.h"
 
 #ifndef NO_GUI
+#include "SocketClient.h"
 #include "UserInterface.h"
 #endif
 
@@ -175,6 +176,30 @@ int main(int argc, char* argv[])
         // Start the GUI
         cliSettings.reset(new Settings(true));
         cliSettings->parseInput(argc, argv);
+        if (!cliSettings->getDeeplink().isEmpty()) {
+            SocketClient c;
+            if (c.connect()) {
+                if (!c.sendHeader(QStringLiteral("deeplink"))) {
+                    std::cerr << "Failed to send deeplink header" << std::endl;
+                    c.close();
+                    return 1;
+                }
+                QLocalSocket& s          = c.getSocket();
+                QByteArray deepLinkBytes = cliSettings->getDeeplink().toLocal8Bit();
+                qint64 bytesWritten      = s.write(deepLinkBytes);
+                s.flush();
+                s.waitForBytesWritten(1000);
+                if (bytesWritten != deepLinkBytes.size()) {
+                    std::cerr << "Failed to send deeplink" << std::endl;
+                    c.close();
+                    return 1;
+                }
+                std::cout << "sent deeplink: " << cliSettings->getDeeplink().toStdString()
+                          << std::endl;
+                c.close();
+                return 0;
+            }
+        }
         interface.reset(new UserInterface(cliSettings));
         interface->start(guiApp);
         return app->exec();
index da21499ae8cc88c7b42d999068b117483ae56cfa..e5713cb27a95dfb945133443438dbe5375caebdf 100644 (file)
@@ -14,13 +14,6 @@ Window {
     property int fontSmall: 10
     property int fontTiny: 8
 
-    property string buttonColour: "#F2F3F3"
-    property string buttonHoverColour: "#E7E8E8"
-    property string buttonPressedColour: "#E7E8E8"
-    property string buttonStroke: "#EAEBEB"
-    property string buttonHoverStroke: "#B0B5B5"
-    property string buttonPressedStroke: "#B0B5B5"
-
     property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
     property string textAreaTextColour: virtualstudio.darkMode ? "#A6A6A6" : "#757575"
     property string textAreaColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
@@ -107,8 +100,11 @@ Window {
                 wrapMode: Text.WordWrap
             }
 
-            Button {
+            StyledButton {
                 id: aboutCloseButton
+                text: "Close"
+                primary: true
+                fontSize: 10
                 anchors.top: aboutCopyright.bottom
                 anchors.topMargin: 16 * virtualstudio.uiScale
                 anchors.left: parent.left
@@ -116,21 +112,6 @@ Window {
                 onClicked: () => {
                     aboutWindow.visible = false;
                 }
-
-                background: Rectangle {
-                    radius: 6 * virtualstudio.uiScale
-                    color: aboutCloseButton.down ? buttonPressedColour : (aboutCloseButton.hovered ? buttonHoverColour : buttonColour)
-                    border.width: 1
-                    border.color: aboutCloseButton.down ? buttonPressedStroke : (aboutCloseButton.hovered ? buttonHoverStroke : buttonStroke)
-                }
-
-                Text {
-                    text: "Close"
-                    font.family: "Poppins"
-                    font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
-                    anchors.horizontalCenter: parent.horizontalCenter
-                    anchors.verticalCenter: parent.verticalCenter
-                }
             }
         }
     }
index d9572d26e6313b3718342644e4f97743bf0016b6..bdd4366addadf55199f9d69a96fadef151b3e4a5 100644 (file)
@@ -1,5 +1,7 @@
 import QtQuick
 import QtQuick.Controls
+import QtWebEngine
+import Qt5Compat.GraphicalEffects
 
 Item {
     width: parent.width; height: parent.height
@@ -11,11 +13,10 @@ Item {
     }
 
     property int buttonHeight: 25
+    property int iconButtonSize: 32
     property int buttonWidth: 103
     property int extraSettingsButtonWidth: 16
     property int emptyListMessageWidth: 450
-    property int createMessageTopMargin: 16
-    property int createButtonTopMargin: 24
     property int fontBig: 28
     property int fontMedium: 11
 
@@ -23,207 +24,301 @@ Item {
 
     property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
     property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
-    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
-    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
-    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
-    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
-    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
-    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
-    property string createButtonStroke: virtualstudio.darkMode ? "#AB0F0F" : "#0F0D0D"
-
-    function refresh() {
-        scrollY = studioListView.contentY;
-        var currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY);
-        if (currentIndex == -1) {
-            currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY + (16 * virtualstudio.uiScale));
-        }
-        virtualstudio.refreshStudios(currentIndex, true)
+    property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+    property int fontTiny: 8
+
+    //property string browseBaseUrl: "http://localhost:3000"
+    property string browseBaseUrl: virtualstudio.apiHost === "test.jacktrip.com" ? "https://next-test.jacktrip.com" : "https://www.jacktrip.com"
+    property string browseDiscoverUrl: `${browseBaseUrl}/discover`
+    property string browseStudiosUrl: `${browseBaseUrl}/studios`
+
+    Loader {
+        id: webLoader
+        anchors.top: parent.top
+        anchors.right: parent.right
+        anchors.left: parent.left
+        anchors.bottom: footer.top
+        sourceComponent: virtualstudio.windowState === "browse" && auth.isAuthenticated ? browseWeb : browseNull
     }
 
     Component {
-        id: footer
+        id: browseNull
         Rectangle {
-            height: 16 * virtualstudio.uiScale
-            x: 16 * virtualstudio.uiScale
-            width: parent.width - (2 * x)
+            anchors.fill: parent
             color: backgroundColour
         }
     }
 
-    ListView {
-        id: studioListView
-        x:0;
-        y: 0;
-        width: parent.width
-        height: parent.height - (36 * virtualstudio.uiScale)
-        spacing: 16 * virtualstudio.uiScale
-        header: footer
-        footer: footer
-        model: virtualstudio.serverModel
-        clip: true
-        boundsBehavior: Flickable.StopAtBounds
-        delegate: Studio {
-            anchors.left: parent ? parent.left : undefined
-            anchors.leftMargin: 16 * virtualstudio.uiScale
-            width: studioListView.width - (32 * virtualstudio.uiScale)
-            serverLocation: virtualstudio.regions[modelData.location] ? "in " + virtualstudio.regions[modelData.location].label : ""
-            flagImage: modelData.bannerURL ? modelData.bannerURL : modelData.flag
-            studioName: modelData.name
-            publicStudio: modelData.isPublic
-            admin: modelData.isAdmin
-            available: modelData.canConnect
-            connected: false
-            studioId: modelData.id ? modelData.id : ""
-            streamId: modelData.streamId ? modelData.streamId : ""
-            inviteKeyString: modelData.inviteKey ? modelData.inviteKey : ""
-            sampleRate: modelData.sampleRate
-        }
-
-        section { property: "modelData.type"; criteria: ViewSection.FullString; delegate: SectionHeading {} }
-
-        // Show sectionHeading if there are no Studios in list
-        SectionHeading {
-            id: emptyListSectionHeading
-            listIsEmpty: true
-            visible: parent.count == 0
-        }
-
-        Text {
-            id: emptyListMessage
-            visible: parent.count == 0
-            text: virtualstudio.refreshInProgress ? "Loading Studios..." : "No studios found that match your filter criteria."
-            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
-            color: textColour
-            width: emptyListMessageWidth
-            wrapMode: Text.Wrap
-            horizontalAlignment: Text.AlignHCenter
-            anchors.horizontalCenter: emptyListSectionHeading.horizontalCenter
-            anchors.verticalCenter: parent.verticalCenter
-        }
+    Component {
+        id: browseWeb
+        WebEngineView {
+            id: webEngineView
+            anchors.fill: parent
+            settings.fullScreenSupportEnabled: true
+            settings.javascriptCanAccessClipboard: true
+            settings.javascriptCanPaste: true
+            settings.screenCaptureEnabled: true
+            settings.playbackRequiresUserGesture: false
+            url: browseStudiosUrl
 
-        Button {
-            id: resetFiltersButton
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: resetFiltersButton.down ? buttonPressedColour : (resetFiltersButton.hovered ? buttonHoverColour : buttonColour)
-                border.width: 1
-                border.color: resetFiltersButton.down ? buttonPressedStroke : (resetFiltersButton.hovered ? buttonHoverStroke : buttonStroke)
-            }
-            visible: parent.count == 0
-            onClicked: {
-                virtualstudio.showSelfHosted = true;
-                virtualstudio.showInactive = true;
-                refresh();
+            onContextMenuRequested: function(request) {
+                // this disables the default context menu: https://doc.qt.io/qt-6.2/qml-qtwebengine-contextmenurequest.html#accepted-prop
+                request.accepted = true;
             }
-            anchors.top: emptyListMessage.bottom
-            anchors.topMargin: createButtonTopMargin
-            anchors.horizontalCenter: emptyListMessage.horizontalCenter
-            width: 120 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
-            Text {
-                text: "Reset Filters"
-                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
-                anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
-                color: textColour
+
+            onNewWindowRequested: function(request) {
+                Qt.openUrlExternally(request.requestedUrl);
             }
-        }
 
-        // Disable momentum scroll
-        MouseArea {
-            z: -1
-            anchors.fill: parent
-            onWheel: function (wheel) {
-                // trackpad
-                studioListView.contentY -= wheel.pixelDelta.y;
-                // mouse wheel
-                studioListView.contentY -= wheel.angleDelta.y;
-                studioListView.returnToBounds();
+            onFeaturePermissionRequested: function(securityOrigin, feature) {
+                webEngineView.grantFeaturePermission(securityOrigin, feature, true);
             }
-        }
 
-        Component.onCompleted: {
-            // Customize scroll properties on different platforms
-            if (Qt.platform.os == "linux" || Qt.platform.os == "osx" ||
-                Qt.platform.os == "unix" || Qt.platform.os == "windows") {
-                var scrollBar = Qt.createQmlObject('import QtQuick.Controls; ScrollBar{}',
-                                                   studioListView,
-                                                   "dynamicSnippet1");
-                scrollBar.policy = ScrollBar.AlwaysOn;
-                ScrollBar.vertical = scrollBar;
+            onRenderProcessTerminated: function(terminationStatus, exitCode) {
+                var status = "";
+                switch (terminationStatus) {
+                case WebEngineView.NormalTerminationStatus:
+                    status = "(normal exit)";
+                    break;
+                case WebEngineView.AbnormalTerminationStatus:
+                    status = "(abnormal exit)";
+                    break;
+                case WebEngineView.CrashedTerminationStatus:
+                    status = "(crashed)";
+                    break;
+                case WebEngineView.KilledTerminationStatus:
+                    status = "(killed)";
+                    break;
+                }
+                console.log("Render process exited with code " + exitCode + " " + status);
             }
         }
     }
 
     Rectangle {
+        id: footer
         x: 0; y: parent.height - 36 * virtualstudio.uiScale; width: parent.width; height: 36 * virtualstudio.uiScale
         border.color: "#33979797"
         color: backgroundColour
 
-        Button {
+        StyledButton {
+            id: studiosButton
+            text: "My Studios"
+            icon { source: "squares-2x2.svg"; color: resolvedTextColor }
+            onClicked: { if (webLoader.item) webLoader.item.url = browseStudiosUrl }
+            enabled: !(webLoader.item && webLoader.item.url && webLoader.item.url.toString().startsWith(browseStudiosUrl))
+            display: AbstractButton.TextBesideIcon
+            fontSize: fontMedium
+            leftPadding: 0
+            rightPadding: 4
+            spacing: 0
+            anchors.verticalCenter: parent.verticalCenter
+            x: 16 * virtualstudio.uiScale
+            width: (buttonWidth + extraSettingsButtonWidth) * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+        }
+
+        StyledButton {
+            id: discoverButton
+            text: "Discover"
+            icon { source: "public.svg"; color: resolvedTextColor }
+            onClicked: { if (webLoader.item) webLoader.item.url = browseDiscoverUrl }
+            enabled: !(webLoader.item && webLoader.item.url && webLoader.item.url.toString().startsWith(browseDiscoverUrl))
+            display: AbstractButton.TextBesideIcon
+            fontSize: fontMedium
+            leftPadding: 0
+            rightPadding: 4
+            spacing: 0
+            anchors.verticalCenter: parent.verticalCenter
+            x: (16 + buttonWidth + extraSettingsButtonWidth + 8) * virtualstudio.uiScale
+            width: (buttonWidth + extraSettingsButtonWidth) * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+        }
+
+        StyledButton {
+            id: goBackButton
+            icon { source: "arrow-left.svg"; color: resolvedTextColor; width: iconButtonSize * virtualstudio.uiScale; height: iconButtonSize * virtualstudio.uiScale }
+            onClicked: { if (webLoader.item) webLoader.item.goBack() }
+            enabled: !!(webLoader.item && webLoader.item.canGoBack)
+            display: AbstractButton.IconOnly
+            showBorder: false
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - (16 + iconButtonSize * 6 + 8 * 5) * virtualstudio.uiScale
+            width: iconButtonSize * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+
+            ToolTip {
+                visible: goBackButton.hovered
+                delay: 500
+                contentItem: Text {
+                    text: qsTr("Go Back")
+                    font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                }
+                background: Rectangle {
+                    color: toolTipBackgroundColour
+                    radius: 4
+                    layer.enabled: true
+                    layer.effect: Glow {
+                        radius: 8
+                        color: "#66000000"
+                        transparentBorder: true
+                    }
+                }
+            }
+        }
+
+        StyledButton {
+            id: goForwardButton
+            icon { source: "arrow-right.svg"; color: resolvedTextColor; width: iconButtonSize * virtualstudio.uiScale; height: iconButtonSize * virtualstudio.uiScale }
+            onClicked: { if (webLoader.item) webLoader.item.goForward() }
+            enabled: !!(webLoader.item && webLoader.item.canGoForward)
+            display: AbstractButton.IconOnly
+            showBorder: false
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - (16 + iconButtonSize * 5 + 8 * 4) * virtualstudio.uiScale
+            width: iconButtonSize * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+
+            ToolTip {
+                visible: goForwardButton.hovered
+                delay: 500
+                contentItem: Text {
+                    text: qsTr("Go Forward")
+                    font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                }
+                background: Rectangle {
+                    color: toolTipBackgroundColour
+                    radius: 4
+                    layer.enabled: true
+                    layer.effect: Glow {
+                        radius: 8
+                        color: "#66000000"
+                        transparentBorder: true
+                    }
+                }
+            }
+        }
+
+        StyledButton {
             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)
+            icon { source: "refresh.svg"; color: resolvedTextColor; width: iconButtonSize * virtualstudio.uiScale; height: iconButtonSize * virtualstudio.uiScale }
+            onClicked: { if (webLoader.item) webLoader.item.reload() }
+            display: AbstractButton.IconOnly
+            showBorder: false
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - (16 + iconButtonSize * 4 + 8 * 3) * virtualstudio.uiScale
+            width: iconButtonSize * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+
+            ToolTip {
+                visible: refreshButton.hovered
+                delay: 500
+                contentItem: Text {
+                    text: qsTr("Refresh")
+                    font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                }
+                background: Rectangle {
+                    color: toolTipBackgroundColour
+                    radius: 4
+                    layer.enabled: true
+                    layer.effect: Glow {
+                        radius: 8
+                        color: "#66000000"
+                        transparentBorder: true
+                    }
+                }
             }
-            onClicked: { refresh() }
+        }
+
+        StyledButton {
+            id: openInBrowserButton
+            icon { source: "arrow-top-right-on-square.svg"; color: resolvedTextColor; width: iconButtonSize * virtualstudio.uiScale; height: iconButtonSize * virtualstudio.uiScale }
+            onClicked: { if (webLoader.item) Qt.openUrlExternally(webLoader.item.url) }
+            display: AbstractButton.IconOnly
+            showBorder: false
             anchors.verticalCenter: parent.verticalCenter
-            x: 16 * virtualstudio.uiScale
-            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
-            Text {
-                text: "Refresh List"
-                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
-                anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
-                color: textColour
+            x: parent.width - (16 + iconButtonSize * 3 + 8 * 2) * virtualstudio.uiScale
+            width: iconButtonSize * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+
+            ToolTip {
+                visible: openInBrowserButton.hovered
+                delay: 500
+                contentItem: Text {
+                    text: qsTr("Open in Browser")
+                    font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                }
+                background: Rectangle {
+                    color: toolTipBackgroundColour
+                    radius: 4
+                    layer.enabled: true
+                    layer.effect: Glow {
+                        radius: 8
+                        color: "#66000000"
+                        transparentBorder: true
+                    }
+                }
             }
         }
 
-        Button {
+        StyledButton {
             id: aboutButton
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: aboutButton.down ? buttonPressedColour : (aboutButton.hovered ? buttonHoverColour : buttonColour)
-                border.width: 1
-                border.color: aboutButton.down ? buttonPressedStroke : (aboutButton.hovered ? buttonHoverStroke : buttonStroke)
-            }
+            icon { source: "question-mark-circle.svg"; color: resolvedTextColor; width: iconButtonSize * virtualstudio.uiScale; height: iconButtonSize * virtualstudio.uiScale }
             onClicked: { virtualstudio.showAbout() }
+            display: AbstractButton.IconOnly
+            showBorder: false
             anchors.verticalCenter: parent.verticalCenter
-            x: parent.width - ((230 + extraSettingsButtonWidth) * virtualstudio.uiScale)
-            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
-            Text {
-                text: "About"
-                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
-                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
-                color: textColour
+            x: parent.width - (16 + iconButtonSize * 2 + 8) * virtualstudio.uiScale
+            width: iconButtonSize * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+
+            ToolTip {
+                visible: aboutButton.hovered
+                delay: 500
+                contentItem: Text {
+                    text: qsTr("About")
+                    font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                }
+                background: Rectangle {
+                    color: toolTipBackgroundColour
+                    radius: 4
+                    layer.enabled: true
+                    layer.effect: Glow {
+                        radius: 8
+                        color: "#66000000"
+                        transparentBorder: true
+                    }
+                }
             }
         }
 
-        Button {
+        StyledButton {
             id: settingsButton
-            text: "Settings"
-            palette.buttonText: textColour
-            icon {
-                source: "cog.svg";
-                color: textColour;
-            }
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: settingsButton.down ? buttonPressedColour : (settingsButton.hovered ? buttonHoverColour : buttonColour)
-                border.width: 1
-                border.color: settingsButton.down ? buttonPressedStroke : (settingsButton.hovered ? buttonHoverStroke : buttonStroke)
-            }
+            icon { source: "cog.svg"; color: resolvedTextColor; width: iconButtonSize * virtualstudio.uiScale; height: iconButtonSize * virtualstudio.uiScale }
             onClicked: { virtualstudio.windowState = "settings"; audio.startAudio(); }
-            display: AbstractButton.TextBesideIcon
-            font {
-                family: "Poppins";
-                pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale;
-            }
-            leftPadding: 0
-            rightPadding: 4
-            spacing: 0
+            display: AbstractButton.IconOnly
+            showBorder: false
             anchors.verticalCenter: parent.verticalCenter
-            x: parent.width - ((119 + extraSettingsButtonWidth) * virtualstudio.uiScale)
-            width: (buttonWidth + extraSettingsButtonWidth) * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+            x: parent.width - (16 + iconButtonSize) * virtualstudio.uiScale
+            width: iconButtonSize * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+
+            ToolTip {
+                visible: settingsButton.hovered
+                delay: 500
+                contentItem: Text {
+                    text: qsTr("Settings")
+                    font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                }
+                background: Rectangle {
+                    color: toolTipBackgroundColour
+                    radius: 4
+                    layer.enabled: true
+                    layer.effect: Glow {
+                        radius: 8
+                        color: "#66000000"
+                        transparentBorder: true
+                    }
+                }
+            }
         }
     }
 
@@ -232,21 +327,4 @@ Item {
 
     FeedbackSurvey {
     }
-
-    Connections {
-        target: virtualstudio
-        // Need to do this to avoid layout issues with our section header.
-        function onNewScale() {
-            studioListView.positionViewAtEnd();
-            studioListView.positionViewAtBeginning();
-            scrollY = studioListView.contentY;
-        }
-        function onRefreshFinished(index) {
-            if (index == -1) {
-                studioListView.contentY = scrollY
-            } else {
-                studioListView.positionViewAtIndex(index, ListView.Beginning);
-            }
-        }
-    }
 }
index cd428d0371bfad805c5af86a2d9d459211025087..a89e7a368cd90dda17282f4b44e2d87ee5a2691e 100644 (file)
@@ -15,7 +15,6 @@ Rectangle {
     property int bottomToolTipMargin: 8
     property int rightToolTipMargin: 4
 
-    property string saveButtonText: "#000000"
     property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
     property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0"
     property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0
@@ -31,13 +30,7 @@ Rectangle {
     property string sliderActiveTrackColour: virtualstudio.darkMode ? "light gray" : "black"
     property string checkboxStroke: "#0062cc"
     property string checkboxPressedStroke: "#007AFF"
-
     property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
-    property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
-    property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
-    property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
-    property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
-    property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
 
     property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
 
@@ -290,28 +283,21 @@ Rectangle {
         }
     }
 
-    Button {
+    StyledButton {
         id: backButton
-        background: Rectangle {
-            radius: 6 * virtualstudio.uiScale
-            color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour)
-        }
+        text: "Go Back"
+        primary: true
+        showBorder: false
+        fontSize: fontMedium
         onClicked: {
             virtualstudio.saveSettings();
             virtualstudio.windowState = "connected";
         }
         anchors.bottom: parent.bottom
-        anchors.bottomMargin: 16 * virtualstudio.uiScale;
+        anchors.bottomMargin: 16 * virtualstudio.uiScale
         anchors.left: parent.left
-        anchors.leftMargin: 16 * virtualstudio.uiScale;
+        anchors.leftMargin: 16 * virtualstudio.uiScale
         width: 150 * virtualstudio.uiScale; height: 36 * virtualstudio.uiScale
-
-        Text {
-            text: "Back"
-            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
-            anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
-            color: textColour
-        }
     }
 
     DeviceWarning {
index 85e73a141b7398df4748a482197993fbf114db54..62e65283b96826fccaf70d39486ff0e5de0e8f13 100644 (file)
@@ -32,9 +32,6 @@ Item {
     property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
     property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
     property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
-    property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
-    property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
-    property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
     property string saveButtonBackgroundColour: "#F2F3F3"
     property string saveButtonPressedColour: "#E7E8E8"
     property string saveButtonStroke: "#EAEBEB"
index 7c68874c8d859e7f61eba56cd4515e8b3cd718f0..2b18cf225d9722a5b21c061f1672de8d017f90e3 100644 (file)
@@ -9,10 +9,6 @@ Item {
     clip: true
 
     property int fontMedium: 12
-    property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
-    property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
-    property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
-    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
 
     Loader {
         id: webLoader
@@ -91,23 +87,17 @@ Item {
                 Layout.fillHeight: true
                 Layout.fillWidth: true
 
-                Button {
+                StyledButton {
                     id: backButton
+                    text: "Back to Studios"
+                    primary: true
+                    buttonRadius: 8
+                    showBorder: false
+                    fontSize: 13
                     anchors.centerIn: parent
                     width: 180 * virtualstudio.uiScale
                     height: 36 * virtualstudio.uiScale
-                    background: Rectangle {
-                        radius: 8 * virtualstudio.uiScale
-                        color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour)
-                    }
                     onClicked: virtualstudio.windowState = "browse"
-
-                    Text {
-                        text: "Back to Studios"
-                        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
-                        anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
-                        color: textColour
-                    }
                 }
             }
         }
@@ -117,7 +107,7 @@ Item {
             width: parent.width
             height: 1
             y: parent.height - footer.height
-            color: buttonStroke
+            color: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
         }
     }
 }
index dc250c8af592430c7a1be44b0c75ef1f78a8408d..7cad14b86e83cd881894e3613535e3b8c47e41ce 100644 (file)
@@ -3,8 +3,6 @@ import QtQuick.Controls
 import QtQuick.Layouts
 
 Rectangle {
-    property string disabledButtonText: "#D3D4D4"
-    property string saveButtonText: "#000000"
     property int fullHeight: 88 * virtualstudio.uiScale
     property int minimumHeight: 48 * virtualstudio.uiScale
 
@@ -38,23 +36,17 @@ Rectangle {
             Layout.fillWidth: true
             visible: !isReady
 
-            Button {
+            StyledButton {
                 id: backButton
+                text: "Back to Studios"
+                primary: true
+                buttonRadius: 8
+                showBorder: false
+                fontSize: 13
                 anchors.centerIn: parent
                 width: 180 * virtualstudio.uiScale
                 height: 36 * virtualstudio.uiScale
-                background: Rectangle {
-                    radius: 8 * virtualstudio.uiScale
-                    color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour)
-                }
                 onClicked: virtualstudio.disconnect()
-
-                Text {
-                    text: "Back to Studios"
-                    font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
-                    anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
-                    color: textColour
-                }
             }
         }
 
@@ -131,16 +123,14 @@ Rectangle {
                     Layout.rightMargin: 2 * virtualstudio.uiScale
                     Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
 
-                    Button {
+                    StyledButton {
                         id: changeDevicesButton
+                        buttonRadius: 8
+                        showBorder: false
                         width: 36 * virtualstudio.uiScale
                         height: 36 * virtualstudio.uiScale
                         anchors.top: parent.top
                         anchors.horizontalCenter: parent.horizontalCenter
-                        background: Rectangle {
-                            radius: 8 * virtualstudio.uiScale
-                            color: changeDevicesButton.down ? browserButtonPressedColour : (changeDevicesButton.hovered ? browserButtonHoverColour : browserButtonColour)
-                        }
                         onClicked: {
                             virtualstudio.windowState = "change_devices"
                             if (!audio.deviceModelsInitialized) {
@@ -298,60 +288,16 @@ Rectangle {
                     visible: showDeviceControls
                 }
 
-                Button {
+                StyledButton {
                     id: closeFeedbackDetectedModalButton
+                    text: "Ok"
+                    fontSize: showDeviceControls ? 11 : 8
                     anchors.right: parent.right
                     anchors.rightMargin: rightMargin * virtualstudio.uiScale
                     anchors.verticalCenter: parent.verticalCenter
-                    width: 128 * virtualstudio.uiScale;
-                    height: 30 * virtualstudio.uiScale
+                    width: showDeviceControls ? 128 * virtualstudio.uiScale : 80 * virtualstudio.uiScale
+                    height: showDeviceControls ? 30 * virtualstudio.uiScale : 24 * virtualstudio.uiScale
                     onClicked: feedbackDetectedModal.close()
-
-                    background: Rectangle {
-                        radius: 6 * virtualstudio.uiScale
-                        color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour)
-                        border.width: 1
-                        border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke)
-                    }
-
-                    Text {
-                        text: "Ok"
-                        font.family: "Poppins"
-                        font.pixelSize: showDeviceControls ? fontSmall * virtualstudio.fontScale * virtualstudio.uiScale : fontTiny * virtualstudio.fontScale * virtualstudio.uiScale
-                        font.weight: Font.Bold
-                        color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText
-                        anchors.horizontalCenter: parent.horizontalCenter
-                        anchors.verticalCenter: parent.verticalCenter
-                    }
-                    visible: showDeviceControls
-                }
-
-                Button {
-                    id: closeFeedbackDetectedModalButtonMinified
-                    anchors.right: parent.right
-                    anchors.rightMargin: rightMargin * virtualstudio.uiScale
-                    anchors.verticalCenter: parent.verticalCenter
-                    width: 80 * virtualstudio.uiScale
-                    height: 24 * virtualstudio.uiScale
-                    onClicked: feedbackDetectedModal.close()
-
-                    background: Rectangle {
-                        radius: 6 * virtualstudio.uiScale
-                        color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour)
-                        border.width: 1
-                        border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke)
-                    }
-
-                    Text {
-                        text: "Ok"
-                        font.family: "Poppins"
-                        font.pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale
-                        font.weight: Font.Bold
-                        color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText
-                        anchors.horizontalCenter: parent.horizontalCenter
-                        anchors.verticalCenter: parent.verticalCenter
-                    }
-                    visible: !showDeviceControls
                 }
             }
         }
index 1c3ed65ee29ba08b714f1f51389825a9ee4abcc9..c9618b2b8cef47afe49e243f91170fdc5d2c22e4 100644 (file)
@@ -1,38 +1,19 @@
 import QtQuick
 import QtQuick.Controls
 
-Button {
+StyledButton {
     id: refreshButton
     text: "Refresh Devices"
+    fontSize: 8
+    display: AbstractButton.TextBesideIcon
 
-    property int fontExtraSmall: 8
-    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
-    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
-    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
-    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
-    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
-    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
-    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
     property var onDeviceRefresh: function () { audio.refreshDevices(); };
 
-    width: 144 * virtualstudio.uiScale;
+    width: 144 * virtualstudio.uiScale
     height: 30 * virtualstudio.uiScale
-    palette.buttonText: textColour
-    display: AbstractButton.TextBesideIcon
-
-    background: Rectangle {
-        radius: 6 * virtualstudio.uiScale
-        color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour)
-        border.width: 1
-        border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke)
-    }
     icon {
-        source: "refresh.svg";
-        color: textColour;
+        source: "refresh.svg"
+        color: resolvedTextColor
     }
     onClicked: { onDeviceRefresh(); }
-    font {
-        family: "Poppins"
-        pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale
-    }
 }
index 66fa6168d14042c77a721968619f0f60c4163935..8b22aa0a211ab9e4134f16f9b9209240634dc2e4 100644 (file)
@@ -9,12 +9,6 @@ Item {
     property int fontSmall: 10
 
     property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
-    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
-    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
-    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
-    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
-    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
-    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
     property string devicesWarningColour: "#F21B1B"
 
     Popup {
@@ -31,7 +25,7 @@ Item {
             color: "transparent"
             radius: 6 * virtualstudio.uiScale
             border.width: 1
-            border.color: buttonStroke
+            border.color: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
             clip: true
         }
 
@@ -94,8 +88,9 @@ Item {
                     visible: Boolean(audio.devicesErrorHelpUrl) || Boolean(audio.devicesWarningHelpUrl)
                 }
 
-                Button {
+                StyledButton {
                     id: backButton
+                    text: "Back to Settings"
                     anchors.left: parent.left
                     anchors.leftMargin: 24 * virtualstudio.uiScale
                     anchors.bottom: parent.bottom
@@ -104,26 +99,12 @@ Item {
                     onClicked: () => {
                         deviceWarningPopup.close();
                     }
-
-                    background: Rectangle {
-                        radius: 6 * virtualstudio.uiScale
-                        color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour)
-                        border.width: 1
-                        border.color: backButton.down ? buttonPressedStroke : (backButton.hovered ? buttonHoverStroke : buttonStroke)
-                    }
-
-                    Text {
-                        text: "Back to Settings"
-                        font.family: "Poppins"
-                        font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
-                        color: textColour
-                        anchors.horizontalCenter: parent.horizontalCenter
-                        anchors.verticalCenter: parent.verticalCenter
-                    }
                 }
 
-                Button {
+                StyledButton {
                     id: connectButton
+                    text: "Connect to Session"
+                    primary: true
                     anchors.right: parent.right
                     anchors.rightMargin: 24 * virtualstudio.uiScale
                     anchors.bottom: parent.bottom
@@ -136,22 +117,6 @@ Item {
                         virtualstudio.saveSettings();
                         virtualstudio.joinStudio();
                     }
-
-                    background: Rectangle {
-                        radius: 6 * virtualstudio.uiScale
-                        color: connectButton.down ? buttonPressedColour : (connectButton.hovered ? buttonHoverColour : buttonColour)
-                        border.width: 1
-                        border.color: connectButton.down ? buttonPressedStroke : (connectButton.hovered ? buttonHoverStroke : buttonStroke)
-                    }
-
-                    Text {
-                        text: "Connect to Session"
-                        font.family: "Poppins"
-                        font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
-                        color: textColour
-                        anchors.horizontalCenter: parent.horizontalCenter
-                        anchors.verticalCenter: parent.verticalCenter
-                    }
                 }
             }
         }
index 956406de82cec285c2ffee58e90e14d057484239..1ba0e8631d066b2d3c347ac2a8baec8aa4a99d37 100644 (file)
@@ -67,7 +67,7 @@ Item {
         anchors.top: ohnoMessage.bottom
         anchors.topMargin: 60 * virtualstudio.uiScale
         Text {
-            text: "Back"
+            text: "Go Back"
             font.family: "Poppins"
             font.pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale
             anchors.horizontalCenter: parent.horizontalCenter
index 678b503d5fdf482acf76df7eca0eaada671312ae..17d51f0a47988f6af7ad8166e52b109e57d4ccdc 100644 (file)
@@ -18,13 +18,6 @@ Item {
     property int bottomToolTipMargin: 8
     property int rightToolTipMargin: 4
 
-    property string buttonColour: "#F2F3F3"
-    property string buttonHoverColour: "#E7E8E8"
-    property string buttonPressedColour: "#E7E8E8"
-    property string buttonStroke: "#EAEBEB"
-    property string buttonHoverStroke: "#B0B5B5"
-    property string buttonPressedStroke: "#B0B5B5"
-
     property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
     property string textAreaTextColour: virtualstudio.darkMode ? "#A6A6A6" : "#757575"
     property string textAreaColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
@@ -53,7 +46,7 @@ Item {
             color: "transparent"
             radius: 6 * virtualstudio.uiScale
             border.width: 1
-            border.color: buttonStroke
+            border.color: "#EAEBEB"
             clip: true
         }
 
@@ -278,7 +271,7 @@ Item {
                         color: textAreaColour
                         radius: 6 * virtualstudio.uiScale
                         border.width: 1
-                        border.color: buttonStroke
+                        border.color: "#EAEBEB"
                         }
                     }
                 }
@@ -291,8 +284,10 @@ Item {
                     anchors.top: messageBoxScrollArea.bottom
                     anchors.topMargin: 24 * virtualstudio.uiScale
 
-                    Button {
+                    StyledButton {
                         id: userFeedbackButton
+                        text: (rating === 0 && messageBox.text === "") ? "Dismiss" : "Submit"
+                        primary: true
                         anchors.right: buttonsArea.right
                         anchors.horizontalCenter: buttonsArea.horizontalCenter
                         anchors.verticalCenter: parent.buttonsArea
@@ -312,23 +307,6 @@ Item {
                             userFeedbackModal.height = 150 * virtualstudio.uiScale
                             submittedFeedbackTimer.start();
                         }
-
-                        background: Rectangle {
-                            radius: 6 * virtualstudio.uiScale
-                            color: userFeedbackButton.down ? buttonPressedColour : (userFeedbackButton.hovered ? buttonHoverColour : buttonColour)
-                            border.width: 1
-                            border.color: userFeedbackButton.down ? buttonPressedStroke : (userFeedbackButton.hovered ? buttonHoverStroke : buttonStroke)
-                        }
-
-                        Text {
-                            text: (rating === 0 && messageBox.text === "") ? "Dismiss" : "Submit"
-                            font.family: "Poppins"
-                            font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
-                            font.weight: Font.Bold
-                            color: "#000000"
-                            anchors.horizontalCenter: parent.horizontalCenter
-                            anchors.verticalCenter: parent.verticalCenter
-                        }
                     }
 
                     Timer {
@@ -379,8 +357,11 @@ Item {
                     wrapMode: Text.WordWrap
                 }
 
-                Button {
+                StyledButton {
                     id: closeButtonFeedback
+                    text: "Close"
+                    primary: true
+                    fontSize: 10
                     anchors.horizontalCenter: parent.horizontalCenter
                     anchors.top: submittedFeedbackText.bottom
                     anchors.topMargin: 16 * virtualstudio.uiScale
@@ -390,21 +371,6 @@ Item {
                         userFeedbackModal.height = 300 * virtualstudio.uiScale
                         userFeedbackModal.close();
                     }
-
-                    background: Rectangle {
-                        radius: 6 * virtualstudio.uiScale
-                        color: closeButtonFeedback.down ? buttonPressedColour : (closeButtonFeedback.hovered ? buttonHoverColour : buttonColour)
-                        border.width: 1
-                        border.color: closeButtonFeedback.down ? buttonPressedStroke : (closeButtonFeedback.hovered ? buttonHoverStroke : buttonStroke)
-                    }
-
-                    Text {
-                        text: "Close"
-                        font.family: "Poppins"
-                        font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
-                        anchors.horizontalCenter: parent.horizontalCenter
-                        anchors.verticalCenter: parent.verticalCenter
-                    }
                 }
             }
         }
index e9a4a78b1ad9301fe7d52ec414c3dcc58ca5c728..2c927c65e879e61f5bd18c7f8d6db90430968247 100644 (file)
@@ -1,38 +1,12 @@
 import QtQuick
 import QtQuick.Controls
 
-Button {
+StyledButton {
     property string url
     property string buttonText: "Learn more"
 
-    width: 150 * virtualstudio.uiScale;
+    text: buttonText
+    width: 150 * virtualstudio.uiScale
     height: 30 * virtualstudio.uiScale
-
-    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
-    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
-    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
-    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
-    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
-    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
-    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
-
-    onClicked: {
-        virtualstudio.openLink(url);
-    }
-
-    background: Rectangle {
-        radius: 6 * virtualstudio.uiScale
-        color: parent.down ? buttonPressedColour : (parent.hovered ? buttonHoverColour : buttonColour)
-        border.width: 1
-        border.color: parent.down ? buttonPressedStroke : (parent.hovered ? buttonHoverStroke : buttonStroke)
-    }
-
-    Text {
-        text: buttonText
-        font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
-        color: textColour
-        horizontalAlignment: Text.AlignHCenter
-        anchors.horizontalCenter: parent.horizontalCenter
-        anchors.verticalCenter: parent.verticalCenter
-    }
+    onClicked: virtualstudio.openLink(url)
 }
index 7ca4dd091b5f76001e94ae256b4db55df627dabe..a19bd62d693838ba060d1b47ea131caec372429f 100644 (file)
@@ -265,7 +265,7 @@ Item {
                 x: (parent.x + parent.width / 2) - backButton.width - 8 * virtualstudio.uiScale
                 width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
                 Text {
-                    text: "Back"
+                    text: "Go Back"
                     font.family: "Poppins"
                     font.underline: true
                     font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
index a4a5b146208c504a16a32883f7bd0ee2e2da39c3..bf5b0d04180b0fd771d9fb5f8713d4dcbad740e4 100644 (file)
@@ -10,12 +10,6 @@ Item {
     property int fontSmall: 11
     property int fontExtraSmall: 8
 
-    property string saveButtonPressedColour: "#E7E8E8"
-    property string saveButtonPressedStroke: "#B0B5B5"
-    property string saveButtonBackgroundColour: "#F2F3F3"
-    property string saveButtonStroke: "#EAEBEB"
-    property string saveButtonText: "#000000"
-
     Item {
         id: requestMicPermissionsItem
         width: parent.width; height: parent.height
@@ -42,17 +36,12 @@ Item {
             smooth: true
         }
 
-        Button {
+        StyledButton {
             id: showPromptButton
+            text: "OK"
+            primary: true
             width: 112 * virtualstudio.uiScale
             height: 30 * virtualstudio.uiScale
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: showPromptButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
-                border.width: 2
-                border.color: showPromptButton.down || showPromptButton.hovered ? saveButtonPressedStroke : saveButtonStroke
-                layer.enabled: showPromptButton.hovered && !showPromptButton.down
-            }
             onClicked: {
                 permissions.getMicPermission();
             }
@@ -60,14 +49,6 @@ Item {
             anchors.rightMargin: 13.5 * virtualstudio.uiScale
             anchors.bottomMargin: 17 * virtualstudio.uiScale
             anchors.bottom: microphonePrompt.bottom
-            Text {
-                text: "OK"
-                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
-                font.weight: Font.Bold
-                color: saveButtonText
-                anchors.horizontalCenter: parent.horizontalCenter
-                anchors.verticalCenter: parent.verticalCenter
-            }
         }
 
         Text {
@@ -121,15 +102,10 @@ Item {
             icon.source: "micoff.svg"
         }
 
-        Button {
+        StyledButton {
             id: openSettingsButton
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: openSettingsButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
-                border.width: 1
-                border.color: openSettingsButton.down || openSettingsButton.hovered ? saveButtonPressedStroke : saveButtonStroke
-                layer.enabled: openSettingsButton.hovered && !openSettingsButton.down
-            }
+            text: "Open Privacy Settings"
+            primary: true
             onClicked: {
                 permissions.openSystemPrivacy();
             }
@@ -138,15 +114,6 @@ Item {
             anchors.bottomMargin: 16 * virtualstudio.uiScale
             anchors.bottom: parent.bottom
             width: 200 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-            Text {
-                text: "Open Privacy Settings"
-                font.family: "Poppins"
-                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
-                font.weight: Font.Bold
-                color: saveButtonText
-                anchors.horizontalCenter: parent.horizontalCenter
-                anchors.verticalCenter: parent.verticalCenter
-            }
         }
 
         Text {
index 43d98be1050c1b3afeb891fbb38dea8cee0cdc64..fd63137892482d9108c11a94ee93b62d4dfdb1ce 100644 (file)
@@ -18,20 +18,8 @@ Item {
 
     property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
     property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
-    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
-    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
-    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
-    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
-    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
-    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
-    property string saveButtonBackgroundColour: "#F2F3F3"
-    property string saveButtonPressedColour: "#E7E8E8"
-    property string saveButtonStroke: "#EAEBEB"
-    property string saveButtonPressedStroke: "#B0B5B5"
-    property string saveButtonText: "#000000"
     property string checkboxStroke: "#0062cc"
     property string checkboxPressedStroke: "#007AFF"
-    property string disabledButtonText: "#D3D4D4"
     property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
 
     property bool currShowRecommendations: virtualstudio.showWarnings
@@ -159,30 +147,16 @@ Item {
             url: "https://support.jacktrip.com/wired-internet-versus-wi-fi"
         }
 
-        Button {
+        StyledButton {
             id: okButtonEthernet
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: okButtonEthernet.down ? saveButtonPressedColour : saveButtonBackgroundColour
-                border.width: 1
-                border.color: okButtonEthernet.down || okButtonEthernet.hovered ? saveButtonPressedStroke : saveButtonStroke
-                layer.enabled: okButtonEthernet.hovered && !okButtonEthernet.down
-            }
+            text: "Continue"
+            primary: true
             onClicked: { recommendationScreen = "fiber" }
             anchors.right: parent.right
             anchors.rightMargin: 16 * virtualstudio.uiScale
             anchors.bottomMargin: 16 * virtualstudio.uiScale
             anchors.bottom: parent.bottom
             width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-            Text {
-                text: "Continue"
-                font.family: "Poppins"
-                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
-                font.weight: Font.Bold
-                color: saveButtonText
-                anchors.horizontalCenter: parent.horizontalCenter
-                anchors.verticalCenter: parent.verticalCenter
-            }
         }
     }
 
@@ -232,30 +206,16 @@ Item {
             url: "https://support.jacktrip.com/how-to-optimize-latency-when-using-jacktrip"
         }
 
-        Button {
+        StyledButton {
             id: okButtonFiber
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: okButtonFiber.down ? saveButtonPressedColour : saveButtonBackgroundColour
-                border.width: 1
-                border.color: okButtonFiber.down || okButtonFiber.hovered ? saveButtonPressedStroke : saveButtonStroke
-                layer.enabled: okButtonFiber.hovered && !okButtonFiber.down
-            }
+            text: "Continue"
+            primary: true
             onClicked: { recommendationScreen = "audiointerface" }
             anchors.right: parent.right
             anchors.rightMargin: 16 * virtualstudio.uiScale
             anchors.bottomMargin: 16 * virtualstudio.uiScale
             anchors.bottom: parent.bottom
             width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-            Text {
-                text: "Continue"
-                font.family: "Poppins"
-                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
-                font.weight: Font.Bold
-                color: saveButtonText
-                anchors.horizontalCenter: parent.horizontalCenter
-                anchors.verticalCenter: parent.verticalCenter
-            }
         }
     }
 
@@ -300,15 +260,10 @@ Item {
             anchors.topMargin: 32 * virtualstudio.uiScale
         }
 
-        Button {
+        StyledButton {
             id: okButtonHeadphones
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: okButtonHeadphones.down ? saveButtonPressedColour : saveButtonBackgroundColour
-                border.width: 1
-                border.color: okButtonHeadphones.down || okButtonHeadphones.hovered ? saveButtonPressedStroke : saveButtonStroke
-                layer.enabled: okButtonHeadphones.hovered && !okButtonHeadphones.down
-            }
+            text: "Continue"
+            primary: true
             onClicked: {
                 recommendationScreen = "acknowledged";
             }
@@ -317,15 +272,6 @@ Item {
             anchors.bottomMargin: 16 * virtualstudio.uiScale
             anchors.bottom: parent.bottom
             width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-            Text {
-                text: "Continue"
-                font.family: "Poppins"
-                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
-                font.weight: Font.Bold
-                color: saveButtonText
-                anchors.horizontalCenter: parent.horizontalCenter
-                anchors.verticalCenter: parent.verticalCenter
-            }
         }
     }
 
@@ -410,15 +356,10 @@ Item {
             url: "https://support.jacktrip.com/recommended-audio-interfaces"
         }
 
-        Button {
+        StyledButton {
             id: okButtonAudioInterface
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: okButtonAudioInterface.down ? saveButtonPressedColour : saveButtonBackgroundColour
-                border.width: 1
-                border.color: okButtonAudioInterface.down || okButtonAudioInterface.hovered ? saveButtonPressedStroke : saveButtonStroke
-                layer.enabled: okButtonAudioInterface.hovered && !okButtonAudioInterface.down
-            }
+            text: "Continue"
+            primary: true
             onClicked: {
                 recommendationScreen = "headphones";
             }
@@ -427,15 +368,6 @@ Item {
             anchors.bottomMargin: 16 * virtualstudio.uiScale
             anchors.bottom: parent.bottom
             width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-            Text {
-                text: "Continue"
-                font.family: "Poppins"
-                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
-                font.weight: Font.Bold
-                color: saveButtonText
-                anchors.horizontalCenter: parent.horizontalCenter
-                anchors.verticalCenter: parent.verticalCenter
-            }
         }
     }
 
@@ -475,17 +407,11 @@ Item {
             anchors.topMargin: 64 * virtualstudio.uiScale
             anchors.horizontalCenter: parent.horizontalCenter
 
-            Button {
+            StyledButton {
                 id: acknowledgedYesButton
+                text: "Yes"
                 anchors.left: parent.left
                 anchors.verticalCenter: parent.verticalCenter
-                background: Rectangle {
-                    radius: 6 * virtualstudio.uiScale
-                    color: acknowledgedYesButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
-                    border.width: 1
-                    border.color: acknowledgedYesButton.down || acknowledgedYesButton.hovered ? saveButtonPressedStroke : saveButtonStroke
-                    layer.enabled: acknowledgedYesButton.hovered && !acknowledgedYesButton.down
-                }
                 onClicked: {
                     virtualstudio.showWarnings = true;
                     virtualstudio.saveSettings();
@@ -499,28 +425,14 @@ Item {
                     }
                 }
                 width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-                Text {
-                    text: "Yes"
-                    font.family: "Poppins"
-                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
-                    font.weight: Font.Bold
-                    color: saveButtonText
-                    anchors.horizontalCenter: parent.horizontalCenter
-                    anchors.verticalCenter: parent.verticalCenter
-                }
             }
 
-            Button {
+            StyledButton {
                 id: acknowledgedNoButton
+                text: "No"
+                primary: true
                 anchors.right: parent.right
                 anchors.verticalCenter: parent.verticalCenter
-                background: Rectangle {
-                    radius: 6 * virtualstudio.uiScale
-                    color: acknowledgedNoButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
-                    border.width: 1
-                    border.color: acknowledgedNoButton.down || acknowledgedNoButton.hovered ? saveButtonPressedStroke : saveButtonStroke
-                    layer.enabled: acknowledgedNoButton.hovered && !acknowledgedNoButton.down
-                }
                 onClicked: {
                     virtualstudio.showWarnings = false;
                     virtualstudio.saveSettings();
@@ -534,15 +446,6 @@ Item {
                     }
                 }
                 width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-                Text {
-                    text: "No"
-                    font.family: "Poppins"
-                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
-                    font.weight: Font.Bold
-                    color: saveButtonText
-                    anchors.horizontalCenter: parent.horizontalCenter
-                    anchors.verticalCenter: parent.verticalCenter
-                }
             }
         }
 
index cd589e2aab929769e75b3aa584ea783ab5ca1c88..ce46c010e8578540f9b5cc5df232533003135b03 100644 (file)
@@ -25,9 +25,6 @@ Item {
     property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
     property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
     property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
-    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
-    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
-    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
     property string sliderColour: virtualstudio.darkMode ? "#BABCBC" :  "#EAECEC"
     property string sliderPressedColour: virtualstudio.darkMode ? "#ACAFAF" : "#DEE0E0"
     property string sliderTrackColour: virtualstudio.darkMode ? "#5B5858" : "light gray"
@@ -38,7 +35,6 @@ Item {
     property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
 
     property string errorFlagColour: "#DB0A0A"
-    property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
 
     property string settingsGroupView: "Audio"
 
@@ -319,7 +315,7 @@ Item {
                 implicitHeight: 26 * virtualstudio.uiScale
                 radius: 13 * virtualstudio.uiScale
                 color: scaleSlider.pressed ? sliderPressedColour : sliderColour
-                border.color: buttonStroke
+                border.color: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
             }
         }
 
@@ -331,25 +327,14 @@ Item {
             color: textColour
         }
 
-        Button {
+        StyledButton {
             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)
-            }
+            text: virtualstudio.darkMode ? "Switch to Light Mode" : "Switch to Dark Mode"
             onClicked: { virtualstudio.darkMode = !virtualstudio.darkMode; }
-            x: parent.width - (232 * virtualstudio.uiScale);
+            x: parent.width - (232 * virtualstudio.uiScale)
             y: scaleSlider.y + (48 * virtualstudio.uiScale)
-            width: 216 * 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 {
@@ -370,35 +355,21 @@ Item {
         color: backgroundColour
         visible: settingsGroupView == "Advanced"
 
-        Button {
+        StyledButton {
             id: modeButton
+            text: virtualstudio.psiBuild ? "Switch to Standard Mode" : "Switch to Classic Mode"
             visible: virtualstudio.hasClassicMode
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: modeButton.down ? buttonPressedColour : (modeButton.hovered ? buttonHoverColour : buttonColour)
-                border.width: 1
-                border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke)
-            }
             onClicked: {
-                // essentially the same here as clicking the cancel button
                 audio.stopAudio();
                 virtualstudio.windowState = "browse";
                 virtualstudio.loadSettings();
                 audio.validateDevices();
-
-                // switch mode
                 virtualstudio.toClassicMode();
             }
-            x: 220 * virtualstudio.uiScale;
+            x: 220 * virtualstudio.uiScale
             y: 48 * virtualstudio.uiScale
-            width: 216 * 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 {
@@ -640,58 +611,29 @@ Item {
             color: textColour
         }
 
-        Button {
+        StyledButton {
             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)
-            }
+            text: "Edit Profile"
             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 {
+        StyledButton {
             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)
-            }
+            text: "Log Out"
             onClicked: { virtualstudio.logout(); }
             anchors.horizontalCenter: parent.horizontalCenter
             y: editButton.y + (48 * virtualstudio.uiScale)
             width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-            Text {
-                text: "Log Out"
-                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
-                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
-                color: textColour
-            }
         }
 
-        Button {
+        StyledButton {
             id: testModeButton
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: testModeButton.down ? buttonPressedColour : (testModeButton.hovered ? buttonHoverColour : buttonColour)
-                border.width: 1
-                border.color: testModeButton.down ? buttonPressedStroke : (testModeButton.hovered ? buttonHoverStroke : buttonStroke)
-            }
+            text: virtualstudio.testMode ? "Switch to Prod Mode" : "Switch to Test Mode"
             onClicked: {
                 virtualstudio.testMode = !virtualstudio.testMode;
-
-                // behave like "Cancel" and switch back to browse mode
                 audio.stopAudio();
                 virtualstudio.windowState = "browse";
                 virtualstudio.loadSettings();
@@ -701,12 +643,6 @@ Item {
             y: logoutButton.y + (48 * virtualstudio.uiScale)
             width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
             visible: virtualstudio.userMetadata.email ? ( virtualstudio.userMetadata.email.endsWith("@jacktrip.org") ? true : false ) : false
-            Text {
-                text: virtualstudio.testMode ? "Switch to Prod Mode" : "Switch to Test Mode"
-                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
-                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
-                color: textColour
-            }
         }
     }
 
@@ -716,14 +652,9 @@ Item {
         border.color: "#33979797"
         color: backgroundColour
 
-        Button {
+        StyledButton {
             id: cancelButton
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: cancelButton.down ? buttonPressedColour : (cancelButton.hovered ? buttonHoverColour : buttonColour)
-                border.width: 1
-                border.color: cancelButton.down ? buttonPressedStroke : (cancelButton.hovered ? buttonHoverStroke : buttonStroke)
-            }
+            text: "Cancel"
             onClicked: {
                 audio.stopAudio();
                 virtualstudio.windowState = "browse";
@@ -733,23 +664,13 @@ Item {
             anchors.verticalCenter: parent.verticalCenter
             x: parent.width - (230 * virtualstudio.uiScale)
             width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
-            Text {
-                text: "Cancel"
-                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
-                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
-                color: textColour
-            }
         }
 
-        Button {
+        StyledButton {
             id: saveButton
+            text: "Save"
+            primary: true
             enabled: !Boolean(audio.devicesError)
-            background: Rectangle {
-                radius: 6 * virtualstudio.uiScale
-                color: saveButton.down ? buttonPressedColour : (saveButton.hovered ? buttonHoverColour : buttonColour)
-                border.width: 1
-                border.color: saveButton.down ? buttonPressedStroke : (saveButton.hovered ? buttonHoverStroke : buttonStroke)
-            }
             onClicked: {
                 audio.stopAudio();
                 virtualstudio.windowState = "browse";
@@ -758,12 +679,6 @@ Item {
             anchors.verticalCenter: parent.verticalCenter
             x: parent.width - (119 * virtualstudio.uiScale)
             width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
-            Text {
-                text: "Save"
-                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
-                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
-                color: Boolean(audio.devicesError) ? disabledButtonTextColour : textColour
-            }
         }
 
         DeviceWarning {
index e2fe2cfaa5a449a5dd049e649788b8a7164fc3b2..35d4ad66e517af548cc76f4f5cd7b7de80153c06 100644 (file)
@@ -15,20 +15,8 @@ Item {
 
     property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
     property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
-    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
-    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"  
-    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
-    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
-    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"  
-    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
-    property string saveButtonBackgroundColour: "#F2F3F3"
-    property string saveButtonPressedColour: "#E7E8E8" 
-    property string saveButtonStroke: "#EAEBEB"
-    property string saveButtonPressedStroke: "#B0B5B5"
-    property string saveButtonText: "#000000"
     property string checkboxStroke: "#0062cc"
     property string checkboxPressedStroke: "#007AFF"
-    property string disabledButtonText: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
 
     Item {
         id: setupItem
@@ -94,28 +82,15 @@ Item {
             border.color: "#33979797"
             color: backgroundColour
 
-            Button {
+            StyledButton {
                 id: backButton
-                background: Rectangle {
-                    radius: 6 * virtualstudio.uiScale
-                    color: backButton.down ? buttonPressedColour : buttonColour
-                    border.width: 1
-                    border.color: backButton.down || backButton.hovered ? buttonPressedStroke : buttonStroke
-                }
+                text: "Go Back"
                 onClicked: { virtualstudio.windowState = "browse"; virtualstudio.studioToJoin = ""; audio.stopAudio(); }
                 anchors.left: parent.left
                 anchors.leftMargin: 16 * virtualstudio.uiScale
                 anchors.bottomMargin: rightMargin * virtualstudio.uiScale
                 anchors.bottom: parent.bottom
                 width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-                Text {
-                    text: "Back"
-                    font.family: "Poppins"
-                    font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
-                    color: textColour
-                    anchors.horizontalCenter: parent.horizontalCenter
-                    anchors.verticalCenter: parent.verticalCenter
-                }
             }
 
             DeviceWarning {
@@ -126,14 +101,10 @@ Item {
                 visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
             }
 
-            Button {
+            StyledButton {
                 id: saveButton
-                background: Rectangle {
-                    radius: 6 * virtualstudio.uiScale
-                    color: saveButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
-                    border.width: 1
-                    border.color: saveButton.down || saveButton.hovered ? saveButtonPressedStroke : saveButtonStroke
-                }
+                text: "Connect to Session"
+                primary: true
                 enabled: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady
                 onClicked: {
                     if (Boolean(audio.devicesWarning)) {
@@ -150,15 +121,6 @@ Item {
                 anchors.bottomMargin: rightMargin * virtualstudio.uiScale
                 anchors.bottom: parent.bottom
                 width: 160 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
-                Text {
-                    text: "Connect to Session"
-                    font.family: "Poppins"
-                    font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
-                    font.weight: Font.Bold
-                    color: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady ? saveButtonText : disabledButtonText
-                    anchors.horizontalCenter: parent.horizontalCenter
-                    anchors.verticalCenter: parent.verticalCenter
-                }
             }
 
             CheckBox {
diff --git a/src/vs/StyledButton.qml b/src/vs/StyledButton.qml
new file mode 100644 (file)
index 0000000..43babb8
--- /dev/null
@@ -0,0 +1,54 @@
+import QtQuick
+import QtQuick.Controls
+
+Button {
+    id: styledButton
+    property bool primary: false
+    property int  fontSize: 11
+    property int  buttonRadius: 6
+    property bool showBorder: true
+
+    readonly property color resolvedTextColor:
+        !enabled    ? (virtualstudio.darkMode ? "#827D7D" : "#BABCBC")
+        : primary   ? "#FFFFFF"
+                    : (virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D")
+
+    palette.buttonText: resolvedTextColor
+
+    font {
+        family: "Poppins"
+        pixelSize: fontSize * virtualstudio.fontScale * virtualstudio.uiScale
+        weight: Font.Normal
+    }
+
+    background: Rectangle {
+        radius: buttonRadius * virtualstudio.uiScale
+        color: "transparent"
+
+        Rectangle {
+            anchors.fill: parent
+            radius: parent.radius
+            visible: !styledButton.primary || !styledButton.enabled
+            color: styledButton.down
+                ? (virtualstudio.darkMode ? "#524F4F" : "#DEE0E0")
+                : styledButton.hovered
+                    ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4")
+                    : (virtualstudio.darkMode ? "#494646" : "#EAECEC")
+            border.width: styledButton.showBorder ? 1 : 0
+            border.color: styledButton.down || styledButton.hovered
+                ? (virtualstudio.darkMode ? "#827D7D" : "#BABCBC")
+                : (virtualstudio.darkMode ? "#80827D7D" : "#34979797")
+        }
+
+        Rectangle {
+            anchors.fill: parent
+            radius: parent.radius
+            visible: styledButton.primary && styledButton.enabled
+            gradient: Gradient {
+                orientation: Gradient.Horizontal
+                GradientStop { position: 0.0; color: styledButton.down ? "#5B21B6" : styledButton.hovered ? "#6D28D9" : "#7C3AED" }
+                GradientStop { position: 1.0; color: styledButton.down ? "#3730A3" : styledButton.hovered ? "#4338CA" : "#4F46E5" }
+            }
+        }
+    }
+}
diff --git a/src/vs/arrow-left.svg b/src/vs/arrow-left.svg
new file mode 100644 (file)
index 0000000..ca5d8ec
--- /dev/null
@@ -0,0 +1,4 @@
+<!-- Heroicons (MIT) https://heroicons.com -->
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
+</svg>
diff --git a/src/vs/arrow-right.svg b/src/vs/arrow-right.svg
new file mode 100644 (file)
index 0000000..f076999
--- /dev/null
@@ -0,0 +1,4 @@
+<!-- Heroicons (MIT) https://heroicons.com -->
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
+</svg>
diff --git a/src/vs/arrow-top-right-on-square.svg b/src/vs/arrow-top-right-on-square.svg
new file mode 100644 (file)
index 0000000..2a42ebe
--- /dev/null
@@ -0,0 +1,4 @@
+<!-- Heroicons (MIT) https://heroicons.com -->
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
+</svg>
diff --git a/src/vs/home.svg b/src/vs/home.svg
new file mode 100644 (file)
index 0000000..f10e8a8
--- /dev/null
@@ -0,0 +1,4 @@
+<!-- Heroicons (MIT) https://heroicons.com -->
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <path d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M2.25 12v7.5a2.25 2.25 0 0 0 2.25 2.25h15a2.25 2.25 0 0 0 2.25-2.25v-7.5m-18-3-4.5 4.5m0 0-4.5-4.5m4.5 4.5V3" />
+</svg>
diff --git a/src/vs/question-mark-circle.svg b/src/vs/question-mark-circle.svg
new file mode 100644 (file)
index 0000000..7cc171b
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.25 12C2.25 6.61522 6.61522 2.25 12 2.25C17.3848 2.25 21.75 6.61522 21.75 12C21.75 17.3848 17.3848 21.75 12 21.75C6.61522 21.75 2.25 17.3848 2.25 12ZM13.6277 8.08328C12.7389 7.30557 11.2616 7.30557 10.3728 8.08328C10.0611 8.35604 9.58723 8.32445 9.31447 8.01272C9.04171 7.701 9.0733 7.22717 9.38503 6.95441C10.8394 5.68186 13.1611 5.68186 14.6154 6.95441C16.1285 8.27835 16.1285 10.4717 14.6154 11.7956C14.3588 12.0202 14.0761 12.2041 13.778 12.3484C13.1018 12.6756 12.7502 13.1222 12.7502 13.5V14.25C12.7502 14.6642 12.4144 15 12.0002 15C11.586 15 11.2502 14.6642 11.2502 14.25V13.5C11.2502 12.221 12.3095 11.3926 13.1246 10.9982C13.3073 10.9098 13.4765 10.799 13.6277 10.6667C14.4577 9.9404 14.4577 8.80959 13.6277 8.08328ZM12 18C12.4142 18 12.75 17.6642 12.75 17.25C12.75 16.8358 12.4142 16.5 12 16.5C11.5858 16.5 11.25 16.8358 11.25 17.25C11.25 17.6642 11.5858 18 12 18Z" fill="#0F172A"/>
+</svg>
diff --git a/src/vs/squares-2x2.svg b/src/vs/squares-2x2.svg
new file mode 100644 (file)
index 0000000..d8a1759
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3 6C3 4.34315 4.34315 3 6 3H8.25C9.90685 3 11.25 4.34315 11.25 6V8.25C11.25 9.90685 9.90685 11.25 8.25 11.25H6C4.34315 11.25 3 9.90685 3 8.25V6ZM12.75 6C12.75 4.34315 14.0931 3 15.75 3H18C19.6569 3 21 4.34315 21 6V8.25C21 9.90685 19.6569 11.25 18 11.25H15.75C14.0931 11.25 12.75 9.90685 12.75 8.25V6ZM3 15.75C3 14.0931 4.34315 12.75 6 12.75H8.25C9.90685 12.75 11.25 14.0931 11.25 15.75V18C11.25 19.6569 9.90685 21 8.25 21H6C4.34315 21 3 19.6569 3 18V15.75ZM12.75 15.75C12.75 14.0931 14.0931 12.75 15.75 12.75H18C19.6569 12.75 21 14.0931 21 15.75V18C21 19.6569 19.6569 21 18 21H15.75C14.0931 21 12.75 19.6569 12.75 18V15.75Z" fill="#0F172A"/>
+</svg>
index 3a8158562e7279f6aab5fd6242d1de7f998cd963..eda6044ed67c7157e7170770887c79a63f59b270 100644 (file)
@@ -69,7 +69,6 @@
 #include "../AudioSocket.h"
 #include "../JackTrip.h"
 #include "../Settings.h"
-#include "../SocketClient.h"
 #include "../SocketServer.h"
 #include "../jacktrip_globals.h"
 #include "JTApplication.h"
@@ -128,10 +127,7 @@ VirtualStudio::VirtualStudio(UserInterface& parent)
 
     // instantiate API
     m_api.reset(new VsApi(m_networkAccessManagerPtr));
-    m_api->setApiHost(PROD_API_HOST);
-    if (m_testMode) {
-        m_api->setApiHost(TEST_API_HOST);
-    }
+    m_api->setApiHost(m_testMode ? TEST_API_HOST : PROD_API_HOST);
 
     // instantiate auth
     m_auth.reset(new VsAuth(m_networkAccessManagerPtr, m_api.data()));
@@ -214,10 +210,10 @@ VirtualStudio::VirtualStudio(UserInterface& parent)
         QStringLiteral("permissions"),
         QVariant::fromValue(&m_audioConfigPtr->getPermissions()));
     m_view->setSource(QUrl(QStringLiteral("qrc:/vs/vs.qml")));
-    m_view->setMinimumSize(QSize(800, 640));
+    m_view->setMinimumSize(QSize(VS_DEFAULT_WINDOW_WIDTH, VS_DEFAULT_WINDOW_HEIGHT));
     // m_view->setMaximumSize(QSize(696, 577));
     m_view->setResizeMode(QQuickView::SizeRootObjectToView);
-    m_view->resize(800 * m_uiScale, 640 * m_uiScale);
+    m_view->resize(m_savedWindowWidth, m_savedWindowHeight);
 
     // Connect our timers
     connect(this, &VirtualStudio::scheduleStudioRefresh, this,
@@ -257,36 +253,11 @@ VirtualStudio::VirtualStudio(UserInterface& parent)
     ts = new QTextStream(&outFile);
     qInstallMessageHandler(qtMessageHandler);
 
-    // check if started with a deep link to handle jacktrip://join/<StudioID>
-    QString deepLinkStr = m_interface.getSettings().getDeeplink();
-    if (!deepLinkStr.isEmpty()) {
-        // started with a deep link; check if another instance is already running
-        SocketClient c;
-        if (c.connect()) {
-            // existing instance found; send deeplink to it and exit
-            if (!c.sendHeader("deeplink")) {
-                c.close();
-                std::cerr << "Failed to send deeplink header" << std::endl;
-                std::exit(1);
-            }
-            QLocalSocket& s          = c.getSocket();
-            QByteArray deepLinkBytes = deepLinkStr.toLocal8Bit();
-            qint64 bytesWritten      = s.write(deepLinkBytes);
-            s.flush();
-            s.waitForBytesWritten(1000);
-            if (bytesWritten != deepLinkBytes.size()) {
-                std::cerr << "Failed to send deeplink" << std::endl;
-                std::exit(1);
-            }
-            std::cout << "sent deeplink: " << deepLinkStr.toStdString() << std::endl;
-            std::exit(0);
-        }
-    }
-
     // prepare handler for deep link requests
     m_deepLinkPtr.reset(new VsDeeplink());
     QObject::connect(m_deepLinkPtr.get(), &VsDeeplink::signalVsDeeplink, this,
                      &VirtualStudio::handleDeeplinkRequest, Qt::QueuedConnection);
+    QString deepLinkStr = m_interface.getSettings().getDeeplink();
     if (!deepLinkStr.isEmpty()) {
         QUrl deepLinkUrl(deepLinkStr);
         m_deepLinkPtr->handleUrl(deepLinkUrl);
@@ -620,14 +591,14 @@ void VirtualStudio::setWindowState(QString state)
 
 QString VirtualStudio::apiHost()
 {
-    return m_apiHost;
+    return m_api.isNull() ? PROD_API_HOST : m_api->getApiHost();
 }
 
 void VirtualStudio::setApiHost(QString host)
 {
-    if (m_apiHost == host)
+    if (m_api.isNull() || m_api->getApiHost() == host)
         return;
-    m_apiHost = host;
+    m_api->setApiHost(host);
     emit apiHostChanged();
 }
 
@@ -1009,9 +980,13 @@ void VirtualStudio::loadSettings()
     // use setters to emit signals for these if they change; otherwise, the
     // user interface will not revert back after cancelling settings changes
     setUiScale(settings.value(QStringLiteral("UiScale"), 1).toFloat());
-    setDarkMode(settings.value(QStringLiteral("DarkMode"), false).toBool());
+    setDarkMode(settings.value(QStringLiteral("DarkMode"), true).toBool());
     setShowDeviceSetup(settings.value(QStringLiteral("ShowDeviceSetup"), true).toBool());
     setShowWarnings(settings.value(QStringLiteral("ShowWarnings"), true).toBool());
+    m_savedWindowWidth =
+        settings.value(QStringLiteral("WindowWidth"), VS_DEFAULT_WINDOW_WIDTH).toInt();
+    m_savedWindowHeight =
+        settings.value(QStringLiteral("WindowHeight"), VS_DEFAULT_WINDOW_HEIGHT).toInt();
     settings.endGroup();
 
     m_audioConfigPtr->loadSettings();
@@ -1026,6 +1001,8 @@ void VirtualStudio::saveSettings()
     settings.setValue(QStringLiteral("DarkMode"), m_darkMode);
     settings.setValue(QStringLiteral("ShowDeviceSetup"), m_showDeviceSetup);
     settings.setValue(QStringLiteral("ShowWarnings"), m_showWarnings);
+    settings.setValue(QStringLiteral("WindowWidth"), m_view->width());
+    settings.setValue(QStringLiteral("WindowHeight"), m_view->height());
     settings.endGroup();
 
     m_audioConfigPtr->saveSettings();
@@ -1348,6 +1325,13 @@ void VirtualStudio::exit()
         emit signalExit();
     }
 
+    // persist window dimensions before exiting
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("WindowWidth"), m_view->width());
+    settings.setValue(QStringLiteral("WindowHeight"), m_view->height());
+    settings.endGroup();
+
     // triggering isExitingChanged will force any WebEngine things to close properly
     m_isExiting = true;
     emit isExitingChanged();
@@ -1371,11 +1355,7 @@ void VirtualStudio::slotAuthSucceeded()
     raiseToTop();
 
     // Determine which API host to use
-    m_apiHost = PROD_API_HOST;
-    if (m_testMode) {
-        m_apiHost = TEST_API_HOST;
-    }
-    m_api->setApiHost(m_apiHost);
+    setApiHost(m_testMode ? TEST_API_HOST : PROD_API_HOST);
 
     // initialize new VsDevice and wire up signals/slots before registering app
     if (!m_devicePtr.isNull()) {
index fd4c24391a7da47a723a5a025dd6f36443dd7c16..118c1655f2e6e8967c6a52c750fb0fcc41cc5554 100644 (file)
@@ -311,7 +311,6 @@ class VirtualStudio : public QObject
     QString m_updateChannel;
     QString m_refreshToken;
     QString m_userId;
-    QString m_apiHost               = PROD_API_HOST;
     ReconnectState m_reconnectState = ReconnectState::NOT_RECONNECTING;
 
     bool m_firstRefresh           = true;
@@ -330,6 +329,8 @@ class VirtualStudio : public QObject
     bool m_authenticated          = false;
     bool m_useStudioQueueBuffer   = true;
     int m_queueBuffer             = 0;
+    int m_savedWindowWidth        = VS_DEFAULT_WINDOW_WIDTH;
+    int m_savedWindowHeight       = VS_DEFAULT_WINDOW_HEIGHT;
     float m_fontScale             = 1;
     float m_uiScale               = 1;
     uint32_t m_webChannelPort     = 1;
index 9a12bea528e5cfa86094b97d975efdd713d9ee19..5504d676340cb66cae9c22fea7b28378c6690445 100644 (file)
@@ -23,6 +23,7 @@
     <file>VolumeSlider.qml</file>
     <file>DeviceControls.qml</file>
     <file>DeviceControlsGroup.qml</file>
+    <file>StyledButton.qml</file>
     <file>DeviceRefreshButton.qml</file>
     <file>DeviceWarning.qml</file>
     <file>DeviceWarningModal.qml</file>
@@ -48,6 +49,9 @@
     <file>start.svg</file>
     <file>star.svg</file>
     <file>cog.svg</file>
+    <file>home.svg</file>
+    <file>squares-2x2.svg</file>
+    <file>question-mark-circle.svg</file>
     <file>mic.svg</file>
     <file>language.svg</file>
     <file>micoff.svg</file>
@@ -56,6 +60,9 @@
     <file>quiet.svg</file>
     <file>loud.svg</file>
     <file>refresh.svg</file>
+    <file>arrow-left.svg</file>
+    <file>arrow-right.svg</file>
+    <file>arrow-top-right-on-square.svg</file>
     <file>ethernet.svg</file>
     <file>networkCheck.svg</file>
     <file>externalMic.svg</file>
index 4665cb207e59b6fbf8ecec90bd4a473100ac8552..7b132a7c53c0eaccaf43d67e1635ac30fcc4e72a 100644 (file)
@@ -1271,7 +1271,19 @@ void VsAudioWorker::validateInputDevicesState()
     // actually exists
     if (getInputDevice() == QStringLiteral("")
         || m_inputDeviceList.indexOf(getInputDevice()) == -1) {
-        m_parentPtr->setInputDevice(m_inputDeviceList[0]);
+        // Prefer the OS default device; fall back to first available
+        QString defaultDevice;
+        for (const auto& device : m_devices) {
+            if (device.isDefaultInput && device.inputChannels > 0) {
+                defaultDevice = QString::fromStdString(device.name);
+                break;
+            }
+        }
+        if (!defaultDevice.isEmpty() && m_inputDeviceList.contains(defaultDevice)) {
+            m_parentPtr->setInputDevice(defaultDevice);
+        } else {
+            m_parentPtr->setInputDevice(m_inputDeviceList[0]);
+        }
     }
 
     // Given the currently selected input device, reset the available input channel
@@ -1396,7 +1408,19 @@ void VsAudioWorker::validateOutputDevicesState()
     // actually exists
     if (getOutputDevice() == QStringLiteral("")
         || m_outputDeviceList.indexOf(getOutputDevice()) == -1) {
-        m_parentPtr->setOutputDevice(m_outputDeviceList[0]);
+        // Prefer the OS default device; fall back to first available
+        QString defaultDevice;
+        for (const auto& device : m_devices) {
+            if (device.isDefaultOutput && device.outputChannels > 0) {
+                defaultDevice = QString::fromStdString(device.name);
+                break;
+            }
+        }
+        if (!defaultDevice.isEmpty() && m_outputDeviceList.contains(defaultDevice)) {
+            m_parentPtr->setOutputDevice(defaultDevice);
+        } else {
+            m_parentPtr->setOutputDevice(m_outputDeviceList[0]);
+        }
     }
 
     // Given the currently selected output device, reset the available output channel
index a4b8aedbffb24ea817b4e08de86ec4d4b2e1f439..31755c5462a6f8377b2b167b015d33f1edfd2cfa 100644 (file)
@@ -39,6 +39,9 @@
 
 #include <QString>
 
+constexpr int VS_DEFAULT_WINDOW_WIDTH  = 800;
+constexpr int VS_DEFAULT_WINDOW_HEIGHT = 640;
+
 const QString AUTH_AUTHORIZE_URI = QStringLiteral("https://auth.jacktrip.org/authorize");
 const QString AUTH_TOKEN_URI   = QStringLiteral("https://auth.jacktrip.org/oauth/token");
 const QString AUTH_AUDIENCE    = QStringLiteral("https://api.jacktrip.org");
index 2a28f076aac5fd57e5eacecd76b0cefbf922a1e0..d906f5c01967078300ac3f7ce384e63ce21ddc30 100644 (file)
@@ -59,7 +59,7 @@ class VsDeviceCodeFlow : public QObject
     virtual ~VsDeviceCodeFlow() { stopPolling(); }
 
     void grant();
-    void refreshAccessToken(){};
+    void refreshAccessToken() {};
     void initDeviceAuthorizationCodeFlow();
 
     bool processDeviceCodeNetworkReply(QNetworkReply* reply);
diff --git a/src/webrtc/WebRtcDataProtocol.cpp b/src/webrtc/WebRtcDataProtocol.cpp
new file mode 100644 (file)
index 0000000..5717ff0
--- /dev/null
@@ -0,0 +1,533 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebRtcDataProtocol.cpp
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#include "WebRtcDataProtocol.h"
+
+#include <QThread>
+#include <cstring>
+#include <iostream>
+#include <rtc/rtc.hpp>
+
+#include "../JackTrip.h"
+#include "../jacktrip_globals.h"
+
+using std::cerr;
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+WebRtcDataProtocol::WebRtcDataProtocol(JackTrip* jacktrip, const runModeT runmode,
+                                       std::shared_ptr<rtc::DataChannel> dataChannel)
+    : DataProtocol(jacktrip, runmode, 0, 0)  // Ports not used for WebRTC
+    , mDataChannel(dataChannel)
+    , mRunMode(runmode)
+    , mChans(0)
+    , mSmplSize(0)
+    , mTotCount(0)
+    , mLostCount(0)
+    , mOutOfOrderCount(0)
+    , mRevivedCount(0)
+    , mStatCount(0)
+    , mChannelOpen(false)
+    , mTimeSinceLastPacket(0)
+    , mControlPacketSize(63)
+    , mStopSignalSent(false)
+    , mLastSeqNum(0)
+    , mInitialState(true)
+{
+    if (mRunMode == RECEIVER) {
+        QObject::connect(this, &WebRtcDataProtocol::signalWaitingTooLong, jacktrip,
+                         &JackTrip::slotUdpWaitingTooLongClientGoneProbably,
+                         Qt::QueuedConnection);
+    }
+
+    // Setup data channel callbacks
+    if (mDataChannel) {
+        mChannelOpen = mDataChannel->isOpen();
+
+        mDataChannel->onOpen([this]() {
+            onDataChannelOpen();
+        });
+
+        mDataChannel->onClosed([this]() {
+            onDataChannelClosed();
+        });
+
+        mDataChannel->onError([this](std::string error) {
+            onDataChannelError(error);
+        });
+
+        mDataChannel->onMessage([this](rtc::message_variant data) {
+            if (std::holds_alternative<rtc::binary>(data)) {
+                onDataChannelMessage(std::get<rtc::binary>(data));
+            }
+        });
+    } else {
+        cerr << "WebRtcDataProtocol::WebRtcDataProtocol: ERROR - "
+             << "Data channel is null!" << endl;
+    }
+}
+
+//*******************************************************************************
+WebRtcDataProtocol::~WebRtcDataProtocol()
+{
+    stop();
+    wait();
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::stop()
+{
+    mChannelOpen = false;
+    DataProtocol::stop();
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::setPeerAddress(const char* /*peerHostOrIP*/)
+{
+    // No-op for WebRTC - peer address is in SDP
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::setPeerPort(int /*port*/)
+{
+    // No-op for WebRTC - port is in SDP
+}
+
+//*******************************************************************************
+#if defined(_WIN32)
+void WebRtcDataProtocol::setSocket(SOCKET& /*socket*/)
+#else
+void WebRtcDataProtocol::setSocket(int& /*socket*/)
+#endif
+{
+    // No-op for WebRTC - we use data channel, not socket
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::onDataChannelOpen()
+{
+    // Check if we're already stopped to prevent use-after-free
+    if (mStopped) {
+        return;
+    }
+    mChannelOpen = true;
+    emit signalDataChannelConnected();
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::onDataChannelClosed()
+{
+    // Check if we're already stopped to prevent use-after-free
+    if (mStopped) {
+        return;
+    }
+    mChannelOpen = false;
+    emit signalDataChannelDisconnected();
+    emit signalCeaseTransmission(QStringLiteral("Data channel closed"));
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::onDataChannelMessage(const std::vector<std::byte>& data)
+{
+    // Check if we're already stopped to prevent use-after-free
+    if (mStopped) {
+        return;
+    }
+
+    // For SENDER mode, we don't process incoming messages
+    if (mRunMode != RECEIVER) {
+        return;
+    }
+
+    // Reset timeout counter - we received a packet
+    mTimeSinceLastPacket = 0;
+
+    // Check for control packet
+    if (data.size() == static_cast<size_t>(mControlPacketSize)) {
+        processControlPacket(reinterpret_cast<const char*>(data.data()), data.size());
+        return;
+    }
+
+    if (data.size() < sizeof(DefaultHeaderStruct)) {
+        return;
+    }
+
+    // Process the packet directly and write to ring buffer
+    // Note: By the time WebRtcDataProtocol is created, JackTripWorker has already
+    // received the first packet and configured all peer settings, so we can
+    // process all packets immediately without special first-packet handling
+    if (data.size() > 0 && mChans > 0) {
+        processReceivedPacket(
+            const_cast<int8_t*>(reinterpret_cast<const int8_t*>(data.data())),
+            data.size(), data.size());
+    }
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::onDataChannelError(const std::string& error)
+{
+    // Check if we're already stopped to prevent use-after-free
+    if (mStopped) {
+        return;
+    }
+
+    cerr << "WebRtcDataProtocol: Data channel error: " << error << endl;
+    emit signalError(error.c_str());
+}
+
+//*******************************************************************************
+bool WebRtcDataProtocol::isChannelOpen() const
+{
+    return mChannelOpen.load();
+}
+
+//*******************************************************************************
+int WebRtcDataProtocol::sendPacket(const char* buf, const size_t n)
+{
+    if (!mDataChannel || !mChannelOpen) {
+        return -1;
+    }
+
+    try {
+        mDataChannel->send(reinterpret_cast<const std::byte*>(buf), n);
+        return static_cast<int>(n);
+
+    } catch (const std::exception& e) {
+        cerr << "WebRtcDataProtocol: Send error: " << e.what() << endl;
+        return -1;
+    }
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::processControlPacket(const char* buf, size_t size)
+{
+    // Control signal (check for exit packet)
+    if (size != static_cast<size_t>(mControlPacketSize)) {
+        return;
+    }
+
+    bool isExit = true;
+    for (size_t i = 0; i < size; i++) {
+        if (buf[i] != static_cast<char>(0xff)) {
+            isExit = false;
+            break;
+        }
+    }
+
+    if (isExit && !mStopSignalSent) {
+        mStopSignalSent = true;
+        emit signalCeaseTransmission(QStringLiteral("Peer Stopped"));
+    }
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::run()
+{
+    // Setup audio packet buffers
+    size_t audio_packet_size = getAudioPacketSizeInBites();
+    mAudioPacket.reset(new int8_t[audio_packet_size]);
+    std::memset(mAudioPacket.get(), 0, audio_packet_size);
+
+    mSmplSize = mJackTrip->getAudioBitResolution() / 8;
+
+    if (mRunMode == RECEIVER) {
+        mChans = mJackTrip->getNumOutputChannels();
+        if (mChans == 0)
+            return;
+    } else {
+        mChans = mJackTrip->getNumInputChannels();
+        if (mChans == 0) {
+            cerr << "WebRtcDataProtocol::run: ERROR - mChans is 0 for SENDER, exiting"
+                 << endl;
+            return;
+        }
+    }
+    int full_packet_size = mJackTrip->getReceivePacketSizeInBytes();
+    mFullPacket.reset(new int8_t[full_packet_size]);
+    std::memset(mFullPacket.get(), 0, full_packet_size);
+    mJackTrip->putHeaderInIncomingPacket(mFullPacket.get(), mAudioPacket.get());
+
+    // Pre-allocate buffer to avoid allocations in the audio hot path
+    // Calculate maximum expected buffer size to handle any packet size without
+    // reallocation
+    int max_buffer_size = mJackTrip->getBufferSizeInSamples() * mChans * mSmplSize;
+    mBuffer.resize(max_buffer_size, 0);
+
+    // Set realtime priority if requested
+    if (mUseRtPriority) {
+#if defined(__APPLE__)
+        setRealtimeProcessPriority(mJackTrip->getBufferSizeInSamples(),
+                                   mJackTrip->getSampleRate());
+#else
+        setRealtimeProcessPriority();
+#endif
+    }
+
+    // Signal that thread has started (both SENDER and RECEIVER)
+    // This allows completeConnection() to proceed
+    threadHasStarted();
+
+    switch (mRunMode) {
+    case RECEIVER:
+        runReceiver(full_packet_size);
+        break;
+
+    case SENDER:
+        runSender(full_packet_size);
+        break;
+    }
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::runReceiver(int full_packet_size)
+{
+    Q_UNUSED(full_packet_size)
+
+    // Wait for data channel to open if not already
+    while (!mChannelOpen && !mStopped) {
+        QThread::msleep(100);
+        if (gVerboseFlag) {
+            std::cout << "Waiting for data channel..." << endl;
+        }
+    }
+
+    if (mStopped)
+        return;
+
+    // Note: By the time WebRtcDataProtocol is created, JackTripWorker has already:
+    // 1. Received the first packet
+    // 2. Extracted and configured peer settings (channels, buffer size, etc.)
+    // 3. Called startProcess() which created this WebRtcDataProtocol
+    // Therefore, we don't need to wait for or process the first packet here.
+    // We can immediately signal that we're ready to receive.
+
+    if (gVerboseFlag) {
+        cout << "Received data channel connection from Peer!" << endl;
+    }
+    emit signalReceivedConnectionFromPeer();
+
+    // Connect signal for logging when waiting too long for packets
+    QObject::connect(this, &WebRtcDataProtocol::signalWaitingTooLong, this,
+                     &WebRtcDataProtocol::printWaitedTooLong, Qt::QueuedConnection);
+
+    // Initialize statistics
+    mTotCount        = 0;
+    mLostCount       = 0;
+    mOutOfOrderCount = 0;
+    mRevivedCount    = 0;
+    mStatCount       = 0;
+    mInitialState    = true;
+
+    // Reset the packet timeout counter
+    mTimeSinceLastPacket = 0;
+
+    // Main receive loop - packets are processed in onDataChannelMessage callback
+    // This thread just monitors for timeout conditions
+    while (!mStopped && mChannelOpen) {
+        QThread::msleep(10);
+
+        // Increment time since last packet
+        int timeSinceLastPacket = mTimeSinceLastPacket.fetch_add(10) + 10;
+
+        // Emit signal every gUdpWaitTimeout ms if no packets have been received
+        if (!(timeSinceLastPacket % gUdpWaitTimeout)) {
+            emit signalWaitingTooLong(timeSinceLastPacket);
+        }
+    }
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::runSender(int full_packet_size)
+{
+    // Wait for data channel to open if not already
+    int waitCount = 0;
+    while (!mChannelOpen && !mStopped) {
+        QThread::msleep(100);
+        waitCount++;
+        if (waitCount % 10 == 0 && gVerboseFlag) {
+            cout << "WebRtcDataProtocol::runSender: Still waiting for channel to open ("
+                 << (waitCount * 100) << "ms elapsed)..." << endl;
+        }
+    }
+
+    if (mStopped) {
+        cout << "WebRtcDataProtocol::runSender: Stopped before channel opened" << endl;
+        return;
+    }
+
+    // Main send loop
+    while (!mStopped && !JackTrip::sSigInt && !JackTrip::sAudioStopped && mChannelOpen) {
+        // Read audio from ring buffer
+        mJackTrip->readAudioBuffer(mAudioPacket.get());
+
+        int8_t* src = mAudioPacket.get();
+
+        // Convert interleaved to non-interleaved if needed
+        if (mChans > 1) {
+            int N       = mJackTrip->getBufferSizeInSamples();
+            int8_t* dst = mBuffer.data();
+            for (int n = 0; n < N; ++n) {
+                for (int c = 0; c < mChans; ++c) {
+                    memcpy(dst + (n + c * N) * mSmplSize,
+                           src + (n * mChans + c) * mSmplSize, mSmplSize);
+                }
+            }
+            src = dst;
+        }
+
+        // Put header in packet
+        mJackTrip->putHeaderInOutgoingPacket(mFullPacket.get(), src);
+
+        // Send packet
+        sendPacket(reinterpret_cast<char*>(mFullPacket.get()), full_packet_size);
+
+        // Increase sequence number
+        mJackTrip->increaseSequenceNumber();
+    }
+
+    // Send exit packet (reuse mFullPacket buffer to avoid allocation)
+    std::memset(mFullPacket.get(), 0xff, mControlPacketSize);
+    sendPacket(reinterpret_cast<char*>(mFullPacket.get()), mControlPacketSize);
+    sendPacket(reinterpret_cast<char*>(mFullPacket.get()),
+               mControlPacketSize);  // Send twice for redundancy
+
+    emit signalCeaseTransmission();
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::processReceivedPacket(int8_t* packet, int packet_size,
+                                               int full_packet_size)
+{
+    Q_UNUSED(full_packet_size)
+
+    // Get sequence number
+    uint16_t seq_num = mJackTrip->getPeerSequenceNumber(packet);
+
+    // Track lost packets
+    int16_t lost = 0;
+    if (!mInitialState) {
+        lost = seq_num - mLastSeqNum - 1;
+        if (lost < 0 || lost > 1000) {
+            // Out of order packet
+            ++mOutOfOrderCount;
+            return;
+        } else if (lost > 0) {
+            mLostCount += lost;
+        }
+        mTotCount += 1 + lost;
+    }
+    mInitialState = false;
+    mLastSeqNum   = seq_num;
+
+    // Extract audio and send to buffer
+    int peer_chans = mJackTrip->getPeerNumOutgoingChannels(packet);
+    int N          = mJackTrip->getPeerBufferSize(packet);
+    int hdr_size   = mJackTrip->getHeaderSizeInBytes();
+
+    // Guard: peer-supplied fields must fit within the received datagram.
+    if (hdr_size + N * peer_chans * mSmplSize > packet_size) {
+        std::cerr << "WebRtcDataProtocol: packet too small for declared audio payload;"
+                     " dropping."
+                  << std::endl;
+        return;
+    }
+
+    int host_buf_size = N * mChans * mSmplSize;
+    int gap_size      = lost * host_buf_size;
+
+    if (static_cast<int>(mBuffer.size()) < host_buf_size) {
+        mBuffer.resize(host_buf_size, 0);
+    }
+
+    int8_t* src = packet + hdr_size;
+
+    // Convert non-interleaved to interleaved if needed
+    if (mChans != 1) {
+        int8_t* dst = mBuffer.data();
+        int C       = std::min(mChans, peer_chans);
+        for (int n = 0; n < N; ++n) {
+            for (int c = 0; c < C; ++c) {
+                memcpy(dst + (n * mChans + c) * mSmplSize, src + (n + c * N) * mSmplSize,
+                       mSmplSize);
+            }
+        }
+        src = dst;
+    }
+
+    // Write to audio buffer
+    bool ok = mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size, seq_num);
+    if (!ok) {
+        emit signalError("Local and Peer buffer settings are incompatible");
+        mStopped = true;
+    }
+}
+
+//*******************************************************************************
+bool WebRtcDataProtocol::getStats(DataProtocol::PktStat* stat)
+{
+    if (mStatCount == 0) {
+        mLostCount       = 0;
+        mOutOfOrderCount = 0;
+        mRevivedCount    = 0;
+    }
+    stat->tot        = mTotCount;
+    stat->lost       = mLostCount;
+    stat->outOfOrder = mOutOfOrderCount;
+    stat->revived    = mRevivedCount;
+    stat->statCount  = mStatCount++;
+    return true;
+}
+
+//*******************************************************************************
+void WebRtcDataProtocol::printWaitedTooLong(int wait_msec)
+{
+    if (!(wait_msec % gUdpWaitTimeout)) {
+        if (wait_msec <= gUdpWaitTimeout) {
+            cerr << "WebRTC waiting too long (more than " << gUdpWaitTimeout
+                 << "ms) for data..." << endl;
+        }
+    }
+    // After gClientGoneTimeoutMs with no received packets the client is probably gone.
+    // Close the data channel so cleanup happens promptly.
+    // Mirrors slotUdpWaitingTooLongClientGoneProbably.
+    if (wait_msec >= gClientGoneTimeoutMs && mDataChannel && mDataChannel->isOpen()) {
+        cerr << "WebRTC: No packets for " << wait_msec
+             << "ms — closing data channel (client probably gone)" << endl;
+        mDataChannel->close();
+    }
+}
diff --git a/src/webrtc/WebRtcDataProtocol.h b/src/webrtc/WebRtcDataProtocol.h
new file mode 100644 (file)
index 0000000..b16109a
--- /dev/null
@@ -0,0 +1,185 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebRtcDataProtocol.h
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#ifndef __WEBRTCDATAPROTOCOL_H__
+#define __WEBRTCDATAPROTOCOL_H__
+
+#include <QScopedPointer>
+#include <QThread>
+#include <atomic>
+#include <memory>
+#include <vector>
+
+#include "../DataProtocol.h"
+#include "../jacktrip_globals.h"
+
+// Forward declarations for libdatachannel types
+namespace rtc
+{
+class DataChannel;
+class PeerConnection;
+}  // namespace rtc
+
+class JackTrip;
+
+/** \brief WebRTC Data Channel implementation of DataProtocol class
+ *
+ * This class implements audio packet transport over WebRTC data channels,
+ * providing NAT traversal capabilities while maintaining low-latency
+ * characteristics similar to UDP.
+ *
+ * The data channel is configured for unordered, unreliable delivery
+ * to minimize latency (similar to UDP behavior).
+ */
+class WebRtcDataProtocol : public DataProtocol
+{
+    Q_OBJECT;
+
+   public:
+    /** \brief The class constructor
+     * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator)
+     * \param runmode Sets the run mode, use either SENDER or RECEIVER
+     * \param dataChannel Shared pointer to the WebRTC data channel
+     */
+    WebRtcDataProtocol(JackTrip* jacktrip, const runModeT runmode,
+                       std::shared_ptr<rtc::DataChannel> dataChannel);
+
+    /** \brief The class destructor
+     */
+    virtual ~WebRtcDataProtocol();
+
+    /// \brief Stops the execution of the Thread
+    virtual void stop() override;
+
+    /** \brief Set the Peer address (no-op for WebRTC, address is in SDP)
+     * \param peerHostOrIP IPv4 number or host name
+     */
+    virtual void setPeerAddress(const char* peerHostOrIP) override;
+
+    /** \brief Set the peer port (no-op for WebRTC, port is in SDP)
+     * \param port Port number
+     */
+    virtual void setPeerPort(int port) override;
+
+    /** \brief Set socket (no-op for WebRTC)
+     */
+#if defined(_WIN32)
+    virtual void setSocket(SOCKET& socket) override;
+#else
+    virtual void setSocket(int& socket) override;
+#endif
+
+    /** \brief Sends a packet over the data channel
+     *
+     * \param buf Buffer containing the packet to send
+     * \param n size of packet
+     * \return number of bytes sent, -1 on error
+     */
+    int sendPacket(const char* buf, const size_t n);
+
+    /** \brief Implements the Thread Loop
+     *
+     * This function runs the send or receive loop depending on the run mode.
+     */
+    virtual void run() override;
+
+    /** \brief Check if the data channel is open
+     * \return true if open, false otherwise
+     */
+    bool isChannelOpen() const;
+
+    /** \brief Get packet statistics
+     */
+    virtual bool getStats(PktStat* stat) override;
+
+   signals:
+    /// \brief Signal emitted when data channel is connected
+    void signalDataChannelConnected();
+
+    /// \brief Signal emitted when data channel is disconnected
+    void signalDataChannelDisconnected();
+
+    /// \brief Signal emitted when waiting too long for data
+    void signalWaitingTooLong(int wait_msec);
+
+   private slots:
+    void printWaitedTooLong(int wait_msec);
+
+   private:
+    // Called by data channel callbacks
+    void onDataChannelOpen();
+    void onDataChannelClosed();
+    void onDataChannelMessage(const std::vector<std::byte>& data);
+    void onDataChannelError(const std::string& error);
+
+    // Process control packets (e.g., exit signal)
+    void processControlPacket(const char* buf, size_t size);
+
+    // Main loop implementations
+    void runReceiver(int full_packet_size);
+    void runSender(int full_packet_size);
+    void processReceivedPacket(int8_t* packet, int packet_size, int full_packet_size);
+
+    std::shared_ptr<rtc::DataChannel> mDataChannel;
+    const runModeT mRunMode;
+
+    // Audio packet buffers
+    QScopedPointer<int8_t> mAudioPacket;
+    QScopedPointer<int8_t> mFullPacket;
+    std::vector<int8_t> mBuffer;
+    int mChans;
+    int mSmplSize;
+
+    // Statistics
+    std::atomic<uint32_t> mTotCount;
+    std::atomic<uint32_t> mLostCount;
+    std::atomic<uint32_t> mOutOfOrderCount;
+    std::atomic<uint32_t> mRevivedCount;
+    uint32_t mStatCount;
+
+    // State tracking
+    std::atomic<bool> mChannelOpen;
+    std::atomic<int> mTimeSinceLastPacket;  // milliseconds since last packet received
+    uint8_t mControlPacketSize;
+    bool mStopSignalSent;
+
+    // Sequence number tracking
+    uint16_t mLastSeqNum;
+    bool mInitialState;
+};
+
+#endif  // __WEBRTCDATAPROTOCOL_H__
diff --git a/src/webrtc/WebRtcPeerConnection.cpp b/src/webrtc/WebRtcPeerConnection.cpp
new file mode 100644 (file)
index 0000000..569dfac
--- /dev/null
@@ -0,0 +1,527 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebRtcPeerConnection.cpp
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#include "WebRtcPeerConnection.h"
+
+#include <QPointer>
+#include <QSslSocket>
+#include <iostream>
+
+#include "WebRtcSignalingProtocol.h"
+#include "WebSocketSignalingConnection.h"
+
+// Include libdatachannel headers
+// Note: Requires libdatachannel to be installed
+// https://github.com/paullouisageneau/libdatachannel
+#include <rtc/rtc.hpp>
+
+using std::cerr;
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+WebRtcPeerConnection::WebRtcPeerConnection(const QStringList& iceServers,
+                                           uint16_t portRangeBegin, uint16_t portRangeEnd,
+                                           QObject* parent)
+    : QObject(parent)
+    , mSignalingConnection(nullptr)
+    , mIceServers(iceServers)
+    , mPortRangeBegin(portRangeBegin)
+    , mPortRangeEnd(portRangeEnd)
+    , mState(STATE_NEW)
+    , mIsOfferer(false)
+    , mRemoteDescriptionSet(false)
+{
+    initPeerConnection();
+}
+
+//*******************************************************************************
+WebRtcPeerConnection::WebRtcPeerConnection(QSslSocket* signalingSocket,
+                                           const QStringList& iceServers,
+                                           uint16_t portRangeBegin, uint16_t portRangeEnd,
+                                           QObject* parent)
+    : QObject(parent)
+    , mSignalingConnection(nullptr)
+    , mIceServers(iceServers)
+    , mPortRangeBegin(portRangeBegin)
+    , mPortRangeEnd(portRangeEnd)
+    , mState(STATE_NEW)
+    , mIsOfferer(false)
+    , mRemoteDescriptionSet(false)
+{
+    initPeerConnection();
+
+    // Create and configure the signaling connection
+    if (signalingSocket) {
+        // Capture peer address from the signaling socket
+        mPeerAddress = signalingSocket->peerAddress().toString();
+
+        mSignalingConnection =
+            new WebSocketSignalingConnection(signalingSocket, -1, this);
+
+        // Connect signaling messages to our handler
+        connect(mSignalingConnection,
+                &WebSocketSignalingConnection::signalingMessageReceived, this,
+                &WebRtcPeerConnection::onSignalingMessageReceived);
+
+        connect(mSignalingConnection, &WebSocketSignalingConnection::connectionClosed,
+                this, &WebRtcPeerConnection::onSignalingConnectionClosed);
+
+        connect(mSignalingConnection, &WebSocketSignalingConnection::error, this,
+                [this](const QString& errorMsg) {
+                    cerr << "WebRTC signaling error: " << errorMsg.toStdString() << endl;
+                    emit connectionFailed(errorMsg);
+                });
+
+        // Connect our local description/candidate signals to send via signaling
+        connect(this, &WebRtcPeerConnection::localDescriptionReady, this,
+                [this](const QString& sdp, const QString& type) {
+                    if (!mSignalingConnection || !mSignalingConnection->isOpen()) {
+                        return;
+                    }
+                    QByteArray msg;
+                    if (type == QStringLiteral("answer")) {
+                        msg = WebRtcSignalingProtocol::createAnswer(sdp);
+                    } else {
+                        msg = WebRtcSignalingProtocol::createOffer(sdp);
+                    }
+                    mSignalingConnection->sendMessage(msg);
+                });
+
+        connect(this, &WebRtcPeerConnection::localCandidateReady, this,
+                [this](const QString& candidate, const QString& sdpMid) {
+                    if (!mSignalingConnection || !mSignalingConnection->isOpen()) {
+                        return;
+                    }
+                    auto msg =
+                        WebRtcSignalingProtocol::createIceCandidate(candidate, sdpMid, 0);
+                    mSignalingConnection->sendMessage(msg);
+                });
+    }
+}
+
+//*******************************************************************************
+WebRtcPeerConnection::~WebRtcPeerConnection()
+{
+    close();
+}
+
+//*******************************************************************************
+void WebRtcPeerConnection::initPeerConnection()
+{
+    try {
+        // Configure the peer connection
+        rtc::Configuration config;
+
+        // Add STUN/TURN servers
+        if (mIceServers.isEmpty()) {
+            // Default to Google's public STUN server
+            config.iceServers.emplace_back("stun:stun.l.google.com:19302");
+        } else {
+            for (const QString& server : mIceServers) {
+                config.iceServers.emplace_back(server.toStdString());
+            }
+        }
+
+        // Restrict ICE UDP port range if configured (useful when only specific
+        // ports are allowed through firewall rules)
+        if (mPortRangeBegin != 0 && mPortRangeEnd != 0) {
+            config.portRangeBegin = mPortRangeBegin;
+            config.portRangeEnd   = mPortRangeEnd;
+        }
+
+        // Create the peer connection
+        mPeerConnection = std::make_shared<rtc::PeerConnection>(config);
+
+        setupPeerConnectionCallbacks();
+
+    } catch (const std::exception& e) {
+        cerr << "Failed to create peer connection: " << e.what() << endl;
+        setState(STATE_FAILED);
+        emit connectionFailed(QString::fromStdString(e.what()));
+    }
+}
+
+//*******************************************************************************
+void WebRtcPeerConnection::setupPeerConnectionCallbacks()
+{
+    if (!mPeerConnection) {
+        return;
+    }
+
+    // Local description generated
+    mPeerConnection->onLocalDescription([this](rtc::Description description) {
+        this->mLocalDescription = QString::fromStdString(std::string(description));
+        QString type            = (description.type() == rtc::Description::Type::Offer)
+                                      ? QStringLiteral("offer")
+                                      : QStringLiteral("answer");
+        emit this->localDescriptionReady(this->mLocalDescription, type);
+    });
+
+    // Local ICE candidate generated
+    mPeerConnection->onLocalCandidate([this](rtc::Candidate candidate) {
+        QString candidateStr = QString::fromStdString(std::string(candidate));
+        QString mid          = QString::fromStdString(candidate.mid());
+        emit this->localCandidateReady(candidateStr, mid);
+    });
+
+    // ICE gathering state change
+    mPeerConnection->onGatheringStateChange(
+        [this](rtc::PeerConnection::GatheringState state) {
+            if (state == rtc::PeerConnection::GatheringState::Complete) {
+                emit this->gatheringComplete();
+            }
+        });
+
+    // Connection state change
+    mPeerConnection->onStateChange([this](rtc::PeerConnection::State state) {
+        switch (state) {
+        case rtc::PeerConnection::State::New:
+            this->setState(STATE_NEW);
+            break;
+        case rtc::PeerConnection::State::Connecting:
+            this->setState(STATE_CONNECTING);
+            break;
+        case rtc::PeerConnection::State::Connected:
+            this->setState(STATE_CONNECTED);
+            break;
+        case rtc::PeerConnection::State::Disconnected:
+        case rtc::PeerConnection::State::Closed:
+            this->setState(STATE_DISCONNECTED);
+            break;
+        case rtc::PeerConnection::State::Failed:
+            this->setState(STATE_FAILED);
+            emit this->connectionFailed(QStringLiteral("ICE connection failed"));
+            break;
+        }
+    });
+
+    // Data channel created by remote peer
+    mPeerConnection->onDataChannel([this](std::shared_ptr<rtc::DataChannel> channel) {
+        this->mDataChannel = channel;
+        this->setupDataChannelCallbacks(channel);
+    });
+}
+
+//*******************************************************************************
+void WebRtcPeerConnection::setupDataChannelCallbacks(
+    std::shared_ptr<rtc::DataChannel> channel)
+{
+    if (!channel) {
+        return;
+    }
+
+    channel->onOpen([this]() {
+        emit this->dataChannelOpen();
+    });
+
+    channel->onClosed([this]() {
+        emit this->dataChannelClosed();
+    });
+
+    channel->onError([this](std::string error) {
+        cerr << "Data channel error: " << error << endl;
+        emit this->connectionFailed(QString::fromStdString(error));
+    });
+
+    channel->onMessage([this](rtc::message_variant data) {
+        // Handle binary data
+        if (std::holds_alternative<rtc::binary>(data)) {
+            const rtc::binary& binary = std::get<rtc::binary>(data);
+            emit this->dataReceived(binary);
+        }
+    });
+}
+
+//*******************************************************************************
+std::shared_ptr<rtc::DataChannel> WebRtcPeerConnection::createAudioDataChannel(
+    const QString& label)
+{
+    if (!mPeerConnection) {
+        return nullptr;
+    }
+
+    try {
+        // Configure data channel for low-latency audio
+        rtc::DataChannelInit dcInit;
+        dcInit.reliability.unordered = true;  // Don't wait for in-order delivery
+
+        // Configure for unreliable delivery (like UDP)
+        // Option 1: Max retransmits = 0 (no retries)
+        dcInit.reliability.maxRetransmits = 0;
+
+        // Option 2: Max packet lifetime (alternative)
+        // dcInit.reliability.maxPacketLifeTime = std::chrono::milliseconds(50);
+
+        auto channel = mPeerConnection->createDataChannel(label.toStdString(), dcInit);
+        setupDataChannelCallbacks(channel);
+
+        return channel;
+
+    } catch (const std::exception& e) {
+        cerr << "Failed to create data channel: " << e.what() << endl;
+        return nullptr;
+    }
+}
+
+//*******************************************************************************
+bool WebRtcPeerConnection::setRemoteOffer(const QString& sdp)
+{
+    if (!mPeerConnection) {
+        return false;
+    }
+
+    try {
+        mIsOfferer = false;
+
+        rtc::Description description(sdp.toStdString(), rtc::Description::Type::Offer);
+        mPeerConnection->setRemoteDescription(description);
+        mRemoteDescriptionSet = true;
+
+        // Flush any candidates that arrived before the offer (trickle ICE buffering).
+        // Some clients (e.g. Safari) send ICE candidates before the SDP offer.
+        flushPendingCandidates();
+
+        return true;
+
+    } catch (const std::exception& e) {
+        cerr << "Failed to set remote offer: " << e.what() << endl;
+        emit connectionFailed(QString::fromStdString(e.what()));
+        return false;
+    }
+}
+
+//*******************************************************************************
+QString WebRtcPeerConnection::getLocalAnswer() const
+{
+    return mLocalDescription;
+}
+
+//*******************************************************************************
+bool WebRtcPeerConnection::addRemoteCandidate(const QString& candidate,
+                                              const QString& sdpMid)
+{
+    if (!mPeerConnection) {
+        return false;
+    }
+
+    // Queue the candidate if setRemoteDescription hasn't been called yet.
+    // Safari (and some other implementations) send ICE candidates before the
+    // SDP offer via trickle ICE, so we must buffer them and apply them once
+    // the remote description is set.
+    if (!mRemoteDescriptionSet) {
+        mPendingCandidates.append(qMakePair(candidate, sdpMid));
+        return true;
+    }
+
+    return applyRemoteCandidate(candidate, sdpMid);
+}
+
+//*******************************************************************************
+bool WebRtcPeerConnection::applyRemoteCandidate(const QString& candidate,
+                                                const QString& sdpMid)
+{
+    try {
+        rtc::Candidate cand(candidate.toStdString(), sdpMid.toStdString());
+        mPeerConnection->addRemoteCandidate(cand);
+        return true;
+
+    } catch (const std::exception& e) {
+        cerr << "Failed to add remote candidate: " << e.what() << endl;
+        return false;
+    }
+}
+
+//*******************************************************************************
+void WebRtcPeerConnection::flushPendingCandidates()
+{
+    for (const auto& pending : mPendingCandidates) {
+        applyRemoteCandidate(pending.first, pending.second);
+    }
+    mPendingCandidates.clear();
+}
+
+//*******************************************************************************
+bool WebRtcPeerConnection::createOffer(const QString& channelLabel)
+{
+    if (!mPeerConnection) {
+        return false;
+    }
+
+    try {
+        mIsOfferer = true;
+
+        // Create the data channel first
+        mDataChannel = createAudioDataChannel(channelLabel);
+        if (!mDataChannel) {
+            return false;
+        }
+
+        // Creating a data channel triggers description generation
+        // The offer will be delivered via onLocalDescription callback
+
+        return true;
+
+    } catch (const std::exception& e) {
+        cerr << "Failed to create offer: " << e.what() << endl;
+        emit connectionFailed(QString::fromStdString(e.what()));
+        return false;
+    }
+}
+
+//*******************************************************************************
+bool WebRtcPeerConnection::setRemoteAnswer(const QString& sdp)
+{
+    if (!mPeerConnection) {
+        return false;
+    }
+
+    try {
+        rtc::Description description(sdp.toStdString(), rtc::Description::Type::Answer);
+        mPeerConnection->setRemoteDescription(description);
+        return true;
+
+    } catch (const std::exception& e) {
+        cerr << "Failed to set remote answer: " << e.what() << endl;
+        emit connectionFailed(QString::fromStdString(e.what()));
+        return false;
+    }
+}
+
+//*******************************************************************************
+std::shared_ptr<rtc::DataChannel> WebRtcPeerConnection::getDataChannel() const
+{
+    return mDataChannel;
+}
+
+//*******************************************************************************
+bool WebRtcPeerConnection::isDataChannelOpen() const
+{
+    if (!mDataChannel) {
+        return false;
+    }
+    return mDataChannel->isOpen();
+}
+
+//*******************************************************************************
+QString WebRtcPeerConnection::getPeerAddress() const
+{
+    return mPeerAddress;
+}
+
+//*******************************************************************************
+QString WebRtcPeerConnection::getClientName() const
+{
+    if (mSignalingConnection) {
+        return mSignalingConnection->getClientName();
+    }
+    return QString();
+}
+
+//*******************************************************************************
+void WebRtcPeerConnection::close()
+{
+    if (mSignalingConnection) {
+        mSignalingConnection->close();
+        mSignalingConnection->deleteLater();
+        mSignalingConnection = nullptr;
+    }
+
+    if (mDataChannel) {
+        mDataChannel->close();
+        mDataChannel.reset();
+    }
+
+    if (mPeerConnection) {
+        mPeerConnection->close();
+        mPeerConnection.reset();
+    }
+
+    setState(STATE_DISCONNECTED);
+}
+
+//*******************************************************************************
+void WebRtcPeerConnection::onSignalingMessageReceived(
+    const WebRtcSignalingProtocol::SignalingMessage& msg)
+{
+    if (!mPeerConnection) {
+        return;
+    }
+
+    switch (msg.type) {
+    case WebRtcSignalingProtocol::OFFER:
+        if (!setRemoteOffer(msg.sdp)) {
+            auto errorMsg = WebRtcSignalingProtocol::createError(
+                QStringLiteral("Failed to process offer"));
+            if (mSignalingConnection) {
+                mSignalingConnection->sendMessage(errorMsg);
+            }
+        }
+        break;
+
+    case WebRtcSignalingProtocol::ANSWER:
+        setRemoteAnswer(msg.sdp);
+        break;
+
+    case WebRtcSignalingProtocol::ICE_CANDIDATE:
+        addRemoteCandidate(msg.candidate, msg.sdpMid);
+        break;
+
+    case WebRtcSignalingProtocol::HANGUP:
+        close();
+        break;
+
+    default:
+        break;
+    }
+}
+
+//*******************************************************************************
+void WebRtcPeerConnection::onSignalingConnectionClosed()
+{
+    // Note: We don't close the peer connection here because the data channel
+    // may still be active. The signaling connection is ephemeral.
+}
+
+//*******************************************************************************
+void WebRtcPeerConnection::setState(ConnectionState state)
+{
+    if (mState != state) {
+        mState = state;
+        emit stateChanged(state);
+    }
+}
diff --git a/src/webrtc/WebRtcPeerConnection.h b/src/webrtc/WebRtcPeerConnection.h
new file mode 100644 (file)
index 0000000..0d9283f
--- /dev/null
@@ -0,0 +1,285 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebRtcPeerConnection.h
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#ifndef __WEBRTCPEERCONNECTION_H__
+#define __WEBRTCPEERCONNECTION_H__
+
+#include <QList>
+#include <QObject>
+#include <QPair>
+#include <QString>
+#include <QStringList>
+#include <memory>
+#include <vector>
+
+#include "WebRtcSignalingProtocol.h"
+
+class QSslSocket;
+class WebSocketSignalingConnection;
+
+// Forward declarations for libdatachannel types
+namespace rtc
+{
+class PeerConnection;
+class DataChannel;
+struct Configuration;
+}  // namespace rtc
+
+/** \brief WebRTC Peer Connection wrapper
+ *
+ * This class wraps a WebRTC peer connection and manages the data channel
+ * for audio transport. It handles ICE negotiation and provides a simple
+ * interface for the JackTrip hub server.
+ *
+ * The data channel is configured for low-latency audio:
+ * - Unordered delivery (no head-of-line blocking)
+ * - Unreliable (no retransmissions, like UDP)
+ */
+class WebRtcPeerConnection : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief Connection state enumeration
+    enum ConnectionState {
+        STATE_NEW,           ///< Initial state
+        STATE_CONNECTING,    ///< ICE/DTLS in progress
+        STATE_CONNECTED,     ///< Data channel open
+        STATE_DISCONNECTED,  ///< Connection closed
+        STATE_FAILED         ///< Connection failed
+    };
+    Q_ENUM(ConnectionState)
+
+    /** \brief Constructor
+     * \param iceServers List of STUN/TURN server URLs
+     * \param portRangeBegin First UDP port for ICE candidates (0 = system default)
+     * \param portRangeEnd Last UDP port for ICE candidates (0 = system default)
+     * \param parent QObject parent
+     */
+    explicit WebRtcPeerConnection(const QStringList& iceServers = QStringList(),
+                                  uint16_t portRangeBegin = 0, uint16_t portRangeEnd = 0,
+                                  QObject* parent = nullptr);
+
+    /** \brief Constructor with signaling socket
+     * \param signalingSocket The SSL socket for WebSocket signaling (ownership
+     * transferred)
+     * \param iceServers List of STUN/TURN server URLs
+     * \param portRangeBegin First UDP port for ICE candidates (0 = system default)
+     * \param portRangeEnd Last UDP port for ICE candidates (0 = system default)
+     * \param parent QObject parent
+     */
+    explicit WebRtcPeerConnection(QSslSocket* signalingSocket,
+                                  const QStringList& iceServers = QStringList(),
+                                  uint16_t portRangeBegin = 0, uint16_t portRangeEnd = 0,
+                                  QObject* parent = nullptr);
+
+    /** \brief Destructor
+     */
+    virtual ~WebRtcPeerConnection();
+
+    //--------------------------------------------------------------------------
+    // Connection setup (server-side flow)
+    //--------------------------------------------------------------------------
+
+    /** \brief Set remote SDP offer from client and create answer
+     *
+     * Call this when the server receives an SDP offer from a client.
+     * This will set the remote description and generate a local answer.
+     *
+     * \param sdp The SDP offer from the client
+     * \return true on success, false on error
+     */
+    bool setRemoteOffer(const QString& sdp);
+
+    /** \brief Get the local SDP answer
+     *
+     * After calling setRemoteOffer(), this returns the generated answer.
+     *
+     * \return The local SDP answer, or empty string if not available
+     */
+    QString getLocalAnswer() const;
+
+    /** \brief Add a remote ICE candidate
+     *
+     * Called when an ICE candidate is received from the client.
+     *
+     * \param candidate The ICE candidate string
+     * \param sdpMid The SDP media ID
+     * \return true on success, false on error
+     */
+    bool addRemoteCandidate(const QString& candidate, const QString& sdpMid);
+
+    //--------------------------------------------------------------------------
+    // Connection setup (client-side flow)
+    //--------------------------------------------------------------------------
+
+    /** \brief Create an SDP offer (for client-side use)
+     *
+     * Creates a data channel and generates an SDP offer.
+     * The offer will be available via localDescriptionReady signal.
+     *
+     * \param channelLabel Label for the data channel
+     * \return true on success, false on error
+     */
+    bool createOffer(const QString& channelLabel = QStringLiteral("audio"));
+
+    /** \brief Set remote SDP answer from server
+     *
+     * Call this when the client receives an SDP answer from the server.
+     *
+     * \param sdp The SDP answer from the server
+     * \return true on success, false on error
+     */
+    bool setRemoteAnswer(const QString& sdp);
+
+    //--------------------------------------------------------------------------
+    // Data channel access
+    //--------------------------------------------------------------------------
+
+    /** \brief Get the data channel
+     *
+     * \return Shared pointer to the data channel, or nullptr if not available
+     */
+    std::shared_ptr<rtc::DataChannel> getDataChannel() const;
+
+    /** \brief Check if the data channel is open
+     * \return true if open and ready for data
+     */
+    bool isDataChannelOpen() const;
+
+    //--------------------------------------------------------------------------
+    // State and status
+    //--------------------------------------------------------------------------
+
+    /** \brief Get current connection state
+     * \return The current state
+     */
+    ConnectionState getState() const { return mState; }
+
+    /** \brief Get peer address (from ICE)
+     * \return Remote peer IP address, or empty if not connected
+     */
+    QString getPeerAddress() const;
+
+    /** \brief Get client name from WebSocket URL
+     * \return Client name from URL query parameter, or empty if not provided
+     */
+    QString getClientName() const;
+
+    /** \brief Close the connection
+     */
+    void close();
+
+   signals:
+    /// \brief Emitted when local SDP description is ready (offer or answer)
+    void localDescriptionReady(const QString& sdp, const QString& type);
+
+    /// \brief Emitted when a local ICE candidate is available
+    void localCandidateReady(const QString& candidate, const QString& sdpMid);
+
+    /// \brief Emitted when ICE gathering is complete
+    void gatheringComplete();
+
+    /// \brief Emitted when the data channel opens
+    void dataChannelOpen();
+
+    /// \brief Emitted when the data channel closes
+    void dataChannelClosed();
+
+    /// \brief Emitted when data is received on the data channel
+    void dataReceived(const std::vector<std::byte>& data);
+
+    /// \brief Emitted when connection state changes
+    void stateChanged(ConnectionState state);
+
+    /// \brief Emitted on connection failure
+    void connectionFailed(const QString& reason);
+
+   private slots:
+    /// \brief Handle incoming signaling messages from WebSocket connection
+    void onSignalingMessageReceived(const WebRtcSignalingProtocol::SignalingMessage& msg);
+
+    /// \brief Handle signaling connection closed
+    void onSignalingConnectionClosed();
+
+   private:
+    // Initialize the peer connection with configuration
+    void initPeerConnection();
+
+    // Setup callbacks for the peer connection
+    void setupPeerConnectionCallbacks();
+
+    // Setup callbacks for a data channel
+    void setupDataChannelCallbacks(std::shared_ptr<rtc::DataChannel> channel);
+
+    // Create data channel with appropriate settings for audio
+    std::shared_ptr<rtc::DataChannel> createAudioDataChannel(const QString& label);
+
+    // Apply a single remote candidate (requires remote description to be set)
+    bool applyRemoteCandidate(const QString& candidate, const QString& sdpMid);
+
+    // Flush candidates that were queued before the remote description was set
+    void flushPendingCandidates();
+
+    // State management
+    void setState(ConnectionState state);
+
+    // WebSocket signaling connection (owned, may be null)
+    WebSocketSignalingConnection* mSignalingConnection = nullptr;
+
+    // Configuration
+    QStringList mIceServers;
+    uint16_t mPortRangeBegin = 0;
+    uint16_t mPortRangeEnd   = 0;
+    std::unique_ptr<rtc::Configuration> mConfig;
+
+    // libdatachannel objects
+    std::shared_ptr<rtc::PeerConnection> mPeerConnection;
+    std::shared_ptr<rtc::DataChannel> mDataChannel;
+
+    // State
+    ConnectionState mState;
+    QString mLocalDescription;
+    QString mPeerAddress;
+    bool mIsOfferer;             // true if we created the offer
+    bool mRemoteDescriptionSet;  // true after setRemoteDescription has been called
+
+    // Candidates received before setRemoteDescription was called (trickle ICE buffering)
+    QList<QPair<QString, QString>> mPendingCandidates;
+};
+
+#endif  // __WEBRTCPEERCONNECTION_H__
diff --git a/src/webrtc/WebRtcSignalingProtocol.cpp b/src/webrtc/WebRtcSignalingProtocol.cpp
new file mode 100644 (file)
index 0000000..688d055
--- /dev/null
@@ -0,0 +1,339 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebRtcSignalingProtocol.cpp
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#include "WebRtcSignalingProtocol.h"
+
+#include <QDataStream>
+#include <QIODevice>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QtEndian>
+#include <iostream>
+
+//*******************************************************************************
+WebRtcSignalingProtocol::WebRtcSignalingProtocol(QObject* parent) : QObject(parent) {}
+
+//*******************************************************************************
+WebRtcSignalingProtocol::~WebRtcSignalingProtocol() {}
+
+//*******************************************************************************
+QByteArray WebRtcSignalingProtocol::encodeMessage(const SignalingMessage& msg)
+{
+    QJsonObject json;
+    json[QStringLiteral("version")] = PROTOCOL_VERSION;
+
+    switch (msg.type) {
+    case PROTOCOL_DETECT:
+        json[QStringLiteral("type")] = QStringLiteral("protocol");
+        if (msg.protocol == PROTOCOL_UDP) {
+            json[QStringLiteral("protocol")] = QStringLiteral("udp");
+        } else if (msg.protocol == PROTOCOL_WEBRTC) {
+            json[QStringLiteral("protocol")] = QStringLiteral("webrtc");
+        }
+        if (!msg.clientName.isEmpty()) {
+            json[QStringLiteral("client_name")] = msg.clientName;
+        }
+        if (msg.udpPort > 0) {
+            json[QStringLiteral("udp_port")] = msg.udpPort;
+        }
+        break;
+
+    case OFFER:
+        json[QStringLiteral("type")] = QStringLiteral("offer");
+        json[QStringLiteral("sdp")]  = msg.sdp;
+        break;
+
+    case ANSWER:
+        json[QStringLiteral("type")] = QStringLiteral("answer");
+        json[QStringLiteral("sdp")]  = msg.sdp;
+        break;
+
+    case ICE_CANDIDATE:
+        json[QStringLiteral("type")]          = QStringLiteral("ice");
+        json[QStringLiteral("candidate")]     = msg.candidate;
+        json[QStringLiteral("sdpMid")]        = msg.sdpMid;
+        json[QStringLiteral("sdpMLineIndex")] = msg.sdpMLineIndex;
+        break;
+
+    case HANGUP:
+        json[QStringLiteral("type")] = QStringLiteral("hangup");
+        break;
+
+    case ERROR_MSG:
+        json[QStringLiteral("type")]    = QStringLiteral("error");
+        json[QStringLiteral("message")] = msg.errorMessage;
+        break;
+
+    default:
+        // Unknown type, return empty
+        return QByteArray();
+    }
+
+    QJsonDocument doc(json);
+    return doc.toJson(QJsonDocument::Compact);
+}
+
+//*******************************************************************************
+WebRtcSignalingProtocol::SignalingMessage WebRtcSignalingProtocol::decodeMessage(
+    const QByteArray& data)
+{
+    SignalingMessage msg;
+
+    QJsonParseError parseError;
+    QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
+
+    if (parseError.error != QJsonParseError::NoError) {
+        std::cerr << "WebRtcSignalingProtocol: JSON parse error: "
+                  << parseError.errorString().toStdString() << std::endl;
+        return msg;
+    }
+
+    if (!doc.isObject()) {
+        std::cerr << "WebRtcSignalingProtocol: Expected JSON object" << std::endl;
+        return msg;
+    }
+
+    QJsonObject json = doc.object();
+
+    // Get version
+    msg.version = json.value(QStringLiteral("version")).toInt(0);
+
+    // Get message type
+    QString typeStr = json.value(QStringLiteral("type")).toString();
+
+    if (typeStr == QStringLiteral("protocol")) {
+        msg.type            = PROTOCOL_DETECT;
+        QString protocolStr = json.value(QStringLiteral("protocol")).toString();
+        if (protocolStr == QStringLiteral("udp")) {
+            msg.protocol = PROTOCOL_UDP;
+        } else if (protocolStr == QStringLiteral("webrtc")) {
+            msg.protocol = PROTOCOL_WEBRTC;
+        }
+        msg.clientName = json.value(QStringLiteral("client_name")).toString();
+        msg.udpPort    = json.value(QStringLiteral("udp_port")).toInt(0);
+
+    } else if (typeStr == QStringLiteral("offer")) {
+        msg.type = OFFER;
+        msg.sdp  = json.value(QStringLiteral("sdp")).toString();
+
+    } else if (typeStr == QStringLiteral("answer")) {
+        msg.type = ANSWER;
+        msg.sdp  = json.value(QStringLiteral("sdp")).toString();
+
+    } else if (typeStr == QStringLiteral("ice")) {
+        msg.type          = ICE_CANDIDATE;
+        msg.candidate     = json.value(QStringLiteral("candidate")).toString();
+        msg.sdpMid        = json.value(QStringLiteral("sdpMid")).toString();
+        msg.sdpMLineIndex = json.value(QStringLiteral("sdpMLineIndex")).toInt(-1);
+
+    } else if (typeStr == QStringLiteral("hangup")) {
+        msg.type = HANGUP;
+
+    } else if (typeStr == QStringLiteral("error")) {
+        msg.type         = ERROR_MSG;
+        msg.errorMessage = json.value(QStringLiteral("message")).toString();
+
+    } else {
+        msg.type = UNKNOWN;
+    }
+
+    return msg;
+}
+
+//*******************************************************************************
+bool WebRtcSignalingProtocol::isWebRtcSignaling(const QByteArray& data)
+{
+    // Quick checks to determine if this is JSON (WebRTC signaling)
+    // vs raw binary data (legacy UDP port number)
+
+    if (data.isEmpty()) {
+        return false;
+    }
+
+    // Legacy UDP clients send exactly 4 bytes (32-bit port number)
+    // or may send more with a client name string
+    // JSON messages always start with '{'
+
+    // Check if it starts with '{'
+    if (data.at(0) == '{') {
+        // Looks like JSON, try to parse it
+        QJsonParseError parseError;
+        QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
+
+        if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
+            // Check for expected WebRTC signaling fields
+            QJsonObject json = doc.object();
+            return json.contains(QStringLiteral("type"))
+                   || json.contains(QStringLiteral("protocol"));
+        }
+    }
+
+    return false;
+}
+
+//*******************************************************************************
+int WebRtcSignalingProtocol::readLegacyUdpPort(const QByteArray& data)
+{
+    if (data.size() < 4) {
+        return -1;
+    }
+
+    // Legacy format: 32-bit port number in network byte order
+    qint32 port;
+    QDataStream stream(data);
+    stream.setByteOrder(QDataStream::BigEndian);
+    stream >> port;
+
+    // Port values above 65535 may indicate authentication tokens
+    // (see existing UdpHubListener code)
+    return port;
+}
+
+//*******************************************************************************
+QByteArray WebRtcSignalingProtocol::createProtocolMessage(ProtocolType protocol,
+                                                          const QString& clientName)
+{
+    SignalingMessage msg;
+    msg.type       = PROTOCOL_DETECT;
+    msg.protocol   = protocol;
+    msg.clientName = clientName;
+    return encodeMessage(msg);
+}
+
+//*******************************************************************************
+QByteArray WebRtcSignalingProtocol::createOffer(const QString& sdp)
+{
+    SignalingMessage msg;
+    msg.type = OFFER;
+    msg.sdp  = sdp;
+    return encodeMessage(msg);
+}
+
+//*******************************************************************************
+QByteArray WebRtcSignalingProtocol::createAnswer(const QString& sdp)
+{
+    SignalingMessage msg;
+    msg.type = ANSWER;
+    msg.sdp  = sdp;
+    return encodeMessage(msg);
+}
+
+//*******************************************************************************
+QByteArray WebRtcSignalingProtocol::createIceCandidate(const QString& candidate,
+                                                       const QString& sdpMid,
+                                                       int sdpMLineIndex)
+{
+    SignalingMessage msg;
+    msg.type          = ICE_CANDIDATE;
+    msg.candidate     = candidate;
+    msg.sdpMid        = sdpMid;
+    msg.sdpMLineIndex = sdpMLineIndex;
+    return encodeMessage(msg);
+}
+
+//*******************************************************************************
+QByteArray WebRtcSignalingProtocol::createHangup()
+{
+    SignalingMessage msg;
+    msg.type = HANGUP;
+    return encodeMessage(msg);
+}
+
+//*******************************************************************************
+QByteArray WebRtcSignalingProtocol::createError(const QString& errorMessage)
+{
+    SignalingMessage msg;
+    msg.type         = ERROR_MSG;
+    msg.errorMessage = errorMessage;
+    return encodeMessage(msg);
+}
+
+//*******************************************************************************
+QByteArray WebRtcSignalingProtocol::frameMessage(const QByteArray& message)
+{
+    QByteArray framed;
+    QDataStream stream(&framed, QIODevice::WriteOnly);
+    stream.setByteOrder(QDataStream::BigEndian);
+
+    // Write 4-byte length prefix
+    stream << static_cast<quint32>(message.size());
+
+    // Append message data
+    framed.append(message);
+
+    return framed;
+}
+
+//*******************************************************************************
+bool WebRtcSignalingProtocol::extractFramedMessage(QByteArray& buffer,
+                                                   QByteArray& message)
+{
+    // Need at least 4 bytes for length prefix
+    if (buffer.size() < 4) {
+        return false;
+    }
+
+    // Read length prefix
+    QDataStream stream(buffer);
+    stream.setByteOrder(QDataStream::BigEndian);
+
+    quint32 length;
+    stream >> length;
+
+    // Sanity check on length (max 1MB for signaling messages)
+    if (length > 1024 * 1024) {
+        std::cerr << "WebRtcSignalingProtocol: Invalid message length: " << length
+                  << std::endl;
+        // Clear buffer to recover
+        buffer.clear();
+        return false;
+    }
+
+    // Check if we have the complete message
+    if (buffer.size() < static_cast<int>(4 + length)) {
+        return false;
+    }
+
+    // Extract the message
+    message = buffer.mid(4, static_cast<int>(length));
+
+    // Remove the processed data from buffer
+    buffer.remove(0, static_cast<int>(4 + length));
+
+    return true;
+}
diff --git a/src/webrtc/WebRtcSignalingProtocol.h b/src/webrtc/WebRtcSignalingProtocol.h
new file mode 100644 (file)
index 0000000..c98d727
--- /dev/null
@@ -0,0 +1,219 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebRtcSignalingProtocol.h
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#ifndef __WEBRTCSIGNALINGPROTOCOL_H__
+#define __WEBRTCSIGNALINGPROTOCOL_H__
+
+#include <QByteArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QObject>
+#include <QString>
+
+/** \brief WebRTC signaling protocol handler
+ *
+ * This class handles the encoding and decoding of WebRTC signaling messages
+ * used during connection setup. Messages are JSON-encoded for simplicity
+ * and compatibility with web clients.
+ *
+ * Message types:
+ * - PROTOCOL_DETECT: Initial message to detect UDP vs WebRTC client
+ * - OFFER: SDP offer from client
+ * - ANSWER: SDP answer from server
+ * - ICE_CANDIDATE: ICE candidate (bidirectional)
+ * - HANGUP: Connection termination
+ */
+class WebRtcSignalingProtocol : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief Message types for signaling protocol
+    enum MessageType {
+        UNKNOWN         = 0,
+        PROTOCOL_DETECT = 1,  ///< Initial protocol detection message
+        OFFER           = 2,  ///< SDP offer
+        ANSWER          = 3,  ///< SDP answer
+        ICE_CANDIDATE   = 4,  ///< ICE candidate
+        HANGUP          = 5,  ///< Connection termination
+        ERROR_MSG       = 6   ///< Error message
+    };
+
+    /// \brief Client protocol types
+    enum ProtocolType {
+        PROTOCOL_UNKNOWN = 0,
+        PROTOCOL_UDP     = 1,  ///< Traditional UDP client
+        PROTOCOL_WEBRTC  = 2   ///< WebRTC data channel client
+    };
+
+    /// \brief Signaling message structure
+    struct SignalingMessage {
+        MessageType type      = UNKNOWN;
+        ProtocolType protocol = PROTOCOL_UNKNOWN;
+        QString sdp;             ///< SDP content for OFFER/ANSWER
+        QString candidate;       ///< ICE candidate string
+        QString sdpMid;          ///< SDP media ID for ICE
+        int sdpMLineIndex = -1;  ///< SDP media line index for ICE
+        QString clientName;      ///< Client name for PROTOCOL_DETECT
+        int version = 0;         ///< Protocol version
+        QString errorMessage;    ///< Error message for ERROR_MSG
+        int udpPort = 0;         ///< UDP port for legacy clients
+    };
+
+    WebRtcSignalingProtocol(QObject* parent = nullptr);
+    virtual ~WebRtcSignalingProtocol();
+
+    //--------------------------------------------------------------------------
+    // Static encoding/decoding methods
+    //--------------------------------------------------------------------------
+
+    /** \brief Encode a signaling message to JSON
+     * \param msg The message to encode
+     * \return JSON-encoded byte array
+     */
+    static QByteArray encodeMessage(const SignalingMessage& msg);
+
+    /** \brief Decode a signaling message from JSON
+     * \param data The JSON data to decode
+     * \return Decoded message structure
+     */
+    static SignalingMessage decodeMessage(const QByteArray& data);
+
+    /** \brief Check if data looks like a WebRTC signaling message (JSON)
+     *
+     * Legacy UDP clients send a raw 32-bit port number.
+     * WebRTC clients send JSON with a "protocol" field.
+     *
+     * \param data The initial data received from client
+     * \return true if data appears to be WebRTC signaling
+     */
+    static bool isWebRtcSignaling(const QByteArray& data);
+
+    /** \brief Read legacy UDP port from client data
+     * \param data The data received from legacy client
+     * \return Port number, or -1 on error
+     */
+    static int readLegacyUdpPort(const QByteArray& data);
+
+    //--------------------------------------------------------------------------
+    // Convenience methods for creating specific message types
+    //--------------------------------------------------------------------------
+
+    /** \brief Create a protocol detection message
+     * \param protocol The protocol type (UDP or WEBRTC)
+     * \param clientName Optional client name
+     * \return Encoded message
+     */
+    static QByteArray createProtocolMessage(ProtocolType protocol,
+                                            const QString& clientName = QString());
+
+    /** \brief Create an SDP offer message
+     * \param sdp The SDP offer content
+     * \return Encoded message
+     */
+    static QByteArray createOffer(const QString& sdp);
+
+    /** \brief Create an SDP answer message
+     * \param sdp The SDP answer content
+     * \return Encoded message
+     */
+    static QByteArray createAnswer(const QString& sdp);
+
+    /** \brief Create an ICE candidate message
+     * \param candidate The ICE candidate string
+     * \param sdpMid The SDP media ID
+     * \param sdpMLineIndex The SDP media line index
+     * \return Encoded message
+     */
+    static QByteArray createIceCandidate(const QString& candidate, const QString& sdpMid,
+                                         int sdpMLineIndex);
+
+    /** \brief Create a hangup message
+     * \return Encoded message
+     */
+    static QByteArray createHangup();
+
+    /** \brief Create an error message
+     * \param errorMessage The error description
+     * \return Encoded message
+     */
+    static QByteArray createError(const QString& errorMessage);
+
+    //--------------------------------------------------------------------------
+    // Message length framing for TCP transport
+    //--------------------------------------------------------------------------
+
+    /** \brief Prefix a message with its length for TCP framing
+     *
+     * Format: [4-byte length][message data]
+     *
+     * \param message The message to frame
+     * \return Framed message with length prefix
+     */
+    static QByteArray frameMessage(const QByteArray& message);
+
+    /** \brief Extract a framed message from a buffer
+     *
+     * \param buffer The input buffer (will be modified to remove extracted data)
+     * \param message Output: the extracted message (if complete)
+     * \return true if a complete message was extracted, false if more data needed
+     */
+    static bool extractFramedMessage(QByteArray& buffer, QByteArray& message);
+
+   signals:
+    /// \brief Emitted when an SDP offer is received
+    void offerReceived(const QString& sdp);
+
+    /// \brief Emitted when an SDP answer is received
+    void answerReceived(const QString& sdp);
+
+    /// \brief Emitted when an ICE candidate is received
+    void iceCandidateReceived(const QString& candidate, const QString& sdpMid,
+                              int sdpMLineIndex);
+
+    /// \brief Emitted when hangup is received
+    void hangupReceived();
+
+    /// \brief Emitted when an error message is received
+    void errorReceived(const QString& errorMessage);
+
+   private:
+    // Protocol version
+    static const int PROTOCOL_VERSION = 1;
+};
+
+#endif  // __WEBRTCSIGNALINGPROTOCOL_H__
diff --git a/src/webrtc/WebSocketSignalingConnection.cpp b/src/webrtc/WebSocketSignalingConnection.cpp
new file mode 100644 (file)
index 0000000..ddcbaef
--- /dev/null
@@ -0,0 +1,309 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebSocketSignalingConnection.cpp
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#include "WebSocketSignalingConnection.h"
+
+#include <QCryptographicHash>
+#include <QUrl>
+#include <QUrlQuery>
+#include <QtEndian>
+#include <iostream>
+
+using std::cerr;
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+WebSocketSignalingConnection::WebSocketSignalingConnection(QSslSocket* socket,
+                                                           int workerId, QObject* parent)
+    : QObject(parent), mSocket(socket), mWorkerId(workerId), mUpgradeComplete(false)
+{
+    if (mSocket) {
+        mSocket->setParent(this);
+        connect(mSocket, &QIODevice::readyRead, this,
+                &WebSocketSignalingConnection::onReadyRead);
+        connect(mSocket, &QAbstractSocket::disconnected, this,
+                &WebSocketSignalingConnection::onDisconnected);
+
+        // If there's already data available on the socket, process it now
+        // (The readyRead signal won't fire again for data that was already in the buffer)
+        if (mSocket->bytesAvailable() > 0) {
+            onReadyRead();
+        }
+    }
+}
+
+//*******************************************************************************
+WebSocketSignalingConnection::~WebSocketSignalingConnection()
+{
+    close();
+}
+
+//*******************************************************************************
+bool WebSocketSignalingConnection::isOpen() const
+{
+    return mSocket && mSocket->isOpen();
+}
+
+//*******************************************************************************
+void WebSocketSignalingConnection::sendMessage(const QByteArray& message)
+{
+    if (!isOpen() || !mUpgradeComplete) {
+        return;
+    }
+
+    QByteArray frame = encodeWebSocketFrame(message);
+    mSocket->write(frame);
+    mSocket->flush();
+}
+
+//*******************************************************************************
+void WebSocketSignalingConnection::close()
+{
+    if (mSocket) {
+        mSocket->close();
+        mSocket->deleteLater();
+        mSocket = nullptr;
+    }
+}
+
+//*******************************************************************************
+void WebSocketSignalingConnection::onReadyRead()
+{
+    if (!mSocket) {
+        return;
+    }
+
+    // If we haven't completed the WebSocket upgrade yet, check for it
+    if (!mUpgradeComplete) {
+        QByteArray peekData = mSocket->peek(256);
+        if (peekData.startsWith("GET")) {
+            QByteArray data = mSocket->readAll();
+            if (handleWebSocketUpgrade(data)) {
+                mUpgradeComplete = true;
+                emit upgradeComplete();
+            } else {
+                emit error(QStringLiteral("WebSocket upgrade failed"));
+                close();
+            }
+        }
+        return;
+    }
+
+    // Continue WebSocket signaling - read and decode frames
+    mBuffer.append(mSocket->readAll());
+
+    // Try to extract complete WebSocket frames
+    QByteArray payload;
+    while (decodeWebSocketFrame(mBuffer, payload)) {
+        auto msg = WebRtcSignalingProtocol::decodeMessage(payload);
+        emit signalingMessageReceived(msg);
+    }
+}
+
+//*******************************************************************************
+void WebSocketSignalingConnection::onDisconnected()
+{
+    emit connectionClosed();
+}
+
+//*******************************************************************************
+bool WebSocketSignalingConnection::handleWebSocketUpgrade(const QByteArray& data)
+{
+    // Parse HTTP headers to find Sec-WebSocket-Key and extract client name from URL
+    QString request   = QString::fromUtf8(data);
+    QStringList lines = request.split(QStringLiteral("\r\n"));
+
+    QString wsKey;
+    QString requestPath;
+
+    // Parse request line (e.g., "GET /path?name=ClientName HTTP/1.1")
+    if (!lines.isEmpty()) {
+        QStringList requestLineParts = lines[0].split(QStringLiteral(" "));
+        if (requestLineParts.size() >= 2) {
+            requestPath = requestLineParts[1];
+        }
+    }
+
+    // Extract client name from "name" query parameter in the path
+    // e.g., "/path?name=ClientName" -> extract "ClientName"
+    QUrl url(QStringLiteral("http://localhost") + requestPath);
+    QUrlQuery query(url);
+    if (query.hasQueryItem(QStringLiteral("name"))) {
+        mClientName = query.queryItemValue(QStringLiteral("name"));
+    }
+
+    for (const QString& line : lines) {
+        if (line.startsWith(QStringLiteral("Sec-WebSocket-Key:"), Qt::CaseInsensitive)) {
+            wsKey = line.mid(18).trimmed();
+            break;
+        }
+    }
+
+    if (wsKey.isEmpty()) {
+        cerr << "WebSocketSignalingConnection: Invalid WebSocket upgrade request" << endl;
+        return false;
+    }
+
+    // Calculate Sec-WebSocket-Accept per RFC 6455
+    // Accept = Base64(SHA1(Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
+    const QString magicString = QStringLiteral("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
+    QByteArray acceptData     = (wsKey + magicString).toUtf8();
+    QByteArray sha1Hash = QCryptographicHash::hash(acceptData, QCryptographicHash::Sha1);
+    QString acceptKey   = QString::fromLatin1(sha1Hash.toBase64());
+
+    // Send WebSocket upgrade response
+    QString response = QStringLiteral(
+                           "HTTP/1.1 101 Switching Protocols\r\n"
+                           "Upgrade: websocket\r\n"
+                           "Connection: Upgrade\r\n"
+                           "Sec-WebSocket-Accept: %1\r\n"
+                           "\r\n")
+                           .arg(acceptKey);
+
+    mSocket->write(response.toUtf8());
+    mSocket->flush();
+
+    return true;
+}
+
+//*******************************************************************************
+bool WebSocketSignalingConnection::decodeWebSocketFrame(QByteArray& buffer,
+                                                        QByteArray& payload)
+{
+    // WebSocket frame format:
+    // Byte 0: FIN (1 bit) + RSV (3 bits) + Opcode (4 bits)
+    // Byte 1: MASK (1 bit) + Payload length (7 bits)
+    // If length == 126: next 2 bytes are length
+    // If length == 127: next 8 bytes are length
+    // If MASK: next 4 bytes are masking key
+    // Remaining bytes: payload (XOR with mask if MASK bit set)
+
+    if (buffer.size() < 2) {
+        return false;  // Need at least 2 bytes
+    }
+
+    int pos        = 0;
+    quint8 byte0   = static_cast<quint8>(buffer[pos++]);
+    quint8 byte1   = static_cast<quint8>(buffer[pos++]);
+    bool fin       = (byte0 & 0x80) != 0;
+    quint8 opcode  = byte0 & 0x0F;
+    bool masked    = (byte1 & 0x80) != 0;
+    quint64 length = byte1 & 0x7F;
+
+    Q_UNUSED(fin)
+
+    // Handle extended length
+    if (length == 126) {
+        if (buffer.size() < pos + 2) {
+            return false;
+        }
+        length = (static_cast<quint8>(buffer[pos]) << 8)
+                 | static_cast<quint8>(buffer[pos + 1]);
+        pos += 2;
+    } else if (length == 127) {
+        if (buffer.size() < pos + 8) {
+            return false;
+        }
+        length = 0;
+        for (int i = 0; i < 8; i++) {
+            length = (length << 8) | static_cast<quint8>(buffer[pos + i]);
+        }
+        pos += 8;
+    }
+
+    // Get masking key if present
+    QByteArray mask;
+    if (masked) {
+        if (buffer.size() < pos + 4) {
+            return false;
+        }
+        mask = buffer.mid(pos, 4);
+        pos += 4;
+    }
+
+    // Check if we have the full payload
+    if (buffer.size() < pos + static_cast<int>(length)) {
+        return false;
+    }
+
+    // Extract and unmask payload
+    payload = buffer.mid(pos, static_cast<int>(length));
+    if (masked) {
+        for (int i = 0; i < payload.size(); i++) {
+            payload[i] = payload[i] ^ mask[i % 4];
+        }
+    }
+
+    // Remove processed frame from buffer
+    buffer.remove(0, pos + static_cast<int>(length));
+
+    // Handle close frame
+    if (opcode == 0x08) {
+        return false;  // Connection closing
+    }
+
+    return true;
+}
+
+//*******************************************************************************
+QByteArray WebSocketSignalingConnection::encodeWebSocketFrame(const QByteArray& payload)
+{
+    QByteArray frame;
+    int length = payload.size();
+
+    // Byte 0: FIN + text opcode (0x81)
+    frame.append(static_cast<char>(0x81));
+
+    // Length (server->client messages are not masked)
+    if (length < 126) {
+        frame.append(static_cast<char>(length));
+    } else if (length < 65536) {
+        frame.append(static_cast<char>(126));
+        frame.append(static_cast<char>((length >> 8) & 0xFF));
+        frame.append(static_cast<char>(length & 0xFF));
+    } else {
+        frame.append(static_cast<char>(127));
+        for (int i = 7; i >= 0; i--) {
+            frame.append(static_cast<char>((length >> (i * 8)) & 0xFF));
+        }
+    }
+
+    // Payload (no masking from server)
+    frame.append(payload);
+    return frame;
+}
diff --git a/src/webrtc/WebSocketSignalingConnection.h b/src/webrtc/WebSocketSignalingConnection.h
new file mode 100644 (file)
index 0000000..04f1a4a
--- /dev/null
@@ -0,0 +1,163 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebSocketSignalingConnection.h
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#ifndef __WEBSOCKETSIGNALINGCONNECTION_H__
+#define __WEBSOCKETSIGNALINGCONNECTION_H__
+
+#include <QByteArray>
+#include <QObject>
+#include <QSslSocket>
+#include <QString>
+
+#include "WebRtcSignalingProtocol.h"
+
+/** \brief WebSocket signaling connection for WebRTC
+ *
+ * This class encapsulates a WebSocket connection used for WebRTC signaling.
+ * It handles the WebSocket protocol (upgrade handshake, frame encoding/decoding)
+ * and buffers incomplete frames.
+ *
+ * The class owns the QSslSocket and manages its lifecycle.
+ */
+class WebSocketSignalingConnection : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    /** \brief Constructor
+     * \param socket The SSL socket for this connection (ownership transferred)
+     * \param workerId The worker ID associated with this connection
+     * \param parent QObject parent
+     */
+    explicit WebSocketSignalingConnection(QSslSocket* socket, int workerId = -1,
+                                          QObject* parent = nullptr);
+
+    /** \brief Destructor
+     */
+    virtual ~WebSocketSignalingConnection();
+
+    /** \brief Get the worker ID
+     * \return The worker ID, or -1 if not assigned
+     */
+    int getWorkerId() const { return mWorkerId; }
+
+    /** \brief Set the worker ID
+     * \param workerId The worker ID to assign
+     */
+    void setWorkerId(int workerId) { mWorkerId = workerId; }
+
+    /** \brief Get the underlying socket
+     * \return Pointer to the SSL socket (ownership retained by this object)
+     */
+    QSslSocket* getSocket() const { return mSocket; }
+
+    /** \brief Check if the connection is still open
+     * \return true if the socket is connected
+     */
+    bool isOpen() const;
+
+    /** \brief Get the client name from the WebSocket URL
+     * \return The client name if provided in URL query parameters, empty string otherwise
+     */
+    QString getClientName() const { return mClientName; }
+
+    /** \brief Send a WebRTC signaling message
+     * \param message The message to send (will be WebSocket-framed)
+     */
+    void sendMessage(const QByteArray& message);
+
+    /** \brief Close the connection
+     */
+    void close();
+
+   signals:
+    /** \brief Emitted when the WebSocket upgrade handshake is complete
+     */
+    void upgradeComplete();
+
+    /** \brief Emitted when a complete signaling message is received
+     * \param message The decoded signaling message
+     */
+    void signalingMessageReceived(
+        const WebRtcSignalingProtocol::SignalingMessage& message);
+
+    /** \brief Emitted when the connection is closed or fails
+     */
+    void connectionClosed();
+
+    /** \brief Emitted on error
+     * \param errorMessage Description of the error
+     */
+    void error(const QString& errorMessage);
+
+   private slots:
+    /** \brief Handle incoming data from the socket
+     */
+    void onReadyRead();
+
+    /** \brief Handle socket disconnection
+     */
+    void onDisconnected();
+
+   private:
+    /** \brief Handle WebSocket upgrade handshake
+     * \param data The HTTP request data
+     * \return true on success, false on error
+     */
+    bool handleWebSocketUpgrade(const QByteArray& data);
+
+    /** \brief Decode a WebSocket frame and extract the payload
+     * \param buffer The buffer containing WebSocket frames
+     * \param payload Output parameter for the decoded payload
+     * \return true if a complete frame was decoded, false if need more data
+     */
+    bool decodeWebSocketFrame(QByteArray& buffer, QByteArray& payload);
+
+    /** \brief Encode data as a WebSocket text frame
+     * \param payload The data to encode
+     * \return The encoded WebSocket frame
+     */
+    QByteArray encodeWebSocketFrame(const QByteArray& payload);
+
+    QSslSocket* mSocket;    ///< The SSL socket (owned by this object)
+    int mWorkerId;          ///< Associated worker ID (-1 if not assigned)
+    QByteArray mBuffer;     ///< Buffer for incomplete WebSocket frames
+    bool mUpgradeComplete;  ///< true after WebSocket upgrade handshake
+    QString mClientName;    ///< Client name from URL query parameters
+};
+
+#endif  // __WEBSOCKETSIGNALINGCONNECTION_H__
diff --git a/src/webtransport/WebTransportDataProtocol.cpp b/src/webtransport/WebTransportDataProtocol.cpp
new file mode 100644 (file)
index 0000000..79262d3
--- /dev/null
@@ -0,0 +1,652 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebTransportDataProtocol.cpp
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#include "WebTransportDataProtocol.h"
+
+#include <QThread>
+#include <algorithm>
+#include <chrono>
+#include <cstring>
+#include <iostream>
+
+#include "../JackTrip.h"
+#include "WebTransportSession.h"
+
+using std::cerr;
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+WebTransportDataProtocol::WebTransportDataProtocol(JackTrip* jacktrip,
+                                                   const runModeT runmode,
+                                                   WebTransportSession* session)
+    : DataProtocol(jacktrip, runmode, 0, 0)  // Ports not used for WebTransport
+    , mSession(session)
+    , mRunMode(runmode)
+    , mChans(0)
+    , mSmplSize(0)
+    , mTotCount(0)
+    , mLostCount(0)
+    , mOutOfOrderCount(0)
+    , mRevivedCount(0)
+    , mStatCount(0)
+    , mSessionConnected(false)
+    , mTimeSinceLastPacket(0)
+    , mControlPacketSize(63)
+    , mStopSignalSent(false)
+    , mLastSeqNum(0)
+    , mInitialState(true)
+{
+    // Note: Buffer pool is initialized in run() once we know the packet size
+    // Zero out pool entries for safety
+    for (size_t i = 0; i < BUFFER_POOL_SIZE; ++i) {
+        mBufferPool[i].buffer = nullptr;
+        mBufferPool[i].inUse.store(false, std::memory_order_relaxed);
+    }
+
+    // Connect to session signals
+    if (mSession) {
+        // For receiver mode, register direct callback for audio path
+        // This bypasses Qt signals/slots for zero overhead in the audio hot path.
+        // The callback is invoked directly from the msquic thread.
+        if (mRunMode == RECEIVER) {
+            mSession->setDatagramCallback([this](const uint8_t* data, size_t len) {
+                this->onDatagramReceived(data, len);
+            });
+        }
+
+        // Connect to session closed signal (non-audio path, Qt signal is fine)
+        connect(mSession, &WebTransportSession::sessionClosed, this,
+                &WebTransportDataProtocol::onSessionClosed, Qt::QueuedConnection);
+
+        // Also stop loops when the session fails (transport-initiated shutdown sets
+        // STATE_FAILED, which prevents SHUTDOWN_COMPLETE from emitting sessionClosed)
+        connect(
+            mSession, &WebTransportSession::sessionFailed, this,
+            [this](const QString&) {
+                this->onSessionClosed();
+            },
+            Qt::QueuedConnection);
+    }
+
+    // Connect waiting too long signal
+    connect(this, &WebTransportDataProtocol::signalWaitingTooLong, this,
+            &WebTransportDataProtocol::printWaitedTooLong, Qt::QueuedConnection);
+}
+
+//*******************************************************************************
+WebTransportDataProtocol::~WebTransportDataProtocol()
+{
+    // Unregister callbacks before destruction
+    if (mSession) {
+        mSession->setDatagramCallback(nullptr);
+    }
+    if (mJackTrip) {
+        mJackTrip->setDirectSendCallback(nullptr);
+    }
+    stop();
+
+    // Clean up buffer pool
+    for (size_t i = 0; i < BUFFER_POOL_SIZE; ++i) {
+        if (mBufferPool[i].buffer) {
+            // Wait for any in-use buffers to be released (should be quick)
+            int retries = 0;
+            while (mBufferPool[i].inUse.load(std::memory_order_acquire)
+                   && retries < 100) {
+                QThread::msleep(10);
+                ++retries;
+            }
+            delete[] mBufferPool[i].buffer;
+            mBufferPool[i].buffer = nullptr;
+        }
+    }
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::stop()
+{
+    mStopped = true;
+
+    // Wait for thread to finish
+    if (isRunning()) {
+        wait(1000);
+    }
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::setPeerAddress(const char* peerHostOrIP)
+{
+    // No-op for WebTransport - address is managed by QUIC connection
+    Q_UNUSED(peerHostOrIP)
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::setPeerPort(int port)
+{
+    // No-op for WebTransport - port is managed by QUIC connection
+    Q_UNUSED(port)
+}
+
+//*******************************************************************************
+#if defined(_WIN32)
+void WebTransportDataProtocol::setSocket(SOCKET& socket)
+{
+    Q_UNUSED(socket)
+}
+#else
+void WebTransportDataProtocol::setSocket(int& socket)
+{
+    Q_UNUSED(socket)
+}
+#endif
+
+//*******************************************************************************
+int WebTransportDataProtocol::acquirePoolBuffer()
+{
+    // Try to acquire a buffer from the pool (lock-free)
+    size_t startIndex = mNextBufferIndex.load(std::memory_order_relaxed);
+
+    for (size_t i = 0; i < BUFFER_POOL_SIZE; ++i) {
+        size_t index  = (startIndex + i) % BUFFER_POOL_SIZE;
+        bool expected = false;
+        if (mBufferPool[index].inUse.compare_exchange_strong(
+                expected, true, std::memory_order_acquire, std::memory_order_relaxed)) {
+            mNextBufferIndex.store((index + 1) % BUFFER_POOL_SIZE,
+                                   std::memory_order_relaxed);
+            return static_cast<int>(index);
+        }
+    }
+
+    return -1;  // No buffers available
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::releasePoolBuffer(int index)
+{
+    if (index < 0 || index >= static_cast<int>(BUFFER_POOL_SIZE)) {
+        return;
+    }
+    mBufferPool[index].inUse.store(false, std::memory_order_release);
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::releaseSendContext(SendContext* ctx)
+{
+    if (!ctx || !ctx->owner) {
+        return;
+    }
+
+    // Find the index of this buffer
+    for (size_t i = 0; i < BUFFER_POOL_SIZE; ++i) {
+        if (ctx->owner->mBufferPool[i].buffer == ctx->buffer) {
+            ctx->owner->releasePoolBuffer(static_cast<int>(i));
+            return;
+        }
+    }
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::sendPacketDirect(const int8_t* audioPacket,
+                                                [[maybe_unused]] int audioPacketSize)
+{
+    // Called from audio thread - must be real-time safe!
+    // Uses buffer pool to avoid heap allocations
+    // Note: This callback is only registered after pool is initialized,
+    // so we don't need to check if buffers are ready
+
+    if (!mSession || !mSession->isConnected() || !mJackTrip) {
+        return;
+    }
+
+    int fullPacketSize = mJackTrip->getSendPacketSizeInBytes();
+
+    // Acquire buffer from pool (lock-free)
+    int bufferIndex = acquirePoolBuffer();
+    if (bufferIndex < 0) {
+        // No buffers available - skip this packet
+        return;
+    }
+
+    uint8_t* buffer = mBufferPool[bufferIndex].buffer;
+
+    // Write packet directly into pool buffer
+    int8_t* fullPacket = reinterpret_cast<int8_t*>(buffer);
+    int headerSize     = mJackTrip->getHeaderSizeInBytes();
+    int8_t* audioDest  = fullPacket + headerSize;
+
+    // Convert interleaved to non-interleaved directly into the buffer
+    if (mChans > 1) {
+        int N = mJackTrip->getBufferSizeInSamples();
+        for (int n = 0; n < N; ++n) {
+            for (int c = 0; c < mChans; ++c) {
+                memcpy(audioDest + (n + c * N) * mSmplSize,
+                       audioPacket + (n * mChans + c) * mSmplSize, mSmplSize);
+            }
+        }
+    } else {
+        // Single channel - just copy directly
+        int audioSize = mJackTrip->getTotalAudioInputPacketSizeInBytes();
+        memcpy(audioDest, audioPacket, audioSize);
+    }
+
+    // Add header - audio is already in place, just write header
+    mJackTrip->putHeaderInOutgoingPacket(fullPacket, nullptr);
+
+    // Setup send context for cleanup
+    mSendContextPool[bufferIndex].buffer = buffer;
+    mSendContextPool[bufferIndex].owner  = this;
+
+    // Send the buffer - WebTransportSession will handle cleanup via callback
+    if (!mSession->sendDatagram(buffer, fullPacketSize,
+                                &mSendContextPool[bufferIndex].quicBuffer,
+                                &mSendContextPool[bufferIndex])) {
+        // Send failed - release buffer
+        releasePoolBuffer(bufferIndex);
+        return;
+    }
+
+    // Increment sequence number
+    mJackTrip->increaseSequenceNumber();
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::run()
+{
+    // Signal that the thread has started (unblocks waitForStart())
+    threadHasStarted();
+
+    // Verify pointers
+    if (!mJackTrip) {
+        cerr << "WebTransportDataProtocol: ERROR - mJackTrip is null!" << endl;
+        return;
+    }
+    if (!mSession) {
+        cerr << "WebTransportDataProtocol: ERROR - mSession is null!" << endl;
+        return;
+    }
+
+    // Setup audio packet buffer (without header)
+    size_t audio_packet_size = getAudioPacketSizeInBites();
+    mAudioPacket.reset(new int8_t[audio_packet_size]);
+    std::memset(mAudioPacket.data(), 0, audio_packet_size);
+
+    // Get full packet size and sample info
+    int full_packet_size;
+    mSmplSize = mJackTrip->getAudioBitResolution() / 8;
+
+    if (mRunMode == RECEIVER) {
+        // Store channel count for later use
+        mChans = mJackTrip->getNumOutputChannels();
+        if (mChans == 0) {
+            cerr << "WebTransportDataProtocol: ERROR - mChans is 0 for RECEIVER" << endl;
+            return;
+        }
+        full_packet_size = mJackTrip->getReceivePacketSizeInBytes();
+    } else {
+        mChans = mJackTrip->getNumInputChannels();
+        if (mChans == 0) {
+            cerr << "WebTransportDataProtocol: ERROR - mChans is 0 for SENDER" << endl;
+            return;
+        }
+        full_packet_size = mJackTrip->getSendPacketSizeInBytes();
+    }
+
+    if (full_packet_size <= 0) {
+        cerr << "WebTransportDataProtocol: ERROR - invalid packet size!" << endl;
+        return;
+    }
+
+    if (mRunMode == SENDER) {
+        // For SENDER mode: Initialize buffer pool for direct send from audio thread
+        // The entire packet (header + audio with channel conversion) is built directly
+        // in the pool buffer
+        // NOTE: Add 8 bytes for maximum QUIC varint prefix (stream ID)
+        mPoolBufferSize = static_cast<size_t>(full_packet_size) + 8;
+
+        for (size_t i = 0; i < BUFFER_POOL_SIZE; ++i) {
+            mBufferPool[i].buffer = new uint8_t[mPoolBufferSize];
+            mBufferPool[i].inUse.store(false, std::memory_order_relaxed);
+        }
+
+        // Register direct send callback AFTER pool is initialized
+        // This prevents race condition where audio callback fires before pool is ready
+        if (mJackTrip) {
+            mJackTrip->setDirectSendCallback([this](const int8_t* packet, int size) {
+                this->sendPacketDirect(packet, size);
+            });
+        }
+    } else {
+        // For RECEIVER mode: Allocate full packet buffer (with header)
+        mFullPacket.reset(new int8_t[full_packet_size]);
+        std::memset(mFullPacket.data(), 0, full_packet_size);
+
+        // Initialize header in the first packet
+        mJackTrip->putHeaderInIncomingPacket(mFullPacket.data(), mAudioPacket.data());
+
+        // Pre-allocate buffer for channel conversion if needed
+        if (mChans > 1) {
+            int max_buffer_size =
+                mJackTrip->getBufferSizeInSamples() * mChans * mSmplSize;
+            mBuffer.resize(max_buffer_size, 0);
+        }
+    }
+
+    // Wait for session to be connected
+    while (!mStopped && mSession && !mSession->isConnected()) {
+        QThread::msleep(10);
+    }
+
+    if (mSession && mSession->isConnected()) {
+        mSessionConnected = true;
+        emit signalSessionConnected();
+    }
+
+    if (mStopped) {
+        return;
+    }
+
+    // Run appropriate loop based on mode
+    if (mRunMode == RECEIVER) {
+        runReceiver(full_packet_size);
+    } else if (mRunMode == SENDER) {
+        runSender(full_packet_size);
+    }
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::runReceiver(int full_packet_size)
+{
+    Q_UNUSED(full_packet_size);
+
+    if (gVerboseFlag)
+        cout << "WebTransportDataProtocol::runReceiver starting" << endl;
+
+    // Main receive loop - packets are processed in onDatagramReceived callback
+    // This thread just monitors for timeout conditions (like WebRTC)
+    while (!mStopped && mSessionConnected) {
+        QThread::msleep(10);
+
+        // Increment time since last packet atomically
+        int timeSinceLastPacket = mTimeSinceLastPacket.fetch_add(10) + 10;
+
+        // Emit signal every gUdpWaitTimeout ms if no packets have been received
+        if (!(timeSinceLastPacket % gUdpWaitTimeout)) {
+            emit signalWaitingTooLong(timeSinceLastPacket);
+        }
+    }
+
+    // If the loop exited because mStopped was set (e.g., exit control packet) but the
+    // session is still open, close it explicitly. This fires sessionClosed on both the
+    // RECEIVER and SENDER instances, stopping the sender and triggering full cleanup.
+    // Mirrors the printWaitedTooLong path for idle timeout.
+    if (mStopped && mSession && mSession->isConnected()) {
+        mSession->close();
+    }
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::runSender(int full_packet_size)
+{
+    Q_UNUSED(full_packet_size);
+
+    if (gVerboseFlag)
+        cout << "WebTransportDataProtocol::runSender starting (direct send mode)" << endl;
+
+    // Packets are sent directly from audio thread via sendPacketDirect()
+    // This thread just monitors for stop condition
+    while (!mStopped && mSessionConnected) {
+        QThread::msleep(100);
+    }
+
+    if (gVerboseFlag)
+        cout << "WebTransportDataProtocol::runSender: Exiting" << endl;
+
+    // Send exit packets using pool buffer
+    int bufferIndex = acquirePoolBuffer();
+    if (bufferIndex >= 0) {
+        uint8_t* buffer = mBufferPool[bufferIndex].buffer;
+        std::memset(buffer, 0xFF, mControlPacketSize);
+
+        // Send twice for redundancy
+        mSendContextPool[bufferIndex].buffer = buffer;
+        mSendContextPool[bufferIndex].owner  = this;
+        mSession->sendDatagram(buffer, mControlPacketSize,
+                               &mSendContextPool[bufferIndex].quicBuffer,
+                               &mSendContextPool[bufferIndex]);
+
+        // Note: MsQuic will release the buffer via callback
+        // For the second packet, we'd need another buffer or wait, but exit packets
+        // are best-effort anyway
+    }
+
+    emit signalCeaseTransmission();
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::processReceivedPacket(int8_t* packet, int packet_size,
+                                                     int full_packet_size)
+{
+    Q_UNUSED(full_packet_size)
+
+    // Get sequence number
+    uint16_t current_seq = mJackTrip->getPeerSequenceNumber(packet);
+
+    // Track lost packets
+    int16_t lost = 0;
+    if (!mInitialState) {
+        lost = current_seq - mLastSeqNum - 1;
+        if (lost < 0 || lost > 1000) {
+            // Out of order packet
+            ++mOutOfOrderCount;
+            return;
+        } else if (lost > 0) {
+            mLostCount += lost;
+        }
+        mTotCount += 1 + lost;
+    }
+    mInitialState = false;
+    mLastSeqNum   = current_seq;
+
+    // Check peer settings on first packet
+    if (mTotCount == 1) {
+        if (!mJackTrip->checkPeerSettings(packet)) {
+            cerr << "WebTransportDataProtocol: Peer settings mismatch" << endl;
+        }
+    }
+
+    // Extract audio and send to buffer
+    int peer_chans = mJackTrip->getPeerNumOutgoingChannels(packet);
+    int N          = mJackTrip->getPeerBufferSize(packet);
+    int hdr_size   = mJackTrip->getHeaderSizeInBytes();
+
+    // Guard: peer-supplied fields must fit within the received datagram.
+    if (hdr_size + N * peer_chans * mSmplSize > packet_size) {
+        std::cerr
+            << "WebTransportDataProtocol: packet too small for declared audio payload;"
+               " dropping."
+            << std::endl;
+        return;
+    }
+
+    int host_buf_size = N * mChans * mSmplSize;
+    int gap_size      = lost * host_buf_size;
+
+    // Ensure buffer is large enough (pre-allocated in run(), but check anyway)
+    if (static_cast<int>(mBuffer.size()) < host_buf_size) {
+        mBuffer.resize(host_buf_size, 0);
+    }
+
+    // Point to audio data after header
+    int8_t* src = packet + hdr_size;
+
+    // Convert non-interleaved to interleaved if needed
+    if (mChans != 1) {
+        int8_t* dst = mBuffer.data();
+        int C       = std::min(mChans, peer_chans);
+        for (int n = 0; n < N; ++n) {
+            for (int c = 0; c < C; ++c) {
+                memcpy(dst + (n * mChans + c) * mSmplSize, src + (n + c * N) * mSmplSize,
+                       mSmplSize);
+            }
+        }
+        src = dst;
+    }
+
+    // Write to audio buffer
+    bool ok = mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size, current_seq);
+    if (!ok) {
+        emit signalError("Local and Peer buffer settings are incompatible");
+        mStopped = true;
+    }
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::processControlPacket(const char* buf, size_t size)
+{
+    if (size != static_cast<size_t>(mControlPacketSize)) {
+        return;
+    }
+
+    // Check for exit signal (all 0xFF)
+    bool isExit = true;
+    for (size_t i = 0; i < size; i++) {
+        if (static_cast<uint8_t>(buf[i]) != 0xFF) {
+            isExit = false;
+            break;
+        }
+    }
+
+    if (isExit) {
+        if (gVerboseFlag)
+            cout << "WebTransportDataProtocol: Received exit signal" << endl;
+        mStopped = true;
+    }
+}
+
+//*******************************************************************************
+bool WebTransportDataProtocol::isSessionConnected() const
+{
+    return mSession && mSession->isConnected();
+}
+
+//*******************************************************************************
+bool WebTransportDataProtocol::getStats(PktStat* stat)
+{
+    if (!stat) {
+        return false;
+    }
+
+    stat->tot        = mTotCount.load();
+    stat->lost       = mLostCount.load();
+    stat->outOfOrder = mOutOfOrderCount.load();
+    stat->revived    = mRevivedCount.load();
+    stat->statCount  = mStatCount++;
+
+    return true;
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::printWaitedTooLong(int wait_msec)
+{
+    if (gVerboseFlag) {
+        cerr << "WebTransportDataProtocol: Waited " << wait_msec << " ms for packet"
+             << endl;
+    }
+
+    // After 10 seconds of no received packets the client is probably gone.
+    // Actively close the session so cleanup happens promptly rather than waiting
+    // for the QUIC idle timeout (~30s). Mirrors slotUdpWaitingTooLongClientGoneProbably.
+    if (wait_msec >= gClientGoneTimeoutMs && mSession && mSession->isConnected()) {
+        cerr << "WebTransportDataProtocol: No packets for " << wait_msec
+             << "ms — closing session (client probably gone)" << endl;
+        mSession->close();
+    }
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::onDatagramReceived(const uint8_t* data, size_t len)
+{
+    // LOCK-FREE & ZERO-COPY: Process datagrams directly in callback (like WebRTC)
+    // This is called from the msquic callback thread
+    // No heap allocations - works directly with the provided buffer pointer
+
+    // Check if we're already stopped to prevent use-after-free
+    if (mStopped) {
+        return;
+    }
+
+    // For SENDER mode, we don't process incoming messages
+    if (mRunMode != RECEIVER) {
+        return;
+    }
+
+    // Check for control packet BEFORE resetting the idle timer so that exit packets
+    // don't appear as audio activity and suppress the printWaitedTooLong fallback.
+    if (len == static_cast<size_t>(mControlPacketSize)) {
+        processControlPacket(reinterpret_cast<const char*>(data), len);
+        return;
+    }
+
+    // Reset timeout counter atomically (audio packets only)
+    mTimeSinceLastPacket.store(0);
+
+    if (len < sizeof(DefaultHeaderStruct)) {
+        return;
+    }
+
+    // Process the packet directly and write to ring buffer (zero-copy)
+    if (len > 0 && mChans > 0) {
+        // Get full packet size (for RECEIVER, use getReceivePacketSizeInBytes)
+        int full_packet_size = mJackTrip->getReceivePacketSizeInBytes();
+
+        // Process directly from the provided buffer pointer (no copy needed)
+        processReceivedPacket(const_cast<int8_t*>(reinterpret_cast<const int8_t*>(data)),
+                              static_cast<int>(len), full_packet_size);
+    }
+}
+
+//*******************************************************************************
+void WebTransportDataProtocol::onSessionClosed()
+{
+    if (gVerboseFlag)
+        cout << "WebTransportDataProtocol: Session closed" << endl;
+    mSessionConnected = false;
+    emit signalSessionDisconnected();
+
+    // Stop the protocol
+    mStopped = true;
+}
diff --git a/src/webtransport/WebTransportDataProtocol.h b/src/webtransport/WebTransportDataProtocol.h
new file mode 100644 (file)
index 0000000..6927ef4
--- /dev/null
@@ -0,0 +1,214 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebTransportDataProtocol.h
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#ifndef __WEBTRANSPORTDATAPROTOCOL_H__
+#define __WEBTRANSPORTDATAPROTOCOL_H__
+
+#include <msquic.h>  // For QUIC_BUFFER
+
+#include <QScopedPointer>
+#include <QThread>
+#include <atomic>
+#include <condition_variable>
+#include <memory>
+#include <mutex>
+
+#include "../DataProtocol.h"
+#include "../jacktrip_globals.h"
+
+class JackTrip;
+class WebTransportSession;
+
+/** \brief WebTransport implementation of DataProtocol class using msquic
+ *
+ * This class implements audio packet transport over WebTransport using
+ * msquic for native QUIC support with unreliable datagrams (RFC 9221).
+ *
+ * QUIC datagrams provide UDP-like semantics:
+ * - No retransmissions (unreliable delivery)
+ * - No head-of-line blocking
+ * - Built-in encryption (TLS 1.3)
+ * - NAT traversal via connection migration
+ */
+class WebTransportDataProtocol : public DataProtocol
+{
+    Q_OBJECT;
+
+   public:
+    /** \brief The class constructor
+     * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator)
+     * \param runmode Sets the run mode, use either SENDER or RECEIVER
+     * \param session Pointer to the WebTransport session (not owned)
+     */
+    WebTransportDataProtocol(JackTrip* jacktrip, const runModeT runmode,
+                             WebTransportSession* session);
+
+    /** \brief The class destructor
+     */
+    virtual ~WebTransportDataProtocol();
+
+    /// \brief Stops the execution of the Thread
+    virtual void stop() override;
+
+    /** \brief Set the Peer address (no-op for WebTransport, address is in session)
+     * \param peerHostOrIP IPv4 number or host name
+     */
+    virtual void setPeerAddress(const char* peerHostOrIP) override;
+
+    /** \brief Set the peer port (no-op for WebTransport, port is in session)
+     * \param port Port number
+     */
+    virtual void setPeerPort(int port) override;
+
+    /** \brief Set socket (no-op for WebTransport)
+     */
+#if defined(_WIN32)
+    virtual void setSocket(SOCKET& socket) override;
+#else
+    virtual void setSocket(int& socket) override;
+#endif
+
+    /** \brief Implements the Thread Loop
+     *
+     * This function runs the send or receive loop depending on the run mode.
+     */
+    virtual void run() override;
+
+    /** \brief Check if the session is connected
+     * \return true if connected, false otherwise
+     */
+    bool isSessionConnected() const;
+
+    /** \brief Get packet statistics
+     */
+    virtual bool getStats(PktStat* stat) override;
+
+    //--------------------------------------------------------------------------
+    // Public types and methods for buffer pool management
+    // (must be public for MsQuic callback in WebTransportSession)
+    //--------------------------------------------------------------------------
+
+    /** \brief Context passed to MsQuic for buffer cleanup */
+    struct SendContext {
+        uint8_t* buffer;                  ///< Buffer to release
+        WebTransportDataProtocol* owner;  ///< Protocol that owns the buffer
+        QUIC_BUFFER quicBuffer;           ///< MsQuic buffer struct (must stay alive!)
+    };
+
+    /** \brief Static callback for MsQuic to release buffer
+     *
+     * Called from WebTransportSession's MsQuic callback when datagram send completes
+     */
+    static void releaseSendContext(SendContext* ctx);
+
+   signals:
+    /// \brief Signal emitted when session is connected
+    void signalSessionConnected();
+
+    /// \brief Signal emitted when session is disconnected
+    void signalSessionDisconnected();
+
+    /// \brief Signal emitted when waiting too long for data
+    void signalWaitingTooLong(int wait_msec);
+
+   private slots:
+    void printWaitedTooLong(int wait_msec);
+    void onSessionClosed();
+
+   private:
+    // Called by session datagram callback (lock-free, zero-copy)
+    void onDatagramReceived(const uint8_t* data, size_t len);
+
+    // Called from audio thread via direct send callback (real-time safe)
+    void sendPacketDirect(const int8_t* packet, int size);
+
+    // Process control packets (e.g., exit signal)
+    void processControlPacket(const char* buf, size_t size);
+
+    // Main loop implementations
+    void runReceiver(int full_packet_size);
+    void runSender(int full_packet_size);
+    void processReceivedPacket(int8_t* packet, int packet_size, int full_packet_size);
+
+    // Buffer pool management (private)
+    struct BufferPoolEntry {
+        uint8_t* buffer;  ///< Pre-allocated buffer
+        std::atomic<bool> inUse;
+    };
+
+    static constexpr size_t BUFFER_POOL_SIZE = 16;  ///< Number of buffers in pool
+    BufferPoolEntry mBufferPool[BUFFER_POOL_SIZE];
+    SendContext mSendContextPool[BUFFER_POOL_SIZE];  ///< SendContext for each buffer
+    std::atomic<size_t> mNextBufferIndex{0};
+    size_t mPoolBufferSize{
+        0};  ///< Size of each buffer in pool (set during initialization)
+
+    // Acquire a buffer from the pool (lock-free), returns index or -1
+    int acquirePoolBuffer();
+
+    // Release a buffer back to the pool by index
+    void releasePoolBuffer(int index);
+
+    WebTransportSession* mSession;  ///< WebTransport session (not owned)
+    const runModeT mRunMode;
+
+    // Audio packet buffers
+    QScopedPointer<int8_t> mAudioPacket;  ///< Raw audio data buffer (always used)
+    QScopedPointer<int8_t> mFullPacket;   ///< Full packet with header (RECEIVER only)
+    std::vector<int8_t> mBuffer;  ///< Temp buffer for channel conversion (RECEIVER only)
+    int mChans;
+    int mSmplSize;
+
+    // Statistics
+    std::atomic<uint32_t> mTotCount;
+    std::atomic<uint32_t> mLostCount;
+    std::atomic<uint32_t> mOutOfOrderCount;
+    std::atomic<uint32_t> mRevivedCount;
+    uint32_t mStatCount;
+
+    // State tracking
+    std::atomic<bool> mSessionConnected;
+    std::atomic<int> mTimeSinceLastPacket;  // milliseconds since last packet received
+    uint8_t mControlPacketSize;
+    bool mStopSignalSent;
+
+    // Sequence number tracking
+    uint16_t mLastSeqNum;
+    bool mInitialState;
+};
+
+#endif  // __WEBTRANSPORTDATAPROTOCOL_H__
diff --git a/src/webtransport/WebTransportSession.cpp b/src/webtransport/WebTransportSession.cpp
new file mode 100644 (file)
index 0000000..ccca326
--- /dev/null
@@ -0,0 +1,1184 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebTransportSession.cpp
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#include "WebTransportSession.h"
+
+#include <msquic.h>
+
+#include <QMap>
+#include <QUrl>
+#include <QUrlQuery>
+#include <cstring>
+#include <iostream>
+#include <thread>
+
+#include "../http3/Http3Protocol.h"
+#include "../jacktrip_globals.h"
+#include "WebTransportDataProtocol.h"
+
+using std::cerr;
+using std::cout;
+using std::endl;
+
+// Default maximum datagram size (conservative estimate)
+static constexpr size_t DEFAULT_MAX_DATAGRAM_SIZE = 1200;
+
+//*******************************************************************************
+// Static callback handlers for msquic
+//*******************************************************************************
+
+static QUIC_STATUS QUIC_API ConnectionCallback(HQUIC /* connection */, void* context,
+                                               QUIC_CONNECTION_EVENT* event)
+{
+    WebTransportSession* session = static_cast<WebTransportSession*>(context);
+    if (session) {
+        return session->handleConnectionEvent(event);
+    }
+    return QUIC_STATUS_INVALID_STATE;
+}
+
+static QUIC_STATUS QUIC_API StreamCallback(HQUIC stream, void* context,
+                                           QUIC_STREAM_EVENT* event)
+{
+    WebTransportSession* session = static_cast<WebTransportSession*>(context);
+    if (session) {
+        QUIC_STATUS result = session->handleStreamEvent(stream, event);
+        return result;
+    }
+    return QUIC_STATUS_INVALID_STATE;
+}
+
+// Callback for server-initiated infrastructure streams (control, QPACK)
+static QUIC_STATUS QUIC_API InfraStreamCallback(HQUIC stream, void* context,
+                                                QUIC_STREAM_EVENT* event)
+{
+    WebTransportSession* session = static_cast<WebTransportSession*>(context);
+    if (session) {
+        QUIC_STATUS result = session->handleInfraStreamEvent(stream, event);
+        return result;
+    }
+    return QUIC_STATUS_INVALID_STATE;
+}
+
+//*******************************************************************************
+WebTransportSession::WebTransportSession(const QUIC_API_TABLE* api, HQUIC connection,
+                                         const QHostAddress& peerAddress,
+                                         quint16 peerPort, QObject* parent)
+    : QObject(parent)
+    , mApi(api)
+    , mConnection(connection)
+    , mControlStream(nullptr)
+    , mQpackEncoderStream(nullptr)
+    , mQpackDecoderStream(nullptr)
+    , mConnectStream(nullptr)
+    , mConnectStreamId(0)
+    , mState(STATE_NEW)
+    , mPeerAddress(peerAddress)
+    , mPeerPort(peerPort)
+    , mSessionAccepted(false)
+    , mControlStreamReady(false)
+    , mQpackEncoderStreamReady(false)
+    , mQpackDecoderStreamReady(false)
+    , mClientSettingsReceived(false)
+    , mServerSettingsSent(false)
+    , mMaxDatagramSize(DEFAULT_MAX_DATAGRAM_SIZE)
+{
+    if (mConnection && mApi) {
+        // Set the callback context to this session
+        mApi->SetCallbackHandler(mConnection, (void*)ConnectionCallback, this);
+
+        // Enable datagram support
+        QUIC_SETTINGS settings{};
+        std::memset(&settings, 0, sizeof(settings));
+        settings.DatagramReceiveEnabled       = TRUE;
+        settings.IsSet.DatagramReceiveEnabled = TRUE;
+
+        QUIC_STATUS status = mApi->SetParam(mConnection, QUIC_PARAM_CONN_SETTINGS,
+                                            sizeof(settings), &settings);
+        if (QUIC_FAILED(status)) {
+            cerr << "WebTransportSession: Failed to enable datagram support, status: 0x"
+                 << std::hex << status << std::dec << endl;
+        } else {
+            setState(STATE_CONNECTING);
+        }
+    } else {
+        cerr << "WebTransportSession: ERROR - Connection or API is null!" << endl;
+    }
+}
+
+//*******************************************************************************
+WebTransportSession::~WebTransportSession()
+{
+    // If connection is still active, initiate shutdown and wait for it to complete
+    std::unique_lock<std::mutex> lock(mMutex);
+
+    if (mConnection && mApi && !mShutdownComplete) {
+        // Mark as disconnected so SHUTDOWN_COMPLETE callback won't emit signals
+        mState = STATE_DISCONNECTED;
+
+        mApi->ConnectionShutdown(mConnection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0);
+
+        // Wait for SHUTDOWN_COMPLETE callback (releases mMutex while waiting)
+        bool completed = false;
+        do {
+            completed = mShutdownCv.wait_for(lock, std::chrono::seconds(5), [this] {
+                return mShutdownComplete;  // Protected by mMutex (held when predicate is
+                                           // checked)
+            });
+        } while (!completed);
+    }
+
+    // Now safe to close handles - msquic is done with them (still holding mMutex)
+    if (mControlStream && mApi) {
+        mApi->StreamClose(mControlStream);
+        mControlStream = nullptr;
+    }
+
+    if (mQpackEncoderStream && mApi) {
+        mApi->StreamClose(mQpackEncoderStream);
+        mQpackEncoderStream = nullptr;
+    }
+
+    if (mQpackDecoderStream && mApi) {
+        mApi->StreamClose(mQpackDecoderStream);
+        mQpackDecoderStream = nullptr;
+    }
+
+    if (mConnection && mApi) {
+        mApi->ConnectionClose(mConnection);
+        mConnection = nullptr;
+    }
+}
+
+//*******************************************************************************
+bool WebTransportSession::processConnectRequest(const QString& path)
+{
+    std::lock_guard<std::mutex> lock(mMutex);
+
+    if (mState != STATE_CONNECTING) {
+        cerr << "WebTransportSession: Invalid state for CONNECT request (state=" << mState
+             << ", expected=" << STATE_CONNECTING << ")" << endl;
+        return false;
+    }
+
+    // Parse client name from path
+    parseClientNameFromPath(path);
+
+    // Mark session as accepted - will send response
+    mSessionAccepted = true;
+    return true;
+}
+
+//*******************************************************************************
+bool WebTransportSession::sendConnectResponse(int statusCode)
+{
+    std::lock_guard<std::mutex> lock(mMutex);
+
+    if (!mConnection || !mApi) {
+        cerr << "WebTransportSession: Cannot send response - connection or API is null"
+             << endl;
+        return false;
+    }
+
+    // Send the actual HTTP/3 response to the client
+    if (!sendHttp3Response(statusCode)) {
+        cerr << "WebTransportSession: Failed to send HTTP/3 response" << endl;
+        setState(STATE_FAILED);
+        emit sessionFailed(QStringLiteral("Failed to send HTTP/3 response"));
+        return false;
+    }
+
+    if (statusCode == 200) {
+        // Session accepted - transition to connected state
+        setState(STATE_CONNECTED);
+        emit sessionEstablished();
+        return true;
+    } else {
+        // Session rejected
+        cerr << "WebTransportSession: Rejecting session with status " << statusCode
+             << endl;
+        setState(STATE_FAILED);
+        emit sessionFailed(
+            QStringLiteral("CONNECT request rejected with status %1").arg(statusCode));
+        return false;
+    }
+}
+
+//*******************************************************************************
+bool WebTransportSession::sendDatagram(uint8_t* buffer, size_t length,
+                                       QUIC_BUFFER* quicBuf, void* owner)
+{
+    // Lock-free check - safe to call from real-time audio thread
+    if (!isConnected() || !mConnection || !mApi || !buffer || !owner || !quicBuf) {
+        return false;
+    }
+
+    // Calculate total size needed (varint prefix + payload)
+    uint64_t quarterStreamId = mConnectStreamId / 4;
+    uint8_t varintBuf[8];
+    size_t varintLen = Http3::encodeQuicVarint(quarterStreamId, varintBuf);
+    size_t totalLen  = varintLen + length;
+
+    if (totalLen > mMaxDatagramSize) {
+        return false;
+    }
+
+    // Shift data to make room for varint prefix
+    // Move from right to left to avoid overwriting
+    std::memmove(buffer + varintLen, buffer, length);
+
+    // Write varint prefix at the beginning
+    std::memcpy(buffer, varintBuf, varintLen);
+
+    // Setup QUIC buffer in the persistent context (not on stack!)
+    // This must stay alive until MsQuic's worker thread processes it
+    quicBuf->Buffer = buffer;
+    quicBuf->Length = static_cast<uint32_t>(totalLen);
+
+    // Send the datagram - pass owner as context for cleanup in callback
+    QUIC_STATUS status =
+        mApi->DatagramSend(mConnection, quicBuf, 1, QUIC_SEND_FLAG_NONE, owner);
+
+    if (QUIC_FAILED(status)) {
+        // Shift data back on failure
+        std::memmove(buffer, buffer + varintLen, length);
+        return false;
+    }
+
+    return true;
+}
+
+//*******************************************************************************
+void WebTransportSession::close()
+{
+    std::lock_guard<std::mutex> lock(mMutex);
+
+    if (mState == STATE_SHUTTING_DOWN || mState == STATE_DISCONNECTED) {
+        return;
+    }
+
+    setState(STATE_SHUTTING_DOWN);
+
+    if (mConnection && mApi && !mShutdownComplete) {
+        // Initiate graceful shutdown - SHUTDOWN_COMPLETE callback will emit sessionClosed
+        mApi->ConnectionShutdown(mConnection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0);
+    }
+}
+
+//*******************************************************************************
+void WebTransportSession::setState(SessionState state)
+{
+    if (mState != state) {
+        mState = state;
+        emit stateChanged(state);
+    }
+}
+
+//*******************************************************************************
+void WebTransportSession::parseClientNameFromPath(const QString& path)
+{
+    QUrl url(QStringLiteral("http://localhost") + path);
+    QUrlQuery query(url);
+    if (query.hasQueryItem(QStringLiteral("name"))) {
+        mClientName = query.queryItemValue(QStringLiteral("name"));
+    }
+}
+
+//*******************************************************************************
+void WebTransportSession::createInfrastructureStreams()
+{
+    // Called from QUIC_CONNECTION_EVENT_CONNECTED
+    // Create all three HTTP/3 infrastructure streams:
+    // 1. Control stream (type 0x00) - required for HTTP/3
+    // 2. QPACK encoder stream (type 0x02) - required by browsers
+    // 3. QPACK decoder stream (type 0x03) - required by browsers
+
+    if (!createInfraStream(H3_STREAM_CONTROL, &mControlStream)) {
+        cerr << "WebTransportSession: Failed to create control stream" << endl;
+        return;
+    }
+
+    if (!createInfraStream(H3_STREAM_QPACK_ENCODER, &mQpackEncoderStream)) {
+        cerr << "WebTransportSession: Failed to create QPACK encoder stream" << endl;
+        return;
+    }
+
+    if (!createInfraStream(H3_STREAM_QPACK_DECODER, &mQpackDecoderStream)) {
+        cerr << "WebTransportSession: Failed to create QPACK decoder stream" << endl;
+        return;
+    }
+}
+
+//*******************************************************************************
+bool WebTransportSession::createInfraStream(H3StreamType type, HQUIC* streamHandle)
+{
+    if (!mConnection || !mApi || !streamHandle) {
+        return false;
+    }
+
+    const char* streamNames[] = {"control", "push", "QPACK encoder", "QPACK decoder"};
+
+    // Open a unidirectional stream with our callback handler
+    // The callback will be invoked when START_COMPLETE fires
+    QUIC_STATUS status =
+        mApi->StreamOpen(mConnection, QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL,
+                         InfraStreamCallback,  // Use infrastructure stream callback
+                         this, streamHandle);
+
+    if (QUIC_FAILED(status)) {
+        cerr << "WebTransportSession: Failed to open " << streamNames[type]
+             << " stream, status: 0x" << std::hex << status << std::dec << endl;
+        return false;
+    }
+
+    // Start the stream - data will be sent when START_COMPLETE fires
+    status = mApi->StreamStart(*streamHandle, QUIC_STREAM_START_FLAG_NONE);
+    if (QUIC_FAILED(status)) {
+        cerr << "WebTransportSession: Failed to start " << streamNames[type]
+             << " stream, status: 0x" << std::hex << status << std::dec << endl;
+        mApi->StreamClose(*streamHandle);
+        *streamHandle = nullptr;
+        return false;
+    }
+
+    return true;
+}
+
+//*******************************************************************************
+void WebTransportSession::sendStreamType(HQUIC stream, H3StreamType type)
+{
+    if (!stream || !mApi) {
+        return;
+    }
+
+    // Allocate buffer that will persist until SEND_COMPLETE
+    // msquic requires the buffer to remain valid until the send completes
+    uint8_t* typeData = new uint8_t[1];
+    typeData[0]       = static_cast<uint8_t>(type);
+
+    QUIC_BUFFER* buffer = new QUIC_BUFFER;
+    buffer->Buffer      = typeData;
+    buffer->Length      = 1;
+
+    // Send the stream type byte
+    // Context is used to free the buffer in SEND_COMPLETE
+    QUIC_STATUS status = mApi->StreamSend(stream, buffer, 1, QUIC_SEND_FLAG_NONE, buffer);
+    if (QUIC_FAILED(status)) {
+        cerr << "WebTransportSession: Failed to send stream type, status: 0x" << std::hex
+             << status << std::dec << endl;
+        delete[] typeData;
+        delete buffer;
+    }
+}
+
+//*******************************************************************************
+std::vector<uint8_t> WebTransportSession::buildSettingsFrame()
+{
+    // Build HTTP/3 SETTINGS frame
+    std::vector<uint8_t> frame;
+
+    // Build SETTINGS payload first to calculate length
+    std::vector<uint8_t> settingsPayload;
+
+    // SETTINGS_QPACK_MAX_TABLE_CAPACITY (0x01) = 0 (we don't use dynamic table)
+    settingsPayload.push_back(0x01);
+    settingsPayload.push_back(0x00);
+
+    // SETTINGS_QPACK_BLOCKED_STREAMS (0x07) = 0
+    settingsPayload.push_back(0x07);
+    settingsPayload.push_back(0x00);
+
+    // SETTINGS_ENABLE_CONNECT_PROTOCOL (0x08) = 1
+    // Required for extended CONNECT (WebTransport)
+    settingsPayload.push_back(0x08);
+    settingsPayload.push_back(0x01);
+
+    // SETTINGS_H3_DATAGRAM (0x33 = 51 decimal) = 1
+    // This enables datagrams (HTTP Datagrams RFC 9297)
+    settingsPayload.push_back(0x33);
+    settingsPayload.push_back(0x01);
+
+    // SETTINGS_ENABLE_WEBTRANSPORT (0x2b603742 = 729713730 decimal) = 1
+    // Encoded as 4-byte varint: top 2 bits = 0b10 (4 byte)
+    settingsPayload.push_back(0xab);  // 0b10101011 (2-bit prefix 10 = 4 bytes)
+    settingsPayload.push_back(0x60);
+    settingsPayload.push_back(0x37);
+    settingsPayload.push_back(0x42);
+    settingsPayload.push_back(0x01);  // value = 1 (enabled)
+
+    // Frame type: 0x04 (SETTINGS)
+    frame.push_back(0x04);
+
+    // Frame length (varint)
+    size_t payloadLen = settingsPayload.size();
+    if (payloadLen < 64) {
+        frame.push_back(static_cast<uint8_t>(payloadLen));
+    } else {
+        // Multi-byte varint (shouldn't happen for our small payload)
+        frame.push_back(0x40 | static_cast<uint8_t>(payloadLen >> 8));
+        frame.push_back(static_cast<uint8_t>(payloadLen & 0xFF));
+    }
+
+    // Append payload
+    frame.insert(frame.end(), settingsPayload.begin(), settingsPayload.end());
+
+    return frame;
+}
+
+//*******************************************************************************
+void WebTransportSession::sendSettingsFrame()
+{
+    // Called after receiving client's SETTINGS and our control stream is ready
+    if (mServerSettingsSent) {
+        return;
+    }
+
+    if (!mControlStream || !mApi) {
+        cerr << "WebTransportSession: Cannot send SETTINGS - control stream not available"
+             << endl;
+        return;
+    }
+
+    if (!mControlStreamReady) {
+        return;
+    }
+
+    if (!mClientSettingsReceived) {
+        return;
+    }
+
+    mServerSettingsSent = true;
+
+    // Build the SETTINGS frame
+    std::vector<uint8_t> settingsFrame = buildSettingsFrame();
+
+    // Allocate buffer that persists until SEND_COMPLETE
+    size_t dataSize    = settingsFrame.size();
+    uint8_t* frameData = new uint8_t[dataSize];
+    std::memcpy(frameData, settingsFrame.data(), dataSize);
+
+    QUIC_BUFFER* buffer = new QUIC_BUFFER;
+    buffer->Buffer      = frameData;
+    buffer->Length      = static_cast<uint32_t>(dataSize);
+
+    QUIC_STATUS status =
+        mApi->StreamSend(mControlStream, buffer, 1, QUIC_SEND_FLAG_NONE, buffer);
+    if (QUIC_FAILED(status)) {
+        cerr << "WebTransportSession: Failed to send SETTINGS, status: 0x" << std::hex
+             << status << std::dec << endl;
+        mServerSettingsSent = false;
+        delete[] frameData;
+        delete buffer;
+        return;
+    }
+}
+
+//*******************************************************************************
+std::vector<uint8_t> WebTransportSession::buildResponseFrame(int statusCode)
+{
+    // Build HTTP/3 HEADERS frame with :status response
+    // Using QPACK static table encoding
+    std::vector<uint8_t> frame;
+
+    // Build QPACK-encoded headers
+    std::vector<uint8_t> qpackPayload;
+
+    // QPACK prefix: Required Insert Count = 0, Delta Base = 0
+    qpackPayload.push_back(0x00);  // Required Insert Count (8-bit prefix)
+    qpackPayload.push_back(0x00);  // Delta Base with S=0 (7-bit prefix)
+
+    // Encode :status header using QPACK static table
+    // Format: Indexed Header Field (1T pattern, T=1 for static table)
+    // QPACK static table indices for :status:
+    //   25 = :status: 200
+    //   63 = :status: 100
+    //   64 = :status: 204
+    //   67 = :status: 400
+    //   68 = :status: 403
+    //   27 = :status: 404
+    //   71 = :status: 500
+    //   28 = :status: 503
+
+    int staticIndex = -1;
+    switch (statusCode) {
+    case 200:
+        staticIndex = 25;
+        break;
+    case 100:
+        staticIndex = 63;
+        break;
+    case 204:
+        staticIndex = 64;
+        break;
+    case 400:
+        staticIndex = 67;
+        break;
+    case 403:
+        staticIndex = 68;
+        break;
+    case 404:
+        staticIndex = 27;
+        break;
+    case 500:
+        staticIndex = 71;
+        break;
+    case 503:
+        staticIndex = 28;
+        break;
+    default:
+        // For other status codes, use literal encoding
+        staticIndex = -1;
+        break;
+    }
+
+    if (staticIndex >= 0 && staticIndex < 64) {
+        // Indexed Header Field: 1 1 index (6-bit prefix)
+        // Pattern: 11xxxxxx for static table index
+        qpackPayload.push_back(0xC0 | static_cast<uint8_t>(staticIndex));
+    } else {
+        // Literal Header Field With Name Reference
+        // Use static table index 24 for :status name (":status", "103")
+        // Then provide literal value
+        // Pattern: 0101 NNNN for static table name reference
+        qpackPayload.push_back(
+            0x5F);  // 01011111 = literal with name ref, static, index needs continuation
+        qpackPayload.push_back(0x09);  // Index 24 encoded with 4-bit prefix (24 - 15 = 9)
+
+        // Status value as literal string
+        std::string statusStr = std::to_string(statusCode);
+        qpackPayload.push_back(
+            static_cast<uint8_t>(statusStr.length()));  // Length (no huffman)
+        for (char c : statusStr) {
+            qpackPayload.push_back(static_cast<uint8_t>(c));
+        }
+    }
+
+    // Add sec-webtransport-http3-draft header for WebTransport compatibility
+    // This is a literal header with literal name and value
+    // QPACK literal with literal name format: 0010 NHLL
+    //   N = never index (1), H = huffman for name (0), LL = 3-bit length prefix
+    if (statusCode == 200) {
+        const char* headerName  = "sec-webtransport-http3-draft";
+        const char* headerValue = "draft02";
+        size_t nameLen          = strlen(headerName);
+        size_t valueLen         = strlen(headerValue);
+
+        // First byte: 0010 N H LL (N=1 never index, H=0 no huffman)
+        // 0x2? where ? encodes the length with 3-bit prefix
+        if (nameLen < 8) {
+            qpackPayload.push_back(0x20 | static_cast<uint8_t>(nameLen));
+        } else {
+            // Length >= 8, need continuation
+            qpackPayload.push_back(0x27);  // 0010 0111 = max 3-bit prefix value
+            // Continuation: nameLen - 7 encoded as varint
+            size_t remaining = nameLen - 7;
+            while (remaining >= 128) {
+                qpackPayload.push_back(0x80 | (remaining & 0x7F));
+                remaining >>= 7;
+            }
+            qpackPayload.push_back(static_cast<uint8_t>(remaining));
+        }
+
+        // Name bytes (lowercase, no huffman)
+        for (size_t i = 0; i < nameLen; i++) {
+            qpackPayload.push_back(static_cast<uint8_t>(headerName[i]));
+        }
+
+        // Value length (7-bit prefix, H=0 no huffman)
+        if (valueLen < 128) {
+            qpackPayload.push_back(static_cast<uint8_t>(valueLen));
+        } else {
+            qpackPayload.push_back(0x7F);
+            size_t remaining = valueLen - 127;
+            while (remaining >= 128) {
+                qpackPayload.push_back(0x80 | (remaining & 0x7F));
+                remaining >>= 7;
+            }
+            qpackPayload.push_back(static_cast<uint8_t>(remaining));
+        }
+
+        // Value bytes
+        for (size_t i = 0; i < valueLen; i++) {
+            qpackPayload.push_back(static_cast<uint8_t>(headerValue[i]));
+        }
+    }
+
+    // Build the HEADERS frame
+    // Frame type: 0x01 (HEADERS)
+    frame.push_back(0x01);
+
+    // Frame length (varint)
+    size_t payloadLen = qpackPayload.size();
+    if (payloadLen < 64) {
+        frame.push_back(static_cast<uint8_t>(payloadLen));
+    } else {
+        frame.push_back(0x40 | static_cast<uint8_t>(payloadLen >> 8));
+        frame.push_back(static_cast<uint8_t>(payloadLen & 0xFF));
+    }
+
+    // Append QPACK payload
+    frame.insert(frame.end(), qpackPayload.begin(), qpackPayload.end());
+
+    return frame;
+}
+
+//*******************************************************************************
+bool WebTransportSession::sendHttp3Response(int statusCode)
+{
+    if (!mConnectStream || !mApi) {
+        cerr << "WebTransportSession: Cannot send HTTP/3 response - no CONNECT stream"
+             << endl;
+        return false;
+    }
+
+    // Build the response frame
+    std::vector<uint8_t> responseFrame = buildResponseFrame(statusCode);
+
+    // Allocate buffer that persists until SEND_COMPLETE
+    size_t dataSize    = responseFrame.size();
+    uint8_t* frameData = new uint8_t[dataSize];
+    std::memcpy(frameData, responseFrame.data(), dataSize);
+
+    QUIC_BUFFER* buffer = new QUIC_BUFFER;
+    buffer->Buffer      = frameData;
+    buffer->Length      = static_cast<uint32_t>(dataSize);
+
+    // Send the response - use the peer's CONNECT stream
+    // The buffer context is used to free memory in the stream callback
+    QUIC_STATUS status =
+        mApi->StreamSend(mConnectStream, buffer, 1, QUIC_SEND_FLAG_NONE, buffer);
+    if (QUIC_FAILED(status)) {
+        cerr << "WebTransportSession: Failed to send HTTP/3 response, status: 0x"
+             << std::hex << status << std::dec << endl;
+        delete[] frameData;
+        delete buffer;
+        return false;
+    }
+
+    return true;
+}
+
+//*******************************************************************************
+unsigned int WebTransportSession::handleConnectionEvent(void* eventPtr)
+{
+    QUIC_CONNECTION_EVENT* event = static_cast<QUIC_CONNECTION_EVENT*>(eventPtr);
+
+    switch (event->Type) {
+    case QUIC_CONNECTION_EVENT_CONNECTED:
+        if (gVerboseFlag) {
+            cout << "WebTransportSession: QUIC connection established from "
+                 << mPeerAddress.toString().toStdString() << ":" << mPeerPort << endl;
+        }
+
+        // Create HTTP/3 infrastructure streams immediately on connection
+        // This follows the libwtf pattern - create control + QPACK streams right away
+        // The streams will send their type byte when START_COMPLETE fires
+        createInfrastructureStreams();
+
+        // Connection is ready, but we wait for HTTP/3 CONNECT before declaring
+        // session established
+        break;
+
+    case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
+        // Common QUIC error codes for debugging:
+        // 0x65 (101) = Connection refused or protocol error
+        // 0x0A (10)  = No application protocol
+        // 0x01 (1)   = Internal error
+        setState(STATE_FAILED);
+        // Cleanup will happen in SHUTDOWN_COMPLETE
+        emit sessionFailed(QStringLiteral("Transport shutdown"));
+        break;
+
+    case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
+        // Peer initiated shutdown - transition to SHUTTING_DOWN and wait for
+        // SHUTDOWN_COMPLETE to emit the sessionClosed signal
+        if (mState == STATE_CONNECTED) {
+            setState(STATE_SHUTTING_DOWN);
+        }
+        break;
+
+    case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: {
+        bool shouldEmitClosed = false;
+        {
+            // Set shutdown complete flag, check/update state, all protected by mutex
+            std::lock_guard<std::mutex> lock(mMutex);
+            mShutdownComplete = true;
+
+            // Emit sessionClosed signal only if we're in SHUTTING_DOWN state
+            // (i.e., close() was called but destructor hasn't run yet)
+            // If state is DISCONNECTED, the destructor is running and we shouldn't emit
+            if (mState == STATE_SHUTTING_DOWN) {
+                setState(STATE_DISCONNECTED);
+                shouldEmitClosed = true;
+            }
+        }  // mMutex released here
+
+        // Notify outside the lock (standard pattern - avoids waking waiting thread while
+        // we hold lock)
+        mShutdownCv.notify_all();
+
+        // Emit signal outside the lock to avoid holding mutex during potentially slow
+        // signal/slot calls
+        if (shouldEmitClosed) {
+            emit sessionClosed();
+        }
+
+        break;
+    }
+
+    case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED:
+        // A new stream was started by the peer
+        // This could be the HTTP/3 control stream or the CONNECT stream
+        if (mApi) {
+            mApi->SetCallbackHandler(event->PEER_STREAM_STARTED.Stream,
+                                     (void*)StreamCallback, this);
+        }
+        break;
+
+    case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED: {
+        // Received a QUIC datagram - strip the quarter stream ID prefix
+        const QUIC_BUFFER* buffer = event->DATAGRAM_RECEIVED.Buffer;
+        if (buffer && buffer->Length > 0) {
+            const uint8_t* data = buffer->Buffer;
+            size_t len          = buffer->Length;
+
+            // Decode QUIC varint to get prefix length
+            size_t varintLen = 0;
+            if (len > 0) {
+                uint8_t firstByte = data[0];
+                uint8_t prefix    = firstByte >> 6;
+                varintLen         = 1 << prefix;  // 1, 2, 4, or 8 bytes
+            }
+
+            // Skip the quarter stream ID prefix and deliver directly (zero-copy)
+            if (len > varintLen) {
+                const uint8_t* payload = data + varintLen;
+                size_t payloadLen      = len - varintLen;
+
+                // Direct callback invocation (audio hot path, no Qt overhead)
+                if (mDatagramCallback) {
+                    mDatagramCallback(payload, payloadLen);
+                }
+            }
+        }
+        break;
+    }
+
+    case QUIC_CONNECTION_EVENT_DATAGRAM_STATE_CHANGED:
+        // Update maximum datagram size based on path MTU
+        if (event->DATAGRAM_STATE_CHANGED.SendEnabled) {
+            mMaxDatagramSize = event->DATAGRAM_STATE_CHANGED.MaxSendLength;
+        } else {
+            cerr << "WebTransportSession: WARNING - Datagrams NOT enabled by peer!"
+                 << endl;
+        }
+        break;
+
+    case QUIC_CONNECTION_EVENT_DATAGRAM_SEND_STATE_CHANGED:
+        // Datagram send state changed - only release buffer on final states
+        // States: 0=Sent, 1=LostSuspect, 2=LostDiscarded, 3=Acknowledged,
+        //         4=AcknowledgedSpurious, 5=Canceled
+        // Final states are >= 2 (LostDiscarded, Acknowledged, AcknowledgedSpurious,
+        // Canceled)
+        if (event->DATAGRAM_SEND_STATE_CHANGED.State
+            >= QUIC_DATAGRAM_SEND_LOST_DISCARDED) {
+            void* ctx = event->DATAGRAM_SEND_STATE_CHANGED.ClientContext;
+            if (ctx && reinterpret_cast<uintptr_t>(ctx) > 0x10000) {
+                // This is a SendContext from the pool - release it
+                WebTransportDataProtocol::releaseSendContext(
+                    static_cast<WebTransportDataProtocol::SendContext*>(ctx));
+            }
+        }
+        break;
+
+    case QUIC_CONNECTION_EVENT_IDEAL_PROCESSOR_CHANGED:
+        // CPU affinity hint - can be safely ignored
+        break;
+
+    case QUIC_CONNECTION_EVENT_STREAMS_AVAILABLE:
+        if (gVerboseFlag) {
+            cout << "WebTransportSession: STREAMS_AVAILABLE - Bidi: "
+                 << event->STREAMS_AVAILABLE.BidirectionalCount
+                 << ", Unidi: " << event->STREAMS_AVAILABLE.UnidirectionalCount << endl;
+        }
+        break;
+
+    case QUIC_CONNECTION_EVENT_PEER_NEEDS_STREAMS:
+        if (gVerboseFlag) {
+            cout << "WebTransportSession: PEER_NEEDS_STREAMS - Bidi: "
+                 << (event->PEER_NEEDS_STREAMS.Bidirectional ? "yes" : "no") << endl;
+        }
+        break;
+
+    case QUIC_CONNECTION_EVENT_RESUMED:
+        if (gVerboseFlag) {
+            cout << "WebTransportSession: CONNECTION_RESUMED" << endl;
+        }
+        break;
+
+    case QUIC_CONNECTION_EVENT_RESUMPTION_TICKET_RECEIVED:
+        if (gVerboseFlag) {
+            cout << "WebTransportSession: RESUMPTION_TICKET_RECEIVED" << endl;
+        }
+        break;
+
+    case QUIC_CONNECTION_EVENT_PEER_CERTIFICATE_RECEIVED:
+        if (gVerboseFlag) {
+            cout << "WebTransportSession: PEER_CERTIFICATE_RECEIVED" << endl;
+        }
+        break;
+
+    default:
+        if (gVerboseFlag) {
+            cout << "WebTransportSession: Connection event type: " << event->Type
+                 << " (unhandled)" << endl;
+        }
+        break;
+    }
+
+    return QUIC_STATUS_SUCCESS;
+}
+
+//*******************************************************************************
+unsigned int WebTransportSession::handleStreamEvent(HQUIC stream, void* eventPtr)
+{
+    QUIC_STREAM_EVENT* event = static_cast<QUIC_STREAM_EVENT*>(eventPtr);
+
+    switch (event->Type) {
+    case QUIC_STREAM_EVENT_START_COMPLETE:
+        break;
+
+    case QUIC_STREAM_EVENT_RECEIVE: {
+        // Received data on stream - could be HTTP/3 frames
+        // For WebTransport, we primarily use datagrams for audio
+        // but streams are used for signaling (CONNECT request)
+
+        // Check for FIN flag indicating stream closure
+        bool finReceived = (event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) != 0;
+
+        // First, determine if this is a unidirectional or bidirectional stream
+        // In QUIC: stream_id bit 1 (0x02) = 1 means unidirectional
+        uint64_t streamId     = 0;
+        uint32_t streamIdSize = sizeof(streamId);
+        bool isUnidirectional = false;
+
+        if (mApi) {
+            QUIC_STATUS status =
+                mApi->GetParam(stream, QUIC_PARAM_STREAM_ID, &streamIdSize, &streamId);
+            if (QUIC_SUCCEEDED(status)) {
+                isUnidirectional = (streamId & 0x02) != 0;
+            }
+        }
+
+        // If this is the CONNECT stream and we received FIN after being connected,
+        // treat it as a client disconnect
+        if (finReceived && stream == mConnectStream && mState == STATE_CONNECTED) {
+            if (gVerboseFlag) {
+                cout
+                    << "WebTransportSession: Client closing CONNECT stream (FIN received)"
+                    << endl;
+            }
+            // Client is disconnecting - close the connection gracefully
+            close();
+            break;
+        }
+
+        // Parse the HTTP/3 CONNECT request from the stream data
+        if (event->RECEIVE.TotalBufferLength > 0) {
+            QByteArray data;
+            for (uint32_t i = 0; i < event->RECEIVE.BufferCount; i++) {
+                data.append(
+                    reinterpret_cast<const char*>(event->RECEIVE.Buffers[i].Buffer),
+                    event->RECEIVE.Buffers[i].Length);
+            }
+
+            // Handle unidirectional streams (control, QPACK encoder/decoder)
+            // These start with a stream type byte
+            if (isUnidirectional && data.size() > 0) {
+                uint8_t streamType = static_cast<uint8_t>(data[0]);
+
+                // Check for HTTP/3 unidirectional stream types
+                // 0x00 = Control, 0x02 = QPACK Encoder, 0x03 = QPACK Decoder
+                if (streamType == 0x00 || streamType == 0x02 || streamType == 0x03) {
+                    // If this is the client's control stream, look for SETTINGS frame
+                    if (streamType == 0x00 && data.size() > 1) {
+                        // Parse SETTINGS frame after stream type byte
+                        size_t pos        = 1;  // Skip stream type byte
+                        int64_t frameType = Http3::readVarint(
+                            reinterpret_cast<const uint8_t*>(data.constData()),
+                            data.size(), pos);
+
+                        if (frameType == Http3::FRAME_SETTINGS) {
+                            // Read frame length
+                            int64_t frameLen = Http3::readVarint(
+                                reinterpret_cast<const uint8_t*>(data.constData()),
+                                data.size(), pos);
+
+                            if (frameLen >= 0) {
+                                // Mark client settings as received
+                                mClientSettingsReceived = true;
+
+                                // Now we can send our SETTINGS in response
+                                // (if control stream is ready)
+                                sendSettingsFrame();
+                            }
+                        }
+                    }
+
+                    // For infrastructure streams, don't process as CONNECT request
+                    break;
+                }
+
+                // Unknown unidirectional stream type - log and ignore
+                if (gVerboseFlag) {
+                    cout << "WebTransportSession: Unknown unidirectional stream type: 0x"
+                         << std::hex << static_cast<int>(streamType) << std::dec << endl;
+                }
+                break;
+            }
+
+            // Bidirectional stream - this should be the CONNECT request
+            // These start directly with HTTP/3 frames (no stream type byte)
+
+            // Parse HTTP/3 frame to extract QPACK payload
+            const uint8_t* qpackPayload = nullptr;
+            size_t qpackLen             = 0;
+
+            if (!Http3::parseHttp3Frame(
+                    reinterpret_cast<const uint8_t*>(data.constData()), data.size(),
+                    qpackPayload, qpackLen)) {
+                cerr << "WebTransportSession: Failed to parse HTTP/3 frame (not a "
+                        "request stream?)"
+                     << endl;
+                break;
+            }
+
+            // Decode QPACK-encoded HTTP/3 headers
+            QMap<QString, QString> headers;
+
+            if (Http3::decodeQPackHeaders(qpackPayload, qpackLen, headers)) {
+                // Check if this is a CONNECT request for WebTransport
+                QString method    = headers.value(QStringLiteral(":method"));
+                QString protocol  = headers.value(QStringLiteral(":protocol"));
+                QString path      = headers.value(QStringLiteral(":path"),
+                                                  QStringLiteral("/webtransport"));
+                QString authority = headers.value(QStringLiteral(":authority"));
+
+                if (gVerboseFlag) {
+                    cout << "WebTransportSession: Request headers:" << endl;
+                    cout << "  :method = " << method.toStdString() << endl;
+                    cout << "  :protocol = " << protocol.toStdString() << endl;
+                    cout << "  :path = " << path.toStdString() << endl;
+                    cout << "  :authority = " << authority.toStdString() << endl;
+                }
+
+                if (method == QStringLiteral("CONNECT")
+                    && protocol == QStringLiteral("webtransport")) {
+                    // Store the CONNECT stream handle and ID for sending the response
+                    // and for the quarter stream ID in datagrams
+                    mConnectStream   = stream;
+                    mConnectStreamId = streamId;
+
+                    if (processConnectRequest(path)) {
+                        sendConnectResponse(200);
+                    } else {
+                        sendConnectResponse(400);
+                    }
+                } else {
+                    cerr << "WebTransportSession: Invalid request - expected CONNECT "
+                            "with webtransport protocol"
+                         << endl;
+                    cerr << "  Got method=" << method.toStdString()
+                         << ", protocol=" << protocol.toStdString() << endl;
+                    // Store stream for error response too
+                    mConnectStream = stream;
+                    sendConnectResponse(400);
+                }
+            } else {
+                cerr << "WebTransportSession: Failed to decode QPACK headers" << endl;
+            }
+        }
+        break;
+    }
+
+    case QUIC_STREAM_EVENT_SEND_COMPLETE:
+        // Stream send completed - free the buffer we allocated
+        if (event->SEND_COMPLETE.ClientContext) {
+            QUIC_BUFFER* buffer =
+                static_cast<QUIC_BUFFER*>(event->SEND_COMPLETE.ClientContext);
+            if (buffer) {
+                delete[] buffer->Buffer;
+                delete buffer;
+            }
+        }
+        break;
+
+    case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN:
+        if (gVerboseFlag) {
+            cout << "WebTransportSession: PEER_SEND_SHUTDOWN on stream " << stream;
+            if (stream == mConnectStream)
+                cout << " (CONNECT stream)";
+            cout << endl;
+        }
+        // If the CONNECT stream is shut down by peer, treat as disconnect
+        if (stream == mConnectStream && mState == STATE_CONNECTED) {
+            if (gVerboseFlag) {
+                cout << "WebTransportSession: Client disconnected (CONNECT stream "
+                        "shutdown)"
+                     << endl;
+            }
+            close();
+        }
+        break;
+
+    case QUIC_STREAM_EVENT_PEER_SEND_ABORTED:
+        if (gVerboseFlag) {
+            cout << "WebTransportSession: PEER_SEND_ABORTED on stream " << stream
+                 << " (error code: " << event->PEER_SEND_ABORTED.ErrorCode << ")";
+            if (stream == mConnectStream)
+                cout << " (CONNECT stream)";
+            cout << endl;
+        }
+        // If the CONNECT stream is aborted by peer, treat as disconnect
+        if (stream == mConnectStream && mState == STATE_CONNECTED) {
+            if (gVerboseFlag) {
+                cout
+                    << "WebTransportSession: Client disconnected (CONNECT stream aborted)"
+                    << endl;
+            }
+            close();
+        }
+        break;
+
+    case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE:
+        if (gVerboseFlag) {
+            cout << "WebTransportSession: STREAM_SHUTDOWN_COMPLETE on stream " << stream;
+            if (stream == mConnectStream) {
+                cout << " (CONNECT stream - THIS SHOULD NOT HAPPEN DURING ACTIVE "
+                        "SESSION!)";
+            }
+            cout << endl;
+        }
+        if (mApi) {
+            mApi->StreamClose(stream);
+        }
+        break;
+
+    default:
+        if (gVerboseFlag) {
+            // Log unhandled stream events for debugging
+            cout << "WebTransportSession: Unhandled stream event type: " << event->Type
+                 << endl;
+        }
+        break;
+    }
+
+    return QUIC_STATUS_SUCCESS;
+}
+
+//*******************************************************************************
+unsigned int WebTransportSession::handleInfraStreamEvent(HQUIC stream, void* eventPtr)
+{
+    QUIC_STREAM_EVENT* event = static_cast<QUIC_STREAM_EVENT*>(eventPtr);
+
+    switch (event->Type) {
+    case QUIC_STREAM_EVENT_START_COMPLETE: {
+        // Stream is now ready - we can send data on it
+        QUIC_STATUS status = event->START_COMPLETE.Status;
+
+        if (QUIC_FAILED(status)) {
+            cerr << "WebTransportSession: Infrastructure stream start failed, status: 0x"
+                 << std::hex << status << std::dec << endl;
+            return QUIC_STATUS_SUCCESS;
+        }
+
+        // Determine which stream this is and mark it ready
+        if (stream == mControlStream) {
+            mControlStreamReady = true;
+
+            // Send stream type byte (0x00 for control)
+            sendStreamType(stream, H3_STREAM_CONTROL);
+
+            // Try to send SETTINGS if client settings already received
+            if (mClientSettingsReceived && !mServerSettingsSent) {
+                sendSettingsFrame();
+            }
+        } else if (stream == mQpackEncoderStream) {
+            mQpackEncoderStreamReady = true;
+
+            // Send stream type byte (0x02 for QPACK encoder)
+            sendStreamType(stream, H3_STREAM_QPACK_ENCODER);
+        } else if (stream == mQpackDecoderStream) {
+            mQpackDecoderStreamReady = true;
+
+            // Send stream type byte (0x03 for QPACK decoder)
+            sendStreamType(stream, H3_STREAM_QPACK_DECODER);
+        }
+        break;
+    }
+
+    case QUIC_STREAM_EVENT_SEND_COMPLETE: {
+        // Free the buffer we allocated for sending
+        if (event->SEND_COMPLETE.ClientContext) {
+            QUIC_BUFFER* buffer =
+                static_cast<QUIC_BUFFER*>(event->SEND_COMPLETE.ClientContext);
+            if (buffer) {
+                delete[] buffer->Buffer;
+                delete buffer;
+            }
+        }
+        break;
+    }
+
+    case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN:
+        break;
+
+    case QUIC_STREAM_EVENT_PEER_SEND_ABORTED:
+        cerr << "WebTransportSession: Infrastructure stream peer aborted (error: "
+             << event->PEER_SEND_ABORTED.ErrorCode << ")" << endl;
+        break;
+
+    case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE:
+        // Don't close the stream handle here - cleanup() will handle it
+        break;
+
+    default:
+        break;
+    }
+
+    return QUIC_STATUS_SUCCESS;
+}
diff --git a/src/webtransport/WebTransportSession.h b/src/webtransport/WebTransportSession.h
new file mode 100644 (file)
index 0000000..5768b5b
--- /dev/null
@@ -0,0 +1,314 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2026 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 WebTransportSession.h
+ * \author Mike Dickey + Claude AI
+ * \date 2026
+ */
+
+#ifndef __WEBTRANSPORTSESSION_H__
+#define __WEBTRANSPORTSESSION_H__
+
+#include <QByteArray>
+#include <QHostAddress>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+#include <condition_variable>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <vector>
+
+// Forward declare msquic types to avoid header dependency in header file
+struct QUIC_API_TABLE;
+struct QUIC_HANDLE;
+typedef QUIC_HANDLE* HQUIC;
+struct QUIC_BUFFER;  // Forward declare for sendDatagram parameter
+
+/** \brief WebTransport Session wrapper using msquic
+ *
+ * This class wraps a WebTransport session for audio transport using
+ * Microsoft's msquic library for native QUIC support.
+ *
+ * WebTransport provides low-latency, bidirectional communication
+ * over HTTP/3 (QUIC), with support for unreliable datagrams.
+ *
+ * For audio transport, we use QUIC datagrams (RFC 9221) which provide
+ * UDP-like semantics without head-of-line blocking.
+ *
+ * The session is established through an HTTP/3 CONNECT request
+ * with the :protocol pseudo-header set to "webtransport".
+ */
+class WebTransportSession : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief Session state enumeration
+    enum SessionState {
+        STATE_NEW,            ///< Initial state
+        STATE_CONNECTING,     ///< QUIC handshake in progress
+        STATE_CONNECTED,      ///< Session established, ready for datagrams
+        STATE_SHUTTING_DOWN,  ///< Shutdown initiated, waiting for SHUTDOWN_COMPLETE
+        STATE_DISCONNECTED,   ///< Session closed
+        STATE_FAILED          ///< Session failed
+    };
+    Q_ENUM(SessionState)
+
+    /** \brief Constructor for server-side session
+     *
+     * Creates a session from an incoming QUIC connection.
+     *
+     * \param api The msquic API table
+     * \param connection The QUIC connection handle (ownership transferred)
+     * \param peerAddress The peer's address
+     * \param peerPort The peer's port
+     * \param parent QObject parent
+     */
+    explicit WebTransportSession(const QUIC_API_TABLE* api, HQUIC connection,
+                                 const QHostAddress& peerAddress, quint16 peerPort,
+                                 QObject* parent = nullptr);
+
+    /** \brief Destructor
+     */
+    virtual ~WebTransportSession();
+
+    //--------------------------------------------------------------------------
+    // Session setup
+    //--------------------------------------------------------------------------
+
+    /** \brief Process incoming HTTP/3 CONNECT request
+     *
+     * Called when the HTTP/3 CONNECT request is received.
+     * Validates the request and establishes the WebTransport session.
+     *
+     * \param path The request path (e.g., "/webtransport?name=MyClient")
+     * \return true on success, false on error
+     */
+    bool processConnectRequest(const QString& path);
+
+    /** \brief Send HTTP/3 response to accept the session
+     *
+     * \param statusCode HTTP status code (200 for success)
+     * \return true on success
+     */
+    bool sendConnectResponse(int statusCode);
+
+    //--------------------------------------------------------------------------
+    // Data transmission
+    //--------------------------------------------------------------------------
+
+    /** \brief Send pre-filled pool buffer via QUIC (real-time safe)
+     *
+     * Sends a buffer that was previously acquired from the caller's pool.
+     * The owner pointer is used to release the buffer back to the pool via
+     * callback when MsQuic is done with it.
+     *
+     * \param buffer Pointer to pre-filled buffer
+     * \param length Length of data in buffer
+     * \param owner Pointer to WebTransportDataProtocol that owns this buffer
+     * \return true if sent successfully, false on error
+     */
+    bool sendDatagram(uint8_t* buffer, size_t length, QUIC_BUFFER* quicBuf, void* owner);
+
+    //--------------------------------------------------------------------------
+    // State and status
+    //--------------------------------------------------------------------------
+
+    /** \brief Get current session state
+     * \return The current state
+     */
+    SessionState getState() const { return mState; }
+
+    /** \brief Check if the session is connected
+     * \return true if connected and ready for datagrams
+     */
+    bool isConnected() const { return mState == STATE_CONNECTED; }
+
+    /** \brief Get peer address
+     * \return Remote peer IP address
+     */
+    QString getPeerAddress() const { return mPeerAddress.toString(); }
+
+    /** \brief Get peer port
+     * \return Remote peer port
+     */
+    quint16 getPeerPort() const { return mPeerPort; }
+
+    /** \brief Get the client name (from CONNECT request path)
+     * \return Client name, or empty if not set
+     */
+    QString getClientName() const { return mClientName; }
+
+    /** \brief Close the session
+     *
+     * Gracefully closes the QUIC connection.
+     */
+    void close();
+
+    /** \brief Get the maximum datagram size
+     *
+     * Returns the maximum size for QUIC datagrams based on path MTU.
+     *
+     * \return Maximum datagram size in bytes
+     */
+    size_t getMaxDatagramSize() const { return mMaxDatagramSize; }
+
+    //--------------------------------------------------------------------------
+    // Direct callback (bypasses Qt signals for audio path performance)
+    //--------------------------------------------------------------------------
+
+    /** \brief Callback type for datagram reception (audio hot path)
+     *
+     * This callback is invoked directly from the msquic thread when a datagram
+     * is received. No Qt signal/slot overhead.
+     *
+     * \param data Pointer to datagram payload (after quarter stream ID prefix)
+     * \param len Length of payload in bytes
+     */
+    using DatagramCallback = std::function<void(const uint8_t* data, size_t len)>;
+
+    /** \brief Register a direct callback for datagram reception
+     *
+     * This bypasses Qt signals for maximum performance in the audio path.
+     * Only one callback can be registered at a time.
+     *
+     * \param callback Function to call when datagram is received (or nullptr to
+     * unregister)
+     */
+    void setDatagramCallback(DatagramCallback callback) { mDatagramCallback = callback; }
+
+    //--------------------------------------------------------------------------
+    // msquic callbacks (called from static handlers)
+    //--------------------------------------------------------------------------
+
+    /** \brief Handle QUIC connection event
+     * \return QUIC status code
+     */
+    unsigned int handleConnectionEvent(void* event);
+
+    /** \brief Handle QUIC stream event
+     * \return QUIC status code
+     */
+    unsigned int handleStreamEvent(HQUIC stream, void* event);
+
+    /** \brief Handle infrastructure stream event (control, QPACK streams)
+     * \return QUIC status code
+     */
+    unsigned int handleInfraStreamEvent(HQUIC stream, void* event);
+
+   signals:
+    /// \brief Emitted when the session is established
+    void sessionEstablished();
+
+    /// \brief Emitted when the session closes
+    void sessionClosed();
+
+    /// \brief Emitted when session state changes
+    void stateChanged(SessionState state);
+
+    /// \brief Emitted on session failure
+    void sessionFailed(const QString& reason);
+
+   private:
+    // HTTP/3 stream types
+    enum H3StreamType {
+        H3_STREAM_CONTROL       = 0x00,
+        H3_STREAM_PUSH          = 0x01,
+        H3_STREAM_QPACK_ENCODER = 0x02,
+        H3_STREAM_QPACK_DECODER = 0x03
+    };
+
+    // State management
+    void setState(SessionState state);
+
+    // Parse client name from path query parameters
+    void parseClientNameFromPath(const QString& path);
+
+    // Create HTTP/3 infrastructure streams (control + QPACK)
+    void createInfrastructureStreams();
+
+    // Create a single infrastructure stream
+    bool createInfraStream(H3StreamType type, HQUIC* streamHandle);
+
+    // Send stream type byte on an infrastructure stream
+    void sendStreamType(HQUIC stream, H3StreamType type);
+
+    // Send HTTP/3 SETTINGS frame on control stream (called after START_COMPLETE)
+    void sendSettingsFrame();
+
+    // Build SETTINGS frame payload
+    std::vector<uint8_t> buildSettingsFrame();
+
+    // Build HTTP/3 response HEADERS frame
+    std::vector<uint8_t> buildResponseFrame(int statusCode);
+
+    // Send HTTP/3 response on CONNECT stream
+    bool sendHttp3Response(int statusCode);
+
+    // msquic handles
+    const QUIC_API_TABLE* mApi;  ///< msquic API table (not owned)
+    HQUIC mConnection;           ///< QUIC connection handle
+    HQUIC mControlStream;        ///< HTTP/3 control stream (server-initiated)
+    HQUIC mQpackEncoderStream;   ///< QPACK encoder stream
+    HQUIC mQpackDecoderStream;   ///< QPACK decoder stream
+    HQUIC mConnectStream;  ///< Client's CONNECT request stream (for sending response)
+    uint64_t mConnectStreamId;  ///< Stream ID of CONNECT stream (for quarter stream ID)
+
+    // Session state
+    SessionState mState;
+    QHostAddress mPeerAddress;
+    quint16 mPeerPort;
+    QString mClientName;
+    bool mSessionAccepted;
+
+    // HTTP/3 settings exchange state
+    bool mControlStreamReady;       ///< Control stream START_COMPLETE received
+    bool mQpackEncoderStreamReady;  ///< QPACK encoder stream ready
+    bool mQpackDecoderStreamReady;  ///< QPACK decoder stream ready
+    bool mClientSettingsReceived;   ///< Client's SETTINGS frame received
+    bool mServerSettingsSent;       ///< Our SETTINGS frame sent
+
+    // Datagram configuration
+    size_t mMaxDatagramSize;
+
+    // Direct callback for audio path (bypasses Qt signals)
+    DatagramCallback mDatagramCallback;
+
+    // Thread safety
+    mutable std::mutex mMutex;
+    std::condition_variable mShutdownCv;  ///< Signaled when SHUTDOWN_COMPLETE fires
+    bool mShutdownComplete{false};        ///< Protected by mMutex
+};
+
+#endif  // __WEBTRANSPORTSESSION_H__
diff --git a/subprojects/libdatachannel.wrap b/subprojects/libdatachannel.wrap
new file mode 100644 (file)
index 0000000..ae28a69
--- /dev/null
@@ -0,0 +1,9 @@
+[wrap-file]
+directory = libdatachannel-0.24.0
+source_url = https://files.jacktrip.org/contrib/libdatachannel-0.24.0.tar.gz
+source_filename = libdatachannel-0.24.0.tar.gz
+source_hash = 65b8ffbf50e607c33292d7a5cd929e423e58a0783c81dc8d771bc8b142837a00
+
+[provide]
+dependency_names = libdatachannel
+
diff --git a/subprojects/msquic.wrap b/subprojects/msquic.wrap
new file mode 100644 (file)
index 0000000..39f2aad
--- /dev/null
@@ -0,0 +1,10 @@
+[wrap-file]
+directory = msquic
+source_url = https://files.jacktrip.org/contrib/msquic-85d4a4a5a79a04dc3bf5252156a7a3acb6dfe322.tar.gz
+source_filename = msquic-85d4a4a5a79a04dc3bf5252156a7a3acb6dfe322.tar.gz
+source_hash = 585d79a38e1499a3a33142bbc4f243b7b82ae83eab7d16bbeac58e0db185b1ab
+patch_directory = msquic
+
+[provide]
+dependency_names = msquic
+
diff --git a/subprojects/packagefiles/msquic/CMakeLists.txt b/subprojects/packagefiles/msquic/CMakeLists.txt
new file mode 100644 (file)
index 0000000..eec4010
--- /dev/null
@@ -0,0 +1,961 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+    cmake_minimum_required(VERSION 3.20)
+else()
+    cmake_minimum_required(VERSION 3.16)
+endif()
+
+# Disable in-source builds to prevent source tree corruption.
+if("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}")
+  message(FATAL_ERROR "
+FATAL: In-source builds are not allowed.
+       You should create a separate directory for build files.
+")
+endif()
+
+message(STATUS "CMAKE Version: ${CMAKE_VERSION}")
+
+set_property(GLOBAL PROPERTY USE_FOLDERS ON)
+set(CMAKE_VS_GLOBALS "UseInternalMSUniCrtPackage=true")
+
+message(STATUS "Source Dir: ${CMAKE_CURRENT_SOURCE_DIR}")
+message(STATUS "Host System name: ${CMAKE_HOST_SYSTEM_NAME}")
+if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+    if (NOT DEFINED CMAKE_SYSTEM_VERSION)
+        set(CMAKE_SYSTEM_VERSION 10.0.26100.0 CACHE STRING INTERNAL FORCE)
+    endif()
+endif()
+
+if(POLICY CMP0091)
+    cmake_policy(SET CMP0091 NEW)
+    message(STATUS "Setting policy 0091")
+else()
+    message(WARNING "CMake version too old to support Policy 0091; CRT static linking won't work")
+endif()
+
+if (POLICY CMP0111)
+    cmake_policy(SET CMP0111 NEW)
+endif()
+
+project(msquic)
+
+# Set a default build type if none was specified
+set(default_build_type "Release")
+
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+    set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE
+        STRING "Choose the type of build." FORCE)
+    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+                 "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
+endif()
+
+message(STATUS "System name: ${CMAKE_SYSTEM_NAME}")
+message(STATUS "System version: ${CMAKE_SYSTEM_VERSION}")
+message(STATUS "Platform version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}")
+message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
+
+set(QUIC_MAJOR_VERSION 2)
+set(QUIC_FULL_VERSION 2.6.0)
+
+if (WIN32)
+    set(CX_PLATFORM "windows")
+elseif (APPLE)
+    set(CX_PLATFORM "darwin")
+elseif (UNIX)
+    set(CX_PLATFORM "linux")
+endif()
+message(STATUS "QUIC Platform: ${CX_PLATFORM}")
+
+enable_testing()
+
+if (NOT "${QUIC_TLS}" STREQUAL "")
+    message(FATAL_ERROR "QUIC_TLS is deprecated, Use QUIC_TLS_LIB instead")
+endif()
+
+# Set the default TLS method for each platform.
+if (WIN32)
+    set(QUIC_TLS_LIB "schannel" CACHE STRING "TLS Library to use")
+else()
+    set(QUIC_TLS_LIB "quictls" CACHE STRING "TLS Library to use")
+endif()
+
+option(QUIC_BUILD_TOOLS "Builds the tools code" OFF)
+option(QUIC_BUILD_TEST "Builds the test code" OFF)
+option(QUIC_BUILD_PERF "Builds the perf code" OFF)
+option(QUIC_BUILD_SHARED "Builds msquic as a dynamic library" ON)
+option(QUIC_ENABLE_LOGGING "Enables logging" OFF)
+option(QUIC_ENABLE_ALL_SANITIZERS "Enables all sanitizers" OFF)
+option(QUIC_ENABLE_ASAN "Enables only Address Sanitizer" OFF)
+option(QUIC_ENABLE_LSAN "Enables only Leak Sanitizer" OFF)
+option(QUIC_ENABLE_UBSAN "Enables only Undefined Behavior Sanitizer" OFF)
+option(QUIC_ENABLE_TSAN "Enables only Thread Sanitizer" OFF)
+option(QUIC_ENABLE_EXTRA_SANITIZERS "Enables only extra sanitizers" OFF)
+option(QUIC_SKIP_SANITIZE_SUBMODULES "Skip passing sanitizer settings to submodule build" OFF)
+option(QUIC_ENABLE_POOL_ALLOC "Enables pool allocations" ON)
+option(QUIC_STATIC_LINK_CRT "Statically links the C runtime" ON)
+option(QUIC_STATIC_LINK_PARTIAL_CRT "Statically links the compiler-specific portion of the C runtime" ON)
+option(QUIC_UWP_BUILD "Build for UWP" OFF)
+option(QUIC_GAMECORE_BUILD "Build for GameCore" OFF)
+option(QUIC_EXTERNAL_TOOLCHAIN "Enable if system libs and include paths are configured by CMake toolchain" OFF)
+option(QUIC_PGO "Enables profile guided optimizations" OFF)
+option(QUIC_LINUX_IOURING_ENABLED "Enables io_uring support" OFF)
+option(QUIC_LINUX_XDP_ENABLED "Enables XDP support" OFF)
+option(QUIC_SOURCE_LINK "Enables source linking on MSVC" ON)
+option(QUIC_EMBED_GIT_HASH "Embed git commit hash in the binary" ON)
+option(QUIC_PDBALTPATH "Enable PDBALTPATH setting on MSVC" ON)
+option(QUIC_CODE_CHECK "Run static code checkers" OFF)
+option(QUIC_OPTIMIZE_LOCAL "Optimize code for local machine architecture" OFF)
+option(QUIC_CI "CI Specific build" OFF)
+option(QUIC_SKIP_CI_CHECKS "Disable CI specific build checks" OFF)
+option(QUIC_TELEMETRY_ASSERTS "Enable telemetry asserts in release builds" OFF)
+option(QUIC_USE_SYSTEM_LIBCRYPTO "Use system libcrypto if quictls TLS" OFF)
+option(QUIC_HIGH_RES_TIMERS "Configure the system to use high resolution timers" OFF)
+option(QUIC_OFFICIAL_RELEASE "Configured the build for an official release" OFF)
+set(QUIC_FOLDER_PREFIX "" CACHE STRING "Optional prefix for source group folders when using an IDE generator")
+set(QUIC_LIBRARY_NAME "msquic" CACHE STRING "Override the output library name")
+set(QUIC_LOGGING_TYPE "" CACHE STRING "Explicitly choose logging backend (\"etw\", \"lttng\" or \"stdout\")")
+string(TOLOWER "${QUIC_LOGGING_TYPE}" QUIC_LOGGING_TYPE)
+if(NOT "${QUIC_LOGGING_TYPE}" MATCHES "^(etw|lttng|stdout|)$")
+    message(FATAL_ERROR "QUIC_LOGGING_TYPE must be \"etw\", \"lttng\" or \"stdout\", not \"${QUIC_LOGGING_TYPE}\"")
+endif()
+
+if (QUIC_ENABLE_ALL_SANITIZERS)
+    set(QUIC_SANITIZER_ACTIVE ON)
+    set(QUIC_ENABLE_ASAN ON)
+    set(QUIC_ENABLE_LSAN ON)
+    set(QUIC_ENABLE_UBSAN ON)
+    set(QUIC_ENABLE_EXTRA_SANITIZERS ON)
+elseif (QUIC_ENABLE_ASAN)
+    set(QUIC_SANITIZER_ACTIVE ON)
+elseif (QUIC_ENABLE_LSAN)
+    set(QUIC_SANITIZER_ACTIVE ON)
+elseif (QUIC_ENABLE_UBSAN)
+    set(QUIC_SANITIZER_ACTIVE ON)
+elseif (QUIC_ENABLE_TSAN)
+    set(QUIC_SANITIZER_ACTIVE ON)
+elseif (QUIC_ENABLE_EXTRA_SANITIZERS)
+    set(QUIC_SANITIZER_ACTIVE ON)
+else()
+    set(QUIC_SANITIZER_ACTIVE OFF)
+endif()
+
+if (QUIC_GAMECORE_BUILD)
+    if(${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION} VERSION_LESS "10.0.20348.0")
+        message(FATAL_ERROR "gamecore builds require Windows 10 SDK version 20348 or later.")
+    endif()
+endif()
+
+if (QUIC_UWP_BUILD OR QUIC_GAMECORE_BUILD)
+    message(STATUS "UWP And GameCore builds disable all executables, and force shared CRT")
+    set(QUIC_BUILD_TOOLS OFF)
+    set(QUIC_BUILD_TEST OFF)
+    set(QUIC_BUILD_PERF OFF)
+    set(QUIC_STATIC_LINK_CRT OFF)
+    set(QUIC_STATIC_LINK_PARTIAL_CRT OFF)
+endif()
+
+if (QUIC_STATIC_LINK_PARTIAL_CRT)
+    set(QUIC_STATIC_LINK_CRT ON)
+endif()
+
+if (NOT QUIC_BUILD_SHARED)
+    cmake_minimum_required(VERSION 3.20)
+endif()
+
+set(BUILD_SHARED_LIBS ${QUIC_BUILD_SHARED})
+
+if (QUIC_PDBALTPATH AND MSVC)
+#    Disabled in all cases because generation is broken.
+#    file(READ ${CMAKE_CURRENT_LIST_DIR}/cmake/PdbAltPath.txt PDBALTPATH)
+#    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${PDBALTPATH}")
+#    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${PDBALTPATH}")
+#    message(STATUS ${CMAKE_EXE_LINKER_FLAGS})
+endif()
+
+include(${PROJECT_SOURCE_DIR}/cmake/GitCommands.cmake)
+get_git_current_hash(${PROJECT_SOURCE_DIR} GIT_CURRENT_HASH)
+
+if(NOT GIT_CURRENT_HASH)
+    message("Failed to get git hash. Binary will not contain git hash")
+    set(QUIC_SOURCE_LINK OFF)
+    set(QUIC_EMBED_GIT_HASH OFF)
+endif()
+
+if (QUIC_SOURCE_LINK AND MSVC)
+    if ("${CMAKE_C_COMPILER_VERSION}" VERSION_GREATER_EQUAL "19.20")
+        include(${PROJECT_SOURCE_DIR}/cmake/SourceLink.cmake)
+        file(TO_NATIVE_PATH "${PROJECT_BINARY_DIR}/source_link.json" SOURCE_LINK_JSON)
+        file(TO_NATIVE_PATH "${PROJECT_SOURCE_DIR}/cmake/source_link.json.in" SOURCE_LINK_JSON_INPUT)
+        source_link(${PROJECT_SOURCE_DIR} ${SOURCE_LINK_JSON} ${SOURCE_LINK_JSON_INPUT})
+        set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} /INCREMENTAL:NO")
+        set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} /INCREMENTAL:NO")
+        set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} /INCREMENTAL:NO")
+        set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO} /INCREMENTAL:NO")
+        set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:PGOREPRO /SOURCELINK:\"${SOURCE_LINK_JSON}\"")
+        set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /FORCE:PGOREPRO /SOURCELINK:\"${SOURCE_LINK_JSON}\"")
+    else()
+        message(WARNING "Disabling SourceLink due to old version of MSVC. Please update to VS2019 or greater!")
+    endif()
+endif()
+
+set(QUIC_BUILD_DIR ${CMAKE_CURRENT_BINARY_DIR})
+set(QUIC_OUTPUT_DIR ${QUIC_BUILD_DIR}/bin/$<IF:$<CONFIG:Debug>,Debug,Release> CACHE STRING "Output directory for build artifacts")
+
+string(GENEX_STRIP ${QUIC_OUTPUT_DIR} QUIC_OUTPUT_DIR_STRIPPED)
+
+string(COMPARE EQUAL ${QUIC_OUTPUT_DIR} ${QUIC_OUTPUT_DIR_STRIPPED} QUIC_HAS_GENERATOR_OUTPUT_DIR)
+
+if (QUIC_HAS_GENERATOR_OUTPUT_DIR)
+    set(QUIC_PGO_DIR ${QUIC_OUTPUT_DIR})
+else ()
+    set(QUIC_PGO_DIR ${QUIC_BUILD_DIR}/PGO)
+endif ()
+
+set(QUIC_VER_BUILD_ID "0" CACHE STRING "The version build ID")
+set(QUIC_VER_SUFFIX "-private" CACHE STRING "The version suffix")
+
+message(STATUS "Version Build ID: ${QUIC_VER_BUILD_ID}")
+message(STATUS "Version Suffix: ${QUIC_VER_SUFFIX}")
+
+set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${QUIC_BUILD_DIR}/obj/$<IF:$<CONFIG:Debug>,Debug,Release>)
+
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${QUIC_OUTPUT_DIR})
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${QUIC_OUTPUT_DIR})
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE ${QUIC_OUTPUT_DIR})
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${QUIC_OUTPUT_DIR})
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG ${QUIC_OUTPUT_DIR})
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${QUIC_OUTPUT_DIR})
+
+set(QUIC_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/inc)
+
+include(GNUInstallDirs)
+
+if (WIN32)
+    set(QUIC_WARNING_FLAGS /WX /W4 /sdl /wd4206 CACHE INTERNAL "")
+    set(QUIC_COMMON_FLAGS "")
+
+    include(CheckCCompilerFlag)
+
+    if(NOT QUIC_SANITIZER_ACTIVE)
+        check_c_compiler_flag(/Qspectre HAS_SPECTRE)
+    endif()
+    if(HAS_SPECTRE)
+        list(APPEND QUIC_COMMON_FLAGS /Qspectre)
+    endif()
+
+    check_c_compiler_flag(/guard:cf HAS_GUARDCF)
+    if(HAS_GUARDCF)
+        list(APPEND QUIC_COMMON_FLAGS /guard:cf)
+    endif()
+
+    # Require /Qspectre and /guard:cf in CI builds
+    if(QUIC_CI AND NOT QUIC_SKIP_CI_CHECKS)
+        if(NOT HAS_GUARDCF)
+            message(FATAL_ERROR "/guard:cf must exist for CI builds")
+        endif()
+        if(NOT HAS_SPECTRE AND NOT QUIC_SANITIZER_ACTIVE AND NOT QUIC_UWP_BUILD)
+            message(FATAL_ERROR "/Qspectre must exist for CI builds")
+        endif()
+    endif()
+
+    if (QUIC_SANITIZER_ACTIVE)
+        message(STATUS "Allowing non-system32 DLLs to be loaded for ASAN")
+    elseif (QUIC_GAMECORE_BUILD)
+        message(STATUS "Allowing non-system32 DLLs to be loaded for GameCore")
+    else()
+        # Configure linker to only load from the system directory.
+        message(STATUS "Configuring linker to only load from the system directory")
+        set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /DEPENDENTLOADFLAG:0x800")
+    endif()
+
+    if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
+        list(APPEND QUIC_COMMON_FLAGS /MP)
+    endif()
+    set(QUIC_COMMON_DEFINES WIN32_LEAN_AND_MEAN SECURITY_WIN32)
+
+    if(QUIC_ENABLE_LOGGING AND QUIC_LOGGING_TYPE STREQUAL "")
+        set(QUIC_LOGGING_TYPE "etw")
+        message(STATUS "Choosing etw as default logging type for platform")
+    endif()
+else()
+
+    include(CheckSymbolExists)
+    include(CheckFunctionExists)
+    include(CheckIncludeFile)
+    check_symbol_exists(_SC_PHYS_PAGES unistd.h HAS__SC_PHYS_PAGES)
+    check_function_exists(sysconf HAS_SYSCONF)
+
+    if (CX_PLATFORM STREQUAL "linux")
+        include(CheckCCompilerFlag)
+
+        check_symbol_exists(UDP_SEGMENT netinet/udp.h HAS_UDP_SEGMENT)
+        if (NOT HAS_UDP_SEGMENT)
+            message(STATUS "UDP_SEGMENT is missing. Send performance will be reduced")
+        endif()
+
+        check_symbol_exists(SO_ATTACH_REUSEPORT_CBPF sys/socket.h HAS_SO_ATTACH_REUSEPORT_CBPF)
+        if(NOT HAS_SO_ATTACH_REUSEPORT_CBPF)
+            message(STATUS "SO_ATTACH_REUSEPORT_CBPF is missing. Server receive performance will be reduced")
+        endif()
+
+        check_function_exists(sendmmsg HAS_SENDMMSG)
+        if(NOT HAS_SENDMMSG)
+            message(STATUS "sendmmsg is missing. Send performance will be reduced")
+        endif()
+
+        # Error if flags are missing in CI
+        if(QUIC_CI AND NOT QUIC_SKIP_CI_CHECKS)
+            if (NOT HAS_UDP_SEGMENT)
+                message(FATAL_ERROR "UDP_SEGMENT must exist for CI builds")
+            endif()
+
+            if(NOT HAS_SO_ATTACH_REUSEPORT_CBPF)
+                message(FATAL_ERROR "SO_ATTACH_REUSEPORT_CBPF must exist for CI builds")
+            endif()
+        endif()
+
+        if(QUIC_ENABLE_LOGGING AND QUIC_LOGGING_TYPE STREQUAL "")
+            set(QUIC_LOGGING_TYPE "lttng")
+            message(STATUS "Choosing lttng as default logging type for platform")
+        endif()
+
+        if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm|aarch64)$")
+            set(QUIC_LINUX_XDP_ENABLED OFF CACHE BOOL "XDP not supported on ARM architectures" FORCE)
+        endif()
+
+    elseif(CX_PLATFORM STREQUAL "darwin")
+        check_function_exists(sysctl HAS_SYSCTL)
+        if(QUIC_ENABLE_LOGGING)
+            if(QUIC_LOGGING_TYPE STREQUAL "stdout")
+                message(STATUS "Choosing stdout as logging type for platform")
+            elseif(QUIC_LOGGING_TYPE STREQUAL "")
+                message(WARNING "Must explicitly set QUIC_LOGGING_TYPE to \"stdout\" to enable due to performance overhead; disabling logging")
+                set(QUIC_ENABLE_LOGGING OFF)
+            endif()
+        endif()
+    endif()
+
+    set(QUIC_COMMON_FLAGS "")
+    set(QUIC_COMMON_DEFINES _GNU_SOURCE)
+    if (HAS_SENDMMSG)
+        list(APPEND QUIC_COMMON_DEFINES HAS_SENDMMSG)
+    endif()
+    if (HAS__SC_PHYS_PAGES)
+         list(APPEND QUIC_COMMON_DEFINES HAS__SC_PHYS_PAGES)
+    endif()
+    if (HAS_SYSCONF)
+         list(APPEND QUIC_COMMON_DEFINES HAS_SYSCONF)
+    endif()
+    if (HAS_SYSCTL)
+         list(APPEND QUIC_COMMON_DEFINES HAS_SYSCTL)
+    endif()
+    set(QUIC_WARNING_FLAGS -Werror -Wall -Wextra -Wformat=2 -Wno-type-limits
+        -Wno-unknown-pragmas -Wno-multichar -Wno-missing-field-initializers
+        CACHE INTERNAL "")
+    if (CMAKE_COMPILER_IS_GNUCC AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0)
+        list(APPEND QUIC_WARNING_FLAGS -Wno-strict-aliasing)
+    elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+        list(APPEND QUIC_WARNING_FLAGS -Wno-missing-braces -Wno-microsoft-anon-tag)
+        if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 18 OR
+           (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16 AND CMAKE_CXX_COMPILER_ID MATCHES "AppleClang"))
+            list(APPEND QUIC_WARNING_FLAGS -Wno-invalid-unevaluated-string)
+        endif()
+    endif()
+endif()
+
+list(APPEND QUIC_COMMON_DEFINES VER_BUILD_ID=${QUIC_VER_BUILD_ID})
+list(APPEND QUIC_COMMON_DEFINES VER_SUFFIX=${QUIC_VER_SUFFIX})
+
+if (QUIC_EMBED_GIT_HASH)
+    list(APPEND QUIC_COMMON_DEFINES VER_GIT_HASH=${GIT_CURRENT_HASH})
+endif()
+
+if(QUIC_TELEMETRY_ASSERTS)
+    list(APPEND QUIC_COMMON_DEFINES QUIC_TELEMETRY_ASSERTS=1)
+endif()
+
+if(QUIC_HIGH_RES_TIMERS)
+    list(APPEND QUIC_COMMON_DEFINES QUIC_HIGH_RES_TIMERS=1)
+endif()
+
+if (QUIC_SANITIZER_ACTIVE OR NOT QUIC_ENABLE_POOL_ALLOC)
+    list(APPEND QUIC_COMMON_DEFINES DISABLE_CXPLAT_POOL=1)
+endif()
+
+if(QUIC_OFFICIAL_RELEASE)
+    list(APPEND QUIC_COMMON_DEFINES QUIC_OFFICIAL_RELEASE=1)
+    message(STATUS "Configured for official release build")
+endif()
+
+if(QUIC_TLS_LIB STREQUAL "schannel")
+    message(STATUS "Enabling Schannel configuration tests")
+    list(APPEND QUIC_COMMON_DEFINES QUIC_TEST_SCHANNEL_FLAGS=1)
+
+    message(STATUS "Disabling PFX tests")
+    list(APPEND QUIC_COMMON_DEFINES QUIC_DISABLE_PFX_TESTS)
+    message(STATUS "Disabling 0-RTT support")
+    list(APPEND QUIC_COMMON_DEFINES QUIC_DISABLE_0RTT_TESTS)
+    message(STATUS "Disabling ChaCha20 support")
+    list(APPEND QUIC_COMMON_DEFINES QUIC_DISABLE_CHACHA20_TESTS)
+    message(STATUS "Enabling anonymous client auth tests")
+    list(APPEND QUIC_COMMON_DEFINES QUIC_ENABLE_ANON_CLIENT_AUTH_TESTS)
+endif()
+
+if(QUIC_TLS_LIB STREQUAL "quictls" OR QUIC_TLS_LIB STREQUAL "openssl")
+    message(STATUS "Enabling quictls/openssl configuration tests")
+    list(APPEND QUIC_COMMON_DEFINES QUIC_TEST_OPENSSL_FLAGS=1)
+    if (NOT WIN32)
+       message(STATUS "Enabling CA file tests")
+       list(APPEND QUIC_COMMON_DEFINES QUIC_ENABLE_CA_CERTIFICATE_FILE_TESTS)
+    endif()
+endif()
+
+if(QUIC_ENABLE_LOGGING)
+    if (QUIC_LOGGING_TYPE STREQUAL "etw")
+        if (WIN32)
+            message(STATUS "Configuring for manifested ETW logging")
+            list(APPEND QUIC_COMMON_DEFINES QUIC_EVENTS_MANIFEST_ETW QUIC_LOGS_MANIFEST_ETW)
+        else()
+            message(WARNING "ETW logging is only available on WIN32. Disabling logging")
+            set(QUIC_ENABLE_LOGGING OFF)
+        endif()
+
+    elseif (QUIC_LOGGING_TYPE STREQUAL "lttng")
+        if (QUIC_SANITIZER_ACTIVE)
+            message(WARNING "LTTng logging is incompatible with sanitizers. Skipping logging")
+            set(QUIC_ENABLE_LOGGING OFF)
+        else()
+            # FindLTTngUST does not exist before CMake 3.6, so disable logging for older cmake versions
+            if (${CMAKE_VERSION} VERSION_LESS "3.6.0")
+                message(WARNING "Logging unsupported on this version of CMake. Please upgrade to 3.6 or later.")
+                set(QUIC_ENABLE_LOGGING OFF)
+            else()
+                check_include_file(lttng/ust-config.h HAS_LTTNG)
+                if (NOT HAS_LTTNG)
+                    message(WARNING "LTTng logging not found. Disabling logging")
+                    set(QUIC_ENABLE_LOGGING OFF)
+                else()
+                    check_symbol_exists(LTTNG_UST_HAVE_SDT_INTEGRATION lttng/ust-config.h HAS_LTTNG_SDT)
+                    if(HAS_LTTNG_SDT)
+                        message(WARNING "LTTng with SDT integration does not work. Disabling logging")
+                        set(QUIC_ENABLE_LOGGING OFF)
+                    else()
+                        message(STATUS "Configuring for LTTng logging")
+                        list(APPEND QUIC_COMMON_DEFINES QUIC_CLOG)
+                        include(FindLTTngUST)
+                    endif()
+                endif()
+            endif()
+        endif()
+
+    elseif (QUIC_LOGGING_TYPE STREQUAL "stdout")
+        message(STATUS "Configuring for stdout logging")
+        list(APPEND QUIC_COMMON_DEFINES QUIC_EVENTS_STDOUT QUIC_LOGS_STDOUT)
+    endif()
+
+else()
+    message(STATUS "QUIC_ENABLE_LOGGING is false. Disabling logging")
+endif()
+
+if (NOT QUIC_ENABLE_LOGGING)
+    list(APPEND QUIC_COMMON_DEFINES QUIC_EVENTS_STUB QUIC_LOGS_STUB)
+endif()
+
+if (NOT MSVC AND NOT APPLE AND NOT ANDROID)
+    find_library(ATOMIC NAMES atomic libatomic.so.1)
+    if (ATOMIC)
+        message(STATUS "Found libatomic: ${ATOMIC}")
+    else()
+        message(STATUS "libatomic not found. If build fails, install libatomic")
+    endif()
+
+    find_library(NUMA NAMES NUMA libnuma.so.1)
+    if (NUMA)
+        message(STATUS "Found libnuma: ${NUMA}")
+        find_file(NUMA-HEADER NAMES "numa.h")
+        if (NUMA-HEADER)
+            message(STATUS "Found numa.h: ${NUMA-HEADER}")
+            list(APPEND QUIC_COMMON_DEFINES CXPLAT_NUMA_AWARE)
+        else()
+            message(STATUS "numa.h not found. If build fails, install libnuma-dev")
+        endif()
+    else()
+        message(STATUS "libnuma not found. If build fails, install libnuma")
+    endif()
+endif()
+
+if (QUIC_LINUX_IOURING_ENABLED)
+    find_library(LIBURING NAMES liburing-ffi.so)
+    if (LIBURING)
+        message(STATUS "Found liburing: ${LIBURING}")
+    else()
+        message(STATUS "liburing not found. If build fails, install liburing")
+    endif()
+endif()
+
+if (CMAKE_GENERATOR_PLATFORM STREQUAL "")
+string(TOLOWER ${CMAKE_SYSTEM_PROCESSOR} SYSTEM_PROCESSOR)
+else()
+string(TOLOWER ${CMAKE_GENERATOR_PLATFORM} SYSTEM_PROCESSOR)
+endif()
+
+if(WIN32)
+    # Generate the MsQuicEtw header file.
+    file(MAKE_DIRECTORY ${QUIC_BUILD_DIR}/inc)
+
+    if(NOT DEFINED CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH OR CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH)
+        find_program(MC_EXE NAMES mc.exe)
+        if(MC_EXE MATCHES "NOTFOUND")
+            # If not found, then VS project generator will set %PATH% env var to WinSDK bin directory
+            set(MC_EXE "mc.exe" CACHE STRING "mc.exe from %PATH%" FORCE)
+        endif()
+    else()
+        find_program(MC_EXE NAMES mc.exe REQUIRED)
+    endif()
+
+    add_custom_command(
+        OUTPUT ${QUIC_BUILD_DIR}/inc/MsQuicEtw.h
+        OUTPUT ${QUIC_BUILD_DIR}/inc/MsQuicEtw.rc
+        DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/manifest/MsQuicEtw.man
+        COMMAND "${MC_EXE}" -um -h ${QUIC_BUILD_DIR}/inc -r ${QUIC_BUILD_DIR}/inc ${CMAKE_CURRENT_SOURCE_DIR}/src/manifest/MsQuicEtw.man)
+    add_custom_target(MsQuicEtw_HeaderBuild
+        DEPENDS ${QUIC_BUILD_DIR}/inc/MsQuicEtw.h)
+
+    set_property(TARGET MsQuicEtw_HeaderBuild PROPERTY FOLDER "${QUIC_FOLDER_PREFIX}helpers")
+
+    add_library(MsQuicEtw_Header INTERFACE)
+    target_include_directories(MsQuicEtw_Header INTERFACE
+        $<BUILD_INTERFACE:${QUIC_BUILD_DIR}/inc>
+        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
+    add_dependencies(MsQuicEtw_Header MsQuicEtw_HeaderBuild)
+
+    add_library(MsQuicEtw_Resource OBJECT ${QUIC_BUILD_DIR}/inc/MsQuicEtw.rc)
+    set_property(TARGET MsQuicEtw_Resource PROPERTY FOLDER "${QUIC_FOLDER_PREFIX}helpers")
+
+    message(STATUS "Disabling (client) shared port support")
+    list(APPEND QUIC_COMMON_DEFINES QUIC_DISABLE_SHARED_PORT_TESTS)
+
+    if (QUIC_UWP_BUILD)
+        list(APPEND QUIC_COMMON_DEFINES QUIC_UWP_BUILD QUIC_RESTRICTED_BUILD)
+        set(CMAKE_CXX_STANDARD_LIBRARIES "")
+        set(CMAKE_CXX_STANDARD_LIBRARIES_INIT "")
+        set(CMAKE_C_STANDARD_LIBRARIES "")
+        set(CMAKE_C_STANDARD_LIBRARIES_INIT "")
+    endif()
+
+    if (QUIC_GAMECORE_BUILD)
+        list(APPEND QUIC_COMMON_DEFINES QUIC_GAMECORE_BUILD QUIC_RESTRICTED_BUILD)
+    endif()
+
+    if (QUIC_GAMECORE_BUILD AND NOT QUIC_EXTERNAL_TOOLCHAIN)
+        list(APPEND QUIC_COMMON_DEFINES WINAPI_FAMILY=WINAPI_FAMILY_GAMES)
+        set(CMAKE_CXX_STANDARD_LIBRARIES "")
+        set(CMAKE_CXX_STANDARD_LIBRARIES_INIT "")
+        set(CMAKE_C_STANDARD_LIBRARIES "")
+        set(CMAKE_C_STANDARD_LIBRARIES_INIT "")
+
+        set(UnsupportedLibs advapi32.lib comctl32.lib comsupp.lib dbghelp.lib gdi32.lib gdiplus.lib guardcfw.lib kernel32.lib mmc.lib msimg32.lib msvcole.lib msvcoled.lib mswsock.lib ntstrsafe.lib ole2.lib ole2autd.lib ole2auto.lib ole2d.lib ole2ui.lib ole2uid.lib ole32.lib oleacc.lib oleaut32.lib oledlg.lib oledlgd.lib oldnames.lib runtimeobject.lib shell32.lib shlwapi.lib strsafe.lib urlmon.lib user32.lib userenv.lib wlmole.lib wlmoled.lib onecore.lib)
+        set(Console_LinkOptions /DYNAMICBASE /NXCOMPAT)
+        foreach(arg ${UnsupportedLibs})
+            list(APPEND Console_LinkOptions "/NODEFAULTLIB:${arg}")
+        endforeach()
+
+        set(Console_ArchOptions /favor:AMD64)
+
+        list(APPEND Console_ArchOptions /arch:AVX)
+
+        # Locate Software Development Kits
+        get_filename_component(Console_SdkRoot "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\GDK;InstallPath]" ABSOLUTE CACHE)
+
+        if(NOT EXISTS ${Console_SdkRoot})
+            if (EXISTS "C:\\Program Files (x86)\\Microsoft GDK")
+                set(Console_SdkRoot "C:\\Program Files (x86)\\Microsoft GDK")
+            else()
+                message(FATAL_ERROR "Could not find GDK install")
+            endif()
+        endif()
+
+        file(GLOB GdkFolders ${Console_SdkRoot}/*)
+
+        set(XdkEditionTarget 5000000000)
+
+        foreach(GdkFolder ${GdkFolders})
+            if (IS_DIRECTORY ${GdkFolder})
+                file(GLOB SubDirs ${GdkFolder}/*)
+                foreach(SubDir ${SubDirs})
+                    if (SubDir MATCHES "GXDK$")
+                        # Make sure library exists
+                        set(GxdkLibDirectory "${SubDir}/gameKit/Lib/amd64")
+                        set(GxdkRuntimeLib "${GxdkLibDirectory}/xgameplatform.lib")
+                        if (EXISTS ${GxdkRuntimeLib})
+                            get_filename_component(PotentialXdkEditionTarget ${GdkFolder} NAME)
+                            # Always select lowest version equal or higher than 211000
+                            if (PotentialXdkEditionTarget LESS XdkEditionTarget AND
+                                PotentialXdkEditionTarget GREATER_EQUAL 211000)
+                                set(XdkEditionTarget ${PotentialXdkEditionTarget})
+                                set(Console_EndpointLibRoot "${GxdkLibDirectory}")
+                            endif()
+                        endif()
+                    endif()
+                endforeach()
+            endif()
+        endforeach()
+
+        if (XdkEditionTarget EQUAL 5000000000)
+            message(FATAL_ERROR "Gxdk Target Not Found")
+        endif()
+
+        message(STATUS "Chosing ${XdkEditionTarget} with root ${Console_EndpointLibRoot}")
+
+    endif()
+
+    if(QUIC_SANITIZER_ACTIVE)
+        # This fails when linking statically, so for today require dynamic linkage
+        if (QUIC_STATIC_LINK_CRT)
+            message(FATAL_ERROR "Static linkage unsupported with Address Sanitizer in Windows")
+        endif()
+        message(STATUS "Configuring sanitizers: ASAN:${QUIC_ENABLE_ASAN} LSAN:${QUIC_ENABLE_LSAN} UBSAN:${QUIC_ENABLE_UBSAN} EXTRA:${QUIC_ENABLE_EXTRA_SANITIZERS}")
+        list(APPEND QUIC_COMMON_FLAGS /fsanitize=address)
+        list(APPEND QUIC_COMMON_DEFINES _DISABLE_VECTOR_ANNOTATION) # For std:vector usage in tests
+    endif()
+
+    set(QUIC_C_FLAGS ${QUIC_COMMON_FLAGS})
+    set(QUIC_CXX_FLAGS ${QUIC_COMMON_FLAGS} /EHsc /permissive-)
+
+    # These cannot be updated until CMake 3.13
+    set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /GL /Zi")
+    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /GL /Zi")
+    set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /LTCG /IGNORE:4075 /DEBUG /OPT:REF /OPT:ICF")
+    set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG /IGNORE:4075 /DEBUG /OPT:REF /OPT:ICF")
+
+    # Find the right PGO file.
+    if(NOT EXISTS "${QUIC_PGO_FILE}")
+        set(QUIC_PGO_NAME "msquic.${QUIC_TLS_LIB}.pgd")
+        set(QUIC_PGO_FILE "${CMAKE_CURRENT_SOURCE_DIR}/src/bin/winuser/pgo_${SYSTEM_PROCESSOR}/${QUIC_PGO_NAME}")
+    endif()
+    if(NOT EXISTS "${QUIC_PGO_FILE}")
+        set(QUIC_PGO_NAME "msquic.pgd")
+        set(QUIC_PGO_FILE "${CMAKE_CURRENT_SOURCE_DIR}/src/bin/winuser/pgo_${SYSTEM_PROCESSOR}/${QUIC_PGO_NAME}")
+    endif()
+    message(STATUS "Using PGO file ${QUIC_PGO_NAME}")
+
+    # Configure PGO linker flags.
+    if(QUIC_PGO)
+        # Configured for training mode. Use the previous PGD file if present.
+        if(EXISTS "${QUIC_PGO_FILE}")
+            message(STATUS "/GENPROFILE:PDG")
+            configure_file("${QUIC_PGO_FILE}" "${QUIC_PGO_DIR}/msquic.pgd" COPYONLY)
+            set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /GENPROFILE:\"PGD=${QUIC_PGO_DIR}/msquic.pgd\"")
+        else()
+            message(STATUS "/GENPROFILE")
+            set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /GENPROFILE")
+        endif()
+        set(CMAKE_VS_SDK_LIBRARY_DIRECTORIES "$(LibraryPath);$(VC_LibraryPath_VC_${SYSTEM_PROCESSOR}_Desktop)")
+    else()
+        # Just doing a normal build. Use the PGD file if present.
+        if(EXISTS "${QUIC_PGO_FILE}")
+            message(STATUS "Using profile-guided optimization")
+            file(MAKE_DIRECTORY ${QUIC_PGO_DIR})
+            configure_file("${QUIC_PGO_FILE}" "${QUIC_PGO_DIR}/msquic.pgd" COPYONLY)
+            set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /USEPROFILE:\"PGD=${QUIC_PGO_DIR}/msquic.pgd\"")
+        endif()
+    endif()
+
+    if(QUIC_STATIC_LINK_CRT)
+        message(STATUS "Configuring for statically-linked CRT")
+        set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
+    endif()
+
+    if(QUIC_STATIC_LINK_PARTIAL_CRT)
+        message(STATUS "Configuring for partially statically-linked CRT")
+        set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib")
+    endif()
+
+    if (NOT QUIC_STATIC_LINK_CRT AND NOT QUIC_STATIC_LINK_PARTIAL_CRT)
+        # We are using dynamic linking. Ensure that only the release version of CRT is used.
+        message(STATUS "Configuring for release version of dynamically linked CRT")
+        set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
+    endif()
+
+else() #!WIN32
+    # Custom build flags.
+
+    if (QUIC_OPTIMIZE_LOCAL AND NOT CMAKE_SYSTEM_PROCESSOR STREQUAL arm)
+        set(MARCH -march=native)
+    endif()
+
+    set(CMAKE_C_FLAGS_DEBUG "-O0 -fno-omit-frame-pointer")
+    set(CMAKE_C_FLAGS_MINSIZEREL "-Os -DNDEBUG")
+    set(CMAKE_C_FLAGS_RELWITHDEBINFO "-O3 -fno-omit-frame-pointer ${MARCH} -DNDEBUG")
+    set(CMAKE_C_FLAGS_RELEASE "-O3 ${MARCH} -DNDEBUG")
+    if (CX_PLATFORM STREQUAL "darwin")
+        set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -gdwarf")
+        set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} -gdwarf")
+    else()
+        set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -ggdb3")
+        set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} -ggdb3")
+    endif()
+    set(CMAKE_CXX_FLAGS_DEBUG ${CMAKE_C_FLAGS_DEBUG})
+    set(CMAKE_CXX_FLAGS_MINSIZEREL ${CMAKE_C_FLAGS_MINSIZEREL})
+    set(CMAKE_CXX_FLAGS_RELWITHDEBINFO ${CMAKE_C_FLAGS_RELWITHDEBINFO})
+    set(CMAKE_CXX_FLAGS_RELEASE ${CMAKE_C_FLAGS_RELEASE})
+
+    list(APPEND QUIC_COMMON_FLAGS -fms-extensions -fPIC)
+    if (CX_PLATFORM STREQUAL "darwin")
+        list(APPEND QUIC_COMMON_DEFINES CX_PLATFORM_DARWIN)
+        list(APPEND QUIC_COMMON_FLAGS -Wno-microsoft-anon-tag -Wno-tautological-constant-out-of-range-compare -Wmissing-field-initializers)
+        message(STATUS "Disabling (client) shared port support")
+        list(APPEND QUIC_COMMON_DEFINES QUIC_DISABLE_SHARED_PORT_TESTS)
+    else()
+        list(APPEND QUIC_COMMON_DEFINES CX_PLATFORM_LINUX)
+        message(STATUS "Enabling shared ephemeral port work around")
+        list(APPEND QUIC_COMMON_DEFINES QUIC_SHARED_EPHEMERAL_WORKAROUND)
+    endif()
+
+    if (QUIC_SANITIZER_ACTIVE)
+        message(STATUS "Configuring sanitizers: ASAN:${QUIC_ENABLE_ASAN} LSAN:${QUIC_ENABLE_LSAN} UBSAN:${QUIC_ENABLE_UBSAN} TSAN:${QUIC_ENABLE_TSAN} EXTRA:${QUIC_ENABLE_EXTRA_SANITIZERS}")
+        # Append common flags for all sanitizer options
+        list(APPEND QUIC_COMMON_FLAGS -O0 -fno-omit-frame-pointer -fno-optimize-sibling-calls)
+
+        # Clang/LLVM doesn't support this flag, but GCC does.
+        check_c_compiler_flag(-fno-var-tracking-assignments HAS_NO_VAR_TRACKING)
+        if (HAS_NO_VAR_TRACKING)
+            list(APPEND QUIC_COMMON_FLAGS -fno-var-tracking-assignments)
+        endif()
+
+        if (CX_PLATFORM STREQUAL "darwin")
+            list(APPEND QUIC_COMMON_FLAGS -gdwarf)
+        else()
+            list(APPEND QUIC_COMMON_FLAGS -ggdb3)
+        endif()
+
+        if (QUIC_ENABLE_ASAN)
+            list(APPEND QUIC_COMMON_FLAGS -fsanitize=address)
+        endif()
+
+        if (QUIC_ENABLE_LSAN)
+            list(APPEND QUIC_COMMON_FLAGS -fsanitize=leak)
+        endif()
+
+        if (QUIC_ENABLE_UBSAN)
+            list(APPEND QUIC_COMMON_FLAGS -fsanitize=undefined)
+        endif()
+
+        if (QUIC_ENABLE_TSAN)
+            list(APPEND QUIC_COMMON_FLAGS -fsanitize=thread)
+            set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=thread")
+        endif()
+
+        if (QUIC_ENABLE_EXTRA_SANITIZERS)
+            list(APPEND QUIC_COMMON_FLAGS -fsanitize=alignment -fsanitize-address-use-after-scope)
+            # Enable additional sanitizers offered by clang
+            if (CMAKE_C_COMPILER_ID MATCHES "Clang")
+                list(APPEND QUIC_COMMON_FLAGS -fsanitize=unsigned-integer-overflow -fsanitize=local-bounds -fsanitize=integer -fsanitize=nullability)
+            endif()
+        endif()
+    endif()
+
+    set(QUIC_C_FLAGS ${QUIC_COMMON_FLAGS})
+    set(QUIC_CXX_FLAGS ${QUIC_COMMON_FLAGS})
+endif()
+
+if(QUIC_TLS_LIB STREQUAL "quictls" OR QUIC_TLS_LIB STREQUAL "openssl")
+    add_library(OpenSSL INTERFACE)
+
+    option(QUIC_USE_EXTERNAL_OPENSSL "Use external OpenSSL instead of building from submodules" OFF)
+
+    if(QUIC_USE_EXTERNAL_OPENSSL)
+        find_package(OpenSSL CONFIG REQUIRED)
+
+        add_library(OpenSSLQuic INTERFACE)
+        # When external OpenSSL is used, just link to the imported targets
+        # No need to add include directories as they are already in OpenSSL::SSL/Crypto
+        target_link_libraries(OpenSSLQuic
+            INTERFACE
+            OpenSSL::SSL
+            OpenSSL::Crypto
+        )
+        add_library(OpenSSLQuic::OpenSSLQuic ALIAS OpenSSLQuic)
+
+    else()
+
+    include(FetchContent)
+
+    FetchContent_Declare(
+        OpenSSLQuic
+        SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/submodules
+        CMAKE_ARGS "-DQUIC_USE_SYSTEM_LIBCRYPTO=${QUIC_USE_SYSTEM_LIBCRYPTO}"
+    )
+    FetchContent_MakeAvailable(OpenSSLQuic)
+
+    endif()
+
+    target_link_libraries(OpenSSL
+        INTERFACE
+        OpenSSLQuic::OpenSSLQuic
+    )
+endif()
+
+if (QUIC_USE_SYSTEM_LIBCRYPTO)
+    list(APPEND QUIC_COMMON_DEFINES CXPLAT_SYSTEM_CRYPTO)
+endif()
+
+if (QUIC_LINUX_XDP_ENABLED)
+    list(APPEND QUIC_COMMON_DEFINES CXPLAT_LINUX_XDP_ENABLED)
+endif()
+
+if (QUIC_LINUX_IOURING_ENABLED)
+    list(APPEND QUIC_COMMON_DEFINES CXPLAT_USE_IO_URING)
+endif()
+
+if(QUIC_CODE_CHECK)
+    find_program(CLANGTIDY NAMES clang-tidy)
+    if(CLANGTIDY)
+        message(STATUS "Found clang-tidy: ${CLANGTIDY}")
+        set(CLANG_TIDY_CHECKS *
+            # add checks to ignore here:
+            -altera-*
+            -android-cloexec-fopen
+            -android-cloexec-socket
+            -bugprone-assignment-in-if-condition
+            -bugprone-casting-through-void
+            -bugprone-easily-swappable-parameters
+            -bugprone-implicit-widening-of-multiplication-result
+            -bugprone-macro-parentheses
+            -bugprone-multi-level-implicit-pointer-conversion
+            -bugprone-narrowing-conversions
+            -bugprone-reserved-identifier
+            -bugprone-sizeof-expression
+            -bugprone-switch-missing-default-case
+            -cert-dcl37-c
+            -cert-dcl51-cpp
+            -cert-err33-c
+            -clang-analyzer-optin.core.EnumCastOutOfRange
+            -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling
+            -clang-diagnostic-invalid-unevaluated-string
+            -clang-diagnostic-microsoft-anon-tag
+            -concurrency-mt-unsafe
+            -cppcoreguidelines-avoid-magic-numbers
+            -cppcoreguidelines-avoid-non-const-global-variables
+            -cppcoreguidelines-init-variables
+            -cppcoreguidelines-macro-to-enum
+            -cppcoreguidelines-narrowing-conversions
+            -google-readability-casting
+            -google-readability-function-size
+            -google-readability-todo
+            -hicpp-function-size
+            -hicpp-no-assembler
+            -hicpp-signed-bitwise
+            -llvmlibc-restrict-system-libc-headers
+            -misc-include-cleaner
+            -misc-no-recursion # do you really need recursion?
+            -misc-header-include-cycle
+            -modernize-macro-to-enum
+            -readability-avoid-const-params-in-decls
+            -readability-avoid-nested-conditional-operator
+            -readability-duplicate-include
+            -readability-function-cognitive-complexity
+            -readability-function-size
+            -readability-identifier-length
+            -readability-identifier-naming
+            -readability-isolate-declaration
+            -readability-magic-numbers
+            -readability-non-const-parameter
+            -readability-redundant-casting
+        )
+        string(REPLACE ";" "," CLANG_TIDY_CHECKS "${CLANG_TIDY_CHECKS}")
+        set(CMAKE_C_CLANG_TIDY_AVAILABLE ${CLANGTIDY} -checks=${CLANG_TIDY_CHECKS}
+            -system-headers --warnings-as-errors=*)
+    else()
+        message(STATUS "clang-tidy not found")
+    endif()
+
+    find_program(CPPCHECK NAMES cppcheck)
+    if(CPPCHECK)
+        message(STATUS "Found cppcheck: ${CPPCHECK}")
+        set(CMAKE_C_CPPCHECK_AVAILABLE ${CPPCHECK} -q --inline-suppr
+            --suppress=duplicateValueTernary --suppress=objectIndex
+            --suppress=varFuncNullUB --suppress=constParameter
+            # these are finding potential logic issues, may want to suppress when focusing on nits:
+            --suppress=nullPointer --suppress=nullPointerRedundantCheck
+            --suppress=knownConditionTrueFalse --suppress=invalidscanf
+            --enable=warning,style,performance,portability -D__linux__)
+    else()
+        message(STATUS "cppcheck not found")
+    endif()
+endif()
+
+add_subdirectory(src/inc)
+add_subdirectory(src/generated)
+
+# Product code
+add_subdirectory(src/core)
+add_subdirectory(src/platform)
+add_subdirectory(src/bin)
+
+# Tool code
+if(QUIC_BUILD_TOOLS)
+    add_subdirectory(src/tools)
+endif()
+
+# Performance code
+if(QUIC_BUILD_PERF)
+    add_subdirectory(src/perf/lib)
+    add_subdirectory(src/perf/bin)
+endif()
+
+# Test code
+if(QUIC_BUILD_TEST)
+    include(FetchContent)
+
+    enable_testing()
+
+    # Build the googletest framework.
+
+    # Enforce static builds for test artifacts
+    set(PREV_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS} CACHE INTERNAL "")
+    set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "")
+    set(BUILD_GMOCK OFF CACHE BOOL "Builds the googlemock subproject")
+    set(INSTALL_GTEST OFF CACHE BOOL "Enable installation of googletest. (Projects embedding googletest may want to turn this OFF.)")
+    if(WIN32 AND QUIC_STATIC_LINK_CRT)
+        option(gtest_force_shared_crt "Use shared (DLL) run-time lib even when Google Test is built as static lib." ON)
+    endif()
+    FetchContent_Declare(
+        googletest
+        SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/submodules/googletest
+    )
+    FetchContent_MakeAvailable(googletest)
+    set(BUILD_SHARED_LIBS ${PREV_BUILD_SHARED_LIBS} CACHE INTERNAL "")
+
+    set_property(TARGET gtest PROPERTY CXX_STANDARD 17)
+    set_property(TARGET gtest PROPERTY FOLDER "${QUIC_FOLDER_PREFIX}tests")
+
+    set_property(TARGET gtest_main PROPERTY CXX_STANDARD 17)
+    set_property(TARGET gtest_main PROPERTY FOLDER "${QUIC_FOLDER_PREFIX}tests")
+    set_property(TARGET gtest_main PROPERTY EXCLUDE_FROM_ALL ON)
+    set_property(TARGET gtest_main PROPERTY EXCLUDE_FROM_DEFAULT_BUILD ON)
+
+    if (HAS_SPECTRE)
+        target_compile_options(gtest PRIVATE /Qspectre)
+        target_compile_options(gtest_main PRIVATE /Qspectre)
+    endif()
+
+    if (HAS_GUARDCF)
+        target_compile_options(gtest PRIVATE /guard:cf)
+        target_compile_options(gtest_main PRIVATE /guard:cf)
+    endif()
+
+    if(WIN32 AND QUIC_ENABLE_ASAN)
+        target_compile_options(gtest PRIVATE /fsanitize=address)
+        target_compile_options(gtest_main PRIVATE /fsanitize=address)
+        target_compile_definitions(gtest PRIVATE _DISABLE_VECTOR_ANNOTATION)
+        target_compile_definitions(gtest_main PRIVATE _DISABLE_VECTOR_ANNOTATION)
+    endif()
+
+    add_subdirectory(src/core/unittest)
+    add_subdirectory(src/platform/unittest)
+    add_subdirectory(src/test/lib)
+    add_subdirectory(src/test/bin)
+endif()
diff --git a/subprojects/packagefiles/msquic/meson.build b/subprojects/packagefiles/msquic/meson.build
new file mode 100644 (file)
index 0000000..bc24f1e
--- /dev/null
@@ -0,0 +1,177 @@
+# Native Meson build for msquic
+# This overlay builds msquic using CMake directly and exposes the dependency
+
+project('msquic', 'c',
+  version: '2.5.6',
+  license: 'MIT'
+)
+
+# Support Linux, macOS, and Windows
+host_system = host_machine.system()
+supported_systems = ['linux', 'darwin', 'windows']
+assert(host_system in supported_systems, 'msquic meson overlay only supports Linux, macOS, and Windows')
+
+cc = meson.get_compiler('c')
+
+# Build configuration
+buildtype = get_option('buildtype')
+if buildtype == 'release' or buildtype == 'plain'
+  cmake_build_type = 'Release'
+else
+  cmake_build_type = 'Debug'
+endif
+
+# Paths
+msquic_src = meson.current_source_dir()
+msquic_build = meson.current_build_dir() / 'cmake_build'
+msquic_inc = msquic_src / 'src' / 'inc'
+
+# Extract macOS deployment target and architecture flags from compiler
+cmake_extra_flags = []
+if host_system == 'darwin'
+  # Extract deployment target and architectures from build flags
+  deployment_target = ''
+  archs = []
+
+  # Combine c_args and c_link_args to search for flags
+  all_flags = get_option('c_args') + get_option('c_link_args')
+
+  # Track if we just saw -arch flag
+  next_is_arch = false
+  foreach flag : all_flags
+    if next_is_arch
+      # This is the architecture value after -arch
+      if flag not in archs
+        archs += [flag]
+      endif
+      next_is_arch = false
+    elif flag.startswith('-mmacosx-version-min=')
+      deployment_target = flag.split('=')[1]
+    elif flag == '-arch'
+      # Next flag will be the architecture
+      next_is_arch = true
+    endif
+  endforeach
+
+  # Set deployment target for CMake
+  if deployment_target != ''
+    cmake_extra_flags += ['-DCMAKE_OSX_DEPLOYMENT_TARGET=' + deployment_target]
+    message('Using macOS deployment target: ' + deployment_target)
+  endif
+
+  # Set architectures for CMake (for universal builds)
+  if archs.length() > 0
+    cmake_extra_flags += ['-DCMAKE_OSX_ARCHITECTURES=' + ';'.join(archs)]
+    message('Using macOS architectures: ' + ';'.join(archs))
+  endif
+endif
+
+# Output paths - platform specific
+if host_system == 'windows'
+  # Windows uses .lib extension
+  lib_ext = '.lib'
+  lib_prefix = ''
+else
+  # Linux and macOS use .a extension
+  lib_ext = '.a'
+  lib_prefix = 'lib'
+endif
+
+if cmake_build_type == 'Release'
+  msquic_lib_path = msquic_build / 'bin' / 'Release' / (lib_prefix + 'msquic' + lib_ext)
+else
+  msquic_lib_path = msquic_build / 'bin' / 'Debug' / (lib_prefix + 'msquic' + lib_ext)
+endif
+
+# CMake configuration command
+cmake_args = [
+  'cmake',
+  '-S', msquic_src,
+  '-B', msquic_build,
+  '-DCMAKE_BUILD_TYPE=' + cmake_build_type,
+  '-DCMAKE_POSITION_INDEPENDENT_CODE=ON',
+  '-DQUIC_BUILD_SHARED=OFF',
+  '-DQUIC_BUILD_TEST=OFF',
+  '-DQUIC_BUILD_TOOLS=OFF',
+  '-DQUIC_BUILD_PERF=OFF',
+  '-DQUIC_ENABLE_LOGGING=OFF',
+  '-DQUIC_USE_EXTERNAL_OPENSSL=ON',
+  '-DQUIC_TLS_LIB=openssl',
+  '-DCMAKE_PREFIX_PATH=' + get_option('qt_prefix'),
+] + cmake_extra_flags
+
+cmake_configure = run_command(
+  cmake_args,
+  check: true,
+  capture: true
+)
+
+message('msquic CMake configure output: ' + cmake_configure.stdout())
+
+# Build msquic using CMake
+# We use a custom_target to build so it happens at build time, not configure time
+cmake_prog = find_program('cmake')
+
+# Custom target to build msquic
+# Copy command differs on Windows
+if host_system == 'windows'
+  copy_cmd = ['cmd', '/c', 'copy', msquic_lib_path, '@OUTPUT@']
+else
+  copy_cmd = ['sh', '-c', 'cp ' + msquic_lib_path + ' @OUTPUT@']
+endif
+
+msquic_build_target = custom_target(
+  'msquic_build',
+  output: lib_prefix + 'msquic' + lib_ext,
+  command: [
+    cmake_prog,
+    '--build', msquic_build,
+    '--config', cmake_build_type,
+    '--parallel',
+    '&&'
+  ] + copy_cmd,
+  build_by_default: true,
+  console: true
+)
+
+# Find system libraries that msquic needs (platform-specific)
+thread_dep = dependency('threads')
+msquic_link_deps = [thread_dep]
+
+if host_system == 'linux'
+  # Linux-specific dependencies
+  atomic_dep = cc.find_library('atomic', required: true)
+  numa_dep = cc.find_library('numa', required: false)
+  msquic_link_deps += [atomic_dep]
+  if numa_dep.found()
+    msquic_link_deps += numa_dep
+  endif
+elif host_system == 'darwin'
+  # macOS might need CoreFoundation and Security frameworks
+  # msquic on macOS typically uses system OpenSSL or provides its own
+  corefoundation_dep = dependency('appleframeworks', modules: ['CoreFoundation', 'Security'], required: false)
+  if corefoundation_dep.found()
+    msquic_link_deps += corefoundation_dep
+  endif
+elif host_system == 'windows'
+  # Windows needs ws2_32, secur32, etc. - typically handled by msquic's CMake
+  # Add Windows-specific libraries if needed
+  ws2_dep = cc.find_library('ws2_32', required: false)
+  if ws2_dep.found()
+    msquic_link_deps += ws2_dep
+  endif
+endif
+
+# Create the msquic dependency
+# Note: We link to the static library via link_args since it's from a custom_target
+msquic_dep = declare_dependency(
+  include_directories: include_directories('src/inc'),
+  compile_args: ['-DQUIC_BUILD_STATIC'],
+  link_args: [
+    msquic_lib_path,
+  ],
+  dependencies: msquic_link_deps,
+  sources: msquic_build_target
+)
+
+meson.override_dependency('msquic', msquic_dep)
diff --git a/subprojects/packagefiles/msquic/meson_options.txt b/subprojects/packagefiles/msquic/meson_options.txt
new file mode 100644 (file)
index 0000000..ecf7804
--- /dev/null
@@ -0,0 +1 @@
+option('qt_prefix', type: 'string', value: '', description: 'Path to Qt installation prefix for OpenSSL discovery')\r
diff --git a/subprojects/packagefiles/msquic/src/bin/CMakeLists.txt b/subprojects/packagefiles/msquic/src/bin/CMakeLists.txt
new file mode 100644 (file)
index 0000000..527dbef
--- /dev/null
@@ -0,0 +1,301 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+if(WIN32)
+    configure_file(winuser/msquic.rc.in ${CMAKE_CURRENT_BINARY_DIR}/msquic.rc )
+    configure_file(winuser/msquic.def.in ${CMAKE_CURRENT_BINARY_DIR}/msquic.def )
+    set(SOURCES winuser/dllmain.c ${CMAKE_CURRENT_BINARY_DIR}/msquic.rc $<TARGET_OBJECTS:MsQuicEtw_Resource>)
+else()
+    set(SOURCES linux/init.c)
+endif()
+
+if(BUILD_SHARED_LIBS)
+    add_library(msquic SHARED ${SOURCES})
+    add_library(msquic::msquic ALIAS msquic)
+    target_include_directories(msquic PUBLIC $<INSTALL_INTERFACE:include>)
+    target_link_libraries(msquic PRIVATE core msquic_platform inc warnings logging base_link main_binary_link_args)
+    set_target_properties(msquic PROPERTIES OUTPUT_NAME ${QUIC_LIBRARY_NAME})
+    if (NOT WIN32)
+        set_target_properties(msquic PROPERTIES SOVERSION ${QUIC_MAJOR_VERSION} VERSION ${QUIC_FULL_VERSION})
+    endif()
+else()
+    add_library(msquic_static STATIC static/empty.c)
+    set_target_properties(msquic_static PROPERTIES OUTPUT_NAME "${QUIC_LIBRARY_NAME}${CMAKE_DEBUG_POSTFIX}")
+    target_link_libraries(msquic_static PRIVATE core msquic_platform inc logging main_binary_link_args)
+    target_compile_definitions(msquic_static PUBLIC QUIC_BUILD_STATIC)
+    set_property(TARGET msquic_static PROPERTY FOLDER "${QUIC_FOLDER_PREFIX}libraries")
+
+    # We need to take given library and walk all its dependencies, including
+    # transient dependencies. For each dependency, if it either compiles
+    # directly to or imports a static library, that library needs to be
+    # added to the QUIC_STATIC_LIBS list. This list is ultimately fed into
+    # the archiving utility to produce a monolithic static library as
+    # static library flags.
+    #
+    # Here, we exclude the inc interface target which includes various system
+    # libraries we do not wish to archive for distribution.
+    set(EXCLUDE_LIST "inc")
+    if (CX_PLATFORM STREQUAL "darwin")
+        list(APPEND EXCLUDE_LIST "-framework CoreFoundation" "-framework Security")
+    endif()
+    set_property(GLOBAL PROPERTY VISITED_TARGETS_PROP "${EXCLUDE_LIST}")
+    set_property(GLOBAL PROPERTY QUIC_STATIC_LIBS "")
+    function(flatten_link_dependencies CURRENT_TARGET)
+        # Check first if we've already flattened the dependencies of this
+        # library (possible if the dependency exists multiple times in the
+        # same dependency graph)
+        get_property(VISITED_TARGETS GLOBAL PROPERTY VISITED_TARGETS_PROP)
+
+        if(${CURRENT_TARGET} IN_LIST VISITED_TARGETS)
+            return()
+        endif()
+
+        # Ensure we don't process this target again
+        list(APPEND VISITED_TARGETS ${CURRENT_TARGET})
+        set_property(GLOBAL PROPERTY VISITED_TARGETS_PROP ${VISITED_TARGETS})
+
+        # Uncomment to understand the dependency walk
+        # message(STATUS "Evaluating linker output and dependencies for ${CURRENT_TARGET}")
+
+        if(NOT TARGET ${CURRENT_TARGET})
+            string(FIND ${CURRENT_TARGET} "$<LINK_ONLY:" LINK_ONLY)
+            string(FIND ${CURRENT_TARGET} "Threads::Threads" THREADS_TARGET)
+            string(FIND ${CURRENT_TARGET} "pthread" PTHREADS_TARGET)
+            string(FIND ${CURRENT_TARGET} "dl" DL_TARGET)
+            string(FIND ${CURRENT_TARGET} "${CMAKE_STATIC_LIBRARY_SUFFIX}" SUFFIX_INDEX)
+            if(${SUFFIX_INDEX} EQUAL "-1")
+                string(APPEND CURRENT_TARGET "${CMAKE_STATIC_LIBRARY_SUFFIX}")
+            endif()
+
+            if(${LINK_ONLY} EQUAL "-1" AND ${THREADS_TARGET} EQUAL "-1" AND ${DL_TARGET} EQUAL "-1" AND ${PTHREADS_TARGET} EQUAL "-1")
+                # This is expected to be a generator expression that maps
+                # to a static library
+                set_property(
+                    GLOBAL
+                    APPEND
+                    PROPERTY QUIC_STATIC_LIBS
+                    \"${CURRENT_TARGET}\"
+                )
+            endif()
+            return()
+        endif()
+
+        # Check if this is an interface target
+        get_target_property(TARGET_TYPE ${CURRENT_TARGET} TYPE)
+
+        get_target_property(TARGET_OUTPUT ${CURRENT_TARGET} OUTPUT_NAME)
+        if(NOT TARGET_OUTPUT)
+            # The target's logical name is used as the base name if the
+            # output name property isn't specified
+            set(TARGET_OUTPUT ${CURRENT_TARGET})
+        endif()
+
+        if(${TARGET_TYPE} STREQUAL "STATIC_LIBRARY")
+            get_target_property(OUTPUT_DIR ${CURRENT_TARGET} ARCHIVE_OUTPUT_DIRECTORY)
+            if(OUTPUT_DIR)
+                set(STATIC_LIB "${OUTPUT_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${TARGET_OUTPUT}${CMAKE_STATIC_LIBRARY_SUFFIX}")
+                set_property(
+                    GLOBAL
+                    APPEND
+                    PROPERTY QUIC_STATIC_LIBS
+                    \"${STATIC_LIB}\"
+                )
+            endif()
+        endif()
+
+        # Next, recursively invoke this function for each dependency
+
+        get_target_property(LINK_LIBS ${CURRENT_TARGET} LINK_LIBRARIES)
+        get_target_property(INTERFACE_LIBS ${CURRENT_TARGET} INTERFACE_LINK_LIBRARIES)
+        get_target_property(DEPS ${CURRENT_TARGET} LINK_DEPENDS)
+
+        if(LINK_LIBS)
+            foreach(DEP ${LINK_LIBS})
+                flatten_link_dependencies(${DEP})
+            endforeach()
+        endif()
+
+        if(INTERFACE_LIBS)
+            foreach(DEP ${INTERFACE_LIBS})
+                flatten_link_dependencies(${DEP})
+            endforeach()
+        endif()
+    endfunction()
+
+    flatten_link_dependencies(msquic_static)
+
+    get_property(DEPS_LIST GLOBAL PROPERTY QUIC_STATIC_LIBS)
+    set(DEPS_STRING "")
+    foreach(line ${DEPS_LIST})
+        set(DEPS_STRING "${DEPS_STRING}${line} ")
+    endforeach(line)
+
+    # Uncomment to analyze which dependencies were traversed
+    # message(STATUS "DEPS: ${DEPS_LIST}, ${QUIC_BUILD_DIR}")
+
+    set(QUIC_STATIC_LIBRARY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${CMAKE_STATIC_LIBRARY_PREFIX}${QUIC_LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX})
+
+    if(APPLE)
+        # Emit all linker inputs at build time to resolve generator expressions
+        set(QUIC_DEPS_FILE ${CMAKE_CURRENT_BINARY_DIR}/$<IF:$<CONFIG:Debug>,Debug,Release>/quicdeps.txt)
+        file(
+            GENERATE
+            OUTPUT ${QUIC_DEPS_FILE}
+            CONTENT "${DEPS_STRING}"
+        )
+
+        # Run archiver tool
+        add_custom_command(
+            OUTPUT ${QUIC_STATIC_LIBRARY}
+            COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
+            COMMAND libtool -static -o \"${QUIC_STATIC_LIBRARY}\" @${QUIC_DEPS_FILE}
+            DEPENDS ${QUIC_DEPS_FILE}
+            DEPENDS core
+            DEPENDS msquic_platform
+            DEPENDS msquic_static
+        )
+    elseif(WIN32)
+        set(NODEFAULTS "")
+        foreach(EXCLUDE ${EXCLUDE_LIST})
+            string(APPEND NODEFAULTS "/NODEFAULTLIB:${EXCLUDE}${CMAKE_STATIC_LIBRARY_SUFFIX} ")
+        endforeach()
+
+        # Exclude libraries for the corresponding msvcrts
+        # https://docs.microsoft.com/en-us/cpp/error-messages/tool-errors/linker-tools-warning-lnk4098?view=msvc-160
+        string(APPEND NODEFAULTS "/NODEFAULTLIB:msvcrt.lib /NODEFAULTLIB:msvcrtd.lib /NODEFAULTLIB:libcmt.lib /NODEFAULTLIB:libcmtd.lib")
+        # Uncomment to inspect which libraries will have referenced symbols ignored
+        # message(STATUS "NODEFAULTS: ${NODEFAULTS}")
+
+        # Emit all linker inputs at build time to resolve generator expressions
+        set(QUIC_DEPS_FILE ${CMAKE_CURRENT_BINARY_DIR}/$<IF:$<CONFIG:Debug>,Debug,Release>/quicdeps.txt)
+        file(
+            GENERATE
+            OUTPUT ${QUIC_DEPS_FILE}
+            CONTENT "/OUT:\"${QUIC_STATIC_LIBRARY}\" ${DEPS_STRING} ${NODEFAULTS}"
+        )
+
+        # Run archiver tool (lib.exe) on our generated linker args
+        add_custom_command(
+            OUTPUT ${QUIC_STATIC_LIBRARY}
+            COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
+            COMMAND ${CMAKE_AR} @${QUIC_DEPS_FILE}
+            COMMAND ${CMAKE_COMMAND} -E copy "${QUIC_STATIC_LIBRARY}" "${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}"
+            DEPENDS ${QUIC_DEPS_FILE}
+            DEPENDS core
+            DEPENDS msquic_platform
+            DEPENDS msquic_static
+        )
+    else()
+        # Emit all linker inputs at build time to resolve generator expressions
+        set(QUIC_DEPS_FILE ${CMAKE_CURRENT_BINARY_DIR}/$<IF:$<CONFIG:Debug>,Debug,Release>/quicdeps.txt)
+        set(DEPS_LINES "")
+        foreach(line ${DEPS_LIST})
+            # message(STATUS ${line})
+            set(DEPS_LINES "${DEPS_LINES}addlib ${line}\n")
+        endforeach(line)
+        file(
+            GENERATE
+            OUTPUT ${QUIC_DEPS_FILE}
+            CONTENT "create \"${QUIC_STATIC_LIBRARY}\"\n${DEPS_LINES}save\nend\n"
+        )
+
+        # Run archiver tool
+        add_custom_command(
+            OUTPUT ${QUIC_STATIC_LIBRARY}
+            COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
+            COMMAND ${CMAKE_AR} -M < ${QUIC_DEPS_FILE}
+            DEPENDS ${QUIC_DEPS_FILE}
+            DEPENDS core
+            DEPENDS msquic_platform
+            DEPENDS msquic_static
+        )
+    endif()
+
+    add_custom_target(
+        msquic_lib ALL
+        DEPENDS
+        ${QUIC_STATIC_LIBRARY}
+    )
+
+    set_property(TARGET msquic_lib PROPERTY FOLDER "${QUIC_FOLDER_PREFIX}libraries")
+
+    message(STATUS "${QUIC_STATIC_LIBRARY}")
+
+    # Provide interface library to link the monolithic library
+    # NOTE: It is important that this interface library NOT link directly to
+    # msquic_static or the linker flags will inadvertently bring in symbols
+    # that are already linked as part of the static library. Users can link to
+    # msquic_static directly if they don't need the monolithic library.
+    add_library(msquic INTERFACE)
+    target_compile_definitions(msquic INTERFACE QUIC_BUILD_STATIC)
+    target_link_libraries(msquic
+        INTERFACE
+        ${QUIC_STATIC_LIBRARY}
+    )
+    if (CX_PLATFORM STREQUAL "darwin")
+        target_link_libraries(msquic INTERFACE "-framework CoreFoundation" "-framework Security")
+    endif()
+    add_dependencies(msquic msquic_lib)
+endif()
+
+set_property(TARGET msquic PROPERTY FOLDER "${QUIC_FOLDER_PREFIX}libraries")
+
+if(WIN32)
+    set(MSQUIC_LINK_FLAGS "/DEF:\"${CMAKE_CURRENT_BINARY_DIR}/msquic.def\"")
+
+    # Indicate CET Shadow Stack support for supported architectures
+    if (${SYSTEM_PROCESSOR} STREQUAL "x64" OR
+        ${SYSTEM_PROCESSOR} STREQUAL "AMD64" OR
+        ${SYSTEM_PROCESSOR} STREQUAL "x86" OR
+        ${SYSTEM_PROCESSOR} STREQUAL "win32")
+
+        string(APPEND MSQUIC_LINK_FLAGS " /CETCOMPAT")
+    endif()
+
+    if(QUIC_CI)
+        string(APPEND MSQUIC_LINK_FLAGS " /PROFILE")
+    endif()
+    SET_TARGET_PROPERTIES(msquic PROPERTIES LINK_FLAGS "${MSQUIC_LINK_FLAGS}")
+elseif (CX_PLATFORM STREQUAL "linux")
+    SET_TARGET_PROPERTIES(msquic
+        PROPERTIES LINK_FLAGS "-Wl,--version-script=\"${CMAKE_CURRENT_SOURCE_DIR}/linux/exports.txt\"")
+elseif (CX_PLATFORM STREQUAL "darwin")
+    SET_TARGET_PROPERTIES(msquic
+        PROPERTIES LINK_FLAGS "-exported_symbols_list \"${CMAKE_CURRENT_SOURCE_DIR}/darwin/exports.txt\"")
+endif()
+
+file(GLOB PUBLIC_HEADERS "../inc/*.h" "../inc/*.hpp")
+
+if(QUIC_TLS_LIB STREQUAL "quictls" OR QUIC_TLS_LIB STREQUAL "openssl")
+    list(APPEND OTHER_TARGETS OpenSSL OpenSSLQuic)
+endif()
+
+if(WIN32)
+    list(APPEND OTHER_TARGETS MsQuicEtw_Header)
+endif()
+
+if(BUILD_SHARED_LIBS)
+    install(TARGETS msquic msquic_platform inc logging_inc warnings main_binary_link_args ${OTHER_TARGETS}
+        EXPORT msquic
+        RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
+        LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+        ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+    )
+else()
+    install(FILES "${QUIC_STATIC_LIBRARY}"
+        DESTINATION lib
+    )
+endif()
+install(FILES ${PUBLIC_HEADERS} DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
+
+configure_file(msquic-config.cmake.in ${CMAKE_BINARY_DIR}/msquic-config.cmake @ONLY)
+
+install(FILES ${CMAKE_BINARY_DIR}/msquic-config.cmake DESTINATION share/msquic)
+
+if(BUILD_SHARED_LIBS)
+    install(EXPORT msquic NAMESPACE msquic:: DESTINATION share/msquic)
+endif()
+
+if (MSVC AND NOT QUIC_SANITIZER_ACTIVE AND BUILD_SHARED_LIBS)
+    target_compile_options(msquic PRIVATE /analyze)
+endif()