buddy

node MVC discord bot
Log | Files | Refs | README

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;