ffcreatorlite
Version:
FFCreatorLite is a lightweight and flexible short video production library
274 lines (234 loc) • 6.79 kB
JavaScript
'use strict';
/**
* FFScene - Scene component, a container used to load display object components.
*
* ####Example:
*
* const scene = new FFScene();
* scene.setBgColor("#ffcc00");
* scene.setDuration(6);
* creator.addChild(scene);
*
* @class
*/
const path = require('path');
const rmfr = require('rmfr');
const fs = require('fs-extra');
const FFCon = require('./cons');
const FFAudio = require('./audio');
const FFImage = require('./image');
const Utils = require('../utils/utils');
const forEach = require('lodash/forEach');
const FFContext = require('../core/context');
const FFmpegUtil = require('../utils/ffmpeg');
const FFBackGround = require('../node/background');
const FFTransition = require('../animate/transition');
class FFScene extends FFCon {
constructor(conf) {
super({ type: 'scene', ...conf });
this.percent = 0;
this.duration = 10;
this.directory = '';
this.context = null;
this.command = null;
this.transition = null;
this.pathId = Utils.uid();
this.setBgColor('#000000');
this.addBlankImage();
}
addBlankImage() {
const blank = path.join(__dirname, '../assets/blank.png');
this.addChild(new FFImage({ path: blank, x: -10, y: 0 }));
}
/**
* Set the time the scene stays in the scree
* @param {number} duration - the time the scene
* @public
*/
setDuration(duration) {
this.background && this.background.setDuration(duration);
this.duration = duration;
}
/**
* Set scene transition animation
* @param {string|object} name - transition animation name or animation conf object
* @param {number} duration - transition animation duration
* @param {object} params - transition animation params
* @public
*/
setTransition(name, duration) {
if (typeof name === 'object') {
name = name.name;
duration = name.duration;
}
this.transition = new FFTransition({ name, duration });
}
/**
* Set background color
* @param {string} bgcolor - background color
* @public
*/
setBgColor(color) {
if (this.background) this.removeChild(this.background);
this.background = new FFBackGround({ color });
this.addChildAt(this.background, 0);
}
getFile() {
return this.filepath;
}
getNormalDuration() {
const transTime = this.transition ? this.transition.duration : 0;
return this.duration - transTime;
}
createFilepath() {
const { id, pathId } = this;
const cacheDir = this.rootConf('cacheDir').replace(/\/$/, '');
const format = this.rootConf('cacheFormat');
const dir = `${cacheDir}/${pathId}`;
const directory = dir.replace(/\/$/, '');
const filepath = `${directory}/${id}.${format}`;
fs.ensureDir(dir);
this.filepath = filepath;
this.directory = directory;
return filepath;
}
// about command
createNewCommand() {
const conf = this.rootConf();
const threads = conf.getVal('threads');
const upStreaming = conf.getVal('upStreaming');
const command = FFmpegUtil.createCommand({ threads });
if (!upStreaming) {
command.setDuration(this.duration);
}
this.command = command;
this.context = new FFContext();
}
toCommand() {
const command = this.command;
this.addNodesInputCommand(command);
this.toFiltersCommand(command);
this.addCommandOptions(command);
this.addCommandOutputs(command);
this.addNodesOutputCommand(command);
this.addCommandEvents(command);
return command;
}
start() {
this.createNewCommand();
this.toCommand();
this.command.run();
}
addCommandOptions(command) {
const conf = this.rootConf();
const upStreaming = conf.getVal('upStreaming');
if (upStreaming) {
FFmpegUtil.addDefaultOptions({ command, conf, audio: true });
} else {
FFmpegUtil.addDefaultOptions({ command, conf, audio: false });
}
const fps = conf.getVal('fps');
if (fps != 25) command.outputFPS(fps);
const { children } = this;
forEach(children, child => child.addOptions(command));
}
addCommandOutputs(command) {
let filepath;
const conf = this.rootConf();
const output = conf.getVal('output');
const upStreaming = conf.getVal('upStreaming');
if (upStreaming) {
filepath = output;
command.outputOptions(['-f', 'flv']);
} else {
filepath = this.createFilepath();
}
command.output(filepath);
}
deleteCacheFile() {
const conf = this.rootConf();
const debug = conf.getVal('debug');
if (!debug && this.directory) rmfr(this.directory);
}
// addInputs
addNodesInputCommand(command) {
forEach(this.children, child => child.addInput(command));
}
// addOutputs
addNodesOutputCommand(command) {
forEach(this.children, child => child.addOutput(command));
}
// filters toCommand
toFiltersCommand(command) {
const filters = this.concatFilters();
command.complexFilter(filters, this.context.input);
}
toTransFilter(offset) {
return this.transition.toFilter(offset);
}
fillTransition() {
if (!this.transition) {
this.setTransition('fade', 0.5);
}
}
/**
* Combine various filters as ffmpeg parameters
* @private
*/
concatFilters() {
forEach(this.children, child => {
const filters = child.concatFilters(this.context);
if (filters.length) this.filters = this.filters.concat(filters);
});
return this.filters;
}
getTotalFrames() {
return this.rootConf().getVal('fps') * this.duration;
}
// add command eventemitter3
addCommandEvents(command) {
const log = this.rootConf('log');
FFmpegUtil.addCommandEvents({
log,
command,
type: 'single',
totalFrames: this.getTotalFrames(),
start: this.commandEventHandler.bind(this),
error: this.commandEventHandler.bind(this),
complete: this.commandEventHandler.bind(this),
progress: this.commandEventHandler.bind(this),
});
}
commandEventHandler(event) {
event.target = this;
this.emits(event);
}
addAudio() {
const conf = this.rootConf();
const audio = conf.getVal('audio');
const loop = conf.getVal('audioLoop');
if (audio) this.addChild(new FFAudio({ audio, loop }));
}
isReady() {
return new Promise(resolve => {
let readyIndex = 0;
forEach(this.children, child => {
child.isReady().then(() => {
readyIndex++;
if (readyIndex >= this.children.length) {
resolve();
}
});
});
});
}
destroy() {
FFmpegUtil.destroy(this.command);
super.destroy();
this.transition.destroy();
this.transition = null;
this.context = null;
this.command = null;
}
}
module.exports = FFScene;