diff --git a/example/index.ts b/example/index.ts index 0d08b94..24e9d48 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,5 +1,5 @@ -import { Client } from "discord.js"; -import { Player } from "../src/index"; +import { Client, Message } from "discord.js"; +import { Player, Queue } from "../src/index"; import { config } from "./config"; const client = new Client({ @@ -7,7 +7,10 @@ const client = new 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; + guildQueue.metadata.channel.send(`🎶 | Now playing: **${track.title}** in **${guildQueue.connection.channel.name}**!`); +}); client.on("ready", () => console.log("Bot is online!")); @@ -20,37 +23,45 @@ client.on("message", async message => { if (!conn) return; 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) { const conn = player.getQueue(message.guild.id); if (!conn) return; conn.setPaused(true); return void message.channel.send("Paused!"); } + if (message.content.startsWith("!resume") && message.guild.me.voice.channelID) { const conn = player.getQueue(message.guild.id); if (!conn) return; conn.setPaused(false); return void message.channel.send("Resumed!"); } + if (message.content.startsWith("!skip") && message.guild.me.voice.channelID) { const conn = player.getQueue(message.guild.id); if (!conn) return; conn.skip(); return void message.channel.send("Done!"); } + if (message.content.startsWith("!queue") && message.guild.me.voice.channelID) { const conn = player.getQueue(message.guild.id); if (!conn) return; return void message.channel.send({ content: conn.toString(), split: true }); } + if (message.content.startsWith("!vol") && message.guild.me.voice.channelID) { const conn = player.getQueue(message.guild.id); if (!conn) return; conn.connection.setVolume(parseInt(message.content.slice(4).trim())); return void message.channel.send("Volume changed!"); } + if (message.content.startsWith("!p") && message.member.voice.channelID) { - const queue = player.createQueue(message.guild); + const queue = player.createQueue(message.guild, { + metadata: message + }); const song = await player.search(message.content.slice(2).trim(), message.author).then(x => x[0]); queue.addTrack(song); @@ -58,7 +69,6 @@ client.on("message", async message => { queue.connect(message.member.voice.channel) .then(async q => { await q.play(); - message.channel.send(`🎶 | Playing: **${song.title}**!`); }); } else { message.channel.send(`🎶 | Queued: **${song.title}**!`); diff --git a/src/Player.ts b/src/Player.ts index bd0a006..72f02e9 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -17,16 +17,18 @@ class DiscordPlayer extends EventEmitter { this.client = client; } - createQueue(guild: Guild, queueInitOptions?: PlayerOptions) { - if (this.queues.has(guild.id)) return this.queues.get(guild.id); + createQueue(guild: Guild, queueInitOptions?: PlayerOptions & { metadata?: any }) { + if (this.queues.has(guild.id)) return this.queues.get(guild.id) as Queue; + const queue = new Queue(this, guild, queueInitOptions); + queue.metadata = queueInitOptions.metadata; this.queues.set(guild.id, queue); - return queue; + return queue as Queue; } - getQueue(guild: Snowflake) { - return this.queues.get(guild); + getQueue(guild: Snowflake) { + return this.queues.get(guild) as Queue; } /** diff --git a/src/Structures/Queue.ts b/src/Structures/Queue.ts index 4a2d096..9c6f640 100644 --- a/src/Structures/Queue.ts +++ b/src/Structures/Queue.ts @@ -2,17 +2,18 @@ import { Guild, StageChannel, VoiceChannel } from "discord.js"; import { Player } from "../Player"; import { StreamDispatcher } from "../VoiceInterface/BasicStreamDispatcher"; import Track from "./Track"; -import { PlayerOptions } from "../types/types"; +import { PlayerOptions, PlayOptions } from "../types/types"; import ytdl from "discord-ytdl-core"; import { AudioResource, StreamType } from "@discordjs/voice"; -class Queue { +class Queue { public readonly guild: Guild; public readonly player: Player; public connection: StreamDispatcher; public tracks: Track[] = []; public options: PlayerOptions; public playing = false; + public metadata?: T = null; constructor(player: Player, guild: Guild, options: PlayerOptions = {}) { this.player = player; @@ -49,6 +50,8 @@ class Queue { const connection = await this.player.voiceUtils.connect(channel); this.connection = connection; + if (channel.type === "stage") await channel.guild.me.voice.setRequestToSpeak(true).catch((e) => {}); + return this; } @@ -77,10 +80,16 @@ class Queue { 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) throw new Error("Voice connection is not available, use .connect()!"); - const track = src ?? this.tracks.shift(); + const track = options.filtersUpdate ? this.current : src ?? this.tracks.shift(); if (!track) return; let resource: AudioResource; @@ -89,7 +98,9 @@ class Queue { 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 opusEncoded: false, - fmt: "s16le" + fmt: "s16le", + encoderArgs: options.encoderArgs ?? [], + seek: options.seek }); 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 opusEncoded: false, - fmt: "s16le" + fmt: "s16le", + encoderArgs: options.encoderArgs ?? [], + seek: options.seek } ); @@ -119,11 +132,13 @@ class Queue { dispatcher.on("start", () => { this.playing = true; - this.player.emit("trackStart", this, this.current); + if (!options.filtersUpdate) this.player.emit("trackStart", this, this.current); }); dispatcher.on("finish", () => { this.playing = false; + if (options.filtersUpdate) return; + if (!this.tracks.length) { this.destroy(); this.player.emit("queueEnd", this); diff --git a/src/VoiceInterface/BasicStreamDispatcher.ts b/src/VoiceInterface/BasicStreamDispatcher.ts index c163539..b95cbc8 100644 --- a/src/VoiceInterface/BasicStreamDispatcher.ts +++ b/src/VoiceInterface/BasicStreamDispatcher.ts @@ -10,6 +10,7 @@ import { VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice"; +import { StageChannel, VoiceChannel } from "discord.js"; import { Duplex, Readable } from "stream"; import { TypedEmitter as EventEmitter } from "tiny-typed-emitter"; import Track from "../Structures/Track"; @@ -25,14 +26,17 @@ export interface VoiceEvents { class BasicStreamDispatcher extends EventEmitter { public readonly voiceConnection: VoiceConnection; public readonly audioPlayer: AudioPlayer; + public readonly channel: VoiceChannel | StageChannel; public connectPromise?: Promise; public audioResource?: AudioResource; + public paused = false; - constructor(connection: VoiceConnection) { + constructor(connection: VoiceConnection, channel: VoiceChannel | StageChannel) { super(); this.voiceConnection = connection; this.audioPlayer = createAudioPlayer(); + this.channel = channel; this.voiceConnection.on("stateChange", (_, newState) => { if (newState.status === VoiceConnectionStatus.Disconnected) { @@ -64,10 +68,12 @@ class BasicStreamDispatcher extends EventEmitter { this.audioPlayer.on("stateChange", (oldState, newState) => { if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) { - this.audioResource = null; - void this.emit("finish"); + if (!this.paused) { + this.audioResource = null; + void this.emit("finish"); + } } 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 { } pause(interpolateSilence?: boolean) { - return this.audioPlayer.pause(interpolateSilence); + const success = this.audioPlayer.pause(interpolateSilence); + this.paused = success; + return success; } resume() { - return this.audioPlayer.unpause(); + const success = this.audioPlayer.unpause(); + this.paused = !success; + return success; } /** diff --git a/src/VoiceInterface/VoiceUtils.ts b/src/VoiceInterface/VoiceUtils.ts index 7db11ad..ceb39ef 100644 --- a/src/VoiceInterface/VoiceUtils.ts +++ b/src/VoiceInterface/VoiceUtils.ts @@ -27,7 +27,7 @@ class VoiceUtils { try { 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); return sub; } catch (err) { diff --git a/src/types/types.ts b/src/types/types.ts index ca5d9d1..8c14795 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -133,3 +133,14 @@ export interface PlayerEvents { trackAdd: () => 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; +}