Merge branch 'develop' into patch-3
This commit is contained in:
commit
1f8b93314b
10 changed files with 573 additions and 974 deletions
26
README.md
26
README.md
|
@ -191,3 +191,29 @@ const player = new Player(client, {
|
|||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom stream Engine
|
||||
|
||||
Discord Player by default uses **[node-ytdl-core](https://github.com/fent/node-ytdl-core)** for youtube and some other extractors for other sources.
|
||||
If you need to modify this behavior without touching extractors, you need to use `createStream` functionality of discord player.
|
||||
Here's an example on how you can use **[play-dl](https://npmjs.com/package/play-dl)** to download youtube streams instead of using ytdl-core.
|
||||
|
||||
```js
|
||||
const playdl = require("play-dl");
|
||||
|
||||
// other code
|
||||
const queue = player.createQueue(..., {
|
||||
...,
|
||||
async onBeforeCreateStream(track, source, _queue) {
|
||||
// only trap youtube source
|
||||
if (source === "youtube") {
|
||||
// track here would be youtube track
|
||||
return (await playdl.stream(track.url)).stream;
|
||||
// we must return readable stream or void (returning void means telling discord-player to look for default extractor)
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`<Queue>.onBeforeCreateStream` is called before actually downloading the stream. It is a different concept from extractors, where you are **just** downloading
|
||||
streams. `source` here will be a video source. Streams from `onBeforeCreateStream` are then piped to `FFmpeg` and finally sent to Discord voice servers.
|
47
docs/extractors/create_stream.md
Normal file
47
docs/extractors/create_stream.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Create Stream
|
||||
|
||||
This is a checkpoint where discord-player calls `createStream` before downloading stream.
|
||||
|
||||
### Custom stream Engine
|
||||
|
||||
Discord Player by default uses **[node-ytdl-core](https://github.com/fent/node-ytdl-core)** for youtube and some other extractors for other sources.
|
||||
If you need to modify this behavior without touching extractors, you need to use `createStream` functionality of discord player.
|
||||
Here's an example on how you can use **[play-dl](https://npmjs.com/package/play-dl)** to download youtube streams instead of using ytdl-core.
|
||||
|
||||
```js
|
||||
const playdl = require("play-dl");
|
||||
|
||||
// other code
|
||||
const queue = player.createQueue(..., {
|
||||
...,
|
||||
async onBeforeCreateStream(track, source, _queue) {
|
||||
// only trap youtube source
|
||||
if (source === "youtube") {
|
||||
// track here would be youtube track
|
||||
return (await playdl.stream(track.url)).stream;
|
||||
// we must return readable stream or void (returning void means telling discord-player to look for default extractor)
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`<Queue>.onBeforeCreateStream` is called before actually downloading the stream. It is a different concept from extractors, where you are **just** downloading
|
||||
streams. `source` here will be a video source. Streams from `onBeforeCreateStream` are then piped to `FFmpeg` and finally sent to Discord voice servers.
|
||||
|
||||
# FAQ
|
||||
## How can I remove this?
|
||||
|
||||
> If you already made this change and want to switch to default mode in runtime,
|
||||
> you can set `queue.onBeforeCreateStream` to `null` which will make `discord-player` use default config.
|
||||
|
||||
## Which stream format should I return?
|
||||
|
||||
> It's not necessary to return opus format or whatever, since every streams have to be converted to s16le, due to inline volume.
|
||||
|
||||
## Can I use ytdl-core-discord?
|
||||
|
||||
> Yes, you can.
|
||||
|
||||
## Can I use this for other sources, like soundcloud?
|
||||
|
||||
> Absolutely.
|
|
@ -11,6 +11,8 @@
|
|||
files:
|
||||
- name: Extractors API
|
||||
path: extractor.md
|
||||
- name: Creating Stream
|
||||
path: create_stream.md
|
||||
- name: FAQ
|
||||
files:
|
||||
- name: Custom Filters
|
||||
|
|
25
package.json
25
package.json
|
@ -22,6 +22,7 @@
|
|||
"docs": "docgen --jsdoc jsdoc.json --source src/*.ts src/**/*.ts --custom docs/index.yml --output docs/docs.json",
|
||||
"docs:test": "docgen --jsdoc jsdoc.json --source src/*.ts src/**/*.ts --custom docs/index.yml",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"prepare": "husky install",
|
||||
"lint:fix": "eslint src --ext .ts --fix"
|
||||
},
|
||||
"funding": "https://github.com/Androz2091/discord-player?sponsor=1",
|
||||
|
@ -69,27 +70,27 @@
|
|||
"ytdl-core": "^4.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.14.8",
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"@babel/cli": "^7.15.7",
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@devsnowflake/docgen": "devsnowflake/docgen#ts-patch",
|
||||
"@discord-player/extractor": "^3.0.2",
|
||||
"@discordjs/opus": "github:discordjs/opus",
|
||||
"@favware/rollup-type-bundler": "^1.0.3",
|
||||
"@types/node": "^16.6.2",
|
||||
"@types/ws": "^7.4.7",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
||||
"@typescript-eslint/parser": "^4.29.2",
|
||||
"discord-api-types": "^0.22.0",
|
||||
"@types/node": "^16.9.6",
|
||||
"@types/ws": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.31.2",
|
||||
"@typescript-eslint/parser": "^4.31.2",
|
||||
"discord-api-types": "^0.23.1",
|
||||
"discord.js": "^13.1.0",
|
||||
"eslint": "^7.32.0",
|
||||
"gen-esm-wrapper": "^1.1.2",
|
||||
"husky": "^7.0.1",
|
||||
"gen-esm-wrapper": "^1.1.3",
|
||||
"husky": "^7.0.2",
|
||||
"jsdoc-babel": "^0.5.0",
|
||||
"prettier": "^2.3.2",
|
||||
"prettier": "^2.4.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.3.5"
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,14 @@ import { Collection, Guild, StageChannel, VoiceChannel, Snowflake, SnowflakeUtil
|
|||
import { Player } from "../Player";
|
||||
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
|
||||
import Track from "./Track";
|
||||
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, QueueRepeatMode } from "../types/types";
|
||||
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, QueueRepeatMode, TrackSource } from "../types/types";
|
||||
import ytdl from "discord-ytdl-core";
|
||||
import { AudioResource, StreamType } from "@discordjs/voice";
|
||||
import { Util } from "../utils/Util";
|
||||
import YouTube from "youtube-sr";
|
||||
import AudioFilters from "../utils/AudioFilters";
|
||||
import { PlayerError, ErrorStatusCode } from "./PlayerError";
|
||||
import type { Readable } from "stream";
|
||||
|
||||
class Queue<T = unknown> {
|
||||
public readonly guild: Guild;
|
||||
|
@ -27,6 +28,7 @@ class Queue<T = unknown> {
|
|||
private _filtersUpdate = false;
|
||||
#lastVolume = 0;
|
||||
#destroyed = false;
|
||||
public onBeforeCreateStream: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable | undefined> = null;
|
||||
|
||||
/**
|
||||
* Queue constructor
|
||||
|
@ -103,11 +105,14 @@ class Queue<T = unknown> {
|
|||
highWaterMark: 1 << 25
|
||||
},
|
||||
initialVolume: 100,
|
||||
bufferingTimeout: 3000
|
||||
bufferingTimeout: 3000,
|
||||
spotifyBridge: true
|
||||
} as PlayerOptions,
|
||||
options
|
||||
);
|
||||
|
||||
if ("onBeforeCreateStream" in this.options) this.onBeforeCreateStream = this.options.onBeforeCreateStream;
|
||||
|
||||
this.player.emit("debug", this, `Queue initialized:\n\n${this.player.scanDeps()}`);
|
||||
}
|
||||
|
||||
|
@ -486,6 +491,17 @@ class Queue<T = unknown> {
|
|||
return trackFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the specified track. If found, returns the track index else returns -1.
|
||||
* @param {number|Track|Snowflake} track The track
|
||||
* @returns {number}
|
||||
*/
|
||||
getTrackPosition(track: number | Track | Snowflake) {
|
||||
if (this.#watchDestroyed()) return;
|
||||
if (typeof track === "number") return this.tracks[track] != null ? track : -1;
|
||||
return this.tracks.findIndex((pred) => pred.id === (track instanceof Track ? track.id : track));
|
||||
}
|
||||
|
||||
/**
|
||||
* Jumps to particular track
|
||||
* @param {Track|number} track The track
|
||||
|
@ -493,14 +509,26 @@ class Queue<T = unknown> {
|
|||
*/
|
||||
jump(track: Track | number): void {
|
||||
if (this.#watchDestroyed()) return;
|
||||
// remove the track if exists
|
||||
const foundTrack = this.remove(track);
|
||||
if (!foundTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND);
|
||||
// since we removed the existing track from the queue,
|
||||
// we now have to place that to position 1
|
||||
// because we want to jump to that track
|
||||
// this will skip current track and play the next one which will be the foundTrack
|
||||
this.tracks.splice(1, 0, foundTrack);
|
||||
|
||||
this.tracks.splice(0, 0, foundTrack);
|
||||
|
||||
return void this.skip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Jumps to particular track, removing other tracks on the way
|
||||
* @param {Track|number} track The track
|
||||
* @returns {void}
|
||||
*/
|
||||
skipTo(track: Track | number): void {
|
||||
if (this.#watchDestroyed()) return;
|
||||
const trackIndex = this.getTrackPosition(track);
|
||||
const removedTrack = this.remove(track);
|
||||
if (!removedTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND);
|
||||
|
||||
this.tracks.splice(0, trackIndex, removedTrack);
|
||||
|
||||
return void this.skip();
|
||||
}
|
||||
|
@ -511,6 +539,7 @@ class Queue<T = unknown> {
|
|||
* @param {number} [index=0] The index where this track should be
|
||||
*/
|
||||
insert(track: Track, index = 0) {
|
||||
if (this.#watchDestroyed()) return;
|
||||
if (!track || !(track instanceof Track)) throw new PlayerError("track must be the instance of Track", ErrorStatusCode.INVALID_TRACK);
|
||||
if (typeof index !== "number" || index < 0 || !Number.isFinite(index)) throw new PlayerError(`Invalid index "${index}"`, ErrorStatusCode.INVALID_ARG_TYPE);
|
||||
|
||||
|
@ -606,38 +635,61 @@ class Queue<T = unknown> {
|
|||
this.previousTracks.push(track);
|
||||
}
|
||||
|
||||
// TODO: remove discord-ytdl-core
|
||||
let stream;
|
||||
let stream = null;
|
||||
const customDownloader = typeof this.onBeforeCreateStream === "function";
|
||||
|
||||
if (["youtube", "spotify"].includes(track.raw.source)) {
|
||||
if (track.raw.source === "spotify" && !track.raw.engine) {
|
||||
let spotifyResolved = false;
|
||||
if (this.options.spotifyBridge && track.raw.source === "spotify" && !track.raw.engine) {
|
||||
track.raw.engine = await YouTube.search(`${track.author} ${track.title}`, { type: "video" })
|
||||
.then((x) => x[0].url)
|
||||
.catch(() => null);
|
||||
spotifyResolved = true;
|
||||
}
|
||||
const link = track.raw.source === "spotify" ? track.raw.engine : track.url;
|
||||
if (!link) return void this.play(this.tracks.shift(), { immediate: true });
|
||||
|
||||
stream = ytdl(link, {
|
||||
...this.options.ytdlOptions,
|
||||
// discord-ytdl-core
|
||||
opusEncoded: false,
|
||||
fmt: "s16le",
|
||||
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
||||
seek: options.seek ? options.seek / 1000 : 0
|
||||
}).on("error", (err) => {
|
||||
return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err);
|
||||
});
|
||||
if (customDownloader) {
|
||||
stream = (await this.onBeforeCreateStream(track, spotifyResolved ? "youtube" : track.raw.source, this)) ?? null;
|
||||
if (stream)
|
||||
stream = ytdl
|
||||
.arbitraryStream(stream, {
|
||||
opusEncoded: false,
|
||||
fmt: "s16le",
|
||||
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
||||
seek: options.seek ? options.seek / 1000 : 0
|
||||
})
|
||||
.on("error", (err) => {
|
||||
return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err);
|
||||
});
|
||||
} else {
|
||||
stream = ytdl(link, {
|
||||
...this.options.ytdlOptions,
|
||||
// discord-ytdl-core
|
||||
opusEncoded: false,
|
||||
fmt: "s16le",
|
||||
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
||||
seek: options.seek ? options.seek / 1000 : 0
|
||||
}).on("error", (err) => {
|
||||
return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const tryArb = (customDownloader && (await this.onBeforeCreateStream(track, track.raw.source || track.raw.engine, this))) || null;
|
||||
const arbitrarySource = tryArb
|
||||
? tryArb
|
||||
: track.raw.source === "soundcloud"
|
||||
? await track.raw.engine.downloadProgressive()
|
||||
: typeof track.raw.engine === "function"
|
||||
? await track.raw.engine()
|
||||
: track.raw.engine;
|
||||
stream = ytdl
|
||||
.arbitraryStream(
|
||||
track.raw.source === "soundcloud" ? await track.raw.engine.downloadProgressive() : typeof track.raw.engine === "function" ? await track.raw.engine() : track.raw.engine,
|
||||
{
|
||||
opusEncoded: false,
|
||||
fmt: "s16le",
|
||||
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
||||
seek: options.seek ? options.seek / 1000 : 0
|
||||
}
|
||||
)
|
||||
.arbitraryStream(arbitrarySource, {
|
||||
opusEncoded: false,
|
||||
fmt: "s16le",
|
||||
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
||||
seek: options.seek ? options.seek / 1000 : 0
|
||||
})
|
||||
.on("error", (err) => {
|
||||
return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err);
|
||||
});
|
||||
|
|
|
@ -121,7 +121,7 @@ class Track {
|
|||
* @type {Queue}
|
||||
*/
|
||||
get queue(): Queue {
|
||||
return this.player.queues.find((q) => q.tracks.includes(this));
|
||||
return this.player.queues.find((q) => q.tracks.some((ab) => ab.id === this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -173,7 +173,7 @@ class Track {
|
|||
duration: this.duration,
|
||||
durationMS: this.durationMS,
|
||||
views: this.views,
|
||||
requestedBy: this.requestedBy.id,
|
||||
requestedBy: this.requestedBy?.id,
|
||||
playlist: hidePlaylist ? null : this.playlist?.toJSON() ?? null
|
||||
} as TrackJSON;
|
||||
}
|
||||
|
|
|
@ -74,13 +74,21 @@ class StreamDispatcher extends EventEmitter<VoiceEvents> {
|
|||
try {
|
||||
await entersState(this.voiceConnection, VoiceConnectionStatus.Connecting, this.connectionTimeout);
|
||||
} catch {
|
||||
this.voiceConnection.destroy();
|
||||
try {
|
||||
this.voiceConnection.destroy();
|
||||
} catch (err) {
|
||||
this.emit("error", err as AudioPlayerError);
|
||||
}
|
||||
}
|
||||
} else if (this.voiceConnection.rejoinAttempts < 5) {
|
||||
await Util.wait((this.voiceConnection.rejoinAttempts + 1) * 5000);
|
||||
this.voiceConnection.rejoin();
|
||||
} else {
|
||||
this.voiceConnection.destroy();
|
||||
try {
|
||||
this.voiceConnection.destroy();
|
||||
} catch (err) {
|
||||
this.emit("error", err as AudioPlayerError);
|
||||
}
|
||||
}
|
||||
} else if (newState.status === VoiceConnectionStatus.Destroyed) {
|
||||
this.end();
|
||||
|
@ -89,7 +97,13 @@ class StreamDispatcher extends EventEmitter<VoiceEvents> {
|
|||
try {
|
||||
await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, this.connectionTimeout);
|
||||
} catch {
|
||||
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) this.voiceConnection.destroy();
|
||||
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) {
|
||||
try {
|
||||
this.voiceConnection.destroy();
|
||||
} catch (err) {
|
||||
this.emit("error", err as AudioPlayerError);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.readyLock = false;
|
||||
}
|
||||
|
|
|
@ -128,6 +128,8 @@ export interface PlayerProgressbarOptions {
|
|||
* @property {YTDLDownloadOptions} [ytdlOptions={}] The youtube download options
|
||||
* @property {number} [initialVolume=100] The initial player volume
|
||||
* @property {number} [bufferingTimeout=3000] Buffering timeout for the stream
|
||||
* @property {boolean} [spotifyBridge=true] If player should bridge spotify source to youtube
|
||||
* @property {Function} [onBeforeCreateStream] Runs before creating stream
|
||||
*/
|
||||
export interface PlayerOptions {
|
||||
leaveOnEnd?: boolean;
|
||||
|
@ -138,6 +140,8 @@ export interface PlayerOptions {
|
|||
ytdlOptions?: downloadOptions;
|
||||
initialVolume?: number;
|
||||
bufferingTimeout?: number;
|
||||
spotifyBridge?: boolean;
|
||||
onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,7 +21,7 @@ const bass = (g: number) => `bass=g=${g}:f=110:w=0.3`;
|
|||
* @property {string} surrounding The surrounding filter
|
||||
* @property {string} pulsator The pulsator filter
|
||||
* @property {string} subboost The subboost filter
|
||||
* @property {string} kakaoke The kakaoke filter
|
||||
* @property {string} karaoke The kakaoke filter
|
||||
* @property {string} flanger The flanger filter
|
||||
* @property {string} gate The gate filter
|
||||
* @property {string} haas The haas filter
|
||||
|
|
Loading…
Reference in a new issue