Files
NekoMelody/src/player/index.ts
2024-08-18 17:12:29 +07:00

201 lines
5.2 KiB
TypeScript

import { Readable, Stream } from "stream";
import { AudioInformation, Provider } from "../providers/base";
import { SeekableStream } from "../utils/SeekableStream";
import EventEmitter from "events";
export enum LoopMode {
None = "none",
Current = "current",
}
export class Player {
private providers: Provider[];
private currentProvider: Provider | null = null;
private queue: AudioInformation[] = [];
private playerEvent: EventEmitter = new EventEmitter();
private paused: boolean = false;
private currentAudioInformation: AudioInformation | null = null;
private loopMode: LoopMode = LoopMode.None;
private previousAudioInformation: AudioInformation | null = null;
public _stream: SeekableStream | null = null;
private _skipFlag: boolean = false;
constructor(providers: Provider[]) {
this.providers = providers;
}
public get stream() {
return this._stream?.stream;
}
private _createStream(
information: AudioInformation,
url: string,
seekTime: number,
noDestroy: boolean = false,
) {
// If already playing, destroy the current stream
if (this._stream && !noDestroy) {
this._stream.destroy();
}
this._stream = new SeekableStream(information, url, seekTime);
this.currentAudioInformation = information;
this._stream.on("destroy", () => {
console.debug("Stream destroyed");
});
this.playerEvent.emit("play", information);
}
public startCurrentStream() {
if (this._stream) {
this._stream.start();
}
}
public endCurrentStream(skip: boolean = false) {
if (this._skipFlag) {
return;
}
if (skip) {
this._skipFlag = true;
}
if (this._stream) {
this._stream.destroy();
}
this.previousAudioInformation = this.currentAudioInformation;
if (this.loopMode === LoopMode.Current && !skip) {
this._createStream(
this.currentAudioInformation as AudioInformation,
this.currentAudioInformation?.url as string,
0,
true,
);
this._skipFlag = false;
return;
}
if (this.queue.length > 0) {
const next = this.queue.shift();
if (next) {
this._createStream(next, next.url, 0);
}
} else {
this._stream = null;
this.currentAudioInformation = null;
}
this._skipFlag = false;
}
public on(event: string, listener: (...args: any[]) => void) {
this.playerEvent.on(event, listener);
}
public async getInformation(url: string) {
if (!this.currentProvider) {
const providers = this.providers.filter((provider) =>
provider.canPlay(url),
);
if (providers.length === 0) {
throw new Error("No provider can play this URL");
}
this.currentProvider = providers[0];
}
console.debug(
"Using provider",
this.currentProvider.constructor.name,
url,
);
return await this.currentProvider.getInformation(url);
}
public getPreviousAudioInformation() {
return this.previousAudioInformation;
}
public async play(url: string, seekTime: number = 0) {
const information = await this.getInformation(url);
if (information.livestream)
// TODO: Implement livestreams
throw new Error("Livestreams are not supported yet");
this._createStream(information, url, seekTime);
}
public async enqueue(url: string, seekTime: number = 0) {
const information = await this.getInformation(url);
if (information.livestream)
// TODO: Implement livestreams
throw new Error("Livestreams are not supported yet");
this.playerEvent.emit("enqueue", information);
// If queue is empty, no stream is playing and not paused, play the current URL
if (
this.queue.length === 0 &&
!this.currentAudioInformation &&
!this._stream &&
!this.paused
) {
this._createStream(information, url, seekTime);
} else {
this.queue.push(information);
}
return information;
}
public setLoopMode(mode: LoopMode) {
this.loopMode = mode;
}
public getLoopMode() {
return this.loopMode;
}
public async seek(time: number) {
if (!this._stream) throw new Error("No stream to seek");
await this.play(this._stream.referenceUrl, time);
}
public async skip() {
this.endCurrentStream(true);
console.debug("Skipping");
}
public getCurrentSampleRate() {
return this._stream?.information.bitrate || 0;
}
public getCurrentAudioInformation() {
return this.currentAudioInformation;
}
public getQueue() {
return this.queue;
}
public clearQueue() {
this.queue = [];
}
}
export function createPlayer(providers: Provider[]) {
return new Player(providers);
}