text: 'use strict';
  
  /**
   * Synthesis - A class for video synthesis.
   * Mainly rely on the function of ffmpeg to synthesize video and audio.
   *
   * ####Example:
   *
   *     const synthesis = new Synthesis(conf);
   *     synthesis.addStream(stream);
   *     synthesis.addAudios(audios);
   *     synthesis.start();
   *
   *
   * @class
   */
  const path = require('path');
  const isEmpty = require('lodash/isEmpty');
  const forEach = require('lodash/forEach');
  
  const FS = require('../utils/fs');
  const Utils = require('../utils/utils');
  const FFLogger = require('../utils/logger');
  const FFmpegUtil = require('../utils/ffmpeg');
  const FFEventer = require('../event/eventer');
  
  class Synthesis extends FFEventer {
    constructor(conf) {
      super();
  
      this.conf = conf;
      this.cover = null;
      this.audios = null;
      this.duration = 0;
      this.inputCount = 0;
      this.framesNum = 100;
      this.outputOptions = [];
      const threads = conf.getVal('threads');
      this.command = FFmpegUtil.createCommand({ threads });
    }
  
    /**
     * set total frames number
     * @param {number} framesNum - total frames number
     * @public
     */
    setFramesNum(framesNum) {
      const { conf } = this;
      const fps = conf.getVal('fps');
      const rfps = conf.getVal('rfps');
      this.framesNum = Math.ceil((framesNum * fps) / rfps);
    }
  
    /**
     * Set the ffcreator total duration
     * @param {number} duration - total duration time
     * @public
     */
    setDuration(duration) {
      this.duration = duration;
    }
  
    /**
     * Add the intermediate pictures processed in the cache directory to ffmpeg input
     * @param {Stream} stream - A readability stream
     * @public
     */
    addStream(stream) {
      const { conf, command } = this;
      const type = conf.getVal('cacheFormat');
      command.addInput(stream);
      if (type === 'raw') command.inputFormat('rawvideo');
      this.addStreamInputOptions();
      this.inputCount++;
    }
  
    /**
     * Add input options for image input stream
     * @private
     */
    addStreamInputOptions() {
      const { conf, command } = this;
      const size = conf.getWH('x');
      const rfps = conf.getVal('rfps');
      const type = conf.getVal('cacheFormat');
  
      const opts = ['-framerate', rfps];
      // raw (unencoded data in BGRA order on little-endian (most) systems, ARGB on big-endian systems; top-to-bottom)
      const newOpts =
        type === 'raw'
          ? ['-s', size, '-vcodec', 'rawvideo', '-pixel_format', 'bgra', '-video_size', size]
          : [];
  
      command.inputOptions(opts.concat(newOpts));
    }
  
    /**
     * Add ordinary ffmpeg cover input elements
     * @param {string} input - A input elements
     * @public
     */
    addCover(cover) {
      this.cover = cover;
    }
  
    /**
     * add audios to ffmpeg config
     * @public
     */
    addAudios(audios) {
      if (isEmpty(audios)) return;
  
      const { command } = this;
      forEach(audios, audio => {
        command.addInput(audio.path);
        if (audio.loop) command.inputOptions(['-stream_loop', '-1']);
        this.inputCount++;
      });
      this.audios = audios;
    }
  
    /**
     * Add one or more background sounds
     * ##Note: Usage of adelay/volume filter
     * [1]adelay=5000|5000,volume=1[a];
     * [2]adelay=10000|10000,volume=10[b];
     * [3]adelay=15000|15000,volume=2[c];
     * [a][b][c]amix=3[a]
     * @param {array} audios - background sounds
     * @private
     */
    addAudioFilter() {
      const { audios, duration } = this;
      if (isEmpty(audios)) return;
  
      const filters = [];
      let outputs = '';
      forEach(audios, (audio, index) => {
        const output = `audio${index}`;
        const audioCommand = audio.toCommand({ index, duration });
        filters.push(audioCommand);
        outputs += `[${output}]`;
      });
  
      filters.push(`${outputs}amix=${audios.length}`);
      this.command.complexFilter(filters);
    }
  
    /**
     * Add ffmpeg input configuration
     * @private
     */
    addInputOptions() {
      const { conf, command } = this;
      const customOpts = conf.getVal('inputOptions');
      customOpts && command.inputOptions(customOpts);
    }
  
    /**
     * Get default ffmpeg output configuration
     * @private
     */
    getDefaultOutputOptions() {
      const { conf } = this;
      const vb = conf.getVal('vb');
      const fps = conf.getVal('fps');
      const crf = conf.getVal('crf');
      const preset = conf.getVal('preset');
  
      let opts = []
        // misc
        .concat([
          '-hide_banner', // hide_banner - parameter, you can display only meta information
          '-map_metadata',
          '-1',
          '-map_chapters',
          '-1',
        ])
  
        // video
        .concat([
          '-c:v',
          'libx264', // c:v - H.264
          '-profile:v',
          'main', // profile:v - main profile: mainstream image quality. Provide I / P / B frames
          '-preset',
          preset, // preset - compromised encoding speed
          '-crf',
          crf, // crf - The range of quantization ratio is 0 ~ 51, where 0 is lossless mode, 23 is the default value, 51 may be the worst
          '-movflags',
          'faststart',
          '-pix_fmt',
          'yuv420p',
          '-r',
          fps,
        ]);
  
      //---- vb -----
      if (vb) {
        opts = opts.concat(['-vb', vb]);
      }
  
      return opts;
    }
  
    /**
     * Add ffmpeg output configuration
     * @private
     */
    addOutputOptions() {
      const { conf, audios } = this;
      const defaultOutputOptions = conf.getVal('defaultOutputOptions');
  
      // custom
      const customOpts = conf.getVal('outputOptions');
      if (customOpts) FFmpegUtil.concatOpts(this.outputOptions, customOpts);
  
      // default
      const defaultOpts = this.getDefaultOutputOptions(conf);
      if (defaultOutputOptions) FFmpegUtil.concatOpts(this.outputOptions, defaultOpts);
  
      // music
      if (!isEmpty(audios)) {
        FFmpegUtil.concatOpts(this.outputOptions, ['-c:a', 'aac', '-shortest']);
      }
  
      this.command.outputOptions(this.outputOptions);
    }
  
    /**
     * Set ffmpeg input path
     * @private
     */
    addOutput() {
      const finalOutput = this.getOutputPath('final');
      const currOutput = this.getOutputPath('curr');
      FS.ensureDir(finalOutput, true);
      FS.ensureDir(currOutput, true);
      this.command.output(currOutput);
    }
  
    /**
     * Open ffmpeg production and processing
     * @public
     */
    start() {
      this.addInputOptions();
      this.addAudioFilter();
      this.addOutputOptions();
      this.addCommandEvents();
      this.addOutput();
      this.command.run();
    }
  
    /**
     * Get the final output file path
     * @private
     */
    getOutputPath(real = null) {
      const { conf, cover } = this;
      let output = conf.getVal('output');
  
      if (cover && real !== 'final') {
        const pathId = conf.getVal('pathId');
        const dir = conf.getVal('detailedCacheDir');
        output = path.join(dir, pathId + '_cover.mp4');
      }
  
      return path.normalize(output);
    }
  
    /**
     * Add FFmpeg event to command
     * @private
     */
    addCommandEvents() {
      const { conf, cover, command, framesNum } = this;
      const ffmpeglog = conf.getVal('ffmpeglog');
      const highWaterMark = conf.getVal('highWaterMark');
      const video = this.getOutputPath('curr');
      const output = this.getOutputPath('final');
  
      // complete and error callback
      const completeFunc = () => {
        this.emit({ type: 'synthesis-complete', output });
        FFLogger.info({ pos: 'Synthesis', msg: 'synthesis complete.' });
      };
  
      const errorFunc = (error, stdout, stderr) => {
        this.emitError({ error, pos: 'Synthesis' });
        FFLogger.error({
          error,
          pos: 'Synthesis',
          msg: `stdout:${stdout} \n stderr:${stderr}`,
        });
      };
  
      // start event
      command.on('start', commandLine => {
        FFmpegUtil.setHighWaterMark({ command, highWaterMark });
        this.emit({ type: 'synthesis-start', command: commandLine });
        if (ffmpeglog) {
          command.ffmpegProc.stderr.on('data', data => console.log(data));
        }
        // log info
        FFLogger.info({ pos: 'Synthesis', msg: `synthesis start: ${commandLine}` });
      });
  
      // progress event
      command.on('progress', progress => {
        let percent = Utils.floor(progress.frames / framesNum, 2);
        percent = Math.min(1, percent);
        this.emitProgress({ percent });
        // log info
        const percent100 = Math.floor(percent * 100);
        FFLogger.info({ pos: 'Synthesis', msg: `synthesis progress: ${percent100}% done.` });
      });
  
      command.on('end', async () => {
        // check if cover is included
        if (cover) {
          FFmpegUtil.addCoverImage({ video, output, cover })
            .then(completeFunc)
            .catch(([error, stdout, stderr]) => errorFunc(error, stdout, stderr));
        } else {
          completeFunc();
        }
      });
  
      // error
      command.on('error', (error, stdout, stderr) => errorFunc(error, stdout, stderr));
    }
  
    destroy() {
      super.destroy();
      FFmpegUtil.destroy(this.command);
  
      this.conf = null;
      this.cover = null;
      this.audios = null;
      this.command = null;
      this.outputOptions.length = 0;
    }
  }
  
  module.exports = Synthesis;
  