FFmpeg.js (4689B)
1 const ChildProcess = require('child_process'); 2 const { Duplex } = require('stream'); 3 4 let FFMPEG = { 5 command: null, 6 output: null, 7 }; 8 9 const VERSION_REGEX = /version (.+) Copyright/mi; 10 11 Object.defineProperty(FFMPEG, 'version', { 12 get() { 13 return VERSION_REGEX.exec(FFMPEG.output)[1]; 14 }, 15 enumerable: true, 16 }); 17 18 /** 19 * An FFmpeg transform stream that provides an interface to FFmpeg. 20 * @memberof core 21 */ 22 class FFmpeg extends Duplex { 23 /** 24 * Creates a new FFmpeg transform stream 25 * @memberof core 26 * @param {Object} options Options you would pass to a regular Transform stream, plus an `args` option 27 * @param {Array<string>} options.args Arguments to pass to FFmpeg 28 * @example 29 * // By default, if you don't specify an input (`-i ...`) prism will assume you're piping a stream into it. 30 * const transcoder = new prism.FFmpeg({ 31 * args: [ 32 * '-analyzeduration', '0', 33 * '-loglevel', '0', 34 * '-f', 's16le', 35 * '-ar', '48000', 36 * '-ac', '2', 37 * ] 38 * }); 39 * const s16le = mp3File.pipe(transcoder); 40 * const opus = s16le.pipe(new prism.opus.Encoder({ rate: 48000, channels: 2, frameSize: 960 })); 41 */ 42 constructor(options = {}) { 43 super(); 44 this.process = FFmpeg.create(options); 45 const EVENTS = { 46 readable: this._reader, 47 data: this._reader, 48 end: this._reader, 49 unpipe: this._reader, 50 finish: this._writer, 51 drain: this._writer, 52 }; 53 54 this._readableState = this._reader._readableState; 55 this._writableState = this._writer._writableState; 56 57 this._copy(['write', 'end'], this._writer); 58 this._copy(['read', 'setEncoding', 'pipe', 'unpipe'], this._reader); 59 60 for (const method of ['on', 'once', 'removeListener', 'removeListeners', 'listeners']) { 61 this[method] = (ev, fn) => EVENTS[ev] ? EVENTS[ev][method](ev, fn) : Duplex.prototype[method].call(this, ev, fn); 62 } 63 64 const processError = error => this.emit('error', error); 65 this._reader.on('error', processError); 66 this._writer.on('error', processError); 67 } 68 69 get _reader() { return this.process.stdout; } 70 get _writer() { return this.process.stdin; } 71 72 _copy(methods, target) { 73 for (const method of methods) { 74 this[method] = target[method].bind(target); 75 } 76 } 77 78 _destroy(err, cb) { 79 super._destroy(err, cb); 80 this.once('error', () => {}); 81 this.process.kill('SIGKILL'); 82 } 83 84 85 /** 86 * The available FFmpeg information 87 * @typedef {Object} FFmpegInfo 88 * @memberof core 89 * @property {string} command The command used to launch FFmpeg 90 * @property {string} output The output from running `ffmpeg -h` 91 * @property {string} version The version of FFmpeg being used, determined from `output`. 92 */ 93 94 /** 95 * Finds a suitable FFmpeg command and obtains the debug information from it. 96 * @param {boolean} [force=false] If true, will ignore any cached results and search for the command again 97 * @returns {FFmpegInfo} 98 * @throws Will throw an error if FFmpeg cannot be found. 99 * @example 100 * const ffmpeg = prism.FFmpeg.getInfo(); 101 * 102 * console.log(`Using FFmpeg version ${ffmpeg.version}`); 103 * 104 * if (ffmpeg.output.includes('--enable-libopus')) { 105 * console.log('libopus is available!'); 106 * } else { 107 * console.log('libopus is unavailable!'); 108 * } 109 */ 110 static getInfo(force = false) { 111 if (FFMPEG.command && !force) return FFMPEG; 112 const sources = [() => { 113 const ffmpegStatic = require('ffmpeg-static'); 114 return ffmpegStatic.path || ffmpegStatic; 115 }, 'ffmpeg', 'avconv', './ffmpeg', './avconv']; 116 for (let source of sources) { 117 try { 118 if (typeof source === 'function') source = source(); 119 const result = ChildProcess.spawnSync(source, ['-h'], { windowsHide: true }); 120 if (result.error) throw result.error; 121 Object.assign(FFMPEG, { 122 command: source, 123 output: Buffer.concat(result.output.filter(Boolean)).toString(), 124 }); 125 return FFMPEG; 126 } catch (error) { 127 // Do nothing 128 } 129 } 130 throw new Error('FFmpeg/avconv not found!'); 131 } 132 133 /** 134 * Creates a new FFmpeg instance. If you do not include `-i ...` it will be assumed that `-i -` should be prepended 135 * to the options and that you'll be piping data into the process. 136 * @param {String[]} [args=[]] Arguments to pass to FFmpeg 137 * @returns {ChildProcess} 138 * @private 139 * @throws Will throw an error if FFmpeg cannot be found. 140 */ 141 static create({ args = [] } = {}) { 142 if (!args.includes('-i')) args.unshift('-i', '-'); 143 return ChildProcess.spawn(FFmpeg.getInfo().command, args.concat(['pipe:1']), { windowsHide: true }); 144 } 145 } 146 147 module.exports = FFmpeg;