2021-06-11 14:06:51 +05:00
|
|
|
import {
|
|
|
|
AudioPlayer,
|
2021-06-11 14:17:42 +05:00
|
|
|
AudioPlayerError,
|
|
|
|
AudioPlayerStatus,
|
2021-06-11 14:06:51 +05:00
|
|
|
AudioResource,
|
|
|
|
createAudioPlayer,
|
|
|
|
createAudioResource,
|
|
|
|
entersState,
|
|
|
|
StreamType,
|
|
|
|
VoiceConnection,
|
|
|
|
VoiceConnectionStatus
|
|
|
|
} from "@discordjs/voice";
|
2021-06-12 11:37:41 +05:00
|
|
|
import { StageChannel, VoiceChannel } from "discord.js";
|
2021-06-11 14:06:51 +05:00
|
|
|
import { Duplex, Readable } from "stream";
|
2021-06-11 14:17:42 +05:00
|
|
|
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
|
2021-06-11 16:50:43 +05:00
|
|
|
import Track from "../Structures/Track";
|
|
|
|
import PlayerError from "../utils/PlayerError";
|
2021-06-11 14:06:51 +05:00
|
|
|
|
2021-06-11 14:17:42 +05:00
|
|
|
export interface VoiceEvents {
|
|
|
|
error: (error: AudioPlayerError) => any;
|
|
|
|
debug: (message: string) => any;
|
|
|
|
start: () => any;
|
|
|
|
finish: () => any;
|
|
|
|
}
|
|
|
|
|
2021-06-11 23:19:52 +05:00
|
|
|
class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
|
2021-06-11 14:06:51 +05:00
|
|
|
public readonly voiceConnection: VoiceConnection;
|
|
|
|
public readonly audioPlayer: AudioPlayer;
|
2021-06-12 11:37:41 +05:00
|
|
|
public readonly channel: VoiceChannel | StageChannel;
|
2021-06-11 14:06:51 +05:00
|
|
|
public connectPromise?: Promise<void>;
|
2021-06-11 16:50:43 +05:00
|
|
|
public audioResource?: AudioResource<Track>;
|
2021-06-11 14:06:51 +05:00
|
|
|
|
2021-06-12 11:37:41 +05:00
|
|
|
constructor(connection: VoiceConnection, channel: VoiceChannel | StageChannel) {
|
2021-06-11 14:17:42 +05:00
|
|
|
super();
|
|
|
|
|
2021-06-11 14:06:51 +05:00
|
|
|
this.voiceConnection = connection;
|
|
|
|
this.audioPlayer = createAudioPlayer();
|
2021-06-12 11:37:41 +05:00
|
|
|
this.channel = channel;
|
2021-06-11 14:06:51 +05:00
|
|
|
|
|
|
|
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) {
|
2021-06-11 23:19:52 +05:00
|
|
|
this.end();
|
2021-06-13 13:06:19 +05:00
|
|
|
} else if (!this.connectPromise && (newState.status === VoiceConnectionStatus.Connecting || newState.status === VoiceConnectionStatus.Signalling)) {
|
2021-06-11 14:06:51 +05:00
|
|
|
this.connectPromise = entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000)
|
|
|
|
.then(() => undefined)
|
|
|
|
.catch(() => {
|
2021-06-13 13:06:19 +05:00
|
|
|
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) this.voiceConnection.destroy();
|
2021-06-11 14:06:51 +05:00
|
|
|
})
|
|
|
|
.finally(() => (this.connectPromise = undefined));
|
|
|
|
}
|
|
|
|
});
|
2021-06-11 14:17:42 +05:00
|
|
|
|
|
|
|
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
|
|
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
|
2021-06-12 11:37:41 +05:00
|
|
|
if (!this.paused) {
|
|
|
|
this.audioResource = null;
|
|
|
|
void this.emit("finish");
|
|
|
|
}
|
2021-06-11 14:17:42 +05:00
|
|
|
} else if (newState.status === AudioPlayerStatus.Playing) {
|
2021-06-12 11:37:41 +05:00
|
|
|
if (!this.paused) void this.emit("start");
|
2021-06-11 14:17:42 +05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.audioPlayer.on("debug", (m) => void this.emit("debug", m));
|
|
|
|
this.audioPlayer.on("error", (error) => void this.emit("error", error));
|
|
|
|
this.voiceConnection.subscribe(this.audioPlayer);
|
2021-06-11 14:06:51 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates stream
|
|
|
|
* @param {Readable|Duplex|string} src The stream source
|
2021-06-11 23:19:52 +05:00
|
|
|
* @param {({type?:StreamType;data?:any;})} [ops] Options
|
2021-06-11 14:06:51 +05:00
|
|
|
* @returns {AudioResource}
|
|
|
|
*/
|
2021-06-11 23:19:52 +05:00
|
|
|
createStream(src: Readable | Duplex | string, ops?: { type?: StreamType; data?: any }) {
|
2021-06-11 16:50:43 +05:00
|
|
|
this.audioResource = createAudioResource(src, {
|
2021-06-11 14:06:51 +05:00
|
|
|
inputType: ops?.type ?? StreamType.Arbitrary,
|
|
|
|
metadata: ops?.data,
|
2021-06-11 23:19:52 +05:00
|
|
|
inlineVolume: true // we definitely need volume controls, right?
|
2021-06-11 14:06:51 +05:00
|
|
|
});
|
2021-06-11 16:50:43 +05:00
|
|
|
|
|
|
|
return this.audioResource;
|
2021-06-11 14:06:51 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The player status
|
|
|
|
*/
|
|
|
|
get status() {
|
|
|
|
return this.audioPlayer.state.status;
|
|
|
|
}
|
|
|
|
|
2021-06-11 14:28:21 +05:00
|
|
|
/**
|
|
|
|
* Disconnects from voice
|
|
|
|
*/
|
|
|
|
disconnect() {
|
2021-06-12 00:18:53 +05:00
|
|
|
try {
|
|
|
|
this.voiceConnection.destroy();
|
|
|
|
} catch {}
|
2021-06-11 14:28:21 +05:00
|
|
|
}
|
|
|
|
|
2021-06-11 14:06:51 +05:00
|
|
|
/**
|
|
|
|
* Stops the player
|
|
|
|
*/
|
2021-06-11 23:19:52 +05:00
|
|
|
end() {
|
2021-06-11 14:06:51 +05:00
|
|
|
this.audioPlayer.stop();
|
|
|
|
}
|
|
|
|
|
2021-06-11 14:41:48 +05:00
|
|
|
pause(interpolateSilence?: boolean) {
|
2021-06-12 11:37:41 +05:00
|
|
|
const success = this.audioPlayer.pause(interpolateSilence);
|
|
|
|
return success;
|
2021-06-11 14:41:48 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
resume() {
|
2021-06-12 11:37:41 +05:00
|
|
|
const success = this.audioPlayer.unpause();
|
|
|
|
return success;
|
2021-06-11 14:41:48 +05:00
|
|
|
}
|
|
|
|
|
2021-06-11 14:06:51 +05:00
|
|
|
/**
|
|
|
|
* Play stream
|
|
|
|
* @param {AudioResource} resource The audio resource to play
|
|
|
|
*/
|
2021-06-12 00:18:53 +05:00
|
|
|
async playStream(resource: AudioResource<Track> = this.audioResource) {
|
2021-06-11 16:50:43 +05:00
|
|
|
if (!resource) throw new PlayerError("Audio resource is not available!");
|
2021-06-11 23:19:52 +05:00
|
|
|
if (!this.audioResource) this.audioResource = resource;
|
2021-06-13 13:06:19 +05:00
|
|
|
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Ready) await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000);
|
2021-06-11 14:06:51 +05:00
|
|
|
this.audioPlayer.play(resource);
|
2021-06-11 14:38:47 +05:00
|
|
|
|
|
|
|
return this;
|
2021-06-11 14:06:51 +05:00
|
|
|
}
|
2021-06-11 16:50:43 +05:00
|
|
|
|
2021-06-11 23:19:52 +05:00
|
|
|
setVolume(value: number) {
|
2021-06-12 00:18:53 +05:00
|
|
|
if (!this.audioResource || isNaN(value) || value < 0 || value > Infinity) return false;
|
2021-06-11 23:19:52 +05:00
|
|
|
|
|
|
|
// ye boi logarithmic ✌
|
|
|
|
this.audioResource.volume.setVolumeLogarithmic(value / 200);
|
2021-06-12 00:18:53 +05:00
|
|
|
return true;
|
2021-06-11 23:19:52 +05:00
|
|
|
}
|
|
|
|
|
2021-06-13 13:06:19 +05:00
|
|
|
get volume() {
|
|
|
|
if (!this.audioResource || !this.audioResource.volume) return 100;
|
|
|
|
const currentVol = this.audioResource.volume.volume;
|
|
|
|
return Math.round(Math.pow(currentVol, 1 / 1.660964) * 200);
|
|
|
|
}
|
|
|
|
|
2021-06-11 16:50:43 +05:00
|
|
|
get streamTime() {
|
|
|
|
if (!this.audioResource) return 0;
|
|
|
|
return this.audioResource.playbackDuration;
|
|
|
|
}
|
2021-06-13 15:28:02 +05:00
|
|
|
|
|
|
|
get paused() {
|
2021-06-13 17:03:20 +05:00
|
|
|
return [AudioPlayerStatus.AutoPaused, AudioPlayerStatus.Paused].includes(this.audioPlayer.state.status);
|
2021-06-13 15:28:02 +05:00
|
|
|
}
|
2021-06-11 14:06:51 +05:00
|
|
|
}
|
|
|
|
|
2021-06-11 23:19:52 +05:00
|
|
|
export { BasicStreamDispatcher as StreamDispatcher };
|