UNPKG

ffcreator

Version:

FFCreator is a lightweight and flexible short video production library

320 lines (280 loc) 8.14 kB
'use strict'; /** * Renderer - Core classes for rendering animations and videos. * * ####Example: * * const renderer = new Renderer({ creator: this }); * renderer.on("progress", progressHandler); * renderer.on("render-start", startHandler); * renderer.on("render-error", errorHandler); * * * ####Note: * Rendering process * 1. Render the InkPaint scene and save the data to frames * 2. Save frame file and add transition animation * 3. Use ffmpeg to synthesize video files * 4. Delete cache folder * * @class */ // const GLReset = require("gl-reset"); const { gl } = require('inkpaint'); const forEach = require('lodash/forEach'); const FS = require('../utils/fs'); const GLUtil = require('../utils/gl'); const Perf = require('../utils/perf'); const FFBase = require('../core/base'); const Synthesis = require('./synthesis'); const FFStream = require('../utils/stream'); const FFLogger = require('../utils/logger'); const CanvasUtil = require('../utils/canvas'); const Timeline = require('../timeline/timeline'); class Renderer extends FFBase { constructor({ creator }) { super({ type: 'renderer' }); this.stop = false; this.parent = creator; } /** * Start rendering * @async * @public */ async start() { this.emit({ type: 'start' }); await this.preProcessing(); Perf.start(); this.createGL(); this.transBindGL(); this.configCache(); this.createStream(); this.createTimeline(); this.createSynthesis(); } /** * Confirm that there must be a cache folder * @private */ configCache() { const conf = this.rootConf(); const type = conf.getVal('cacheFormat'); const dir = conf.getVal('detailedCacheDir'); FS.ensureDir(dir); FS.setCacheFormat(type); } /** * Prepare processing materials in advance * @private */ async preProcessing() { try { const preload = this.rootConf('preload'); const creator = this.getCreator(); const { loader } = creator; // sub child preProcessing await creator.runScenesFunc(async node => await node.preProcessing()); // sub child preload await creator.runScenesFunc(async node => { if (preload && node.preload) { const urls = node.getPreloadUrls(); forEach(urls, url => { if (url) { if (/.+(\.[\d\w]{1,10})$/.test(url)) loader.add(url, url); else loader.add(url, url, { loadType: 2 }); } }); } }); await loader.loadAsync(); creator.removeAllDisplayChildren(); } catch (error) { this.emitError({ error, pos: 'preProcessing' }); } } /** * Create webgl environment context * @private */ createGL() { const width = this.rootConf('width'); const height = this.rootConf('height'); this.gl = gl(width, height); } /** * Binding gl context for each transition animation * @private */ transBindGL() { forEach(this.getScenes(), scene => scene.transition.bindGL(this.gl)); } /** * Create a stream pipeline for data transmission * @private */ createStream() { const conf = this.rootConf(); const size = conf.getVal('highWaterMark'); const parallel = conf.getVal('parallel'); const stream = new FFStream({ size, parallel }); stream.addPullFunc(this.renderFrame.bind(this)); stream.on('error', error => this.emitError({ error, pos: 'FFStream' })); this.stream = stream; } /** * Create a timeline to manage animation * @private */ createTimeline() { const scenes = this.getScenes(); const fps = this.rootConf('fps'); const timeline = new Timeline(fps); timeline.annotate(scenes); this.timeline = timeline; } /** * Render a single frame, They are normal clips and transition animation clips. * @private */ async renderFrame(callback) { const { timeline, gl } = this; const frameData = timeline.getFrameData(); if (timeline.isOver()) { callback && callback(null); return null; } const { type, progress, sceneStart, sceneOver, isLast, scenesIndex } = frameData; const creator = this.getCreator(); let scene, data; // Various scene states // scene frameData start event if (sceneStart) { const cindex = frameData.getLastSceneIndex(); scene = this.getSceneByIndex(cindex); creator.addDisplayChild(scene); scene.start(); } // scene frame end event if (sceneOver && !isLast) { const pindex = frameData.getPrevSceneIndex(); scene = this.getSceneByIndex(pindex); creator.removeDisplayChild(scene); } FFLogger.info({ pos: 'renderFrame', msg: `current frame - ${timeline.frame}` }); timeline.nextFrame(); // Rendering logic part if (type === 'normal') { data = this.snapshotToBuffer(); } else { const currScene = this.getSceneByIndex(scenesIndex[0]); const nextScene = this.getSceneByIndex(scenesIndex[1]); creator.addOnlyDisplayChild(currScene); const fromBuff = this.snapshotToBuffer(); creator.addOnlyDisplayChild(nextScene); const toBuff = this.snapshotToBuffer(); const { transition } = currScene; const conf = this.rootConf(); const width = conf.getVal('width'); const height = conf.getVal('height'); const quality = conf.getVal('cacheQuality'); const cacheFormat = conf.getVal('cacheFormat'); await transition.render({ type: cacheFormat, fromBuff, toBuff, progress }); data = GLUtil.getPixelsByteArray({ gl, width, height }); if (cacheFormat !== 'raw') { const canvas = CanvasUtil.draw({ width, height, data }); data = CanvasUtil.toBuffer({ type: cacheFormat, canvas, quality }); } } callback && callback(data); return data; } /** * Take a screenshot of node-canvas and convert it to buffer. * @private */ snapshotToBuffer() { const conf = this.rootConf(); const creator = this.getCreator(); creator.render(); const cacheFormat = conf.getVal('cacheFormat'); const quality = conf.getVal('cacheQuality'); const canvas = creator.app.view; const buffer = CanvasUtil.toBuffer({ type: cacheFormat, canvas, quality }); return buffer; } /** * synthesis Video Function * @private */ createSynthesis() { const { stream, timeline } = this; const conf = this.rootConf(); const cover = conf.getVal('cover'); const creator = this.getCreator(); const audios = creator.concatAudios(); const synthesis = new Synthesis(conf); synthesis.setFramesNum(timeline.framesNum); synthesis.setDuration(timeline.duration); synthesis.addStream(stream); synthesis.addAudios(audios); synthesis.addCover(cover); // add synthesis event this.bubble(synthesis); synthesis.on('synthesis-complete', event => { Perf.end(); const useage = Perf.getInfo(); event = { ...event, useage }; this.emit('complete', event); }); synthesis.start(); this.synthesis = synthesis; } getScenes() { const creator = this.getCreator(); return creator.children; } /** * Get the scene through index * @private */ getSceneByIndex(index) { const scenes = this.getScenes(); return scenes[index]; } /** * Get parent creator * @private */ getCreator() { return this.parent; } /** * Delete the cache intermediate folder * @private */ removeCacheFiles() { if (this.rootConf('debug')) return; const dir = this.rootConf('detailedCacheDir'); FS.rmDir(dir); } destroy() { // GLReset(this.gl)(); try{ this.stream.destroy(); this.timeline.destroy(); this.synthesis.destroy(); this.removeCacheFiles(); this.removeAllListeners(); super.destroy(); }catch(e) { } this.stop = true; this.gl = null; this.parent = null; this.stream = null; this.timeline = null; this.synthesis = null; } } module.exports = Renderer;