UNPKG

@skybloxsystems/ticket-bot

Version:
161 lines (142 loc) 5.14 kB
const ChildProcess = require('child_process'); const { Duplex } = require('stream'); let FFMPEG = { command: null, output: null, }; const VERSION_REGEX = /version (.+) Copyright/mi; Object.defineProperty(FFMPEG, 'version', { get() { return VERSION_REGEX.exec(FFMPEG.output)[1]; }, enumerable: true, }); /** * An FFmpeg transform stream that provides an interface to FFmpeg. * @memberof core */ class FFmpeg extends Duplex { /** * Creates a new FFmpeg transform stream * @memberof core * @param {Object} options Options you would pass to a regular Transform stream, plus an `args` option * @param {Array<string>} options.args Arguments to pass to FFmpeg * @param {boolean} [options.shell=false] Whether FFmpeg should be spawned inside a shell * @example * // By default, if you don't specify an input (`-i ...`) prism will assume you're piping a stream into it. * const transcoder = new prism.FFmpeg({ * args: [ * '-analyzeduration', '0', * '-loglevel', '0', * '-f', 's16le', * '-ar', '48000', * '-ac', '2', * ] * }); * const s16le = mp3File.pipe(transcoder); * const opus = s16le.pipe(new prism.opus.Encoder({ rate: 48000, channels: 2, frameSize: 960 })); */ constructor(options = {}) { super(); this.process = FFmpeg.create({ shell: false, ...options }); const EVENTS = { readable: this._reader, data: this._reader, end: this._reader, unpipe: this._reader, finish: this._writer, drain: this._writer, }; this._readableState = this._reader._readableState; this._writableState = this._writer._writableState; this._copy(['write', 'end'], this._writer); this._copy(['read', 'setEncoding', 'pipe', 'unpipe'], this._reader); for (const method of ['on', 'once', 'removeListener', 'removeListeners', 'listeners']) { this[method] = (ev, fn) => EVENTS[ev] ? EVENTS[ev][method](ev, fn) : Duplex.prototype[method].call(this, ev, fn); } const processError = error => this.emit('error', error); this._reader.on('error', processError); this._writer.on('error', processError); } get _reader() { return this.process.stdout; } get _writer() { return this.process.stdin; } _copy(methods, target) { for (const method of methods) { this[method] = target[method].bind(target); } } _destroy(err, cb) { this._cleanup(); return cb ? cb(err) : undefined; } _final(cb) { this._cleanup(); cb(); } _cleanup() { if (this.process) { this.once('error', () => {}); this.process.kill('SIGKILL'); this.process = null; } } /** * The available FFmpeg information * @typedef {Object} FFmpegInfo * @memberof core * @property {string} command The command used to launch FFmpeg * @property {string} output The output from running `ffmpeg -h` * @property {string} version The version of FFmpeg being used, determined from `output`. */ /** * Finds a suitable FFmpeg command and obtains the debug information from it. * @param {boolean} [force=false] If true, will ignore any cached results and search for the command again * @returns {FFmpegInfo} * @throws Will throw an error if FFmpeg cannot be found. * @example * const ffmpeg = prism.FFmpeg.getInfo(); * * console.log(`Using FFmpeg version ${ffmpeg.version}`); * * if (ffmpeg.output.includes('--enable-libopus')) { * console.log('libopus is available!'); * } else { * console.log('libopus is unavailable!'); * } */ static getInfo(force = false) { if (FFMPEG.command && !force) return FFMPEG; const sources = [() => { const ffmpegStatic = require('ffmpeg-static'); return ffmpegStatic.path || ffmpegStatic; }, 'ffmpeg', 'avconv', './ffmpeg', './avconv']; for (let source of sources) { try { if (typeof source === 'function') source = source(); const result = ChildProcess.spawnSync(source, ['-h'], { windowsHide: true }); if (result.error) throw result.error; Object.assign(FFMPEG, { command: source, output: Buffer.concat(result.output.filter(Boolean)).toString(), }); return FFMPEG; } catch (error) { // Do nothing } } throw new Error('FFmpeg/avconv not found!'); } /** * Creates a new FFmpeg instance. If you do not include `-i ...` it will be assumed that `-i -` should be prepended * to the options and that you'll be piping data into the process. * @param {String[]} [args=[]] Arguments to pass to FFmpeg * @returns {ChildProcess} * @private * @throws Will throw an error if FFmpeg cannot be found. */ static create({ args = [], shell = false } = {}) { if (!args.includes('-i')) args.unshift('-i', '-'); return ChildProcess.spawn(FFmpeg.getInfo().command, args.concat(['pipe:1']), { windowsHide: true, shell }); } } module.exports = FFmpeg;