Add ability to joining a voice room while it's in progress

This commit is contained in:
mplorentz
2026-03-03 08:54:42 -05:00
committed by hodlbod
parent 9bd57b0caa
commit 559df4b948
2 changed files with 59 additions and 12 deletions
+21 -7
View File
@@ -6,7 +6,12 @@
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import RoomName from "@app/components/RoomName.svelte" import RoomName from "@app/components/RoomName.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {deriveVoiceParticipants, joinVoiceRoom, currentVoiceSession} from "@app/voice" import {
deriveVoiceParticipants,
joinVoiceRoom,
leaveVoiceRoom,
currentVoiceSession,
} from "@app/voice"
interface Props { interface Props {
url: string url: string
@@ -18,17 +23,29 @@
const participants = deriveVoiceParticipants(url, h) const participants = deriveVoiceParticipants(url, h)
const isActive = $derived($currentVoiceSession?.url === url && $currentVoiceSession?.h === h) const isActive = $derived($currentVoiceSession?.url === url && $currentVoiceSession?.h === h)
let isJoining = $state(false) let isJoining = $state(false)
let joinAbortController: AbortController | undefined
const handleClick = async () => { const handleClick = async () => {
if (isJoining) return if (isActive) {
await leaveVoiceRoom()
return
}
if (isJoining) {
joinAbortController?.abort()
return
}
joinAbortController = new AbortController()
isJoining = true isJoining = true
try { try {
await joinVoiceRoom(url, h) await joinVoiceRoom(url, h, joinAbortController.signal)
} catch (e) { } 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) const message = e instanceof Error ? e.message : String(e)
pushToast({theme: "error", message: `Failed to join voice room: ${message}`}) pushToast({theme: "error", message: `Failed to join voice room: ${message}`})
} finally { } finally {
isJoining = false isJoining = false
joinAbortController = undefined
} }
} }
@@ -40,10 +57,7 @@
</script> </script>
<div> <div>
<SecondaryNavItem <SecondaryNavItem onclick={handleClick} class={isActive ? "!bg-base-100 !text-base-content" : ""}>
onclick={handleClick}
disabled={isJoining}
class={isActive ? "!bg-base-100 !text-base-content" : ""}>
{#if isJoining} {#if isJoining}
<span class="loading loading-spinner loading-sm"></span> <span class="loading loading-spinner loading-sm"></span>
{:else} {:else}
+38 -5
View File
@@ -24,6 +24,7 @@ export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
const fetchLivekitToken = async ( const fetchLivekitToken = async (
url: string, url: string,
groupId: string, groupId: string,
signal?: AbortSignal,
): Promise<{server_url: string; participant_token: string}> => { ): Promise<{server_url: string; participant_token: string}> => {
const httpUrl = url const httpUrl = url
.replace(/^wss:\/\//, "https://") .replace(/^wss:\/\//, "https://")
@@ -34,6 +35,8 @@ const fetchLivekitToken = async (
const $signer = signer.get() const $signer = signer.get()
if (!$signer) throw new Error("No signer available") if (!$signer) throw new Error("No signer available")
if (signal?.aborted) throw new Error("Join cancelled")
const authHeader = await getToken( const authHeader = await getToken(
endpoint, endpoint,
"GET", "GET",
@@ -47,9 +50,16 @@ const fetchLivekitToken = async (
true, true,
) )
const response = await fetch(endpoint, { let response: Response
headers: {Authorization: authHeader}, 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) { if (!response.ok) {
const text = await response.text() 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<void> => {
const session = get(currentVoiceSession) const session = get(currentVoiceSession)
if (session) { if (session) {
@@ -112,7 +126,9 @@ export const joinVoiceRoom = async (url: string, h: string) => {
await leaveVoiceRoom() 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({ const room = new Room({
adaptiveStream: true, adaptiveStream: true,
@@ -144,6 +160,17 @@ export const joinVoiceRoom = async (url: string, h: string) => {
track.detach().forEach(el => el.remove()) 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 const CONNECT_TIMEOUT_MS = 5_000
try { try {
@@ -158,8 +185,14 @@ export const joinVoiceRoom = async (url: string, h: string) => {
]) ])
} catch (e) { } catch (e) {
room.disconnect() room.disconnect()
if (signal?.aborted) {
throw new Error("Join cancelled")
}
throw e throw e
} finally {
signal?.removeEventListener("abort", onAbort)
} }
if (signal?.aborted) throw new Error("Join cancelled")
await room.localParticipant.setMicrophoneEnabled(true) await room.localParticipant.setMicrophoneEnabled(true)
currentVoiceSession.set({url, h, room, muted: false}) currentVoiceSession.set({url, h, room, muted: false})