2026-05-15 04:25:06 +07:00
|
|
|
import { Readable } from "node:stream";
|
2026-05-13 15:28:25 +07:00
|
|
|
import {
|
|
|
|
|
AudioPlayer,
|
|
|
|
|
AudioPlayerStatus,
|
2026-05-17 04:52:20 +07:00
|
|
|
type AudioResource,
|
2026-05-13 15:28:25 +07:00
|
|
|
createAudioPlayer,
|
|
|
|
|
createAudioResource,
|
|
|
|
|
StreamType,
|
|
|
|
|
VoiceConnection,
|
2026-05-13 00:32:27 +07:00
|
|
|
} from "@discordjs/voice";
|
2026-05-17 04:52:20 +07:00
|
|
|
import type {
|
|
|
|
|
DiscordPlayOptions,
|
|
|
|
|
DiscordPlayerOwner,
|
|
|
|
|
} from "./media/mediaTypes";
|
2026-05-13 01:03:46 +07:00
|
|
|
|
2026-05-13 00:32:27 +07:00
|
|
|
export class DiscordPlayer {
|
2026-05-13 15:28:25 +07:00
|
|
|
private player: AudioPlayer;
|
|
|
|
|
private connection: VoiceConnection | null = null;
|
2026-05-16 15:48:28 +07:00
|
|
|
private owner: DiscordPlayerOwner = "none";
|
2026-05-17 04:52:20 +07:00
|
|
|
private resource: AudioResource | null = null;
|
|
|
|
|
private musicVolume = 1;
|
2026-05-13 15:28:25 +07:00
|
|
|
|
|
|
|
|
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}`);
|
2026-05-16 15:48:28 +07:00
|
|
|
this.owner = "none";
|
2026-05-17 04:52:20 +07:00
|
|
|
this.resource = null;
|
2026-05-13 15:28:25 +07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setConnection(connection: VoiceConnection) {
|
|
|
|
|
this.connection = connection;
|
|
|
|
|
this.connection.subscribe(this.player);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 15:48:28 +07:00
|
|
|
public getOwner(): DiscordPlayerOwner {
|
|
|
|
|
return this.owner;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 17:17:17 +07:00
|
|
|
public isConnected(): boolean {
|
|
|
|
|
return this.connection !== null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 04:52:20 +07:00
|
|
|
public playStream(
|
|
|
|
|
stream: Readable,
|
|
|
|
|
owner: DiscordPlayerOwner,
|
|
|
|
|
options: DiscordPlayOptions = {},
|
|
|
|
|
) {
|
2026-05-16 15:48:28 +07:00
|
|
|
if (owner === "none") {
|
|
|
|
|
throw new Error("Discord audio player owner is required");
|
|
|
|
|
}
|
|
|
|
|
this.assertOwnerAvailable(owner);
|
2026-05-13 15:28:25 +07:00
|
|
|
|
|
|
|
|
const resource = createAudioResource(stream, {
|
2026-05-17 04:52:20 +07:00
|
|
|
inputType: options.inputType ?? StreamType.OggOpus,
|
|
|
|
|
inlineVolume: options.inlineVolume ?? false,
|
2026-05-13 15:28:25 +07:00
|
|
|
});
|
|
|
|
|
|
2026-05-16 15:48:28 +07:00
|
|
|
if (this.owner === owner) {
|
|
|
|
|
this.player.stop();
|
|
|
|
|
}
|
2026-05-17 04:52:20 +07:00
|
|
|
this.resource = resource;
|
2026-05-16 15:48:28 +07:00
|
|
|
this.owner = owner;
|
2026-05-17 04:52:20 +07:00
|
|
|
if (owner === "music") {
|
|
|
|
|
const nextVolume =
|
|
|
|
|
options.volume !== undefined
|
|
|
|
|
? this.normalizeVolume(options.volume)
|
|
|
|
|
: this.musicVolume;
|
|
|
|
|
this.musicVolume = nextVolume;
|
|
|
|
|
this.setResourceVolume(nextVolume);
|
|
|
|
|
}
|
2026-05-13 15:28:25 +07:00
|
|
|
this.player.play(resource);
|
2026-05-15 22:23:29 +07:00
|
|
|
this.connection?.subscribe(this.player);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getStatus(): AudioPlayerStatus {
|
|
|
|
|
return this.player.state.status;
|
2026-05-13 15:28:25 +07:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 15:48:28 +07:00
|
|
|
public pause(owner?: DiscordPlayerOwner) {
|
|
|
|
|
if (!this.canControl(owner)) return;
|
2026-05-13 15:28:25 +07:00
|
|
|
this.player.pause(true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 15:48:28 +07:00
|
|
|
public unpause(owner?: DiscordPlayerOwner): boolean {
|
|
|
|
|
if (!this.canControl(owner)) return false;
|
2026-05-15 22:23:29 +07:00
|
|
|
return this.player.unpause();
|
2026-05-13 15:28:25 +07:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 15:48:28 +07:00
|
|
|
public stop(owner?: DiscordPlayerOwner) {
|
|
|
|
|
if (!this.canControl(owner)) return;
|
2026-05-13 15:28:25 +07:00
|
|
|
this.player.stop();
|
2026-05-16 15:48:28 +07:00
|
|
|
this.owner = "none";
|
2026-05-17 04:52:20 +07:00
|
|
|
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);
|
|
|
|
|
}
|
2026-05-16 15:48:28 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-13 15:28:25 +07:00
|
|
|
}
|
2026-05-17 04:52:20 +07:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-05-13 00:32:27 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const discordPlayer = new DiscordPlayer();
|