stoat-for-desktop/components/rtc/state.tsx

211 lines
5.2 KiB
TypeScript

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<Channel | undefined>;
#setChannel: Setter<Channel | undefined>;
room: Accessor<Room | undefined>;
#setRoom: Setter<Room | undefined>;
state: Accessor<State>;
#setState: Setter<State>;
deafen: Accessor<boolean>;
#setDeafen: Setter<boolean>;
microphone: Accessor<boolean>;
#setMicrophone: Setter<boolean>;
video: Accessor<boolean>;
#setVideo: Setter<boolean>;
screenshare: Accessor<boolean>;
#setScreenshare: Setter<boolean>;
constructor(voiceSettings: VoiceSettings) {
this.#settings = voiceSettings;
const [channel, setChannel] = createSignal<Channel>();
this.channel = channel;
this.#setChannel = setChannel;
const [room, setRoom] = createSignal<Room>();
this.room = room;
this.#setRoom = setRoom;
const [state, setState] = createSignal<State>("READY");
this.state = state;
this.#setState = setState;
const [deafen, setDeafen] = createSignal<boolean>(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<Voice>(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 (
<voiceContext.Provider value={voice}>
<RoomContext.Provider value={voice.room}>
<VoiceCallCardContext>{props.children}</VoiceCallCardContext>
<InRoom>
<RoomAudioManager />
</InRoom>
</RoomContext.Provider>
</voiceContext.Provider>
);
}
export const useVoice = () => useContext(voiceContext);