Merge feature/98-audio-settings-in-call into video-demo

Resolve VoiceWidget conflict: keep camera, screen share, and call settings.
Extend join flow with optional camera on join and device picker.
Add camera to in-call settings; rename UI to Call settings.

Made-with: Cursor
This commit is contained in:
mplorentz
2026-04-02 11:21:29 -04:00
4 changed files with 265 additions and 3 deletions
+55 -1
View File
@@ -9,7 +9,9 @@ import {
Room as LiveKitRoom,
RoomEvent,
Track,
supportsAudioOutputSelection,
type AudioCaptureOptions,
type VideoCaptureOptions,
} from "livekit-client"
import {derived, get, writable} from "svelte/store"
import {map, removeUndefined, uniqBy} from "@welshman/lib"
@@ -25,6 +27,8 @@ export const LIVEKIT_PARTICIPANTS = 39004
export {checkRelayHasLivekit} from "$lib/livekit"
export {supportsAudioOutputSelection}
export type VoiceSession = {
url: string
h: string
@@ -46,6 +50,40 @@ export enum VoiceState {
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
const LIVEKIT_DEFAULT_DEVICE_ID = "default"
export enum DeviceKind {
AudioInput = "audioinput",
AudioOutput = "audiooutput",
VideoInput = "videoinput",
}
export const switchVoiceActiveDevice = async (
kind: DeviceKind,
targetDeviceId: string,
): Promise<void> => {
const session = get(currentVoiceSession)
if (!session) return
const id = targetDeviceId === "" ? LIVEKIT_DEFAULT_DEVICE_ID : targetDeviceId
try {
await session.room.switchActiveDevice(kind, id)
} catch {
let label: string
switch (kind) {
case DeviceKind.AudioInput:
label = "microphone"
break
case DeviceKind.AudioOutput:
label = "speaker"
break
case DeviceKind.VideoInput:
label = "camera"
break
}
pushToast({theme: "error", message: `Error changing ${label}`})
}
}
export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
export const currentVoiceRoom = writable<Room | undefined>(undefined)
@@ -253,6 +291,8 @@ export const joinVoiceRoom = async (
h: string,
startMuted = true,
preferredMicId?: string,
joinWithCamera = false,
preferredCameraId?: string,
): Promise<void> => {
cancelJoinVoiceRoom()
@@ -303,12 +343,26 @@ export const joinVoiceRoom = async (
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
let cameraOn = false
if (joinWithCamera) {
const videoCapture: VideoCaptureOptions | undefined = preferredCameraId
? {deviceId: preferredCameraId}
: undefined
try {
await liveKitRoom.localParticipant.setCameraEnabled(true, videoCapture)
cameraOn = true
bumpVideoCallLayoutRevision()
} catch (e) {
pushToast({theme: "error", message: "Could not access camera"})
}
}
currentVoiceSession.set({
url,
h,
room: liveKitRoom,
muted,
cameraOn: false,
cameraOn,
screenShareOn: false,
})
voiceState.set(VoiceState.Connected)