diff --git a/.prettierrc b/.prettierrc index ab07ced..a8cf444 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,7 @@ { "printWidth": 120, "trailingComma": "none", - "singleQuote": true, - "tabWidth": 4 + "singleQuote": false, + "tabWidth": 4, + "semi": true } \ No newline at end of file diff --git a/src/VoiceNative/VoiceSubscription.ts b/src/VoiceNative/VoiceSubscription.ts new file mode 100644 index 0000000..4b9b394 --- /dev/null +++ b/src/VoiceNative/VoiceSubscription.ts @@ -0,0 +1,90 @@ +import { + AudioPlayer, + AudioResource, + createAudioPlayer, + createAudioResource, + entersState, + StreamType, + VoiceConnection, + VoiceConnectionStatus +} from "@discordjs/voice"; +import { Duplex, Readable } from "stream"; + +class VoiceSubscription { + public readonly voiceConnection: VoiceConnection; + public readonly audioPlayer: AudioPlayer; + public connectPromise?: Promise; + + constructor(connection: VoiceConnection) { + this.voiceConnection = connection; + this.audioPlayer = createAudioPlayer(); + + connection.subscribe(this.audioPlayer); + + this.voiceConnection.on("stateChange", (_, newState) => { + if (newState.status === VoiceConnectionStatus.Disconnected) { + if (this.voiceConnection.reconnectAttempts < 5) { + setTimeout(() => { + if (this.voiceConnection.state.status === VoiceConnectionStatus.Disconnected) { + this.voiceConnection.reconnect(); + } + }, (this.voiceConnection.reconnectAttempts + 1) * 5000).unref(); + } else { + this.voiceConnection.destroy(); + } + } else if (newState.status === VoiceConnectionStatus.Destroyed) { + this.stop(); + } else if ( + !this.connectPromise && + (newState.status === VoiceConnectionStatus.Connecting || + newState.status === VoiceConnectionStatus.Signalling) + ) { + this.connectPromise = entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000) + .then(() => undefined) + .catch(() => { + if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) + this.voiceConnection.destroy(); + }) + .finally(() => (this.connectPromise = undefined)); + } + }); + } + + /** + * Creates stream + * @param {Readable|Duplex|string} src The stream source + * @param {({type?:StreamType;data?:any;inlineVolume?:boolean})} [ops] Options + * @returns {AudioResource} + */ + createStream(src: Readable | Duplex | string, ops?: { type?: StreamType, data?: any, inlineVolume?: boolean }) { + return createAudioResource(src, { + inputType: ops?.type ?? StreamType.Arbitrary, + metadata: ops?.data, + inlineVolume: Boolean(ops?.inlineVolume) + }); + } + + /** + * The player status + */ + get status() { + return this.audioPlayer.state.status; + } + + /** + * Stops the player + */ + stop() { + this.audioPlayer.stop(); + } + + /** + * Play stream + * @param {AudioResource} resource The audio resource to play + */ + playStream(resource: AudioResource) { + this.audioPlayer.play(resource); + } +} + +export { VoiceSubscription }; diff --git a/src/VoiceNative/VoiceUtils.ts b/src/VoiceNative/VoiceUtils.ts new file mode 100644 index 0000000..a694c90 --- /dev/null +++ b/src/VoiceNative/VoiceUtils.ts @@ -0,0 +1,49 @@ +import { VoiceChannel, StageChannel } from "discord.js"; +import { entersState, joinVoiceChannel, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice"; +import { VoiceSubscription } from "./VoiceSubscription"; + +class VoiceUtils { + + constructor() { + throw new Error("Cannot instantiate static class!"); + } + + /** + * Joins a voice channel + * @param {StageChannel|VoiceChannel} channel The voice channel + * @param {({deaf?: boolean;maxTime?: number;})} [options] Join options + * @returns {Promise} + */ + public static async connect( + channel: VoiceChannel | StageChannel, + options?: { + deaf?: boolean, + maxTime?: number + }): Promise { + let conn = joinVoiceChannel({ + guildId: channel.guild.id, + channelId: channel.id, + adapterCreator: channel.guild.voiceAdapterCreator, + selfDeaf: Boolean(options?.deaf) + }); + + try { + conn = await entersState(conn, VoiceConnectionStatus.Ready, options?.maxTime ?? 20000); + return new VoiceSubscription(conn); + } catch(err) { + conn.destroy(); + throw err; + } + } + + /** + * Disconnects voice connection + * @param {VoiceConnection} connection The voice connection + */ + public static disconnect(connection: VoiceConnection) { + connection.destroy(); + } + +} + +export { VoiceUtils } \ No newline at end of file