@deepauto/ffcreatorlite
Version:
FFCreatorLite is a lightweight and flexible short video production library
251 lines (220 loc) • 8.87 kB
JavaScript
'use strict';
/**
* FFSubtitle - 字幕组件
* @class
*/
const FFText = require('../node/text');
const Utils = require('../utils/utils');
const fse = require('fs-extra');
const path = require('path');
const FFAudio = require('./audio');
function convertColor(hexColor) {
console.log(hexColor);
// 将 RGB 颜色代码转换为 BGR 颜色代码
const r = parseInt(hexColor.substring(1, 3), 16);
const g = parseInt(hexColor.substring(3, 5), 16);
const b = parseInt(hexColor.substring(5, 7), 16);
// 格式化为FFmpeg字幕字体颜色格式
const ffmpegColor = `&H00${b.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${r
.toString(16)
.padStart(2, '0')}&`;
return ffmpegColor.toUpperCase();
}
function beautifyStyles() {
// 5种花字(粗一点,有描边),随机选择一种
// 随机选择字体大小25-45
// 随机选择字体颜色
// 随机选择描边颜色
const colors = ['000000', '0ab50a', '068506', 'ffbf00', 'b5a000', '00a1a1', '794885'];
const PrimaryColour = convertColor(colors[Math.floor(Math.random() * colors.length)]);
const borderColors = ['cd242a', 'bc24cd', '2447cd', '0000FF', 'FFFF00', '6db9b9', 'e388e3'];
const OutlineColour = convertColor(borderColors[Math.floor(Math.random() * borderColors.length)]);
const Fontsize = Math.floor(Math.random() * 10) + 20;
const backColours = ['48494f', '32bf3e', 'bf5a32', 'bd2828', 'e1d57b', '00FFFF', 'e5d0e5'];
const BackColour = convertColor(backColours[Math.floor(Math.random() * backColours.length)]);
console.log(PrimaryColour, OutlineColour, Fontsize, BackColour);
return { PrimaryColour, OutlineColour, Fontsize, BackColour };
}
class FFSubtitle extends FFText {
constructor(conf = { x: 0, y: 0, animations: [] }) {
super({ type: 'subtitle', ...conf });
const {
color = '000000',
backgroundColor,
fontSize,
text = '',
font,
fontfile,
fontFamily,
vertical,
} = conf;
this.text = text;
this.fontcolor = convertColor(color);
if (color === '000000') {
this.fontcolor = null;
}
this.fontsize = fontSize;
this.boxcolor = backgroundColor;
this.fontFamily = font || fontFamily;
this.fontfile = fontfile;
this.vertical = vertical;
this.ttss = [];
}
async setTTS(ttsConf) {
if (!ttsConf.generateFromSRT) {
throw new Error(
'generateFromSRT function is required and must return {voicePath, startTime}',
);
}
const srtContent = fse.readFileSync(this.conf.path, 'utf8');
const results = await ttsConf.generateFromSRT(srtContent);
this.ttss = results.map(res => {
const audio = new FFAudio({
path: res.voicePath,
startTime: Utils.timeToSeconds(res.startTime),
});
return audio;
});
return this.ttss;
}
/**
* Set font
* @param {string} font - font
* @public
*/
setFont(font) {
this.fontFamily = font;
}
/**
* Set text color value
* @param {string} color - the text color value
* @public
*/
setColor(color) {
this.fontcolor = color;
}
/**
* Set text style by object
* @param {object} style - style by object
* @public
*/
setStyle(style) {
super.setStyle(style);
if (style.color) this.fontcolor = convertColor(style.color);
if (style.fill) this.fontcolor = convertColor(style.fill);
if (style.borderColor) this.bordercolor = convertColor(style.borderColor);
if (style.backgroundColor) this.boxcolor = convertColor(style.backgroundColor);
if (style.charSpacing) this.char_spacing = style.charSpacing;
if (style.fontWeight && style.fontWeight == 'bold') this.bold = -1;
if (style.shadow && style.shadow.color) this.boxcolor = convertColor(style.shadow.color); // 阴影颜色
}
/**
* concatFilters - Core algorithm: processed into ffmpeg filter syntax
* @param {object} context - context
* @private
*/
concatFilters(context) {
this.animations.replaceEffectConfVal();
this.filters = this.preFilters.concat(this.filters);
this.filters = this.filters.concat(this.customFilters);
const aniFilters = this.animations.concatFilters();
this.resetXYByAnimations(aniFilters);
this.resetAlphaByAnimations(aniFilters);
const filter = this.toFilter();
if (filter) {
this.filters.push(filter);
this.addInputsAndOutputs(context);
}
return this.filters;
}
/**
* Converted to ffmpeg filter command line parameters
* @private
*/
toFilter() {
// 判断是否是竖直对齐,如果是
const rootw = this.rootConf().getVal('width');
const rooth = this.rootConf().getVal('height');
let angle = 0;
let alignment = 2;
let dpi = Math.sqrt(Math.pow(rootw, 2) + Math.pow(rooth, 2)) / 4.8;
let marginl = 0;
let marginv = ((rooth - (this.y + (this.h || 0))) / dpi) * 7.2;
// let marginv = 8;
let borderStyle = 1;
if (this.vertical) {
angle = 90;
alignment = 7;
marginl = (this.x / dpi) * 72;
marginv = 0;
}
if (this.shadowx) this.shadowx = (this.shadowx / dpi) * 72;
if (this.shadowy) this.shadowy = (this.shadowy / dpi) * 72;
const beautifyStyle = beautifyStyles();
this.fontsize = this.fontsize || beautifyStyle.Fontsize;
const styles = {
Spacing:
(this.char_spacing && (((this.char_spacing / 1000) * this.fontsize) / dpi) * 72) ||
undefined, //文字间的额外间隙. 为像素数
Angle: angle, // 按Z轴进行旋转的度数, 原点由alignment进行了定义. 可以为小数
ScaleX: this.scale, // 修改文字的宽度. 为百分数
ScaleY: this.scale, // 修改文字的高度. 为百分数
Bold: this.bold || 0, // -1为粗体, 0为常规
Italic: this.italic || 0, // -1为斜体, 0为常规
Underline: this.underline || 0, // [-1 或者 0] 下划线
Strikeout: this.strikeout || 0, // [-1 或者 0] 中划线/删除线
BorderStyle: borderStyle, // 1=边框+阴影, 3=纯色背景. 当值为3时, 文字下方为轮廓颜色的背景, 最下方为阴影颜色背景.
Shadow: this.shadowx || this.shadowy || 0, //当BorderStyle为1时, 该值定义阴影的深度, 为像素数, 常见有0, 1, 2, 3, 4.
Alignment: alignment, //定义字幕的位置. 字幕在下方时, 1=左对齐, 2=居中, 3=右对齐. 1, 2, 3加上4后字幕出现在屏幕上方. 1, 2, 3加上8后字幕出现在屏幕中间. 例: 11=屏幕中间右对齐. Alignment对于ASS字幕而言, 字幕的位置与小键盘数字对应的位置相同.
OutlineColour: this.bordercolor || beautifyStyle.OutlineColour, //设置轮廓颜色, 为蓝-绿-红三色的十六进制代码相排列, BBGGRR.
Outline: (this.borderw && (this.borderw / dpi) * 72) || 1, //当BorderStyle为1时, 该值定义文字轮廓宽度, 为像素数, 常见有0, 1, 2, 3, 4.
PrimaryColour: this.fontcolor || beautifyStyle.PrimaryColour, // 设置主要颜色, 为蓝-绿-红三色的十六进制代码相排列, BBGGRR. 为字幕填充颜色
BackColour: this.boxcolor || beautifyStyle.BackColour, //设置阴影颜色, 为蓝-绿-红三色的十六进制代码相排列, BBGGRR. ASS的这些字段还包含了alpha通道信息. (AABBGGRR), 注ASS的颜色代码要在前面加上&H
Fontname: this.fontFamily, // 使用的字体名称, 区分大小写
Fontsize: (this.fontsize / dpi) * 72, // 字体的字号
MarginL: marginl, //字幕可出现区域与左边缘的距离, 为像素数
MarginV: marginv, // 垂直距离
};
Utils.deleteUndefined(styles);
const style_str = Object.keys(styles)
.map(function (option) {
let value = styles[option];
return option + '=' + value;
})
.join(',');
const options = {
'': `${this.getPath()}`,
force_style: `'${style_str}'`,
};
if (this.fontfile) {
options['fontsdir'] = this.fontfile;
}
return (
'subtitles=' +
Object.keys(options)
.map(option => {
let value = options[option];
if (!option) {
return value;
}
return option + '=' + value;
})
.join(':')
);
}
/**
* Get subtitle path
* @public
*/
getPath() {
// 要变成 path: '.\\\\\\\\examples\\\\\\\\assets\\\\\\\\comment.srt'
return this.conf.path.replace(/\\/g, '\\\\\\\\').replace(/:\\/, '\\\\:\\');
}
destroy() {
this.ttss.forEach(tts => {
fse.unlinkSync(tts.path);
});
this.ttss = [];
}
}
module.exports = FFSubtitle;