import { Readable } from "node:stream"; import { AudioPlayer, AudioPlayerStatus, type AudioResource, createAudioPlayer, createAudioResource, StreamType, VoiceConnection, } from "@discordjs/voice"; import type { DiscordPlayOptions, DiscordPlayerOwner, } from "./media/mediaTypes"; export class DiscordPlayer { private player: AudioPlayer; private connection: VoiceConnection | null = null; private owner: DiscordPlayerOwner = "none"; private resource: AudioResource | null = null; private musicVolume = 1; constructor() { this.player = createAudioPlayer(); this.player.on(AudioPlayerStatus.Playing, () => { console.log("[player] Audio player is now playing!"); }); this.player.on("error", (error) => { console.error(`[player] Error: ${error.message}`); this.owner = "none"; this.resource = null; }); } public setConnection(connection: VoiceConnection) { this.connection = connection; this.connection.subscribe(this.player); } public getOwner(): DiscordPlayerOwner { return this.owner; } public isConnected(): boolean { return this.connection !== null; } public playStream( stream: Readable, owner: DiscordPlayerOwner, options: DiscordPlayOptions = {}, ) { if (owner === "none") { throw new Error("Discord audio player owner is required"); } this.assertOwnerAvailable(owner); const resource = createAudioResource(stream, { inputType: options.inputType ?? StreamType.OggOpus, inlineVolume: options.inlineVolume ?? false, }); if (this.owner === owner) { this.player.stop(); } this.resource = resource; this.owner = owner; if (owner === "music") { const nextVolume = options.volume !== undefined ? this.normalizeVolume(options.volume) : this.musicVolume; this.musicVolume = nextVolume; this.setResourceVolume(nextVolume); } this.player.play(resource); this.connection?.subscribe(this.player); } public getStatus(): AudioPlayerStatus { return this.player.state.status; } public pause(owner?: DiscordPlayerOwner) { if (!this.canControl(owner)) return; this.player.pause(true); } public unpause(owner?: DiscordPlayerOwner): boolean { if (!this.canControl(owner)) return false; return this.player.unpause(); } public stop(owner?: DiscordPlayerOwner) { if (!this.canControl(owner)) return; this.player.stop(); this.owner = "none"; this.resource = null; } public getMusicVolume(): number { return this.musicVolume; } public setMusicVolume(volume: number): void { const nextVolume = this.normalizeVolume(volume); this.musicVolume = nextVolume; if (this.owner === "music") { this.setResourceVolume(nextVolume); } } private assertOwnerAvailable(owner: DiscordPlayerOwner): void { if (this.owner !== "none" && this.owner !== owner) { throw new Error(`Discord audio player is owned by ${this.owner}`); } } private canControl(owner?: DiscordPlayerOwner): boolean { return !owner || this.owner === "none" || this.owner === owner; } private normalizeVolume(volume: number): number { if (!Number.isFinite(volume)) return this.musicVolume; return Math.max(0, Math.min(1, volume)); } private setResourceVolume(volume: number): void { if (!this.resource?.volume) return; this.resource.volume.setVolume(volume); } } export const discordPlayer = new DiscordPlayer();