add video to livekit calls

This commit is contained in:
mplorentz
2026-03-26 10:49:14 -04:00
parent ce30820108
commit edceb92acc
8 changed files with 424 additions and 13 deletions
+88 -4
View File
@@ -22,6 +22,7 @@ export type VoiceSession = {
h: string
room: Room
muted: boolean
cameraOn: boolean
}
export type Pubkey = string
@@ -38,6 +39,11 @@ export const currentVoiceRoom = writable<{url: string; h: string} | undefined>(u
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)
const bumpVideoCallLayoutRevision = () => videoCallLayoutRevision.update(n => n + 1)
const addParticipant = (identity: string) => {
participantPubkeyMap.update(m => {
const next = new Map(m)
@@ -133,6 +139,7 @@ export const deriveVoiceParticipants = (url: string, h: string) =>
)
const onRoomDisconnected = (reason?: DisconnectReason) => {
videoCallLayoutRevision.set(0)
speakingParticipants.set([])
participantPubkeyMap.set(new Map())
currentVoiceSession.set(undefined)
@@ -152,11 +159,16 @@ const onTrackSubscribed = (track: Track) => {
element.style.display = "none"
document.body.appendChild(element)
element.play().catch(() => {})
} else if (track.kind === Track.Kind.Video) {
bumpVideoCallLayoutRevision()
}
}
const onTrackUnsubscribed = (track: Track) => {
track.detach().forEach(el => el.remove())
if (track.kind === Track.Kind.Video) {
bumpVideoCallLayoutRevision()
}
}
const onActiveSpeakersChanged = (participants: {identity: string}[]) => {
@@ -238,7 +250,7 @@ export const joinVoiceRoom = async (url: string, h: string): Promise<void> => {
pushToast({theme: "error", message: "Could not access microphone"})
}
currentVoiceSession.set({url, h, room, muted})
currentVoiceSession.set({url, h, room, muted, cameraOn: false})
voiceState.set("connected")
playJoinSound()
} catch (e) {
@@ -257,11 +269,20 @@ export const leaveVoiceRoom = async () => {
const audio = new Audio("/leave-voice-room.mp3")
audio.play().catch(() => {})
if (session.cameraOn) {
try {
await session.room.localParticipant.setCameraEnabled(false)
} catch {
/* pass */
}
}
voiceState.set("disconnected")
videoCallLayoutRevision.set(0)
currentVoiceSession.set(undefined)
session.room.disconnect()
speakingParticipants.set([])
participantPubkeyMap.set(new Map())
voiceState.set("disconnected")
session.room.disconnect()
currentVoiceSession.set(undefined)
}
export const rejoinVoiceRoom = () => {
@@ -288,3 +309,66 @@ export const toggleMute = async () => {
pushToast({theme: "error", message: "Could not access microphone"})
}
}
const roomHasSubscribedRemoteCamera = (room: Room): boolean => {
for (const p of room.remoteParticipants.values()) {
const pub = p.getTrackPublication(Track.Source.Camera)
if (pub?.isSubscribed && pub.track) return true
}
return false
}
/** True when the connected session has local camera on or any subscribed remote camera track. */
export const videoCallContentActive = derived(
[currentVoiceSession, voiceState, videoCallLayoutRevision],
([$session, $state, _rev]) => {
if ($state !== "connected" || !$session) return false
if ($session.cameraOn) return true
return roomHasSubscribedRemoteCamera($session.room)
},
)
/** Live camera tracks (local + remote) for layout automation. */
const countLiveCameraFeeds = (session: VoiceSession): number => {
const room = session.room
let n = 0
const lp = room.localParticipant
if (session.cameraOn) {
const pub = lp.getTrackPublication(Track.Source.Camera)
if (pub?.track) n += 1
}
for (const rp of room.remoteParticipants.values()) {
const pub = rp.getTrackPublication(Track.Source.Camera)
if (pub?.isSubscribed && pub.track) n += 1
}
return n
}
export const videoTileCount = derived(
[currentVoiceSession, voiceState, videoCallLayoutRevision],
([$session, $state, _rev]) => {
if ($state !== "connected" || !$session) return 0
return countLiveCameraFeeds($session)
},
)
export const toggleCamera = async () => {
const session = get(currentVoiceSession)
if (!session) return
const cameraOn = !session.cameraOn
if (!cameraOn) {
session.room.localParticipant.setCameraEnabled(false)
currentVoiceSession.set({...session, cameraOn})
bumpVideoCallLayoutRevision()
return
}
try {
await session.room.localParticipant.setCameraEnabled(true)
currentVoiceSession.set({...session, cameraOn})
bumpVideoCallLayoutRevision()
} catch (e) {
pushToast({theme: "error", message: "Could not access camera"})
}
}