mirror of
https://github.com/YuzuZensai/NekoMelody.git
synced 2026-01-30 12:32:55 +00:00
201 lines
5.2 KiB
TypeScript
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);
|
|
}
|