player utils
This commit is contained in:
parent
502fe11392
commit
2206d68dfb
6 changed files with 72 additions and 24 deletions
|
@ -1,5 +1,5 @@
|
||||||
import { Client } from "discord.js";
|
import { Client, Message } from "discord.js";
|
||||||
import { Player } from "../src/index";
|
import { Player, Queue } from "../src/index";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
|
@ -7,7 +7,10 @@ const client = new Client({
|
||||||
});
|
});
|
||||||
const player = new Player(client);
|
const player = new Player(client);
|
||||||
|
|
||||||
// player.on("trackStart", (queue, track) => console.log(`Now playing: ${track.title} in ${queue.guild.name}!`));
|
player.on("trackStart", (queue, track) => {
|
||||||
|
const guildQueue = queue as Queue<Message>;
|
||||||
|
guildQueue.metadata.channel.send(`🎶 | Now playing: **${track.title}** in **${guildQueue.connection.channel.name}**!`);
|
||||||
|
});
|
||||||
|
|
||||||
client.on("ready", () => console.log("Bot is online!"));
|
client.on("ready", () => console.log("Bot is online!"));
|
||||||
|
|
||||||
|
@ -20,37 +23,45 @@ client.on("message", async message => {
|
||||||
if (!conn) return;
|
if (!conn) return;
|
||||||
return void message.channel.send(`Now Playing: **${conn.current.title}** (Played **${Math.floor(conn.connection.streamTime / 1000)} seconds**)`);
|
return void message.channel.send(`Now Playing: **${conn.current.title}** (Played **${Math.floor(conn.connection.streamTime / 1000)} seconds**)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content.startsWith("!pause") && message.guild.me.voice.channelID) {
|
if (message.content.startsWith("!pause") && message.guild.me.voice.channelID) {
|
||||||
const conn = player.getQueue(message.guild.id);
|
const conn = player.getQueue(message.guild.id);
|
||||||
if (!conn) return;
|
if (!conn) return;
|
||||||
conn.setPaused(true);
|
conn.setPaused(true);
|
||||||
return void message.channel.send("Paused!");
|
return void message.channel.send("Paused!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content.startsWith("!resume") && message.guild.me.voice.channelID) {
|
if (message.content.startsWith("!resume") && message.guild.me.voice.channelID) {
|
||||||
const conn = player.getQueue(message.guild.id);
|
const conn = player.getQueue(message.guild.id);
|
||||||
if (!conn) return;
|
if (!conn) return;
|
||||||
conn.setPaused(false);
|
conn.setPaused(false);
|
||||||
return void message.channel.send("Resumed!");
|
return void message.channel.send("Resumed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content.startsWith("!skip") && message.guild.me.voice.channelID) {
|
if (message.content.startsWith("!skip") && message.guild.me.voice.channelID) {
|
||||||
const conn = player.getQueue(message.guild.id);
|
const conn = player.getQueue(message.guild.id);
|
||||||
if (!conn) return;
|
if (!conn) return;
|
||||||
conn.skip();
|
conn.skip();
|
||||||
return void message.channel.send("Done!");
|
return void message.channel.send("Done!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content.startsWith("!queue") && message.guild.me.voice.channelID) {
|
if (message.content.startsWith("!queue") && message.guild.me.voice.channelID) {
|
||||||
const conn = player.getQueue(message.guild.id);
|
const conn = player.getQueue(message.guild.id);
|
||||||
if (!conn) return;
|
if (!conn) return;
|
||||||
return void message.channel.send({ content: conn.toString(), split: true });
|
return void message.channel.send({ content: conn.toString(), split: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content.startsWith("!vol") && message.guild.me.voice.channelID) {
|
if (message.content.startsWith("!vol") && message.guild.me.voice.channelID) {
|
||||||
const conn = player.getQueue(message.guild.id);
|
const conn = player.getQueue(message.guild.id);
|
||||||
if (!conn) return;
|
if (!conn) return;
|
||||||
conn.connection.setVolume(parseInt(message.content.slice(4).trim()));
|
conn.connection.setVolume(parseInt(message.content.slice(4).trim()));
|
||||||
return void message.channel.send("Volume changed!");
|
return void message.channel.send("Volume changed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content.startsWith("!p") && message.member.voice.channelID) {
|
if (message.content.startsWith("!p") && message.member.voice.channelID) {
|
||||||
const queue = player.createQueue(message.guild);
|
const queue = player.createQueue<Message>(message.guild, {
|
||||||
|
metadata: message
|
||||||
|
});
|
||||||
const song = await player.search(message.content.slice(2).trim(), message.author).then(x => x[0]);
|
const song = await player.search(message.content.slice(2).trim(), message.author).then(x => x[0]);
|
||||||
queue.addTrack(song);
|
queue.addTrack(song);
|
||||||
|
|
||||||
|
@ -58,7 +69,6 @@ client.on("message", async message => {
|
||||||
queue.connect(message.member.voice.channel)
|
queue.connect(message.member.voice.channel)
|
||||||
.then(async q => {
|
.then(async q => {
|
||||||
await q.play();
|
await q.play();
|
||||||
message.channel.send(`🎶 | Playing: **${song.title}**!`);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
message.channel.send(`🎶 | Queued: **${song.title}**!`);
|
message.channel.send(`🎶 | Queued: **${song.title}**!`);
|
||||||
|
|
|
@ -17,16 +17,18 @@ class DiscordPlayer extends EventEmitter<PlayerEvents> {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
createQueue(guild: Guild, queueInitOptions?: PlayerOptions) {
|
createQueue<T = unknown>(guild: Guild, queueInitOptions?: PlayerOptions & { metadata?: any }) {
|
||||||
if (this.queues.has(guild.id)) return this.queues.get(guild.id);
|
if (this.queues.has(guild.id)) return this.queues.get(guild.id) as Queue<T>;
|
||||||
|
|
||||||
const queue = new Queue(this, guild, queueInitOptions);
|
const queue = new Queue(this, guild, queueInitOptions);
|
||||||
|
queue.metadata = queueInitOptions.metadata;
|
||||||
this.queues.set(guild.id, queue);
|
this.queues.set(guild.id, queue);
|
||||||
|
|
||||||
return queue;
|
return queue as Queue<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
getQueue(guild: Snowflake) {
|
getQueue<T = unknown>(guild: Snowflake) {
|
||||||
return this.queues.get(guild);
|
return this.queues.get(guild) as Queue<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,17 +2,18 @@ import { Guild, StageChannel, VoiceChannel } from "discord.js";
|
||||||
import { Player } from "../Player";
|
import { Player } from "../Player";
|
||||||
import { StreamDispatcher } from "../VoiceInterface/BasicStreamDispatcher";
|
import { StreamDispatcher } from "../VoiceInterface/BasicStreamDispatcher";
|
||||||
import Track from "./Track";
|
import Track from "./Track";
|
||||||
import { PlayerOptions } from "../types/types";
|
import { PlayerOptions, PlayOptions } from "../types/types";
|
||||||
import ytdl from "discord-ytdl-core";
|
import ytdl from "discord-ytdl-core";
|
||||||
import { AudioResource, StreamType } from "@discordjs/voice";
|
import { AudioResource, StreamType } from "@discordjs/voice";
|
||||||
|
|
||||||
class Queue {
|
class Queue<T = unknown> {
|
||||||
public readonly guild: Guild;
|
public readonly guild: Guild;
|
||||||
public readonly player: Player;
|
public readonly player: Player;
|
||||||
public connection: StreamDispatcher;
|
public connection: StreamDispatcher;
|
||||||
public tracks: Track[] = [];
|
public tracks: Track[] = [];
|
||||||
public options: PlayerOptions;
|
public options: PlayerOptions;
|
||||||
public playing = false;
|
public playing = false;
|
||||||
|
public metadata?: T = null;
|
||||||
|
|
||||||
constructor(player: Player, guild: Guild, options: PlayerOptions = {}) {
|
constructor(player: Player, guild: Guild, options: PlayerOptions = {}) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
@ -49,6 +50,8 @@ class Queue {
|
||||||
const connection = await this.player.voiceUtils.connect(channel);
|
const connection = await this.player.voiceUtils.connect(channel);
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
|
|
||||||
|
if (channel.type === "stage") await channel.guild.me.voice.setRequestToSpeak(true).catch((e) => {});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,10 +80,16 @@ class Queue {
|
||||||
return paused ? this.connection.pause() : this.connection.resume();
|
return paused ? this.connection.pause() : this.connection.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
async play(src?: Track) {
|
setBitrate(bitrate: number | "auto") {
|
||||||
|
if (!this.connection?.audioResource?.encoder) return;
|
||||||
|
if (bitrate === "auto") bitrate = this.connection.channel?.bitrate ?? 64000;
|
||||||
|
this.connection.audioResource.encoder.setBitrate(bitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
async play(src?: Track, options: PlayOptions = {}) {
|
||||||
if (!this.connection || !this.connection.voiceConnection)
|
if (!this.connection || !this.connection.voiceConnection)
|
||||||
throw new Error("Voice connection is not available, use <Queue>.connect()!");
|
throw new Error("Voice connection is not available, use <Queue>.connect()!");
|
||||||
const track = src ?? this.tracks.shift();
|
const track = options.filtersUpdate ? this.current : src ?? this.tracks.shift();
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
|
|
||||||
let resource: AudioResource<Track>;
|
let resource: AudioResource<Track>;
|
||||||
|
@ -89,7 +98,9 @@ class Queue {
|
||||||
const stream = ytdl(track.raw.source === "spotify" ? track.raw.engine : track.url, {
|
const stream = ytdl(track.raw.source === "spotify" ? track.raw.engine : track.url, {
|
||||||
// because we don't wanna decode opus into pcm again just for volume, let discord.js handle that
|
// because we don't wanna decode opus into pcm again just for volume, let discord.js handle that
|
||||||
opusEncoded: false,
|
opusEncoded: false,
|
||||||
fmt: "s16le"
|
fmt: "s16le",
|
||||||
|
encoderArgs: options.encoderArgs ?? [],
|
||||||
|
seek: options.seek
|
||||||
});
|
});
|
||||||
|
|
||||||
resource = this.connection.createStream(stream, {
|
resource = this.connection.createStream(stream, {
|
||||||
|
@ -104,7 +115,9 @@ class Queue {
|
||||||
{
|
{
|
||||||
// because we don't wanna decode opus into pcm again just for volume, let discord.js handle that
|
// because we don't wanna decode opus into pcm again just for volume, let discord.js handle that
|
||||||
opusEncoded: false,
|
opusEncoded: false,
|
||||||
fmt: "s16le"
|
fmt: "s16le",
|
||||||
|
encoderArgs: options.encoderArgs ?? [],
|
||||||
|
seek: options.seek
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -119,11 +132,13 @@ class Queue {
|
||||||
|
|
||||||
dispatcher.on("start", () => {
|
dispatcher.on("start", () => {
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
this.player.emit("trackStart", this, this.current);
|
if (!options.filtersUpdate) this.player.emit("trackStart", this, this.current);
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatcher.on("finish", () => {
|
dispatcher.on("finish", () => {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
|
if (options.filtersUpdate) return;
|
||||||
|
|
||||||
if (!this.tracks.length) {
|
if (!this.tracks.length) {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
this.player.emit("queueEnd", this);
|
this.player.emit("queueEnd", this);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
VoiceConnection,
|
VoiceConnection,
|
||||||
VoiceConnectionStatus
|
VoiceConnectionStatus
|
||||||
} from "@discordjs/voice";
|
} from "@discordjs/voice";
|
||||||
|
import { StageChannel, VoiceChannel } from "discord.js";
|
||||||
import { Duplex, Readable } from "stream";
|
import { Duplex, Readable } from "stream";
|
||||||
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
|
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
|
||||||
import Track from "../Structures/Track";
|
import Track from "../Structures/Track";
|
||||||
|
@ -25,14 +26,17 @@ export interface VoiceEvents {
|
||||||
class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
|
class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
|
||||||
public readonly voiceConnection: VoiceConnection;
|
public readonly voiceConnection: VoiceConnection;
|
||||||
public readonly audioPlayer: AudioPlayer;
|
public readonly audioPlayer: AudioPlayer;
|
||||||
|
public readonly channel: VoiceChannel | StageChannel;
|
||||||
public connectPromise?: Promise<void>;
|
public connectPromise?: Promise<void>;
|
||||||
public audioResource?: AudioResource<Track>;
|
public audioResource?: AudioResource<Track>;
|
||||||
|
public paused = false;
|
||||||
|
|
||||||
constructor(connection: VoiceConnection) {
|
constructor(connection: VoiceConnection, channel: VoiceChannel | StageChannel) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.voiceConnection = connection;
|
this.voiceConnection = connection;
|
||||||
this.audioPlayer = createAudioPlayer();
|
this.audioPlayer = createAudioPlayer();
|
||||||
|
this.channel = channel;
|
||||||
|
|
||||||
this.voiceConnection.on("stateChange", (_, newState) => {
|
this.voiceConnection.on("stateChange", (_, newState) => {
|
||||||
if (newState.status === VoiceConnectionStatus.Disconnected) {
|
if (newState.status === VoiceConnectionStatus.Disconnected) {
|
||||||
|
@ -64,10 +68,12 @@ class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
|
||||||
|
|
||||||
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
||||||
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
|
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
|
||||||
this.audioResource = null;
|
if (!this.paused) {
|
||||||
void this.emit("finish");
|
this.audioResource = null;
|
||||||
|
void this.emit("finish");
|
||||||
|
}
|
||||||
} else if (newState.status === AudioPlayerStatus.Playing) {
|
} else if (newState.status === AudioPlayerStatus.Playing) {
|
||||||
void this.emit("start");
|
if (!this.paused) void this.emit("start");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -116,11 +122,15 @@ class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pause(interpolateSilence?: boolean) {
|
pause(interpolateSilence?: boolean) {
|
||||||
return this.audioPlayer.pause(interpolateSilence);
|
const success = this.audioPlayer.pause(interpolateSilence);
|
||||||
|
this.paused = success;
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
resume() {
|
resume() {
|
||||||
return this.audioPlayer.unpause();
|
const success = this.audioPlayer.unpause();
|
||||||
|
this.paused = !success;
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,7 +27,7 @@ class VoiceUtils {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
conn = await entersState(conn, VoiceConnectionStatus.Ready, options?.maxTime ?? 20000);
|
conn = await entersState(conn, VoiceConnectionStatus.Ready, options?.maxTime ?? 20000);
|
||||||
const sub = new StreamDispatcher(conn);
|
const sub = new StreamDispatcher(conn, channel);
|
||||||
this.cache.set(channel.guild.id, sub);
|
this.cache.set(channel.guild.id, sub);
|
||||||
return sub;
|
return sub;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -133,3 +133,14 @@ export interface PlayerEvents {
|
||||||
trackAdd: () => any;
|
trackAdd: () => any;
|
||||||
trackStart: (queue: Queue, track: Track) => any;
|
trackStart: (queue: Queue, track: Track) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlayOptions {
|
||||||
|
/** If this play is triggered for filters update */
|
||||||
|
filtersUpdate?: boolean;
|
||||||
|
|
||||||
|
/** ffmpeg args passed to encoder */
|
||||||
|
encoderArgs?: string[];
|
||||||
|
|
||||||
|
/** Time to seek to before playing */
|
||||||
|
seek?: number;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue