diff --git a/.cursor/plans/voice_rooms_production_f5249058.plan.md b/.cursor/plans/voice_rooms_production_f5249058.plan.md new file mode 100644 index 00000000..d6a10a19 --- /dev/null +++ b/.cursor/plans/voice_rooms_production_f5249058.plan.md @@ -0,0 +1,320 @@ +--- +name: Voice Rooms Production +overview: Implement production voice rooms by moving LiveKit token generation to the zooid relay (NIP-29 extension), making voice rooms conditional on group metadata tags, and updating flotilla to fetch tokens from the relay via NIP-98 authenticated HTTP requests. +todos: + - id: zooid-config + content: "Zooid: Add [livekit] config section (server_url, api_key, api_secret) to config.go" + status: pending + - id: zooid-livekit-go + content: "Zooid: Create livekit.go with token generation (livekit/protocol/auth), room creation (Twirp API), and response type" + status: pending + - id: zooid-endpoint + content: "Zooid: Register GET /.well-known/nip29/livekit/{groupId} endpoint on instance router with NIP-98 auth, membership check, livekit tag check" + status: pending + - id: zooid-nip98-util + content: "Zooid: Extract/reuse NIP-98 auth validation for the livekit endpoint (from api.go pattern)" + status: pending + - id: zooid-notext + content: "Zooid: Add no-text enforcement in CheckWrite -- reject non-moderation events for groups with no-text tag" + status: pending + - id: zooid-test-cp1 + content: "Zooid: Test checkpoint 1+2 -- curl the token endpoint, verify livekit/no-text tag behavior" + status: pending + - id: flotilla-remove-poc + content: "Flotilla: Remove jose dependency, VITE_LIVEKIT_* env vars, client-side JWT generation, live activity (kind:30311) publishing" + status: completed + - id: flotilla-nip98-fetch + content: "Flotilla: Implement NIP-98 auth header construction and relay token HTTP fetch in voice.ts" + status: completed + - id: flotilla-update-join + content: "Flotilla: Update joinVoiceRoom/leaveVoiceRoom to use relay-provided token and server_url, use hex pubkey identity" + status: completed + - id: flotilla-presence-fix + content: "Flotilla: Simplify presence events to use h tag directly (drop a tag referencing live activity)" + status: completed + - id: flotilla-conditional-ui + content: "Flotilla: Add roomHasLivekit/roomIsNoText helpers; update SpaceMenu to conditionally render voice/text rooms" + status: completed + - id: flotilla-test-e2e + content: "Flotilla: End-to-end test -- join voice room via relay token, verify audio, verify conditional display" + status: completed +isProject: false +--- + +# Voice Rooms Production Implementation + +This plan implements the proposed [NIP-29 LiveKit extension](https://github.com/nostr-protocol/nips/pull/2238) across two repositories: **zooid** (relay) and **flotilla** (client). Changes are organized into checkpoints so each can be tested independently. Reference implementation: [pyramid commit 4ac5418](https://github.com/fiatjaf/pyramid/commit/4ac5418175c7ea7b53e81858aa69be6f8f56c691). + +--- + +## Architecture + +```mermaid +sequenceDiagram + participant F as Flotilla Client + participant Z as Zooid Relay + participant LK as LiveKit Server + + F->>Z: Subscribe kind:39000 (room metadata) + Z-->>F: Room with ["livekit"] tag + + F->>F: Show voice room in sidebar + + Note over F: User clicks voice room + + F->>F: Build NIP-98 auth event (kind 27235) + F->>Z: GET /.well-known/nip29/livekit/{groupId}
Authorization: Nostr base64(event) + Z->>Z: Verify signature, membership, livekit tag + Z->>LK: Create room if needed (Twirp API) + Z-->>F: { server_url, participant_token } + F->>LK: Connect with JWT token + F->>Z: Publish kind:10312 presence event +``` + +--- + +## Zooid Changes (relay) + +### Checkpoint 1: LiveKit config and token endpoint + +**Goal**: Zooid serves LiveKit JWTs to authenticated group members. + +**Config changes** ([zooid/config.go](zooid/config.go)): + +- Add a `[livekit]` section to the TOML config struct: + +```go + Livekit struct { + ServerURL string `toml:"server_url" json:"server_url"` + APIKey string `toml:"api_key" json:"api_key"` + APISecret string `toml:"api_secret" json:"api_secret"` + } `toml:"livekit" json:"livekit"` + + +``` + +**New file** `zooid/livekit.go`: + +- `generateLivekitToken(apiKey, apiSecret, room string, pubkey nostr.PubKey) string` -- Uses `github.com/livekit/protocol/auth` to create a JWT with `RoomJoin` grant and `SetIdentity(pubkey.Hex())` (hex pubkey per the NIP). +- `ensureLivekitRoom(apiKey, apiSecret, serverURL, roomName string)` -- Calls LiveKit's Twirp API to lazily create rooms. Cache known rooms in a `map[string]bool` with a mutex (same pattern as pyramid). +- `TokenEndpointResponse` struct: `{ ServerURL string, ParticipantToken string }`. + +**HTTP endpoint** ([zooid/instance.go](zooid/instance.go)): + +- In the router setup (after the existing `/static/` handler), register: + +```go + router.HandleFunc("GET /.well-known/nip29/livekit/{groupId}", instance.livekitTokenHandler) + + +``` + +- The handler (`livekitTokenHandler` on `*Instance`): + 1. Check `instance.Config.Livekit.APIKey` is configured (return 404 if not). + 2. Parse `{groupId}` from the path. + 3. Validate NIP-98 auth header: `Authorization: Nostr `. Reuse the same pattern from `api.go`'s `authenticateNIP98`. Verify: signature, kind == 27235, `u` tag matches the request URL, timestamp is recent. + 4. Check the group exists: call `instance.Groups.GetMetadata(groupId)`. + 5. Check the group's kind:39000 event has a `livekit` tag (use `HasTag(meta.Tags, "livekit")`). + 6. Check the user is a group member: `instance.Groups.HasAccess(groupId, pubkey)`. + 7. Ensure the LiveKit room exists. + 8. Generate a token and return JSON `{ "server_url": "...", "participant_token": "..." }`. + +**NIP-98 validation**: Extract from `api.go`'s `authenticateNIP98` into a shared utility in `zooid/util.go` (or call it directly), since both the REST API and the livekit endpoint need it. The key difference: the API validates `method` tag too; the livekit endpoint should also validate method is "GET". + +**Dependency**: Add `github.com/livekit/protocol` to `go.mod`. + +**Test**: With a zooid instance running with `[livekit]` config and a group that has a `livekit` tag on its kind:39000 event: + +```bash +# Manually test with a NIP-98 auth header (use nak or a script to create the auth event) +curl -H "Authorization: Nostr " \ + https://your-relay/.well-known/nip29/livekit/ +# Expect: { "server_url": "wss://...", "participant_token": "eyJ..." } +``` + +### Checkpoint 2: livekit and no-text tag support + +**Goal**: Admins can set `livekit` and `no-text` tags on groups; `no-text` groups reject text events. + +**Tag passthrough** -- Zooid's `UpdateMetadata` in [zooid/groups.go](zooid/groups.go) already copies all tags from edit events to the kind:39000 metadata event (converting `h` to `d`). The `livekit` and `no-text` tags will pass through naturally when an admin sends a `KindSimpleGroupEditMetadata` event containing them. No code change needed for this. + +**no-text enforcement** -- In `CheckWrite` in [zooid/groups.go](zooid/groups.go), after existing access checks, add: + +```go +if HasTag(meta.Tags, "no-text") && + !slices.Contains(nip29.ModerationEventKinds, event.Kind) && + event.Kind != nostr.KindSimpleGroupJoinRequest && + event.Kind != nostr.KindSimpleGroupLeaveRequest { + return "blocked: this group does not allow text events" +} +``` + +This mirrors pyramid's `reject-event.go` logic. + +**Test**: + +- Send a `KindSimpleGroupEditMetadata` event with `["livekit"]` and `["no-text"]` tags. Query kind:39000 and verify the tags appear. +- Try sending a regular message to a `no-text` group; expect rejection. +- Try the token endpoint for a group with vs without the `livekit` tag; expect 403 for non-livekit groups. + +--- + +## Flotilla Changes (client) + +### Checkpoint 3: NIP-98 token fetching from relay + +**Goal**: Replace client-side JWT generation with relay HTTP request. + +**Remove POC artifacts** from [src/app/voice.ts](src/app/voice.ts): + +- Remove the `jose` import and the `generateToken` function. +- Remove `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET` constants. +- Remove `LIVE_ACTIVITY` constant and `publishLiveActivity` function (dropping kind:30311). +- Keep `ROOM_PRESENCE` and presence-related code (kind:10312). + +**Remove env vars** from [.env.template](.env.template): + +- Remove `VITE_LIVEKIT_URL`, `VITE_LIVEKIT_API_KEY`, `VITE_LIVEKIT_API_SECRET`. + +**Remove dependency**: Run `pnpm remove jose`. + +**Implement NIP-98 auth + token fetch** in [src/app/voice.ts](src/app/voice.ts): + +```typescript +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) throw new Error(`Token request failed: ${response.status}`) + return response.json() +} +``` + +**Build NIP-98 auth event**: Create a helper that uses the welshman signer to sign a kind 27235 event: + +```typescript +const buildNip98AuthEvent = async (url: string, method: string) => { + const event = makeEvent(27235, { + tags: [ + ["u", url], + ["method", method], + ], + }) + // Sign using welshman's signer + return signEvent(event) +} +``` + +Look at how `publishThunk` signs events for the pattern. The signer is available via `@welshman/app`. The event must be a complete signed event (with id, pubkey, sig). + +**Update `joinVoiceRoom`**: + +- Replace `generateToken(roomName, identity)` with `fetchLivekitToken(url, h)`. +- Use the returned `server_url` for `room.connect()` instead of the hardcoded `LIVEKIT_URL`. +- Change identity from `nip19.npubEncode(pk)` to just `pk` (hex pubkey) since the relay now sets it in the JWT. The `nip19` import can be removed. +- Remove the `publishLiveActivity` call; keep `startPresenceHeartbeat`. +- Remove `makeLivekitRoomName` since the relay now determines the room name. + +**Update `leaveVoiceRoom`**: + +- Remove `publishLiveActivity(session.url, session.h, "ended")` call. + +**Update sync** in [src/app/core/sync.ts](src/app/core/sync.ts): + +- Remove `LIVE_ACTIVITY` from the import and the subscription filter. Change `[LIVE_ACTIVITY, ROOM_PRESENCE]` to just `[ROOM_PRESENCE]`. + +**Update presence derivation** in [src/app/voice.ts](src/app/voice.ts): + +- Remove the reference to `LIVE_ACTIVITY` in the `a` tag matching inside `deriveVoiceParticipants`. The presence event currently references an `a` tag containing the live activity address. Since we're dropping live activities, switch to using the `h` tag directly on the presence event: + +```typescript +const event = makeEvent(ROOM_PRESENCE, { + tags: [["h", h]], +}) +``` + +And in deriveVoiceParticipants, match on the `h` tag instead of the `a` tag. + +**Test**: With zooid running the checkpoint 1+2 changes, and a group with the `livekit` tag: + +- Click a voice room in flotilla. +- Verify the NIP-98 HTTP request is sent to the relay. +- Verify the token is received and the LiveKit connection succeeds. +- Verify audio works between two clients. + +### Checkpoint 4: Conditional voice room display + +**Goal**: Only show voice rooms for groups with the `livekit` tag; show `no-text` groups as voice-only. + +**Read tags from room metadata** -- The `Room` type in [src/app/core/state.ts](src/app/core/state.ts) extends `PublishedRoomMeta`, which has an `event: TrustedEvent` field. The raw kind:39000 event tags are accessible via `room.event.tags`. Check for the `livekit` tag: + +```typescript +export const roomHasLivekit = (room: Room) => + room.event?.tags?.some(t => t[0] === "livekit") ?? false + +export const roomIsNoText = (room: Room) => room.event?.tags?.some(t => t[0] === "no-text") ?? false +``` + +Add these helpers to [src/app/core/state.ts](src/app/core/state.ts). + +**Update SpaceMenu** ([src/app/components/SpaceMenu.svelte](src/app/components/SpaceMenu.svelte)): + +- Currently, every room gets a `` unconditionally. Change this to only render `` when the room has the `livekit` tag. +- For `no-text` rooms, do NOT render `` (the text channel). Only render ``. +- This requires deriving the room metadata for each `h` value. Use `getRoom(makeRoomId(url, h))` to access the room data and check tags: + +```svelte +{#each $userRooms as h (h)} + {@const room = getRoom(makeRoomId(url, h))} + {#if !roomIsNoText(room)} + + {/if} + {#if roomHasLivekit(room)} + + {/if} +{/each} +``` + +Apply the same pattern to the `$roomSearch.searchValues(term)` loop. + +**Test**: + +- Create a group WITHOUT `livekit` tag -- verify no voice room appears. +- Add `livekit` tag to a group -- verify voice room appears alongside text channel. +- Create a group with both `livekit` and `no-text` tags -- verify only voice room appears (no text channel). + +--- + +## Summary of changes by repo + +### Zooid (relay) + +| File | Change | +| ------------------------ | -------------------------------------------------------- | +| `zooid/config.go` | Add `[livekit]` config section | +| `zooid/livekit.go` (new) | Token generation, room creation, response type | +| `zooid/instance.go` | Register HTTP endpoint, add handler method | +| `zooid/util.go` | Extract NIP-98 validation from api.go (or add alongside) | +| `zooid/groups.go` | Add `no-text` enforcement in `CheckWrite` | +| `go.mod` | Add `github.com/livekit/protocol` dependency | + +### Flotilla (client) + +| File | Change | +| ------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `src/app/voice.ts` | Replace client-side JWT with relay HTTP fetch; add NIP-98 auth; drop live activity; simplify presence | +| `src/app/core/state.ts` | Add `roomHasLivekit()` and `roomIsNoText()` helpers | +| `src/app/core/sync.ts` | Remove `LIVE_ACTIVITY` from sync filters | +| `src/app/components/SpaceMenu.svelte` | Conditionally show voice/text rooms based on tags | +| `.env.template` | Remove `VITE_LIVEKIT_` vars | +| `package.json` | Remove `jose` dependency | diff --git a/.env.template b/.env.template index 9d7cf686..5067aedd 100644 --- a/.env.template +++ b/.env.template @@ -18,8 +18,5 @@ VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,nostr-01.uid.ovh,relay.keychat.io,relay.0xchat.com VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y -VITE_LIVEKIT_URL= -VITE_LIVEKIT_API_KEY= -VITE_LIVEKIT_API_SECRET= VITE_GLITCHTIP_API_KEY= GLITCHTIP_AUTH_TOKEN= diff --git a/package.json b/package.json index 3f3b2cd4..d2369b2d 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ "fuse.js": "^7.1.0", "husky": "^9.1.7", "idb": "^8.0.3", - "jose": "^6.1.3", "livekit-client": "^2.17.2", "nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main", "nostr-tools": "^2.19.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdf0d005..08b3cf33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,9 +134,6 @@ importers: idb: specifier: ^8.0.3 version: 8.0.3 - jose: - specifier: ^6.1.3 - version: 6.1.3 livekit-client: specifier: ^2.17.2 version: 2.17.2(@types/dom-mediacapture-record@1.0.22) diff --git a/src/app/components/SpaceMenu.svelte b/src/app/components/SpaceMenu.svelte index 086c6298..dccff979 100644 --- a/src/app/components/SpaceMenu.svelte +++ b/src/app/components/SpaceMenu.svelte @@ -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 @@
Your Rooms {/if} - {#each $userRooms as h, i (h)} - - + {#each $userRooms as h (h)} + {#if !roomIsNoText(url, h)} + + {/if} + {#if roomHasLivekit(url, h)} + + {/if} {/each} {#if $otherRooms.length > 0}
@@ -277,9 +283,13 @@ {/if} - {#each $roomSearch.searchValues(term) as h, i (h)} - - + {#each $roomSearch.searchValues(term) as h (h)} + {#if !roomIsNoText(url, h)} + + {/if} + {#if roomHasLivekit(url, h)} + + {/if} {/each} {#if $canCreateRoom} diff --git a/src/app/core/state.ts b/src/app/core/state.ts index 7e64a12f..8b25bf7e 100644 --- a/src/app/core/state.ts +++ b/src/app/core/state.ts @@ -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({ diff --git a/src/app/core/sync.ts b/src/app/core/sync.ts index 82ee874d..f5737d45 100644 --- a/src/app/core/sync.ts +++ b/src/app/core/sync.ts @@ -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() diff --git a/src/app/voice.ts b/src/app/voice.ts index 4e0fc9a5..8013a654 100644 --- a/src/app/voice.ts +++ b/src/app/voice.ts @@ -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(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) - } - } -}