web-video-creator
Version:
A framework for creating videos based on Node.js + Puppeteer + FFmpeg.
156 lines (143 loc) • 5.59 kB
JavaScript
import assert from "assert";
import AsyncLock from "async-lock";
import _ from "lodash";
import ChunkSynthesizer from "../core/ChunkSynthesizer.js";
import ChunkVideo from "./ChunkVideo.js";
import Font from "../entity/Font.js";
import logger from "../lib/logger.js";
/**
* 多幕视频
*/
export default class MultiVideo extends ChunkSynthesizer {
/** @type {number} - 开始捕获时间点 */
startTime;
/** @type {Font[]} - 注册的字体 */
fonts = [];
/** @type {Function} - 页面预处理函数 */
pagePrepareFn;
/** @type {{[key: number]: Function}} - 动作序列 */
timeActions;
/** @type {Function} - 页面获取函数 */
#pageAcquireFn = null;
/** @type {AsyncLock} - 异步锁 */
#asyncLock = new AsyncLock();
/**
* 构造函数
*
* @param {Object} options - 序列帧合成器选项
* @param {string} options.outputPath - 导出视频路径
* @param {number} options.width - 视频宽度
* @param {number} options.height - 视频高度
* @param {ChunkVideo[]} options.chunks - 分块视频列表
* @param {number} [options.startTime=0] - 开始捕获时间点
* @param {number} [options.fps=30] - 视频合成帧率
* @param {string} [options.format] - 导出视频格式(mp4/webm)
* @param {string} [options.attachCoverPath] - 附加到视频首帧的封面路径
* @param {string} [options.coverCapture=false] - 是否捕获封面并输出
* @param {number} [options.coverCaptureTime] - 封面捕获时间点(毫秒)
* @param {string} [options.coverCaptureFormat="jpg"] - 封面捕获格式(jpg/png/bmp)
* @param {string} [options.videoEncoder="libx264"] - 视频编码器
* @param {number} [options.videoQuality=100] - 视频质量(0-100)
* @param {string} [options.videoBitrate] - 视频码率(设置码率将忽略videoQuality)
* @param {string} [options.pixelFormat="yuv420p"] - 像素格式(yuv420p/yuv444p/rgb24)
* @param {string} [options.audioEncoder="aac"] - 音频编码器
* @param {string} [options.audioBitrate] - 音频码率
* @param {number} [options.volume] - 视频音量(0-100)
* @param {number} [options.parallelWriteFrames=10] - 并行写入帧数
* @param {boolean} [options.showProgress=false] - 是否在命令行展示进度
* @param {Function} [options.pagePrepareFn] - 页面预处理函数
*/
constructor(options) {
super(options);
const { startTime, pagePrepareFn } = options;
assert(_.isUndefined(startTime) || _.isFinite(startTime), "startTime must be number");
assert(_.isUndefined(pagePrepareFn) || _.isFunction(pagePrepareFn), "pagePrepareFn must be Function");
this.startTime = startTime;
this.pagePrepareFn = pagePrepareFn;
}
/**
* 启动合成
*/
start() {
this.#asyncLock.acquire("start", () => this.#synthesize())
.catch(err => logger.error(err));
}
/**
* 启动并等待
*/
async startAndWait() {
await this.#asyncLock.acquire("start", () => this.#synthesize());
}
/**
* 输入分块视频
*
* @param {ChunkVideo} chunk - 分块视频
* @param {Transition} [transition] - 进入下一分块的转场对象
*/
input(chunk, transition) {
_.isFinite(this.width) && (chunk.width = _.defaultTo(chunk.width, this.width));
_.isFinite(this.height) && (chunk.height = _.defaultTo(chunk.height, this.height));
_.isFinite(this.fps) && (chunk.fps = _.defaultTo(chunk.fps, this.fps));
if (!(chunk instanceof ChunkVideo))
chunk = new ChunkVideo(chunk);
super.input(chunk, transition);
chunk.onPageAcquire(async () => await this.#acquirePage());
}
/**
* 注册字体
*
* @param {Font} font - 字体对象
*/
registerFont(font) {
if (!(font instanceof Font))
font = new Font(font);
// 开始加载字体
font.load();
this.fonts.push(font);
}
/**
* 注册多个字体
*
* @param {Font[]} fonts - 字体对象列表
*/
registerFonts(fonts = []) {
fonts.forEach(font => this.registerFont(font));
}
/**
* 合成处理
*/
async #synthesize() {
this.chunks.forEach(chunk => {
if (_.isUndefined(chunk.startTime) && this.startTime)
chunk.startTime = this.startTime;
if (_.isUndefined(chunk.pagePrepareFn) && this.pagePrepareFn)
chunk.pagePrepareFn = this.pagePrepareFn;
if (this.fonts.length > 0)
chunk.registerFonts(this.fonts);
});
return await new Promise((resolve, reject) => {
this.once("error", reject);
this.once("completed", resolve);
super.start();
});
}
/**
* 注册页面获取函数
*
* @param {Function} fn
*/
onPageAcquire(fn) {
assert(_.isFunction(fn), "Page acquire function must be Function");
this.#pageAcquireFn = fn;
}
/**
* 获取渲染页面
*
* @protected
* @returns {Page} - 页面对象
*/
async #acquirePage() {
assert(_.isFunction(this.#pageAcquireFn), "Page acquire function must be Function");
return await this.#pageAcquireFn();
}
}