import { Accessor, JSX, Setter, batch, createContext, createSignal, useContext, } from "solid-js"; import { RoomContext } from "solid-livekit-components"; import { Room } from "livekit-client"; import { Channel } from "stoat.js"; import { useState } from "@revolt/state"; import { Voice as VoiceSettings } from "@revolt/state/stores/Voice"; import { VoiceCallCardContext } from "@revolt/ui/components/features/voice/callCard/VoiceCallCard"; import { InRoom } from "./components/InRoom"; import { RoomAudioManager } from "./components/RoomAudioManager"; type State = | "READY" | "DISCONNECTED" | "CONNECTING" | "CONNECTED" | "RECONNECTING"; class Voice { #settings: VoiceSettings; channel: Accessor; #setChannel: Setter; room: Accessor; #setRoom: Setter; state: Accessor; #setState: Setter; deafen: Accessor; #setDeafen: Setter; microphone: Accessor; #setMicrophone: Setter; video: Accessor; #setVideo: Setter; screenshare: Accessor; #setScreenshare: Setter; constructor(voiceSettings: VoiceSettings) { this.#settings = voiceSettings; const [channel, setChannel] = createSignal(); this.channel = channel; this.#setChannel = setChannel; const [room, setRoom] = createSignal(); this.room = room; this.#setRoom = setRoom; const [state, setState] = createSignal("READY"); this.state = state; this.#setState = setState; const [deafen, setDeafen] = createSignal(false); this.deafen = deafen; this.#setDeafen = setDeafen; const [microphone, setMicrophone] = createSignal(false); this.microphone = microphone; this.#setMicrophone = setMicrophone; const [video, setVideo] = createSignal(false); this.video = video; this.#setVideo = setVideo; const [screenshare, setScreenshare] = createSignal(false); this.screenshare = screenshare; this.#setScreenshare = setScreenshare; } async connect(channel: Channel, auth?: { url: string; token: string }) { this.disconnect(); const room = new Room({ audioCaptureDefaults: { deviceId: this.#settings.preferredAudioInputDevice, echoCancellation: this.#settings.echoCancellation, noiseSuppression: this.#settings.noiseSupression, }, audioOutput: { deviceId: this.#settings.preferredAudioOutputDevice, }, }); batch(() => { this.#setRoom(room); this.#setChannel(channel); this.#setState("CONNECTING"); this.#setMicrophone(false); this.#setDeafen(false); this.#setVideo(false); this.#setScreenshare(false); if (this.speakingPermission) room.localParticipant .setMicrophoneEnabled(true) .then((track) => this.#setMicrophone(typeof track !== "undefined")); }); room.addListener("connected", () => this.#setState("CONNECTED")); room.addListener("disconnected", () => this.#setState("DISCONNECTED")); if (!auth) { auth = await channel.joinCall("worldwide"); } await room.connect(auth.url, auth.token, { autoSubscribe: false, }); } disconnect() { const room = this.room(); if (!room) return; room.removeAllListeners(); room.disconnect(); batch(() => { this.#setState("READY"); this.#setRoom(undefined); this.#setChannel(undefined); }); } async toggleDeafen() { this.#setDeafen((s) => !s); } async toggleMute() { const room = this.room(); if (!room) throw "invalid state"; await room.localParticipant.setMicrophoneEnabled( !room.localParticipant.isMicrophoneEnabled, ); this.#setMicrophone(room.localParticipant.isMicrophoneEnabled); } async toggleCamera() { const room = this.room(); if (!room) throw "invalid state"; await room.localParticipant.setCameraEnabled( !room.localParticipant.isCameraEnabled, ); this.#setVideo(room.localParticipant.isCameraEnabled); } async toggleScreenshare() { const room = this.room(); if (!room) throw "invalid state"; await room.localParticipant.setScreenShareEnabled( !room.localParticipant.isScreenShareEnabled, ); this.#setScreenshare(room.localParticipant.isScreenShareEnabled); } getConnectedUser(userId: string) { return this.room()?.getParticipantByIdentity(userId); } get listenPermission() { return !!this.channel()?.havePermission("Listen"); } get speakingPermission() { return !!this.channel()?.havePermission("Speak"); } } const voiceContext = createContext(null as unknown as Voice); /** * Mount global voice context and room audio manager */ export function VoiceContext(props: { children: JSX.Element }) { const state = useState(); const voice = new Voice(state.voice); return ( {props.children} ); } export const useVoice = () => useContext(voiceContext);