|
|
@@ -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}`); |
|
|
} |
|
|
} |
|
|
} |