Skip to content

Instantly share code, notes, and snippets.

@erhhung
Created October 7, 2016 02:02
Show Gist options
  • Select an option

  • Save erhhung/d29c9a8c099e39d4c1cb0e80cf508c99 to your computer and use it in GitHub Desktop.

Select an option

Save erhhung/d29c9a8c099e39d4c1cb0e80cf508c99 to your computer and use it in GitHub Desktop.

Revisions

  1. erhhung created this gist Oct 7, 2016.
    109 changes: 109 additions & 0 deletions images2video.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,109 @@
    /*
    * encode image sequence into WEBM video
    */
    import {exec} from 'child-process-promise';
    import format from 'string-template';
    import fsAsync from 'file-async';
    import shell from 'shelljs';
    import path from 'path';
    import os from 'os';

    /*
    * ffmpeg must be installed with libvpx support (specify
    * --with-libvpx when using Homebrew and --enable-libvpx
    * when building from source), and be on the system path
    */
    const FFMPEG_BIN = shell.which('ffmpeg');
    if (! FFMPEG_BIN) {
    throw new Error(`FFmpeg not found`);
    }

    const DEV_NULL = /Windows/.test(os.type()) ? '\\\\.\\NUL' : '/dev/null'
    , FFMPEG_TEMP = path.join(os.tmpdir(), 'ffmpeg2pass')
    , FFMPEG_LOG = FFMPEG_TEMP + '-0.log';

    // ffmpeg VP9 encoding guide:
    // http://wiki.webmproject.org/ffmpeg/vp9-encoding-guide

    const FFMPEG_BASE = FFMPEG_BIN + `\
    -loglevel error -y -i {input} -r {rate}\
    -c:v libvpx-vp9 -pass {pass} -passlogfile ${FFMPEG_TEMP}\
    -b:v 0 -crf 55 -pix_fmt yuv420p -threads 8 -speed {speed}\
    -tile-columns 6 -frame-parallel 1`
    , FFMPEG_PASS1 = FFMPEG_BASE + `\
    -f webm {output}`
    , FFMPEG_PASS2 = FFMPEG_BASE + `\
    -auto-alt-ref 1 -lag-in-frames 25 {output}`
    , PASS1_SPEED = 4
    , PASS2_SPEED = 1;

    export default class Images2Video {
    /**
    * @param filePattern - file path plus name pattern
    * containing "%d", "%02d" etc
    * @param frameRate - playback rate (default 30)
    */
    constructor(filePattern, frameRate = 30) {
    if (!/%0?\d?d/.test(filePattern)) {
    throw new Error(`invalid file name pattern`);
    }
    this.args = {
    input: filePattern
    , rate: +frameRate
    };
    }

    /**
    * encode image files into video
    *
    * @param destFile - output .webm file
    * @return {Promise}
    */
    async encode(destFile = "video.webm") {
    if (!destFile.endsWith('.webm')) {
    throw new Error(`output file must be .webm`);
    }
    await this._pass1();
    await this._pass2(destFile);
    }

    async _pass1() {
    console.log(`FFmpeg pass 1:`, FFMPEG_LOG);

    let args = Object.assign({
    pass: 1
    , speed: PASS1_SPEED
    , output: DEV_NULL
    }, this.args);

    let command = format(FFMPEG_PASS1, args)
    , result = await exec(command);

    if (result.stderr ||
    !await fsAsync.exists(FFMPEG_LOG)) {
    throw new Error(`FFmpeg pass 1 failed:
    ${result.stderr}`);
    }
    }

    async _pass2(destFile) {
    console.log(`FFmpeg pass 2:`, destFile);

    let args = Object.assign({
    pass: 2
    , speed: PASS2_SPEED
    , output: destFile
    }, this.args);

    let command = format(FFMPEG_PASS2, args)
    , result = await exec(command);

    await fsAsync.remove(FFMPEG_LOG);

    if (result.stderr ||
    !await fsAsync.exists(destFile)) {
    throw new Error(`FFmpeg pass 2 failed:
    ${result.stderr}`);
    }
    }
    }