diff --git a/voice/package.json b/voice/package.json index 60af0b9..bad44cf 100644 --- a/voice/package.json +++ b/voice/package.json @@ -32,7 +32,7 @@ "mediasoup-client": "^3.6.84", "msc-node": "^0.0.10", "prism-media": "github:itzTheMeow/prism-media", - "revkit": "^1.1.3" + "revkit": "^1.1.8" }, "devDependencies": { "@types/node": "^20.1.1", diff --git a/voice/pnpm-lock.yaml b/voice/pnpm-lock.yaml index 6b9b424..5642e12 100644 --- a/voice/pnpm-lock.yaml +++ b/voice/pnpm-lock.yaml @@ -27,8 +27,8 @@ dependencies: specifier: github:itzTheMeow/prism-media version: github.com/itzTheMeow/prism-media/f3e33538bbd00cd9f2a2184a75a832f961ba5409(ffmpeg-static@5.1.0) revkit: - specifier: ^1.1.3 - version: 1.1.3(ws@8.13.0) + specifier: ^1.1.8 + version: 1.1.8(ws@8.13.0) devDependencies: '@types/node': @@ -943,8 +943,8 @@ packages: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} dev: false - /revkit@1.1.3(ws@8.13.0): - resolution: {integrity: sha512-q8WA5fAP/9sSHQQt2VR0ZLlevDTVUr8oeFNYVFCT1FJLcSjnkhgjwGRuEXRfIU/aMm0qoh5eeHAd1NEavQ4Fbw==} + /revkit@1.1.8(ws@8.13.0): + resolution: {integrity: sha512-BobFrWckabQHMgknJqki/0esjOd33vsKVfD7ag56lLGVN4ywff7+OZ3vGLQ81CgU9RckhvylJWpJIvxuHAme/Q==} dependencies: '@insertish/exponential-backoff': 3.1.0-patch.2 '@insertish/isomorphic-ws': 4.0.1(ws@8.13.0) diff --git a/voice/src/Signaling.ts b/voice/src/Signaling.ts index b8e4f5f..3072d5f 100644 --- a/voice/src/Signaling.ts +++ b/voice/src/Signaling.ts @@ -17,6 +17,7 @@ export default class Signaling extends EventEmitte error: (event: WebSocket.Event) => void; data: (data: any) => void; }> { + /** Vortex WebSocket */ public ws: WebSocket | null = null; /** Index for pending packets. */ private index: number = 0; @@ -27,7 +28,8 @@ export default class Signaling extends EventEmitte super(); } - connected(): boolean { + /** If the vortex socket is connected. */ + get connected(): boolean { return ( !!this.ws && this.ws.readyState !== WebSocket.CLOSING && @@ -35,6 +37,10 @@ export default class Signaling extends EventEmitte ); } + /** + * Connects to vortex. + * @param address The socket address to use. + */ connect(address: string): Promise { this.disconnect(); this.ws = new WebSocket(address); @@ -59,13 +65,9 @@ export default class Signaling extends EventEmitte }); } + /** Disconnects the websocket. */ disconnect() { - if ( - this.ws && - this.ws.readyState !== WebSocket.CLOSED && - this.ws.readyState !== WebSocket.CLOSING - ) - this.ws.close(1000); + if (this.connected) this.ws.close(1000); } private parseData(event: WebSocket.MessageEvent) { @@ -117,10 +119,15 @@ export default class Signaling extends EventEmitte }); } + /** Authenticates with vortex. */ public authenticate(token: string, roomId: string): Promise> { return this.sendRequest(WSCommands.Authenticate, { token, roomId }); } + /** + * Gets information about the currently joined channel. + * @returns Room info. + */ public async roomInfo(): Promise { const room = await this.sendRequest(WSCommands.RoomInfo); return { diff --git a/voice/src/VoiceClient.ts b/voice/src/VoiceClient.ts index 981ac7b..5d7fb10 100644 --- a/voice/src/VoiceClient.ts +++ b/voice/src/VoiceClient.ts @@ -1,11 +1,11 @@ import EventEmitter from "eventemitter3"; import type * as MSCBrowser from "mediasoup-client"; import type * as MSCNode from "msc-node"; -import { Client, MiniMapEmitter } from "revkit"; +import { Client, DMChannel, GroupDMChannel, MiniMapEmitter, VoiceChannel } from "revkit"; import Signaling from "./Signaling"; import type { MSCPlatform, MediaSoup } from "./msc"; import { - RevkitClientOptions, + VoiceClientOptions, VoiceStatus, WSEvents, type ProduceType, @@ -62,8 +62,7 @@ export class VoiceClient< return this.status == VoiceStatus.CONNECTED && !!this.channelID; } - public token: string; - public type: "user" | "bot"; + private sessionDetails: { token: string; type: "user" | "bot" } | null = null; public client = new Client(); public channelID: string | null = null; public get channel() { @@ -78,7 +77,7 @@ export class VoiceClient< */ constructor( public readonly platform: Platform, - client: Client | RevkitClientOptions, + client: Client | VoiceClientOptions, private msc: Platform extends "node" ? typeof MSCNode : typeof MSCBrowser, private createDevice: () => MSC["Device"], private consumeTrack?: VoiceClientConsumer @@ -87,12 +86,10 @@ export class VoiceClient< this.supported = this.msc.detectDevice() !== undefined; if (client instanceof Client) { this.client = client; - this.token = this.client.session.token; - this.type = this.client.session.type; + this.sessionDetails = { token: this.client.session.token, type: this.client.session.type }; } else { - this.client = new Client(client.baseURL ? { apiURL: client.baseURL } : undefined); - this.token = client.token; - this.type = client.type; + this.client = new Client(client.apiURL ? { apiURL: client.apiURL } : undefined); + this.sessionDetails = { token: client.token, type: client.type }; } this.signaling.on( @@ -202,7 +199,7 @@ export class VoiceClient< } public disconnectTransport(error?: VoiceError, forceDisconnect = false) { - if (!this.signaling.connected() && !forceDisconnect) return; + if (!this.signaling.connected && !forceDisconnect) return; this.signaling.disconnect(); this.participants.clear(); this.participants.fireUpdate([]); @@ -390,18 +387,28 @@ export class VoiceClient< this.emit("status", status); } - public async connect(channelID?: string) { + /** + * Connect to a channel. + * @param channel Voice channel, DM/GDM, or channel ID. + * @returns Itself. + */ + public async connect(channelResolvable: VoiceChannel | DMChannel | GroupDMChannel | string) { if (!this.client.session) { - await this.client.login(this.token, this.type, false); - await this.client.users.fetch("@me"); + if (this.sessionDetails) { + await this.client.login(this.sessionDetails.token, this.sessionDetails.type, false); + await this.client.users.fetch("@me"); + } } const channel = - channelID == undefined ? this.channel : await this.client.channels.fetch(channelID); + typeof channelResolvable == "string" + ? await this.client.channels.fetch(channelResolvable) + : channelResolvable; if (this.status > VoiceStatus.READY) return; if (!this.supported) throw new Error("RTC is unavailable."); - if (!channel.isVoice()) throw new Error("Not a voice channel."); + if (!channel.isVoice() && !channel.isDMBased()) + throw new Error("Not a voice or (Group) DM channel."); await this.client.fetchConfiguration(); const vortexURL = this.client.config?.features.voso?.enabled ? this.client.config.features.voso.ws diff --git a/voice/src/browser.ts b/voice/src/browser.ts index a20101b..9b7370b 100644 --- a/voice/src/browser.ts +++ b/voice/src/browser.ts @@ -1,8 +1,9 @@ import * as MSC from "mediasoup-client"; -import { VoiceClient as BaseVoiceClient, type VoiceClientConsumer } from "./VoiceClient"; -import type { ProduceType, RevkitClientOptions } from "./types"; import { Client } from "revkit"; +import { VoiceClient as BaseVoiceClient, type VoiceClientConsumer } from "./VoiceClient"; +import type { ProduceType, VoiceClientOptions } from "./types"; +/** A default audio consumer for voice. (plays the audio with `Audio`) */ export const DEFAULT_CONSUMER: VoiceClientConsumer<"browser"> = (type, track) => { if (type == "audio") { const mediaStream = new MediaStream([track]); @@ -22,7 +23,7 @@ export default class VoiceClient extends BaseVoiceClient<"browser"> { /** * @param trackConsumer A function that is called when there is a new `MediaStreamTrack` to play. The function returned will be called when the track ends. */ - constructor(client: Client | RevkitClientOptions, trackConsumer?: VoiceClientConsumer<"browser">) { + constructor(client: Client | VoiceClientOptions, trackConsumer?: VoiceClientConsumer<"browser">) { super("browser", client, MSC, () => new MSC.Device(), trackConsumer); } diff --git a/voice/src/node.ts b/voice/src/node.ts index 278922a..8b81640 100644 --- a/voice/src/node.ts +++ b/voice/src/node.ts @@ -7,14 +7,15 @@ import { FFmpeg, VolumeTransformer } from "prism-media"; import { Client } from "revkit"; import { Readable } from "stream"; import { VoiceClient as BaseVoiceClient } from "./VoiceClient"; -import type { ProduceType, RevkitClientOptions, VoiceParticipant } from "./types"; +import type { ProduceType, VoiceClientOptions, VoiceParticipant } from "./types"; const AUDIO_ENCODING = "s16le", RTP_PAYLOAD_TYPE = 100, PORT_MIN = 5030, PORT_MAX = 65535; -export interface VoiceClientOptions { +/** Options for the node voice client. (most of these don't need changed) */ +export interface NodeVoiceClientOptions { /** Additional FFmpeg args to use. */ args: string[]; /** Number of channels for the audio played. (default 2) */ @@ -34,14 +35,16 @@ export interface VoiceClientOptions { * A VoiceClient implementation for use with node.js. */ export default class VoiceClient extends BaseVoiceClient<"node"> { - public options: VoiceClientOptions; + /** Node-specific client options. */ + public options: NodeVoiceClientOptions; + /** The port used for ffmpeg. */ public port: number = 5002; /** * @param options Additional options for the player. Some of them also apply to incoming tracks. (you shouldn't need to mess with these) */ - constructor(client: Client | RevkitClientOptions, options: Partial = {}) { - const opts: VoiceClientOptions = { + constructor(client: Client | VoiceClientOptions, options: Partial = {}) { + const opts: NodeVoiceClientOptions = { args: [], audioChannels: 2, frameSize: 960, diff --git a/voice/src/types.ts b/voice/src/types.ts index 326f7f0..3c28381 100644 --- a/voice/src/types.ts +++ b/voice/src/types.ts @@ -115,8 +115,12 @@ export enum VoiceStatus { CONNECTED, } -export interface RevkitClientOptions { +/** Options for creating a RevKit client for the voice client. */ +export interface VoiceClientOptions { + /** Bot or user session token. */ token: string; + /** Type for the token. */ type: "user" | "bot"; - baseURL?: string; + /** Optional API url for custom instances. (passed to `ClientOptions.apiURL`) */ + apiURL?: string; }