for-legacy-web/src/lib/vortex/VoiceState.ts

212 lines
5.9 KiB
TypeScript

import { action, makeAutoObservable, runInAction } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Nullable, toNullable } from "revolt.js/dist/util/null";
import type { ProduceType, VoiceUser } from "./Types";
import type VoiceClient from "./VoiceClient";
export enum VoiceStatus {
LOADING = 0,
UNAVAILABLE,
ERRORED,
READY = 3,
CONNECTING = 4,
UNLOADED = 5,
AUTHENTICATING,
RTC_CONNECTING,
CONNECTED,
// RECONNECTING
}
// This is an example of how to implement MobX state.
// * Note for better implementation:
// * MobX state should be implemented on the VoiceClient itself.
class VoiceStateReference {
client?: VoiceClient;
connecting?: boolean;
status: VoiceStatus;
roomId: Nullable<string>;
participants: Map<string, VoiceUser>;
constructor() {
this.roomId = null;
this.status = VoiceStatus.UNLOADED;
this.participants = new Map();
this.syncState = this.syncState.bind(this);
this.connect = this.connect.bind(this);
this.disconnect = this.disconnect.bind(this);
makeAutoObservable(this, {
client: false,
connecting: false,
});
}
// This takes information from the voice
// client and applies it to the state here.
@action syncState() {
if (!this.client) return;
this.roomId = toNullable(this.client.roomId);
this.participants.clear();
this.client.participants.forEach((v, k) => this.participants.set(k, v));
}
// This imports and constructs the voice client.
@action async loadVoice() {
if (this.status !== VoiceStatus.UNLOADED) return;
this.status = VoiceStatus.LOADING;
try {
const { default: VoiceClient } = await import("./VoiceClient");
const client = new VoiceClient();
client.on("startProduce", this.syncState);
client.on("stopProduce", this.syncState);
client.on("userJoined", this.syncState);
client.on("userLeft", this.syncState);
client.on("userStartProduce", this.syncState);
client.on("userStopProduce", this.syncState);
runInAction(() => {
if (!client.supported()) {
this.status = VoiceStatus.UNAVAILABLE;
} else {
this.status = VoiceStatus.READY;
this.client = client;
}
});
} catch (err) {
console.error("Failed to load voice library!", err);
runInAction(() => {
this.status = VoiceStatus.UNAVAILABLE;
});
}
}
// Connect to a voice channel.
@action async connect(channel: Channel) {
if (!this.client?.supported()) throw new Error("RTC is unavailable");
this.connecting = true;
this.status = VoiceStatus.CONNECTING;
try {
const call = await channel.joinCall();
await this.client.connect(
channel.client.configuration!.features.voso.ws,
channel._id,
);
runInAction(() => {
this.status = VoiceStatus.AUTHENTICATING;
});
await this.client.authenticate(call.token);
this.syncState();
runInAction(() => {
this.status = VoiceStatus.RTC_CONNECTING;
});
await this.client.initializeTransports();
} catch (err) {
console.error(err);
runInAction(() => {
this.status = VoiceStatus.READY;
});
return channel;
}
runInAction(() => {
this.status = VoiceStatus.CONNECTED;
this.connecting = false;
});
return channel;
}
// Disconnect from current channel.
@action disconnect() {
this.connecting = false;
this.status = VoiceStatus.READY;
this.client?.disconnect();
this.syncState();
}
isProducing(type: ProduceType) {
switch (type) {
case "audio":
return this.client?.audioProducer !== undefined;
}
}
isDeaf() {
if (!this.client) return false;
return this.client.isDeaf;
}
async startDeafen() {
if (!this.client) return console.log("No client object"); // ! TODO: let the user know
if (this.client.isDeaf) return;
this.client.isDeaf = true;
this.client?.consumers.forEach((consumer) => {
consumer.audio?.pause();
});
this.syncState();
}
async stopDeafen() {
if (!this.client) return console.log("No client object"); // ! TODO: let the user know
if (!this.client.isDeaf) return;
this.client.isDeaf = false;
this.client?.consumers.forEach((consumer) => {
consumer.audio?.resume();
});
this.syncState();
}
async startProducing(type: ProduceType) {
switch (type) {
case "audio": {
if (this.client?.audioProducer !== undefined)
return console.log("No audio producer."); // ! TODO: let the user know
if (navigator.mediaDevices === undefined)
return console.log("No media devices."); // ! TODO: let the user know
const mediaDevice =
window.localStorage.getItem("audioInputDevice");
const mediaStream = await navigator.mediaDevices.getUserMedia({
audio: mediaDevice ? { deviceId: mediaDevice } : true,
});
await this.client?.startProduce(
mediaStream.getAudioTracks()[0],
"audio",
);
}
}
this.syncState();
}
async stopProducing(type: ProduceType) {
await this.client?.stopProduce(type);
this.syncState();
}
}
export const voiceState = new VoiceStateReference();