From aa162d88c5337a77653e90779d717f8ea8320a1c Mon Sep 17 00:00:00 2001 From: jevb Date: Fri, 20 Mar 2026 05:46:19 +0100 Subject: [PATCH] feat: replace client WebRTC with LiveKit SDK (Phase 2) Client changes: - Create livekitSession.ts (~400 lines, replaces 1105-line voiceSession.ts): Room lifecycle, device switching via switchActiveDevice, RNNoise pre-processing for enhanced NS - types.ts: add VoiceTokenPayload, remove VoiceOffer/Answer/Ice payloads, remove ThresholdMode - dispatcher.ts: replace voice_offer/answer/ice handlers with single voice_token handler - VoiceCallbacks.ts: swap imports to livekitSession - VoiceAudioTab.ts: remove silence suppression toggle, inline threshold math (removed vad.ts dependency) - Update all files importing from deleted modules Deleted files (11 source + 5 test): - webrtc.ts, vad.ts, voiceSession.ts, audio.ts, video.ts, Soundboard.ts + their test files Kept: noise-suppression.ts + @jitsi/rnnoise-wasm (Krisp is LiveKit Cloud only, not available for self-hosted) Added: livekit-client@^2.17.3 TypeScript compiles with zero errors (tsc --noEmit). --- Client/tauri-client/package-lock.json | 139 ++- Client/tauri-client/package.json | 3 +- .../src/components/ChannelSidebar.ts | 2 +- .../tauri-client/src/components/Soundboard.ts | 117 -- .../src/components/VoiceChannel.ts | 2 +- .../src/components/settings/LogsTab.ts | 142 +-- .../src/components/settings/VoiceAudioTab.ts | 16 +- Client/tauri-client/src/lib/audio.ts | 155 --- Client/tauri-client/src/lib/dispatcher.ts | 22 +- Client/tauri-client/src/lib/livekitSession.ts | 605 +++++++++ Client/tauri-client/src/lib/ptt.ts | 2 +- Client/tauri-client/src/lib/types.ts | 35 +- Client/tauri-client/src/lib/vad.ts | 183 --- Client/tauri-client/src/lib/video.ts | 144 --- Client/tauri-client/src/lib/voiceSession.ts | 1104 ----------------- Client/tauri-client/src/lib/webrtc.ts | 394 ------ Client/tauri-client/src/main.ts | 2 +- Client/tauri-client/src/pages/MainPage.ts | 13 +- .../pages/main-page/VideoModeController.ts | 2 +- .../src/pages/main-page/VoiceCallbacks.ts | 2 +- Client/tauri-client/src/stores/auth.store.ts | 2 +- Client/tauri-client/tests/unit/ptt.test.ts | 8 +- .../tests/unit/settings-overlay.test.ts | 2 +- .../tests/unit/soundboard.test.ts | 147 --- Client/tauri-client/tests/unit/types.test.ts | 10 +- Client/tauri-client/tests/unit/vad.test.ts | 35 - .../tests/unit/video-mode-controller.test.ts | 2 +- Client/tauri-client/tests/unit/video.test.ts | 242 ---- .../tests/unit/voice-callbacks.test.ts | 2 +- .../tests/unit/voice-session-video.test.ts | 435 ------- Client/tauri-client/tests/unit/webrtc.test.ts | 247 ---- 31 files changed, 781 insertions(+), 3435 deletions(-) delete mode 100644 Client/tauri-client/src/components/Soundboard.ts delete mode 100644 Client/tauri-client/src/lib/audio.ts create mode 100644 Client/tauri-client/src/lib/livekitSession.ts delete mode 100644 Client/tauri-client/src/lib/vad.ts delete mode 100644 Client/tauri-client/src/lib/video.ts delete mode 100644 Client/tauri-client/src/lib/voiceSession.ts delete mode 100644 Client/tauri-client/src/lib/webrtc.ts delete mode 100644 Client/tauri-client/tests/unit/soundboard.test.ts delete mode 100644 Client/tauri-client/tests/unit/vad.test.ts delete mode 100644 Client/tauri-client/tests/unit/video.test.ts delete mode 100644 Client/tauri-client/tests/unit/voice-session-video.test.ts delete mode 100644 Client/tauri-client/tests/unit/webrtc.test.ts diff --git a/Client/tauri-client/package-lock.json b/Client/tauri-client/package-lock.json index f678639b..928e163e 100644 --- a/Client/tauri-client/package-lock.json +++ b/Client/tauri-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "owncord-client", - "version": "0.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "owncord-client", - "version": "0.1.0", + "version": "1.2.0", "dependencies": { "@jitsi/rnnoise-wasm": "^0.2.1", "@tauri-apps/api": "^2.10.1", @@ -18,7 +18,8 @@ "@tauri-apps/plugin-opener": "^2.5.3", "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-store": "^2", - "@tauri-apps/plugin-updater": "^2.10.0" + "@tauri-apps/plugin-updater": "^2.10.0", + "livekit-client": "^2.17.3" }, "devDependencies": { "@playwright/test": "^1", @@ -178,6 +179,12 @@ "specificity": "bin/cli.js" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@csstools/color-helpers": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", @@ -850,6 +857,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@livekit/mutex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@livekit/mutex/-/mutex-1.1.1.tgz", + "integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==", + "license": "Apache-2.0" + }, + "node_modules/@livekit/protocol": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.44.0.tgz", + "integrity": "sha512-/vfhDUGcUKO8Q43r6i+5FrDhl5oZjm/X3U4x2Iciqvgn5C8qbj+57YPcWSJ1kyIZm5Cm6AV2nAPjMm3ETD/iyg==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1553,6 +1575,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dom-mediacapture-record": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", + "integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==", + "license": "MIT", + "peer": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2011,6 +2040,15 @@ "@types/estree": "^1.0.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2250,6 +2288,15 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -2308,6 +2355,39 @@ "node": "20 || >=22" } }, + "node_modules/livekit-client": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.17.3.tgz", + "integrity": "sha512-htwsAL/BMylY/zwdcT/z00U789csbi9DldSW7DO+5tz7Q15pwu++E1X+ZdtZDfkmlysfQLLibdcqlyg9FY7veQ==", + "license": "Apache-2.0", + "dependencies": { + "@livekit/mutex": "1.1.1", + "@livekit/protocol": "1.44.0", + "events": "^3.3.0", + "jose": "^6.1.0", + "loglevel": "^1.9.2", + "sdp-transform": "^2.15.0", + "tslib": "2.8.1", + "typed-emitter": "^2.1.0", + "webrtc-adapter": "^9.0.1" + }, + "peerDependencies": { + "@types/dom-mediacapture-record": "^1" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -2644,6 +2724,16 @@ "fsevents": "~2.3.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -2657,6 +2747,21 @@ "node": ">=v12.22.7" } }, + "node_modules/sdp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz", + "integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==", + "license": "MIT" + }, + "node_modules/sdp-transform": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", + "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==", + "license": "MIT", + "bin": { + "sdp-verify": "checker.js" + } + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -3003,6 +3108,21 @@ "node": ">=20" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3221,6 +3341,19 @@ "node": ">=20" } }, + "node_modules/webrtc-adapter": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.4.tgz", + "integrity": "sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/whatwg-mimetype": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", diff --git a/Client/tauri-client/package.json b/Client/tauri-client/package.json index 6ecad5a9..812dd9f8 100644 --- a/Client/tauri-client/package.json +++ b/Client/tauri-client/package.json @@ -38,6 +38,7 @@ "@tauri-apps/plugin-opener": "^2.5.3", "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-store": "^2", - "@tauri-apps/plugin-updater": "^2.10.0" + "@tauri-apps/plugin-updater": "^2.10.0", + "livekit-client": "^2.17.3" } } diff --git a/Client/tauri-client/src/components/ChannelSidebar.ts b/Client/tauri-client/src/components/ChannelSidebar.ts index 2f3bd758..c8a5fefc 100644 --- a/Client/tauri-client/src/components/ChannelSidebar.ts +++ b/Client/tauri-client/src/components/ChannelSidebar.ts @@ -26,7 +26,7 @@ import { isCategoryCollapsed, } from "@stores/ui.store"; import { voiceStore, getChannelVoiceUsers } from "@stores/voice.store"; -import { setUserVolume, getUserVolume } from "@lib/voiceSession"; +import { setUserVolume, getUserVolume } from "@lib/livekitSession"; // --------------------------------------------------------------------------- // Per-user volume context menu (right-click on voice user row) diff --git a/Client/tauri-client/src/components/Soundboard.ts b/Client/tauri-client/src/components/Soundboard.ts deleted file mode 100644 index 28f1fc8e..00000000 --- a/Client/tauri-client/src/components/Soundboard.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Step 6.53 — Soundboard component. - * Grid of sound buttons with cooldown enforcement (1 play per 3s). - */ - -import { createElement, appendChildren, setText, clearChildren } from "@lib/dom"; -import type { MountableComponent } from "@lib/safe-render"; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export interface SoundItem { - readonly id: number; - readonly name: string; - readonly durationMs: number; -} - -export interface SoundboardOptions { - readonly sounds: readonly SoundItem[]; - readonly onPlaySound: (soundId: number) => void; -} - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const COOLDOWN_MS = 3_000; - -function formatDuration(ms: number): string { - return `${(ms / 1000).toFixed(1)}s`; -} - -// --------------------------------------------------------------------------- -// Factory -// --------------------------------------------------------------------------- - -export function createSoundboard(options: SoundboardOptions): MountableComponent { - let root: HTMLDivElement | null = null; - let cooldownTimer: ReturnType | null = null; - const ac = new AbortController(); - - function mount(container: Element): void { - root = createElement("div", { class: "soundboard" }); - - if (options.sounds.length === 0) { - const empty = createElement( - "div", - { class: "soundboard__empty" }, - "No sounds available", - ); - root.appendChild(empty); - container.appendChild(root); - return; - } - - const grid = createElement("div", { class: "soundboard__grid" }); - const buttons: HTMLButtonElement[] = []; - - for (const sound of options.sounds) { - const btn = createElement("button", { class: "sound-btn", type: "button" }); - const nameSpan = createElement("span", { class: "sound-btn__name" }, sound.name); - const durSpan = createElement( - "span", - { class: "sound-btn__duration" }, - formatDuration(sound.durationMs), - ); - - appendChildren(btn, nameSpan, durSpan); - - btn.addEventListener("click", () => { - if (btn.disabled) return; - options.onPlaySound(sound.id); - startCooldown(buttons); - }, { signal: ac.signal }); - - buttons.push(btn); - grid.appendChild(btn); - } - - root.appendChild(grid); - container.appendChild(root); - } - - function startCooldown(buttons: readonly HTMLButtonElement[]): void { - for (const btn of buttons) { - btn.disabled = true; - btn.classList.add("sound-btn--cooldown"); - } - - if (cooldownTimer !== null) { - clearTimeout(cooldownTimer); - } - - cooldownTimer = setTimeout(() => { - cooldownTimer = null; - for (const btn of buttons) { - btn.disabled = false; - btn.classList.remove("sound-btn--cooldown"); - } - }, COOLDOWN_MS); - } - - function destroy(): void { - ac.abort(); - if (cooldownTimer !== null) { - clearTimeout(cooldownTimer); - cooldownTimer = null; - } - if (root !== null) { - root.remove(); - root = null; - } - } - - return { mount, destroy }; -} diff --git a/Client/tauri-client/src/components/VoiceChannel.ts b/Client/tauri-client/src/components/VoiceChannel.ts index 3f82ed87..fde27da0 100644 --- a/Client/tauri-client/src/components/VoiceChannel.ts +++ b/Client/tauri-client/src/components/VoiceChannel.ts @@ -8,7 +8,7 @@ import { createElement, appendChildren, clearChildren, setText } from "@lib/dom" import { voiceStore } from "@stores/voice.store"; import type { VoiceUser } from "@stores/voice.store"; import { membersStore } from "@stores/members.store"; -import { setUserVolume, getUserVolume } from "@lib/voiceSession"; +import { setUserVolume, getUserVolume } from "@lib/livekitSession"; import { authStore } from "@stores/auth.store"; export interface VoiceChannelOptions { diff --git a/Client/tauri-client/src/components/settings/LogsTab.ts b/Client/tauri-client/src/components/settings/LogsTab.ts index 312c225e..4d3f538f 100644 --- a/Client/tauri-client/src/components/settings/LogsTab.ts +++ b/Client/tauri-client/src/components/settings/LogsTab.ts @@ -6,7 +6,7 @@ import { createElement, appendChildren, clearChildren } from "@lib/dom"; import { getLogBuffer, clearLogBuffer, addLogListener, setLogLevel } from "@lib/logger"; import type { LogEntry, LogLevel } from "@lib/logger"; import type { TabName } from "../SettingsOverlay"; -import { getSessionDebugInfo, measureStreamLevel, getRemoteStreams, getLocalProcessedStream } from "@lib/voiceSession"; +import { getSessionDebugInfo } from "@lib/livekitSession"; // --------------------------------------------------------------------------- // Constants @@ -175,53 +175,7 @@ export function createLogsTab( function refreshDiag(): void { const info = getSessionDebugInfo(); - const ctx = info.sharedAudioCtx as { state: string; sampleRate: number } | null; - const localTracks = info.localTracks as Array<{ id: string; enabled: boolean; muted: boolean; readyState: string }>; - const remoteEls = info.remoteAudioElements as Array<{ - streamId: string; userId: number; audioPaused: boolean; audioMuted: boolean; - audioVolume: number; audioReadyState: number; hasSrcObject: boolean; - gainValue: number | string; - tracks: Array<{ id: string; enabled: boolean; muted: boolean; readyState: string }>; - }>; - const webrtcStreams = info.webrtcRemoteStreams as Array<{ - streamId: string; trackCount: number; - audioTracks: Array<{ id: string; enabled: boolean; muted: boolean; readyState: string }>; - }>; - - const lines: string[] = [ - `=== Session ===`, - `WebRTC: ${info.hasWebrtc} VAD: ${info.hasVad} Suppressor: ${info.hasNoiseSuppressor}`, - `Join in progress: ${info.joinInProgress} Silence suppression: ${info.silenceSuppressionEnabled}`, - `SharedAudioCtx: ${ctx ? `${ctx.state} @ ${ctx.sampleRate}Hz` : "none"}`, - ``, - `=== Local Audio ===`, - `Stream: ${info.hasLocalStream} Processed: ${info.hasProcessedStream}`, - ]; - for (const t of localTracks) { - lines.push(` Track ${t.id.slice(0, 8)}: enabled=${t.enabled} muted=${t.muted} state=${t.readyState}`); - } - - lines.push(``, `=== WebRTC Remote Streams ===`); - if (webrtcStreams.length === 0) lines.push(` (none)`); - for (const s of webrtcStreams) { - lines.push(` Stream ${s.streamId}: ${s.trackCount} tracks`); - for (const t of s.audioTracks) { - lines.push(` Track ${t.id.slice(0, 8)}: enabled=${t.enabled} muted=${t.muted} state=${t.readyState}`); - } - } - - lines.push(``, `=== Remote Audio Elements ===`); - if (remoteEls.length === 0) lines.push(` (none)`); - for (const el of remoteEls) { - lines.push(` [user ${el.userId}] stream=${el.streamId}`); - lines.push(`