Skip to content

Commit

Permalink
fix(Stream): fix stream ended prematurely caused by YouTube throttling
Browse files Browse the repository at this point in the history
  • Loading branch information
skick1234 committed Nov 8, 2023
1 parent 4ece777 commit daad198
Show file tree
Hide file tree
Showing 7 changed files with 1,496 additions and 1,318 deletions.
40 changes: 20 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,43 +61,43 @@
],
"homepage": "https://distube.js.org/",
"dependencies": {
"@distube/ytdl-core": "^4.12.1",
"@distube/ytpl": "^1.1.4",
"@distube/ytsr": "^1.1.10",
"@distube/ytdl-core": "^4.13.2",
"@distube/ytpl": "^1.2.1",
"@distube/ytsr": "^1.2.0",
"prism-media": "npm:@distube/prism-media@latest",
"tiny-typed-emitter": "^2.1.0",
"tough-cookie": "^4.1.3",
"tslib": "^2.6.2",
"undici": "^5.23.0"
"undici": "^5.27.2"
},
"devDependencies": {
"@babel/core": "^7.22.11",
"@babel/core": "^7.23.2",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/plugin-transform-private-methods": "^7.22.5",
"@babel/preset-env": "^7.22.10",
"@babel/preset-typescript": "^7.22.11",
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@babel/preset-env": "^7.23.2",
"@babel/preset-typescript": "^7.23.2",
"@commitlint/cli": "^18.2.0",
"@commitlint/config-conventional": "^18.1.0",
"@discordjs/voice": "^0.16.0",
"@distubejs/docgen": "distubejs/docgen",
"@types/jest": "^29.5.4",
"@types/node": "^20.5.7",
"@types/tough-cookie": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"babel-jest": "^29.6.4",
"@types/jest": "^29.5.8",
"@types/node": "^20.9.0",
"@types/tough-cookie": "^4.0.5",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"babel-jest": "^29.7.0",
"discord.js": "^14.13.0",
"eslint": "^8.48.0",
"eslint": "^8.53.0",
"eslint-config-distube": "^1.6.4",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-deprecation": "^1.5.0",
"eslint-plugin-jsdoc": "^46.5.1",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-jsdoc": "^46.8.2",
"husky": "^8.0.3",
"jest": "^29.6.4",
"jest": "^29.7.0",
"jsdoc-babel": "^0.5.0",
"nano-staged": "^0.8.0",
"npm-check-updates": "^16.13.2",
"npm-check-updates": "^16.14.6",
"pinst": "^3.0.0",
"prettier": "^3.0.3",
"tsup": "^7.2.0",
Expand Down
4 changes: 2 additions & 2 deletions src/core/DisTubeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,9 +369,9 @@ export class DisTubeHandler extends DisTubeBase {
* @param {Song} song A Song
*/
async attachStreamInfo(song: Song) {
const { url, source, formats, streamURL, isLive } = song;
const { url, source, formats, streamURL } = song;
if (source === "youtube") {
if (!formats || !chooseBestVideoFormat(formats, isLive)) {
if (!formats || !chooseBestVideoFormat(song)) {
song._patchYouTube(await this.handler.getYouTubeInfo(url));
}
} else if (!streamURL) {
Expand Down
26 changes: 11 additions & 15 deletions src/core/DisTubeStream.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
import { FFmpeg } from "prism-media";
import { DisTubeError, isURL } from "..";
import { StreamType as DiscordVoiceStreamType } from "@discordjs/voice";
import type ytdl from "@distube/ytdl-core";
import type { StreamType } from "..";
import type { Song, StreamType } from "..";

interface StreamOptions {
seek?: number;
ffmpegArgs?: string[];
isLive?: boolean;
type?: StreamType;
}

export const chooseBestVideoFormat = (formats: ytdl.videoFormat[], isLive = false) => {
let filter = (format: ytdl.videoFormat) => format.hasAudio;
if (isLive) filter = (format: ytdl.videoFormat) => format.hasAudio && format.isHLS;
formats = formats
.filter(filter)
.sort((a, b) => Number(b.audioBitrate) - Number(a.audioBitrate) || Number(a.bitrate) - Number(b.bitrate));
return formats.find(format => !format.hasVideo) || formats.sort((a, b) => Number(a.bitrate) - Number(b.bitrate))[0];
};
export const chooseBestVideoFormat = ({ duration, formats, isLive }: Song) =>
formats &&
formats
.filter(f => f.hasAudio && (duration < 10 * 60 || f.hasVideo) && (!isLive || f.isHLS))
.sort((a, b) => Number(b.audioBitrate) - Number(a.audioBitrate) || Number(a.bitrate) - Number(b.bitrate))[0];

/**
* Create a stream to play with {@link DisTubeVoice}
Expand All @@ -27,17 +22,18 @@ export const chooseBestVideoFormat = (formats: ytdl.videoFormat[], isLive = fals
export class DisTubeStream {
/**
* Create a stream from ytdl video formats
* @param {ytdl.videoFormat[]} formats ytdl video formats
* @param {Song} song A YouTube Song
* @param {StreamOptions} options options
* @returns {DisTubeStream}
* @private
*/
static YouTube(formats: ytdl.videoFormat[] | undefined, options: StreamOptions = {}): DisTubeStream {
if (!formats || !formats.length) throw new DisTubeError("UNAVAILABLE_VIDEO");
static YouTube(song: Song, options: StreamOptions = {}): DisTubeStream {
if (song.source !== "youtube") throw new DisTubeError("INVALID_TYPE", "youtube", song.source, "Song#source");
if (!song.formats?.length) throw new DisTubeError("UNAVAILABLE_VIDEO");
if (!options || typeof options !== "object" || Array.isArray(options)) {
throw new DisTubeError("INVALID_TYPE", "object", options, "options");
}
const bestFormat = chooseBestVideoFormat(formats, options.isLive);
const bestFormat = chooseBestVideoFormat(song);
if (!bestFormat) throw new DisTubeError("UNPLAYABLE_FORMATS");
return new DisTubeStream(bestFormat.url, options);
}
Expand Down
6 changes: 3 additions & 3 deletions src/core/manager/QueueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,14 @@ export class QueueManager extends GuildIdManager<Queue> {
* @returns {DisTubeStream}
*/
createStream(queue: Queue): DisTubeStream {
const { duration, formats, isLive, source, streamURL } = queue.songs[0];
const song = queue.songs[0];
const { duration, source, streamURL } = song;
const streamOptions = {
ffmpegArgs: queue.filters.ffmpegArgs,
seek: duration ? queue.beginTime : undefined,
isLive,
type: this.options.streamType,
};
if (source === "youtube") return DisTubeStream.YouTube(formats, streamOptions);
if (source === "youtube") return DisTubeStream.YouTube(song, streamOptions);
return DisTubeStream.DirectLink(streamURL as string, streamOptions);
}

Expand Down
44 changes: 33 additions & 11 deletions tests/core/DisTubeStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,43 @@ jest.mock("prism-media");
const FFmpeg = _FFmpeg as unknown as jest.Mocked<typeof _FFmpeg>;

const regularItag = 251;
const liveItag = 91;
const liveItag = 95;
const longItag = 18;

afterEach(() => {
jest.resetAllMocks();
});

describe("chooseBestVideoFormat()", () => {
test("Regular video", () => {
expect(chooseBestVideoFormat(regularFormats)).toMatchObject({ itag: regularItag });
expect(chooseBestVideoFormat(<any>{ formats: regularFormats, isLive: false, duration: 60 })).toMatchObject({
itag: regularItag,
});
});

test("Live video", () => {
expect(chooseBestVideoFormat(liveFormats as any, true)).toMatchObject({ itag: liveItag });
// Non-HLS live is not supported
expect(liveFormats.find(f => f.itag === liveItag).isHLS).toBe(true);
expect(chooseBestVideoFormat(<any>{ formats: liveFormats, isLive: true, duration: 0 })).toMatchObject({
itag: liveItag,
isHLS: true,
});
});

test("Long video", () => {
expect(chooseBestVideoFormat(<any>{ formats: regularFormats, isLive: false, duration: 15 * 60 })).toMatchObject({
itag: longItag,
});
});
});

describe("DisTubeStream.YouTube()", () => {
test("Regular video", () => {
const stream = DisTubeStream.YouTube(regularFormats, { ffmpegArgs: ["added", "arguments"], type: StreamType.RAW });
const stream = DisTubeStream.YouTube(
<any>{ source: "youtube", formats: regularFormats, isLive: false, duration: 60 },
{
ffmpegArgs: ["added", "arguments"],
type: StreamType.RAW,
},
);
const url = regularFormats.find(f => f.itag === regularItag).url;
expect(stream).toMatchObject({
url,
Expand Down Expand Up @@ -65,7 +81,9 @@ describe("DisTubeStream.YouTube()", () => {
});

test("Live video", () => {
const stream = DisTubeStream.YouTube(liveFormats as any, { seek: 1, isLive: true });
const stream = DisTubeStream.YouTube(<any>{ source: "youtube", formats: liveFormats, isLive: true, duration: 60 }, {
seek: 1,
});
const url = liveFormats.find(f => f.itag === liveItag).url;
expect(stream).toMatchObject({
url,
Expand Down Expand Up @@ -103,17 +121,21 @@ describe("DisTubeStream.YouTube()", () => {
});

test("Should not return a DisTubeStream", () => {
const s: any = { source: "test" };
expect(() => {
DisTubeStream.YouTube(s);
}).toThrow(new DisTubeError("INVALID_TYPE", "youtube", s.source, "Song#source"));
expect(() => {
DisTubeStream.YouTube([]);
DisTubeStream.YouTube(<any>{ source: "youtube" });
}).toThrow(new DisTubeError("UNAVAILABLE_VIDEO"));
expect(() => {
DisTubeStream.YouTube([{}] as any, 0 as any);
DisTubeStream.YouTube(<any>{ source: "youtube", formats: [{}] }, 0 as any);
}).toThrow(new DisTubeError("INVALID_TYPE", "object", 0, "options"));
expect(() => {
DisTubeStream.YouTube([{}] as any, [] as any);
DisTubeStream.YouTube(<any>{ source: "youtube", formats: [{}] }, [] as any);
}).toThrow(new DisTubeError("INVALID_TYPE", "object", [], "options"));
expect(() => {
DisTubeStream.YouTube([{}] as any);
DisTubeStream.YouTube(<any>{ source: "youtube", formats: [{}] });
}).toThrow(new DisTubeError("UNPLAYABLE_FORMATS"));
expect(FFmpeg).not.toBeCalled();
});
Expand Down
2 changes: 1 addition & 1 deletion tests/core/manager/QueueManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe("QueueManager#createStream()", () => {
expect(result).toBe(stream);
expect(mockFn).toBeCalledTimes(1);
expect(mockFn).toBeCalledWith(
song.formats,
song,
expect.objectContaining({
ffmpegArgs: [],
seek: 1,
Expand Down
Loading

0 comments on commit daad198

Please sign in to comment.