@spikebot/discord-ytdl-core
Version:
Simple ytdl wrapper for discord bots with custom ffmpeg args support.
204 lines (178 loc) • 4.96 kB
text/typescript
import ytdl, { downloadOptions } from '@spikebot/ytdl-core';
import { opus as Opus, FFmpeg } from 'prism-media';
import { Readable, Duplex } from 'stream';
// ytdl events
const evn = [
'info',
'progress',
'abort',
'request',
'response',
'error',
'redirect',
'retry',
'reconnect',
];
interface YTDLStreamOptions extends downloadOptions {
seek?: number;
encoderArgs?: string[];
fmt?: string;
opusEncoded?: boolean;
}
interface StreamOptions {
seek?: number;
encoderArgs?: string[];
fmt?: string;
opusEncoded?: boolean;
}
/**
* Create an opus stream for your video with provided encoder args
* @param url - YouTube URL of the video
* @param options - YTDL options
* @example */
const StreamDownloader = (url: string, options?: YTDLStreamOptions) => {
if (!url) {
throw new Error('No input url provided');
}
if (typeof url !== 'string') {
throw new SyntaxError(
`input URL must be a string. Received ${typeof url}!`
);
}
options ??= {};
let FFmpegArgs: string[] = [
'-analyzeduration',
'0',
'-loglevel',
'0',
'-f',
`${typeof options.fmt === 'string' ? options.fmt : 's16le'}`,
'-ar',
'48000',
'-ac',
'2',
];
if (!isNaN(options.seek)) {
FFmpegArgs.unshift('-ss', options.seek.toString());
}
if (Array.isArray(options.encoderArgs)) {
FFmpegArgs = FFmpegArgs.concat(options.encoderArgs);
}
const transcoder = new FFmpeg({
args: FFmpegArgs,
});
const inputStream = ytdl(url, options);
const output = inputStream.pipe(transcoder);
if (options && !options.opusEncoded) {
for (const event of evn) {
inputStream.on(event, (...args) => output.emit(event, ...args));
}
inputStream.on('error', () => transcoder.destroy());
output.on('close', () => transcoder.destroy());
return output;
}
const opus = new Opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960,
});
const outputStream = output.pipe(opus);
output.on('error', (e) => outputStream.emit('error', e));
for (const event of evn) {
inputStream.on(event, (...args) => outputStream.emit(event, ...args));
}
outputStream.on('close', () => {
transcoder.destroy();
opus.destroy();
});
return outputStream;
};
/**
* Creates arbitraryStream
* @param stream Any readable stream source
* @param options Stream options
* @example const streamSource = "https://listen.moe/kpop/opus";
* let stream = ytdl.arbitraryStream(streamSource, {
* encoderArgs: ["-af", "asetrate=44100*1.25"],
* fmt: "mp3"
* });
*
* stream.pipe(fs.createWriteStream("kpop.mp3"));
*/
const arbitraryStream = (
stream: string | Readable | Duplex,
options?: StreamOptions
) => {
if (!stream) {
throw new Error('No stream source provided');
}
options ??= {};
let FFmpegArgs: string[];
if (typeof stream === 'string') {
FFmpegArgs = [
'-reconnect',
'1',
'-reconnect_streamed',
'1',
'-reconnect_delay_max',
'5',
'-i',
stream,
'-analyzeduration',
'0',
'-loglevel',
'0',
'-f',
`${typeof options.fmt === 'string' ? options.fmt : 's16le'}`,
'-ar',
'48000',
'-ac',
'2',
];
} else {
FFmpegArgs = [
'-analyzeduration',
'0',
'-loglevel',
'0',
'-f',
`${typeof options.fmt === 'string' ? options.fmt : 's16le'}`,
'-ar',
'48000',
'-ac',
'2',
];
}
if (!isNaN(options.seek)) {
FFmpegArgs.unshift('-ss', options.seek.toString());
}
if (Array.isArray(options.encoderArgs)) {
FFmpegArgs = FFmpegArgs.concat(options.encoderArgs);
}
let transcoder = new FFmpeg({
args: FFmpegArgs,
});
if (typeof stream !== 'string') {
transcoder = stream.pipe(transcoder);
stream.on('error', () => transcoder.destroy());
}
if (options && !options.opusEncoded) {
transcoder.on('close', () => transcoder.destroy());
return transcoder;
}
const opus = new Opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960,
});
const outputStream = transcoder.pipe(opus);
outputStream.on('close', () => {
transcoder.destroy();
opus.destroy();
});
return outputStream;
};
StreamDownloader.arbitraryStream = arbitraryStream;
StreamDownloader.version = require('./package.json').version;
const DiscordYTDLCore = Object.assign(StreamDownloader, ytdl);
export = DiscordYTDLCore;