feature/23-voice-room/poc #93

Merged
hodlbod merged 68 commits from feature/23-voice-room/poc into dev 2026-03-16 20:38:06 +00:00
2 changed files with 52 additions and 7 deletions
Showing only changes of commit 6330150c66 - Show all commits
+26 -2
View File
@@ -15,6 +15,7 @@
import ModalBody from "@lib/components/ModalBody.svelte"
import {pushToast} from "@app/util/toast"
import {uploadFile} from "@app/core/commands"
import {checkRelayHasLivekit} from "@app/voice"
type RoomMode = "text" | "voice" | "both"
6
@@ -46,10 +47,29 @@
const values = $state(initialValues)
let roomMode = $state<RoomMode>(getRoomModeFromEvent(initialValues.event))
let relayHasLivekit = $state<boolean | undefined>(undefined)
$effect(() => {
const u = url
let cancelled = false
checkRelayHasLivekit(u).then(has => {
if (!cancelled) relayHasLivekit = has
})
return () => {
cancelled = true
}
})
hodlbod marked this conversation as resolved Outdated
Outdated
Review

Better would be to add a util to app/state called deriveHasLiveKit(url) that can cache the request. This complex effect behavior is something I've noticed a lot recently, but we can assume url will not change here.

Better would be to add a util to app/state called `deriveHasLiveKit(url)` that can cache the request. This complex effect behavior is something I've noticed a lot recently, but we can assume url will not change here.
const submit = async () => {
const room = $state.snapshot(values)
if ((roomMode === "voice" || roomMode === "both") && !relayHasLivekit) {
return pushToast({
theme: "error",
message: "This relay does not support voice rooms.",
})
}
hodlbod marked this conversation as resolved Outdated
Outdated
Review

This should never run if we make the form change below.

This should never run if we make the form change below.
if (imageFile) {
const {error, result} = await uploadFile(imageFile, {
maxWidth: 256,
7
@@ -211,8 +231,12 @@
{#snippet input()}
<select class="select select-bordered w-full" bind:value={roomMode} aria-label="Room type">
<option value="text">Text only</option>
<option value="both">Text and voice</option>
<option value="voice">Voice only</option>
<option value="both" disabled={relayHasLivekit === false}>
Text and voice{relayHasLivekit === false ? " (not setup)" : ""}
</option>
<option value="voice" disabled={relayHasLivekit === false}>
Voice only{relayHasLivekit === false ? " (not setup)" : ""}
</option>
</select>
{/snippet}
</FieldInline>
+26 -5
View File
@@ -1,3 +1,7 @@
/**
* Voice rooms via LiveKit. Note: Voice does not work on localhost in Firefox
* (ICE candidate gathering fails). Use Chrome or test from deployed HTTPS.
*/
import {DisconnectReason, Room, RoomEvent, Track} from "livekit-client"
import {getToken} from "nostr-tools/nip98"
import {derived, get, writable} from "svelte/store"
@@ -9,6 +13,27 @@ import {pushToast} from "@app/util/toast"
export const ROOM_PRESENCE = 10312
const livekitEndpoint = (url: string, groupId: string) => {
const httpUrl = url
.replace(/^wss:\/\//, "https://")
.replace(/^ws:\/\//, "http://")
.replace(/\/$/, "")
return `${httpUrl}/.well-known/nip29/livekit/${groupId}`
}
export const checkRelayHasLivekit = async (url: string): Promise<boolean> => {
const endpoint = livekitEndpoint(url, "nop")
try {
// Currently we are hitting the API with no auth because zooid returns a 401 livekit
// is configured and 404 if it is not. But we need a standardized solution in the NIP.
const response = await fetch(endpoint)
return response.status === 401
} catch {
return false
}
}
const PRESENCE_INTERVAL_MS = 60_000
const PRESENCE_EXPIRY_S = 300
1
@@ -26,11 +51,7 @@ const fetchLivekitToken = async (
groupId: string,
signal?: AbortSignal,
): Promise<{server_url: string; participant_token: string}> => {
const httpUrl = url
.replace(/^wss:\/\//, "https://")
.replace(/^ws:\/\//, "http://")
.replace(/\/$/, "")
const endpoint = `${httpUrl}/.well-known/nip29/livekit/${groupId}`
const endpoint = livekitEndpoint(url, groupId)
const $signer = signer.get()
if (!$signer) throw new Error("No signer available")
7