import {derived, get, writable} from "svelte/store" import {first, uniqBy} from "@welshman/lib" import {sortEventsDesc, tagSpec, tagValues} from "@welshman/util" import {deriveDeduplicated} from "@welshman/store" import {makeRoomKey} from "@welshman/app" import type {Room} from "@welshman/app" import type {Room as LiveKitRoom} from "livekit-client" import {events, network} from "@app/core" export const LIVEKIT_PARTICIPANTS = 39004 export type CallSession = { url: string h: string livekit: LiveKitRoom cameraOn: boolean screenShareOn: boolean } /** Mic mute state is separate so toggling it does not re-render video tiles. */ export const callMicMuted = writable(true) export type CallParticipant = {pubkey?: string; liveKitIdentity: string} export type ParticipantMediaState = { muted: boolean cameraOn: boolean } export enum CallState { Joining = "joining", Connected = "connected", Disconnected = "disconnected", } export enum VideoCallLayout { Chat = "chat", Video = "video", Split = "split", } export enum DeviceKind { AudioInput = "audioinput", AudioOutput = "audiooutput", VideoInput = "videoinput", } export const currentCallSession = writable(undefined) export const callState = writable(CallState.Disconnected) export const callTargetRoom = writable(undefined) export const isCallActive = derived( callState, $state => $state === CallState.Joining || $state === CallState.Connected, ) export const deriveRoomCallState = (url: string, h?: string) => derived([callState, callTargetRoom], ([$callState, $targetRoom]) => $targetRoom?.url === url && $targetRoom.h === h ? $callState : CallState.Disconnected, ) export const deriveIsCallActiveElsewhere = (url: string | undefined, h: string | undefined) => derived( [isCallActive, callTargetRoom], ([$isCallActive, $targetRoom]) => $isCallActive && $targetRoom !== undefined && !($targetRoom.url === url && $targetRoom.h === h), ) // leaveVoiceRoom no-ops during Joining, so cancel the in-flight join instead. export const endCall = async () => { const engine = await import("@app/callEngine") if (get(callState) === CallState.Joining) { engine.cancelJoinVoiceRoom() } else { await engine.leaveVoiceRoom() } } export const speakingParticipants = writable([]) export const participantMediaState = writable(new Map()) export const mediaStateByIdentity = derived( [participantMediaState, currentCallSession, callMicMuted], ([$media, $session, $micMuted]) => (liveKitIdentity: string) => { if ($session?.livekit.localParticipant.identity === liveKitIdentity) { return {muted: $micMuted, cameraOn: $session.cameraOn} } return $media.get(liveKitIdentity) ?? {muted: true, cameraOn: false} }, ) export const isParticipantSpeaking = derived( speakingParticipants, $participants => (p: CallParticipant) => $participants.some(sp => participantKey(sp) === participantKey(p)), ) export const videoTrackRevision = writable(0) export const triggerVideoTrackRevision = () => { videoTrackRevision.update(n => n + 1) } export const videoCallLayout = writable(VideoCallLayout.Split) export const videoPrimaryTileKey = writable(undefined) export const resetVideoCallLayout = () => { videoCallLayout.set(VideoCallLayout.Chat) } export const toggleVideoPrimaryTile = (key: string) => { videoPrimaryTileKey.update(k => (k === key ? undefined : key)) } export const loadCallParticipants = (url: string, h: string) => network.get().loadLenient({ relays: [url], filters: [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}], }) export const deriveCallParticipants = (url: string, h: string) => // We use the livekit identity list while in a call, and fall back to the list in kind 39004. derived( [ participantMediaState, callTargetRoom, deriveDeduplicated( events.get().forUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]).$, events => first(sortEventsDesc(events)), ), ], ([$participantMediaState, $callTargetRoom, $publishedParticipantList]) => { const inCall = $participantMediaState.size > 0 && $callTargetRoom?.id === makeRoomKey(url, h) const identities = inCall ? [...$participantMediaState.keys()] : tagValues(tagSpec("participant"), $publishedParticipantList?.tags ?? []) return uniqBy( (p: CallParticipant) => participantKey(p), identities.map(participantFromLiveKitIdentity), ) }, ) export const pubkeyFromLiveKitIdentity = (liveKitIdentity: string): string | undefined => /^[a-f0-9]{64}$/.test(liveKitIdentity.slice(0, 64)) ? liveKitIdentity.slice(0, 64) : undefined export const participantFromLiveKitIdentity = (liveKitIdentity: string): CallParticipant => { const pk = pubkeyFromLiveKitIdentity(liveKitIdentity) return pk ? {pubkey: pk, liveKitIdentity} : {liveKitIdentity} } export const participantKey = (p: CallParticipant) => p.pubkey ?? p.liveKitIdentity