Move livekit auth to relay
This commit is contained in:
@@ -55,6 +55,8 @@
|
||||
notificationSettings,
|
||||
deriveShouldNotify,
|
||||
displayRoom,
|
||||
roomHasLivekit,
|
||||
roomIsNoText,
|
||||
} from "@app/core/state"
|
||||
import {setSpaceNotifications} from "@app/core/commands"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
@@ -257,9 +259,13 @@
|
||||
<div class="h-2"></div>
|
||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||
{/if}
|
||||
{#each $userRooms as h, i (h)}
|
||||
<SpaceMenuRoomItem notify {replaceState} {url} {h} />
|
||||
<VoiceRoomItem {url} {h} />
|
||||
{#each $userRooms as h (h)}
|
||||
{#if !roomIsNoText(url, h)}
|
||||
<SpaceMenuRoomItem notify {replaceState} {url} {h} />
|
||||
{/if}
|
||||
{#if roomHasLivekit(url, h)}
|
||||
<VoiceRoomItem {url} {h} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if $otherRooms.length > 0}
|
||||
<div class="h-2"></div>
|
||||
@@ -277,9 +283,13 @@
|
||||
<input bind:value={term} onblur={clearTerm} class="grow" />
|
||||
</label>
|
||||
{/if}
|
||||
{#each $roomSearch.searchValues(term) as h, i (h)}
|
||||
<SpaceMenuRoomItem {replaceState} {url} {h} />
|
||||
<VoiceRoomItem {url} {h} />
|
||||
{#each $roomSearch.searchValues(term) as h (h)}
|
||||
{#if !roomIsNoText(url, h)}
|
||||
<SpaceMenuRoomItem {replaceState} {url} {h} />
|
||||
{/if}
|
||||
{#if roomHasLivekit(url, h)}
|
||||
<VoiceRoomItem {url} {h} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if $canCreateRoom}
|
||||
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
||||
|
||||
@@ -663,6 +663,16 @@ export const displayRoom = (url: string, h: string) => getRoom(makeRoomId(url, h
|
||||
|
||||
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
|
||||
|
||||
export const roomHasLivekit = (url: string, h: string) => {
|
||||
const room = getRoom(makeRoomId(url, h))
|
||||
return room?.event?.tags?.some(t => t[0] === "livekit") ?? false
|
||||
}
|
||||
|
||||
export const roomIsNoText = (url: string, h: string) => {
|
||||
const room = getRoom(makeRoomId(url, h))
|
||||
return room?.event?.tags?.some(t => t[0] === "no-text") ?? false
|
||||
}
|
||||
|
||||
// User space/room lists
|
||||
|
||||
export const groupListsByPubkey = deriveItemsByKey({
|
||||
|
||||
@@ -55,7 +55,7 @@ import {
|
||||
loadFeedsForPubkey,
|
||||
} from "@app/core/state"
|
||||
import {hasBlossomSupport} from "@app/core/commands"
|
||||
import {LIVE_ACTIVITY, ROOM_PRESENCE} from "@app/voice"
|
||||
import {ROOM_PRESENCE} from "@app/voice"
|
||||
|
||||
// Utils
|
||||
|
||||
@@ -320,7 +320,7 @@ const syncSpace = (url: string, rooms: string[]) => {
|
||||
pullAndListen({
|
||||
url,
|
||||
signal: controller.signal,
|
||||
filters: [{kinds: [LIVE_ACTIVITY, ROOM_PRESENCE]}],
|
||||
filters: [{kinds: [ROOM_PRESENCE]}],
|
||||
})
|
||||
|
||||
return () => controller.abort()
|
||||
|
||||
+38
-67
@@ -1,18 +1,12 @@
|
||||
import {derived, get, writable} from "svelte/store"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {SignJWT} from "jose"
|
||||
import {Room, RoomEvent, Track} from "livekit-client"
|
||||
import {now} from "@welshman/lib"
|
||||
import {makeEvent, normalizeRelayUrl, getTag} from "@welshman/util"
|
||||
import {pubkey, publishThunk} from "@welshman/app"
|
||||
import {deriveEventsForUrl, displayRoom} from "@app/core/state"
|
||||
import {makeEvent, getTagValue} from "@welshman/util"
|
||||
import {signer, publishThunk} from "@welshman/app"
|
||||
import {deriveEventsForUrl} from "@app/core/state"
|
||||
|
||||
export const LIVE_ACTIVITY = 30311
|
||||
export const ROOM_PRESENCE = 10312
|
||||
|
||||
const LIVEKIT_URL = import.meta.env.VITE_LIVEKIT_URL || ""
|
||||
const LIVEKIT_API_KEY = import.meta.env.VITE_LIVEKIT_API_KEY || ""
|
||||
const LIVEKIT_API_SECRET = import.meta.env.VITE_LIVEKIT_API_SECRET || ""
|
||||
const PRESENCE_INTERVAL_MS = 60_000
|
||||
const PRESENCE_EXPIRY_S = 300
|
||||
|
||||
@@ -25,23 +19,40 @@ export type VoiceSession = {
|
||||
|
||||
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
||||
|
||||
const makeLivekitRoomName = (url: string, h: string) =>
|
||||
`${normalizeRelayUrl(url)}:${h}`.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
const buildNip98AuthEvent = async (url: string, method: string) => {
|
||||
const $signer = signer.get()
|
||||
if (!$signer) throw new Error("No signer available")
|
||||
|
||||
const generateToken = async (roomName: string, identity: string) => {
|
||||
const secret = new TextEncoder().encode(LIVEKIT_API_SECRET)
|
||||
const jwt = await new SignJWT({
|
||||
video: {roomJoin: true, room: roomName, canPublish: true, canSubscribe: true},
|
||||
sub: identity,
|
||||
iss: LIVEKIT_API_KEY,
|
||||
jti: identity,
|
||||
const event = makeEvent(27235, {
|
||||
tags: [
|
||||
["u", url],
|
||||
["method", method],
|
||||
],
|
||||
})
|
||||
.setProtectedHeader({alg: "HS256"})
|
||||
.setIssuedAt()
|
||||
.setExpirationTime("6h")
|
||||
.sign(secret)
|
||||
|
||||
return jwt
|
||||
return $signer.sign(event)
|
||||
}
|
||||
|
||||
const fetchLivekitToken = async (
|
||||
url: string,
|
||||
groupId: string,
|
||||
): Promise<{server_url: string; participant_token: string}> => {
|
||||
const httpUrl = url.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://")
|
||||
const endpoint = `${httpUrl}/.well-known/nip29/livekit/${groupId}`
|
||||
|
||||
const authEvent = await buildNip98AuthEvent(endpoint, "GET")
|
||||
const encoded = btoa(JSON.stringify(authEvent))
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
headers: {Authorization: `Nostr ${encoded}`},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
throw new Error(`Token request failed (${response.status}): ${text}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export const deriveVoiceParticipants = (url: string, h: string) =>
|
||||
@@ -52,11 +63,7 @@ export const deriveVoiceParticipants = (url: string, h: string) =>
|
||||
for (const event of $events) {
|
||||
if (event.created_at < cutoff) continue
|
||||
|
||||
const aTag = getTag("a", event.tags)
|
||||
if (!aTag) continue
|
||||
|
||||
const [, , dTag] = aTag[1].split(":")
|
||||
if (dTag === h) {
|
||||
if (getTagValue("h", event.tags) === h) {
|
||||
pubkeys.push(event.pubkey)
|
||||
}
|
||||
}
|
||||
@@ -64,29 +71,9 @@ export const deriveVoiceParticipants = (url: string, h: string) =>
|
||||
return pubkeys
|
||||
})
|
||||
|
||||
const publishLiveActivity = (url: string, h: string, status: "live" | "ended") => {
|
||||
const pk = get(pubkey)!
|
||||
const title = displayRoom(url, h)
|
||||
const event = makeEvent(LIVE_ACTIVITY, {
|
||||
tags: [
|
||||
["d", h],
|
||||
["h", h],
|
||||
["title", title],
|
||||
["service", LIVEKIT_URL],
|
||||
["status", status],
|
||||
["starts", String(now())],
|
||||
["p", pk, "", "Host"],
|
||||
],
|
||||
})
|
||||
|
||||
return publishThunk({event, relays: [url]})
|
||||
}
|
||||
|
||||
const publishPresence = (url: string, h: string) => {
|
||||
const pk = get(pubkey)!
|
||||
const aTag = `${LIVE_ACTIVITY}:${pk}:${h}`
|
||||
const event = makeEvent(ROOM_PRESENCE, {
|
||||
tags: [["a", aTag, url, "root"]],
|
||||
tags: [["h", h]],
|
||||
})
|
||||
|
||||
return publishThunk({event, relays: [url]})
|
||||
@@ -121,10 +108,7 @@ export const joinVoiceRoom = async (url: string, h: string) => {
|
||||
await leaveVoiceRoom()
|
||||
}
|
||||
|
||||
const pk = get(pubkey)!
|
||||
const identity = nip19.npubEncode(pk)
|
||||
const roomName = makeLivekitRoomName(url, h)
|
||||
const token = await generateToken(roomName, identity)
|
||||
const {server_url, participant_token} = await fetchLivekitToken(url, h)
|
||||
|
||||
const room = new Room({
|
||||
adaptiveStream: true,
|
||||
@@ -149,12 +133,11 @@ export const joinVoiceRoom = async (url: string, h: string) => {
|
||||
track.detach().forEach(el => el.remove())
|
||||
})
|
||||
|
||||
await room.connect(LIVEKIT_URL, token)
|
||||
await room.connect(server_url, participant_token)
|
||||
await room.localParticipant.setMicrophoneEnabled(true)
|
||||
|
||||
currentVoiceSession.set({url, h, room, muted: false})
|
||||
|
||||
publishLiveActivity(url, h, "live")
|
||||
startPresenceHeartbeat(url, h)
|
||||
}
|
||||
|
||||
@@ -165,7 +148,6 @@ export const leaveVoiceRoom = async () => {
|
||||
stopPresenceHeartbeat()
|
||||
session.room.disconnect()
|
||||
deletePresence(session.url)
|
||||
publishLiveActivity(session.url, session.h, "ended")
|
||||
currentVoiceSession.set(undefined)
|
||||
}
|
||||
|
||||
@@ -177,14 +159,3 @@ export const toggleMute = () => {
|
||||
session.room.localParticipant.setMicrophoneEnabled(!muted)
|
||||
currentVoiceSession.set({...session, muted})
|
||||
}
|
||||
|
||||
export const toggleDeafen = () => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
|
||||
for (const participant of session.room.remoteParticipants.values()) {
|
||||
for (const pub of participant.audioTrackPublications.values()) {
|
||||
pub.setEnabled(!pub.isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user