Clean up VideoCallContent

This commit is contained in:
mplorentz
2026-04-08 09:46:33 -04:00
parent 8d10d1700c
commit 3f64db0f0d
3 changed files with 36 additions and 47 deletions
+24 -27
View File
@@ -12,7 +12,6 @@
currentVoiceSession, currentVoiceSession,
currentVoiceRoom, currentVoiceRoom,
VideoCallLayout, VideoCallLayout,
videoCallLayoutRevision,
videoPrimaryTileKey, videoPrimaryTileKey,
toggleVideoPrimaryTile, toggleVideoPrimaryTile,
pubkeyFromLiveKitIdentity, pubkeyFromLiveKitIdentity,
@@ -42,29 +41,27 @@
$currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h, $currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h,
) )
const showPanel = $derived( const showVideoContent = $derived(
isViewingCurrentCallRoom && isViewingCurrentCallRoom &&
(mobile (mobile
? layout === VideoCallLayout.Video ? layout === VideoCallLayout.Video
: layout === VideoCallLayout.Split || layout === VideoCallLayout.Video), : layout === VideoCallLayout.Split || layout === VideoCallLayout.Video),
) )
const tiles = $derived.by(() => { const videoTiles = $derived.by(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- re-run when remote video subscribes
$videoCallLayoutRevision
const session = $currentVoiceSession const session = $currentVoiceSession
if (!session || $currentVoiceRoom?.url !== url || $currentVoiceRoom?.h !== h) { if (!session || $currentVoiceRoom?.url !== url || $currentVoiceRoom?.h !== h) {
return [] return []
} }
const room = session.room const room = session.room
const out: VideoTile[] = [] const videoTiles: VideoTile[] = []
const lp = room.localParticipant const user = room.localParticipant
if (session.cameraOn) { if (session.cameraOn) {
const localPub = lp.getTrackPublication(Track.Source.Camera) const localPub = user.getTrackPublication(Track.Source.Camera)
out.push({ videoTiles.push({
identity: lp.identity, identity: user.identity,
isLocal: true, isLocal: true,
trackSid: localPub?.trackSid ?? "local-camera", trackSid: localPub?.trackSid ?? "local-camera",
attachable: localPub?.track, attachable: localPub?.track,
@@ -73,9 +70,9 @@
} }
if (session.screenShareOn) { if (session.screenShareOn) {
const localPub = lp.getTrackPublication(Track.Source.ScreenShare) const localPub = user.getTrackPublication(Track.Source.ScreenShare)
out.push({ videoTiles.push({
identity: lp.identity, identity: user.identity,
isLocal: true, isLocal: true,
trackSid: localPub?.trackSid ?? "local-screen", trackSid: localPub?.trackSid ?? "local-screen",
attachable: localPub?.track, attachable: localPub?.track,
@@ -86,7 +83,7 @@
for (const rp of room.remoteParticipants.values()) { for (const rp of room.remoteParticipants.values()) {
const camPub = rp.getTrackPublication(Track.Source.Camera) const camPub = rp.getTrackPublication(Track.Source.Camera)
if (camPub?.isSubscribed && camPub.track) { if (camPub?.isSubscribed && camPub.track) {
out.push({ videoTiles.push({
identity: rp.identity, identity: rp.identity,
isLocal: false, isLocal: false,
trackSid: camPub.trackSid, trackSid: camPub.trackSid,
@@ -96,7 +93,7 @@
} }
const screenPub = rp.getTrackPublication(Track.Source.ScreenShare) const screenPub = rp.getTrackPublication(Track.Source.ScreenShare)
if (screenPub?.isSubscribed && screenPub.track) { if (screenPub?.isSubscribed && screenPub.track) {
out.push({ videoTiles.push({
identity: rp.identity, identity: rp.identity,
isLocal: false, isLocal: false,
trackSid: screenPub.trackSid, trackSid: screenPub.trackSid,
@@ -106,7 +103,7 @@
} }
} }
return out return videoTiles
}) })
/** Identity + source only — LiveKit can change trackSid after publish, which broke spotlight + stale-key effect. */ /** Identity + source only — LiveKit can change trackSid after publish, which broke spotlight + stale-key effect. */
@@ -115,29 +112,29 @@
const primaryTile = $derived.by(() => { const primaryTile = $derived.by(() => {
const k = $videoPrimaryTileKey const k = $videoPrimaryTileKey
if (k === undefined) return undefined if (k === undefined) return undefined
return tiles.find(t => tileKey(t) === k) return videoTiles.find(t => tileKey(t) === k)
}) })
const secondaryTiles = $derived.by(() => { const secondaryTiles = $derived.by(() => {
const p = primaryTile const p = primaryTile
if (p === undefined) return tiles if (p === undefined) return videoTiles
const pk = tileKey(p) const pk = tileKey(p)
return tiles.filter(t => tileKey(t) !== pk) return videoTiles.filter(t => tileKey(t) !== pk)
}) })
const useSpotlightLayout = $derived(primaryTile !== undefined) const useSpotlightLayout = $derived(primaryTile !== undefined)
const useMultiGrid = $derived(!useSpotlightLayout && tiles.length > 2) const useMultiGrid = $derived(!useSpotlightLayout && videoTiles.length > 2)
$effect(() => { $effect(() => {
const k = $videoPrimaryTileKey const k = $videoPrimaryTileKey
if (k === undefined) return if (k === undefined) return
if (!tiles.some(t => tileKey(t) === k)) { if (!videoTiles.some(t => tileKey(t) === k)) {
videoPrimaryTileKey.set(undefined) videoPrimaryTileKey.set(undefined)
} }
}) })
$effect(() => { $effect(() => {
for (const t of tiles) { for (const t of videoTiles) {
const pk = pubkeyFromLiveKitIdentity(t.identity) const pk = pubkeyFromLiveKitIdentity(t.identity)
if (pk) loadProfile(pk) if (pk) loadProfile(pk)
} }
@@ -149,7 +146,7 @@
return source === Track.Source.ScreenShare ? `${name} · screen` : name return source === Track.Source.ScreenShare ? `${name} · screen` : name
} }
const showTileGrid = $derived(tiles.length > 0) const showTileGrid = $derived(videoTiles.length > 0)
const spotlightHandlerFor = (key: string) => () => { const spotlightHandlerFor = (key: string) => () => {
toggleVideoPrimaryTile(key) toggleVideoPrimaryTile(key)
@@ -190,7 +187,7 @@
class="pointer-events-none absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-base-100/80 px-1.5 py-0.5 text-xs"> class="pointer-events-none absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-base-100/80 px-1.5 py-0.5 text-xs">
{labelFor(tile.identity, tile.source)}{tile.isLocal ? " (you)" : ""} {labelFor(tile.identity, tile.source)}{tile.isLocal ? " (you)" : ""}
</span> </span>
{#if tiles.length > 1} {#if videoTiles.length > 1}
{@const pinned = $videoPrimaryTileKey === tileKey(tile)} {@const pinned = $videoPrimaryTileKey === tileKey(tile)}
<Button <Button
data-tip={pinned ? "Exit spotlight" : "Spotlight"} data-tip={pinned ? "Exit spotlight" : "Spotlight"}
@@ -223,13 +220,13 @@
{:else if useMultiGrid} {:else if useMultiGrid}
<div <div
class="grid min-h-0 flex-1 grid-cols-1 content-start gap-2 overflow-y-auto sm:grid-cols-2"> class="grid min-h-0 flex-1 grid-cols-1 content-start gap-2 overflow-y-auto sm:grid-cols-2">
{#each tiles as tile (tileKey(tile))} {#each videoTiles as tile (tileKey(tile))}
{@render videoTile(tile, "default")} {@render videoTile(tile, "default")}
{/each} {/each}
</div> </div>
{:else} {:else}
<div class="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto"> <div class="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto">
{#each tiles as tile (tileKey(tile))} {#each videoTiles as tile (tileKey(tile))}
{@render videoTile(tile, "default")} {@render videoTile(tile, "default")}
{/each} {/each}
</div> </div>
@@ -243,7 +240,7 @@
{/if} {/if}
{/snippet} {/snippet}
{#if showPanel} {#if showVideoContent}
<div class={panelChrome}> <div class={panelChrome}>
{#if mobile} {#if mobile}
<div class="flex min-h-0 flex-1 flex-col gap-2"> <div class="flex min-h-0 flex-1 flex-col gap-2">
+9 -20
View File
@@ -102,9 +102,6 @@ const resetVideoCallLayout = () => {
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map()) export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
/** Bumps when remote video is subscribed/unsubscribed so layout/video UI can react. */
export const videoCallLayoutRevision = writable(0)
/** Spotlight tile id — must match VideoCallContent `tileKey` (identity + source, not trackSid). */ /** Spotlight tile id — must match VideoCallContent `tileKey` (identity + source, not trackSid). */
export const videoPrimaryTileKey = writable<string | undefined>(undefined) export const videoPrimaryTileKey = writable<string | undefined>(undefined)
@@ -112,7 +109,9 @@ export const toggleVideoPrimaryTile = (key: string) => {
videoPrimaryTileKey.update(k => (k === key ? undefined : key)) videoPrimaryTileKey.update(k => (k === key ? undefined : key))
} }
const bumpVideoCallLayoutRevision = () => videoCallLayoutRevision.update(n => n + 1) const triggerVideoTileCount = () => {
currentVoiceSession.update(s => (s ? {...s} : s))
}
const addParticipant = (identity: string) => { const addParticipant = (identity: string) => {
participantPubkeyMap.update(m => { participantPubkeyMap.update(m => {
@@ -239,7 +238,6 @@ const setUpMicrophone = async (
} }
const onRoomDisconnected = (reason?: DisconnectReason) => { const onRoomDisconnected = (reason?: DisconnectReason) => {
videoCallLayoutRevision.set(0)
videoPrimaryTileKey.set(undefined) videoPrimaryTileKey.set(undefined)
currentVoiceSession.set(undefined) currentVoiceSession.set(undefined)
resetVideoCallLayout() resetVideoCallLayout()
@@ -262,14 +260,14 @@ const onTrackSubscribed = (track: Track) => {
document.body.appendChild(element) document.body.appendChild(element)
element.play().catch(() => {}) element.play().catch(() => {})
} else if (track.kind === Track.Kind.Video) { } else if (track.kind === Track.Kind.Video) {
bumpVideoCallLayoutRevision() triggerVideoTileCount()
} }
} }
const onTrackUnsubscribed = (track: Track) => { const onTrackUnsubscribed = (track: Track) => {
track.detach().forEach(el => el.remove()) track.detach().forEach(el => el.remove())
if (track.kind === Track.Kind.Video) { if (track.kind === Track.Kind.Video) {
bumpVideoCallLayoutRevision() triggerVideoTileCount()
} }
} }
@@ -300,7 +298,6 @@ const onLocalTrackUnpublished = (
if (!session || participant.identity !== session.room.localParticipant.identity) return if (!session || participant.identity !== session.room.localParticipant.identity) return
if (!session.screenShareOn) return if (!session.screenShareOn) return
currentVoiceSession.set({...session, screenShareOn: false}) currentVoiceSession.set({...session, screenShareOn: false})
bumpVideoCallLayoutRevision()
} }
let joinAbortController: AbortController | undefined let joinAbortController: AbortController | undefined
@@ -407,7 +404,6 @@ export const leaveVoiceRoom = async () => {
} }
voiceState.set(VoiceState.Disconnected) voiceState.set(VoiceState.Disconnected)
videoCallLayoutRevision.set(0)
videoPrimaryTileKey.set(undefined) videoPrimaryTileKey.set(undefined)
currentVoiceSession.set(undefined) currentVoiceSession.set(undefined)
resetVideoCallLayout() resetVideoCallLayout()
@@ -465,13 +461,10 @@ const countLiveVisualFeeds = (session: VoiceSession): number => {
return n return n
} }
export const videoTileCount = derived( export const videoTileCount = derived([currentVoiceSession, voiceState], ([$session, $state]) => {
[currentVoiceSession, voiceState, videoCallLayoutRevision], if ($state !== VoiceState.Connected || !$session) return 0
([$session, $state, _rev]) => { return countLiveVisualFeeds($session)
if ($state !== VoiceState.Connected || !$session) return 0 })
return countLiveVisualFeeds($session)
},
)
export const toggleCamera = async () => { export const toggleCamera = async () => {
const session = get(currentVoiceSession) const session = get(currentVoiceSession)
@@ -481,14 +474,12 @@ export const toggleCamera = async () => {
if (!cameraOn) { if (!cameraOn) {
session.room.localParticipant.setCameraEnabled(false) session.room.localParticipant.setCameraEnabled(false)
currentVoiceSession.set({...session, cameraOn}) currentVoiceSession.set({...session, cameraOn})
bumpVideoCallLayoutRevision()
return return
} }
try { try {
await session.room.localParticipant.setCameraEnabled(true) await session.room.localParticipant.setCameraEnabled(true)
currentVoiceSession.set({...session, cameraOn}) currentVoiceSession.set({...session, cameraOn})
bumpVideoCallLayoutRevision()
} catch (e) { } catch (e) {
pushToast({theme: "error", message: "Could not access camera"}) pushToast({theme: "error", message: "Could not access camera"})
} }
@@ -502,14 +493,12 @@ export const toggleScreenShare = async () => {
if (!screenShareOn) { if (!screenShareOn) {
session.room.localParticipant.setScreenShareEnabled(false) session.room.localParticipant.setScreenShareEnabled(false)
currentVoiceSession.set({...session, screenShareOn}) currentVoiceSession.set({...session, screenShareOn})
bumpVideoCallLayoutRevision()
return return
} }
try { try {
await session.room.localParticipant.setScreenShareEnabled(true) await session.room.localParticipant.setScreenShareEnabled(true)
currentVoiceSession.set({...session, screenShareOn}) currentVoiceSession.set({...session, screenShareOn})
bumpVideoCallLayoutRevision()
} catch (e) { } catch (e) {
pushToast({theme: "error", message: "Could not start screen sharing"}) pushToast({theme: "error", message: "Could not start screen sharing"})
} }
@@ -103,6 +103,9 @@
if (prevVideoTileCount === 0 && n >= 1) { if (prevVideoTileCount === 0 && n >= 1) {
videoCallLayout.set(VideoCallLayout.Video) videoCallLayout.set(VideoCallLayout.Video)
} }
if (prevVideoTileCount >= 1 && n === 0 && $videoCallLayout === VideoCallLayout.Split) {
videoCallLayout.set(VideoCallLayout.Chat)
}
prevVideoTileCount = n prevVideoTileCount = n
}) })
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)