From 8b0ca5399d8017020fd93c3b10edd4e68c4bbd40 Mon Sep 17 00:00:00 2001 From: mplorentz Date: Tue, 3 Mar 2026 08:54:42 -0500 Subject: [PATCH] Add ability to joining a voice room while it's in progress --- src/app/components/VoiceRoomItem.svelte | 28 ++++++++++++---- src/app/voice.ts | 43 ++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/app/components/VoiceRoomItem.svelte b/src/app/components/VoiceRoomItem.svelte index 5df903d5..2fd4f9b7 100644 --- a/src/app/components/VoiceRoomItem.svelte +++ b/src/app/components/VoiceRoomItem.svelte @@ -6,7 +6,12 @@ import ProfileCircle from "@app/components/ProfileCircle.svelte" import RoomName from "@app/components/RoomName.svelte" import {pushToast} from "@app/util/toast" - import {deriveVoiceParticipants, joinVoiceRoom, currentVoiceSession} from "@app/voice" + import { + deriveVoiceParticipants, + joinVoiceRoom, + leaveVoiceRoom, + currentVoiceSession, + } from "@app/voice" interface Props { url: string @@ -18,17 +23,29 @@ const participants = deriveVoiceParticipants(url, h) const isActive = $derived($currentVoiceSession?.url === url && $currentVoiceSession?.h === h) let isJoining = $state(false) + let joinAbortController: AbortController | undefined const handleClick = async () => { - if (isJoining) return + if (isActive) { + await leaveVoiceRoom() + return + } + if (isJoining) { + joinAbortController?.abort() + return + } + joinAbortController = new AbortController() isJoining = true try { - await joinVoiceRoom(url, h) + await joinVoiceRoom(url, h, joinAbortController.signal) } catch (e) { + if (e instanceof Error && e.message === "Join cancelled") return + if (e instanceof DOMException && e.name === "AbortError") return const message = e instanceof Error ? e.message : String(e) pushToast({theme: "error", message: `Failed to join voice room: ${message}`}) } finally { isJoining = false + joinAbortController = undefined } } @@ -40,10 +57,7 @@
- + {#if isJoining} {:else} diff --git a/src/app/voice.ts b/src/app/voice.ts index 365be1e2..b3db81c7 100644 --- a/src/app/voice.ts +++ b/src/app/voice.ts @@ -24,6 +24,7 @@ export const currentVoiceSession = writable(undefined) const fetchLivekitToken = async ( url: string, groupId: string, + signal?: AbortSignal, ): Promise<{server_url: string; participant_token: string}> => { const httpUrl = url .replace(/^wss:\/\//, "https://") @@ -34,6 +35,8 @@ const fetchLivekitToken = async ( const $signer = signer.get() if (!$signer) throw new Error("No signer available") + if (signal?.aborted) throw new Error("Join cancelled") + const authHeader = await getToken( endpoint, "GET", @@ -47,9 +50,16 @@ const fetchLivekitToken = async ( true, ) - const response = await fetch(endpoint, { - headers: {Authorization: authHeader}, - }) + let response: Response + try { + response = await fetch(endpoint, { + headers: {Authorization: authHeader}, + signal, + }) + } catch (e) { + if (e instanceof DOMException && e.name === "AbortError") throw new Error("Join cancelled") + throw e + } if (!response.ok) { const text = await response.text() @@ -104,7 +114,11 @@ const stopPresenceHeartbeat = () => { } } -export const joinVoiceRoom = async (url: string, h: string) => { +export const joinVoiceRoom = async ( + url: string, + h: string, + signal?: AbortSignal, +): Promise => { const session = get(currentVoiceSession) if (session) { @@ -112,7 +126,9 @@ export const joinVoiceRoom = async (url: string, h: string) => { await leaveVoiceRoom() } - const {server_url, participant_token} = await fetchLivekitToken(url, h) + const {server_url, participant_token} = await fetchLivekitToken(url, h, signal) + + if (signal?.aborted) throw new Error("Join cancelled") const room = new Room({ adaptiveStream: true, @@ -144,6 +160,17 @@ export const joinVoiceRoom = async (url: string, h: string) => { track.detach().forEach(el => el.remove()) }) + const onAbort = () => { + room.disconnect() + } + if (signal) { + if (signal.aborted) { + room.disconnect() + throw new Error("Join cancelled") + } + signal.addEventListener("abort", onAbort, {once: true}) + } + const CONNECT_TIMEOUT_MS = 5_000 try { @@ -158,8 +185,14 @@ export const joinVoiceRoom = async (url: string, h: string) => { ]) } catch (e) { room.disconnect() + if (signal?.aborted) { + throw new Error("Join cancelled") + } throw e + } finally { + signal?.removeEventListener("abort", onAbort) } + if (signal?.aborted) throw new Error("Join cancelled") await room.localParticipant.setMicrophoneEnabled(true) currentVoiceSession.set({url, h, room, muted: false})