FFCreator学习笔记

安装

npm会卡主,换成yarn,会卡一阵,但是能成功。

一些registry.m.jd.com的包报502,但是没管也能继续跑。

依赖

Node版本:v18

技术拆解

Task机制

页面结构

Scene-Element

帧循环和帧控制

  • timeline通过Tween实现帧循环机制
  • 每一帧里面,调用每个Node的帧渲染函数

通过Tween控制帧的计算:
FFCreator/lib/timeline/update.js

1
const TWEEN = require('@tweenjs/tween.js');

每个Node通过addFrameCallback方法,注册了帧回调,控制动画。
比如lib/node/chart.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**

* Start rendering

* @public

*/

start() {
this.initECahrts();
this.initTexture();
this.animations.start();
this.updateCallback = this.updateCallback.bind(this);

// 注册帧回调
TimelineUpdate.addFrameCallback(this.updateCallback);

if (this.runUpdateNow) this.userCallback(this.chart);
}

具体的帧回调逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
updateCallback(time, delta) {
const { chart, userCallbackTime } = this;
if (!this.userCallback) return;

this.echartsUpdate(chart, time, delta);
this.time += delta;
if (this.time >= userCallbackTime) {
this.userCallback(chart);
this.time = 0;
}
}

// eslint-disable-next-line
echartsUpdate(chart, time, delta) {
const animation = chart._zr.animation;
if (animation._running && !animation._paused) {
animation.update();
}
}

timeline的frame属性怎么变化的?

1
2
timeline.nextFrame();
TimelineUpdate.update(fps);

单帧的数据结构FrameData:

1
2
3
4
5
6
7
8
9
10
11
FrameData {
type: 'normal',
scenesIndex: [ 0 ],
progress: 0,
sceneStart: false,
sceneEnd: false,
sceneOver: false,
isFirst: false,
isLast: false,
frame: 29
}

视频总帧数的计算(普通帧 + 过渡帧):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 /**
* Get the total number of frames
* @private
*/
getFramesNum() {
const fps = this.rootConf('fps');
return Math.floor(fps * this.duration);
}

/**
* Get the number of frames of transition animation
* @private
*/
getTransFramesNum() {
const fps = this.rootConf('fps');
return Math.floor(fps * this.transition.duration);
}

Renderer

WebGL
用的inkpaint这个库,源自pixi.js:

1
const { gl } = require('inkpaint');

InkPaint is a lightweight node.js canvas graphics library. It forks from the famous canvas engine pixi.js.
https://github.com/drawcall/inkpaint

(TODO)FFStream

A stream class that implements the pond-well-bucket mechanism

合成

https://github.com/fluent-ffmpeg/node-fluent-ffmpeg
内存获取 BGRA 格式的屏幕帧数据,叠加循环背景音乐生成视频

  • **-i pipe:0**:从标准输入(stdin)读取视频流(常用于程序管道传输)
1
ffmpeg -f rawvideo -framerate 30 -s 576x1024 -vcodec rawvideo -pixel_format bgra -video_size 576x1024 -i pipe:0 -stream_loop -1 -i /Users/leozhou/git/test/FFCreator/examples/assets/audio/05.wav -y -filter_complex [1]adelay=0|0[audio0];[audio0]amix=1 -hide_banner -map_metadata -1 -map_chapters -1 -c:v libx264 -profile:v main -preset medium -crf 20 -movflags faststart -pix_fmt yuv420p -r 30 -c:a aac -t 16.5 /Users/leozhou/git/test/FFCreator/examples/output/x54iv4nayflqw2oa.mp4

FFCreator/lib/core/synthesis.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Synthesis extends FFEventer{

start()
/**
* Open ffmpeg production and processing
* @public
*/
start() {
this.addInputOptions();
this.addAudioFilter();
this.addOutputOptions();
this.addCommandEvents();
this.addOutput();
this.command.run();
}


// progress event
command.on('progress', progress => {
let percent = Utils.floor(progress.frames / framesNum, 2);
percent = Math.min(1, percent);
this.emitProgress({ percent });
// log info
const percent100 = Math.floor(percent * 100);
FFLogger.info({ pos: 'Synthesis', msg: `synthesis progress: ${percent100}% done.` });
});
}

生成FF合成命令的参数:getDefaultOutputOptions

图表接入

standardchart.js 实现了一个名为 FFStandardChart 的类,它是一个使用 ECharts 库创建数据可视化图表的视频组件。该组件被设计为与 FFCreator 视频创建框架集成,使开发者能够在视频中添加动态的数据图表。

