import ytdl, { downloadOptions } from "ytdl-core"; import { opus as Opus, FFmpeg } from "prism-media"; import { Readable, Duplex, pipeline } from "stream"; // ytdl events const evn = ["info", "progress", "abort", "request", "response", "error", "redirect", "retry", "reconnect"]; // Opus Filter const opusFilter = (format: ytdl.videoFormat) => format.codecs === "opus" && format.container === "webm" && Number(format.audioSampleRate) === 48000 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 or video info * @param options - YTDL options * @example const ytdl = require("discord-ytdl-core"); * const stream = ytdl("VIDEO_URL", { * seek: 3, * encoderArgs: ["-af", "bass=g=10"], * opusEncoded: true * }); * VoiceConnection.play(stream, { * type: "opus" * }); */ const StreamDownloader = (url: string | ytdl.videoInfo, options: YTDLStreamOptions) => { if (!url) { throw new Error("No input url or videoInfo provided"); } let FFmpegArgs: string[] = [ "-analyzeduration", "0", "-loglevel", "0", "-f", `${options && options.fmt && typeof (options.fmt) == "string" ? options.fmt : "s16le"}`, "-ar", "48000", "-ac", "2" ]; if (options && options.seek && !isNaN(options.seek)) { FFmpegArgs.unshift("-ss", options.seek.toString()); } if (options && options.encoderArgs && Array.isArray(options.encoderArgs)) { FFmpegArgs = FFmpegArgs.concat(options.encoderArgs); } else if ( typeof url !== "string" && url.formats.find(opusFilter) && !Number(options.seek) && Number(url.videoDetails.lengthSeconds) > 0 ) { const demuxer = new Opus.WebmDemuxer(); return pipeline([ ytdl.downloadFromInfo(url, options), demuxer, ], () => { }); } const transcoder = new FFmpeg({ shell: false, args: FFmpegArgs }); const inputStream = typeof url === "string" ? ytdl(url, options) : ytdl.downloadFromInfo(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)); } const _destroy = () => { if (!transcoder.destroyed) transcoder.destroy(); if (!opus.destroyed) opus.destroy(); } outputStream.on("close", _destroy).on("error", _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"); } let FFmpegArgs: string[]; if (typeof stream === "string") { FFmpegArgs = [ '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5', "-i", stream, "-analyzeduration", "0", "-loglevel", "0", "-f", `${options && options.fmt && typeof (options.fmt) == "string" ? options.fmt : "s16le"}`, "-ar", "48000", "-ac", "2" ]; } else { FFmpegArgs = [ "-analyzeduration", "0", "-loglevel", "0", "-f", `${options && options.fmt && typeof (options.fmt) == "string" ? options.fmt : "s16le"}`, "-ar", "48000", "-ac", "2" ]; } if (options && options.seek && !isNaN(options.seek)) { FFmpegArgs.unshift("-ss", options.seek.toString()); } if (options && options.encoderArgs && Array.isArray(options.encoderArgs)) { FFmpegArgs = FFmpegArgs.concat(options.encoderArgs); } let transcoder = new FFmpeg({ shell: false, 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); const _destroy = () => { if (!transcoder.destroyed) transcoder.destroy(); if (!opus.destroyed) opus.destroy(); } outputStream.on("close", _destroy).on("error", _destroy); return outputStream; }; StreamDownloader.arbitraryStream = arbitraryStream; StreamDownloader.version = require("./package.json").version; const DiscordYTDLCore = Object.assign(StreamDownloader, ytdl); export = DiscordYTDLCore;