add video to livekit calls

This commit is contained in:
mplorentz
2026-03-26 10:49:14 -04:00
committed by hodlbod
parent 65ca8a7fd8
commit 9f6b16089b
8 changed files with 411 additions and 15 deletions
+85 -1
View File
@@ -32,6 +32,7 @@ export type VoiceSession = {
h: string
room: LiveKitRoom
muted: boolean
cameraOn: boolean
}
export type Pubkey = string
@@ -82,6 +83,11 @@ export const currentVoiceRoom = writable<Room | undefined>(undefined)
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)
@@ -197,6 +203,7 @@ const setUpMicrophone = async (
}
const onRoomDisconnected = (reason?: DisconnectReason) => {
videoCallLayoutRevision.set(0)
currentVoiceSession.set(undefined)
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
voiceState.set(VoiceState.Disconnected)
@@ -216,11 +223,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}[]) => {
@@ -301,7 +313,7 @@ export const joinVoiceRoom = async (
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
currentVoiceSession.set({url, h, room: liveKitRoom, muted})
currentVoiceSession.set({url, h, room: liveKitRoom, muted, cameraOn: false})
voiceState.set(VoiceState.Connected)
playJoinSound()
} catch (e) {
@@ -320,7 +332,16 @@ 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(VoiceState.Disconnected)
videoCallLayoutRevision.set(0)
currentVoiceSession.set(undefined)
session.room.disconnect()
speakingParticipants.set([])
@@ -352,3 +373,66 @@ export const toggleMute = async () => {
pushToast({theme: "error", message: "Could not access microphone"})
}
}
const roomHasSubscribedRemoteCamera = (room: LiveKitRoom): 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 !== VoiceState.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 !== VoiceState.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"})
}
}