文件主要结构和流程

  1. 依赖和初始化
    导入 StandardChart (ECharts 的封装) 和其他必要依赖
    继承自 FFImage 类,使其具有图像组件的基础功能
    定义 echartsPolyfill 函数,修改 ECharts 原型确保动画功能可用
  2. 生命周期流程
    初始化阶段:

构造函数接收图表配置参数(主题、选项等)
start() 方法调用时,初始化 ECharts 和纹理
在 ECharts 初始化后,注册时间线更新回调
运行阶段:

updateCallback 函数在每一帧被时间线调用
echartsUpdate 处理 ECharts 的动画更新
当达到用户设定的时间间隔时,调用用户的回调函数更新图表数据
清理阶段:

destroy() 方法清理资源和引用

  1. 核心功能
    图表初始化: 通过 initECharts() 创建 canvas 并初始化 ECharts 实例
    平台适配: 通过 setPlatformAPI 实现 ECharts 在 Node.js 环境中的渲染
    动画同步: 通过(注释掉的) fixZrender() 方法将 ECharts 动画系统与 FFCreator 时间线同步
    纹理渲染: 通过 initTexture() 将 canvas 转换为可显示的纹理
  2. 特殊处理
    fixZrender() 方法(当前被注释)重写了 ECharts 的 Zrender 动画系统,目的是将其与 FFCreator 的时间线系统同步
    通过自定义 update 方法使图表动画与视频时间轴同步
    实现了 ECharts 所需的 canvas 和图像加载 API 以在 Node.js 环境中工作
    关键设计点
    集成 ECharts: 将浏览器环境的图表库集成到 Node.js 的视频渲染系统
    动画控制: 确保图表动画与视频时间线同步
    资源管理: 适当地创建和销毁资源
    API 设计: 提供简单的接口用于配置和更新图表
    该文件解决了在非浏览器环境中运行 ECharts 以及将其动画系统与视频创建框架同步的关键挑战。

配置

https://tnfe.github.io/FFCreator/#/guide/config

生态

FFCreatorLite: 一款轻量级版本的FFCreator,特别适合移动端开发,保留核心功能的同时大幅度减少了体积。
FFCRenderServer: 提供云服务解决方案,允许用户在服务器端生成大量视频而无需本地资源消耗。
FFCDesigner: 图形界面编辑器,简化了FFCreator的工作流,使其更加用户友好,尤其受到设计领域专业人士的喜爱。

方案测试

FFCreator方案

帧数可以达到60帧,但是清晰度不行。
原因猜测可能和整个场景的合并有关系(不是渲染单个图表,是将图表和其他元素放一起渲染),因为我单独对ECharts/StandardChart进行服务端Canvas渲染,得到的图片是清晰的(和DPR也有关,DPR=4很清晰,2还是有点模糊)。
我试试不加其他元素,一个Scene就一个ECharts图表的情况:

超采样:先给canvas设置高DPR和大的宽高,然后将其缩小到一定大小,以此来提升canvas的清晰度,这样可行么?

可行的,这种方法通常被称为“**超采样(Supersampling)”或“离屏渲染(Offscreen Rendering)**”。通过先以高分辨率渲染Canvas,然后将其缩小到目标尺寸,确实可以显著提升图像的清晰度,尤其是在高DPR设备上。

超采样:通过在更高的分辨率下渲染图像,然后将其缩小到目标尺寸,可以减少锯齿和模糊,提升图像质量。
超采样(Supersampling) 是一种通过采集高于目标分辨率的原始图像数据,再通过算法缩小至目标分辨率以提升画质的图像处理技术。其核心原理是:
高分辨率采集:传感器以高于输出分辨率的方式记录图像(例如用6K传感器采集数据后输出4K视频)。
缩图降噪:通过整合更多像素信息,减少锯齿(Aliasing)、摩尔纹(Moiré)和噪点,同时增强细节和动态范围
DPR适配:这种方法可以模拟高DPR设备的效果,即使在低DPR设备上也能生成高质量的图像。

待完成的任务:

  • 增加超采样的压缩操作(应该在一个渲染完的callback里面操作,比如onframe?)
  • 图表配置的框、线条、文本的大小,需要乘以DPR系数

WebRTC + MediaRecorder

WebRTC专注于实时通信,支持音视频流的双向传输与网络优化;而MediaRecorder API属于MediaStream Recording标准,主要负责本地录制通过getUserMediagetDisplayMedia获取的媒体流。
组合使用:先用WebRTC的getDisplayMedia捕获屏幕流或摄像头视频流,再通过MediaRecorder将流数据编码存储为文件。例如网页端视频录制功能,可通过以下代码实现协作:

1
2
3
4
5
6
// WebRTC获取屏幕流
const stream = await navigator.mediaDevices.getDisplayMedia({video: true});
// MediaRecorder录制流
const mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (e) => { /* 处理视频片段 */ };
mediaRecorder.start();

但是经过我测试,这个无法突破30帧的限制,且清晰度不行。

Q & A

为什么FFCreator能超越60帧?

之前30帧限制的根本原因是DOM渲染+浏览器截屏的缘故。
FFCreator用2个方法来解决了这个问题:

  • 用WebGL的Canvas替代DOM
  • 用数据的Stream传递,省掉了截屏操作

ECharts的渲染原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
initECahrts() {

const [width, height] = this.getWH();

const ctx = createCanvas(128, 128);

echarts.setCanvasCreator(() => ctx);



const { theme, option, optionOpts } = this;

const canvas = createCanvas(width, height);

const chart = echarts.init(canvas, theme);

chart.setOption(option, optionOpts);

this.fixZrender(chart);



this.ctx = ctx;

this.chart = chart;

this.canvas = canvas;

}

我们要做哪些改造?

SD和VISALL的接入。

之前有些元素是DOM写的,那部分得改造成Canvas。

页面元素必须使用FFCreator的,而不是HTML的DOM元素。

得增加Node环境的支持,比如这种:

1
2
// 初始化StandardChart
_this._$dom.style.userSelect = 'none';

还有埋点统计也是,用到了window对象。
ECharts也有兼容性问题。

服务端渲染文档(我们本质上是服务端渲染为Canvas图片):
https://echarts.apache.org/handbook/zh/how-to/cross-platform/server/

1
const { Image } = require('inkpaint');

TODO:
https://github.com/ecomfe/echarts-for-weixin/pull/919

如何解决清晰度问题?

https://github.com/tnfe/FFCreator/issues/21

思考:
是不是因为这是用的WebGL,类似3D场景的方式,图表是其中一个元素,在整体中进行渲染的,而不是单独渲染图表的image数据,才导致不清晰的?
如果将图表单独拿出来渲染,是不是就好很多?
我单独进行Echarts的服务端Canvas渲染,得到的图片是非常清晰的。

设置crf, vb, preset参数

效果一般。

设置DPR

也不大行。

(TODO)超分

类似通义万相,专业版和极速版其实都是14B的,只不过专业版感觉是原生720P,极速版是直出的480P然后超分到720P的。

为什么浏览器录屏有30帧的限制?

1
2
我用MediaRecorder API录制视频,为什么通过frameRate: { ideal: 60, max: 60 }设置了60帧,录制的视频仍然只有30帧?
哪些浏览器对屏幕录制有默认的30FPS限制?为什么要有这个限制?有没有办法突破这个限制?

主要受限浏览器:
Chrome/Edge:在WebRTC的getDisplayMedia接口中,默认屏幕录制帧率通常为30FPS。
Firefox:对高帧率支持更保守,默认限制为30FPS,尤其在低端设备上会主动降帧。
Safari:对WebRTC的帧率限制最为严格,部分版本甚至强制锁定为24-30FPS。

限制的核心原因:
性能平衡:高帧率(如60FPS)需要更高的CPU/GPU算力,浏览器需避免因资源占用过高导致页面卡顿或崩溃。
编码压力:H.264编码器在实时录制时,60FPS的视频流数据量是30FPS的2倍,对内存和带宽要求更高。
兼容性保障:限制帧率可确保低端设备(如集成显卡电脑、移动设备)的录制稳定性。

录制整个window是可以达到60帧的,我测试过:
https://stackoverflow.com/questions/70175839/electron-screen-recording-stuck-at-10fps
但是清晰度不行…..

关于DPR与Canvas的宽高

DPR设置高一些,确实会清晰
DPR越大,文字图像越大,需要的画布高宽也越大。

html2canvas行不行?

不行,性能太差了,录制出来的帧数非常低,幻灯片。
html2canvas
One big downside to this library is that it is blocking the main thread and can be quite slow when rendering a large amount of HTML.
https://github.com/niklasvh/html2canvas/issues/2880
Most of the time spent is not rendering the canvas, IIRC, but it is building up the clone of the HTML that will be shoved into an SVG that will then be rendered on the canvas. So I don’t know if using a web worker would actually improve anything.

https://github.com/niklasvh/html2canvas/issues/2643

这个人有个不错的优化想法:
https://github.com/niklasvh/html2canvas/issues/2643#issuecomment-1102662120