如何提升短视频生成的效率

尚待尝试的一些想法

原则:对于高时效性要求的场景,从并行执行角度去优化;对于大批量生成的场景,从资源复用角度去优化。

音频生成阶段

根据文本长度和语速,评估语音时长

可以找语音部门的同事问一下他们的语音时长逻辑,然后我们自己可以在没生成语音的情况下,直接通过文本+语速算出时长,这样就省掉了用语音回填网页时长的这一步。

然后语音的生成、网页截屏,可以同步进行,极大缩短单个视频的生成时间。

应用场景:量不大,但是对于生成时效性有很高要求的场景,比如新闻。

语音复用

同一个模板里面,有一些描述性的语音其实是固定不变、或者在有限的几个逻辑中变动的,这部分我们可以提前生成好语音,存起来进行复用。

应用场景:大批量生成的场景,比如个股收评。

原始视频生成阶段

通过浏览器API+Canvas,前端生成视频数据,上传服务端

参考自这个文章:https://mp.weixin.qq.com/s/LUfeTzUoA5IrrbOckONPIA

我试了下,这个方案应该是可行的,无头浏览器也支持这部分api。

录制的话,如果开启的进程比较多,帧数会降低;但是至少一个核录一个页面应该没问题。

按照我们现在截屏的性能来算(录制时长大约是视频播放时长的4倍;单个docker是4C4G),大批量生成的情况下,视频录制环节,可以把时间降低到现有时间的1/16,这是一个非常大的提升了。

待解决问题:

如何保证帧数可控(可能需要重写浏览器的requestAnimationFrame、setTimeout、setInterval等函数)

如何设置生成的视频文件的时长等信息

非Canvas的网页录屏方案

可以参考这个开源的工具,可以录制DOM节点:

https://www.rrweb.io/

https://github.com/rrweb-io/rrweb/blob/master/guide.md

2021.03.18 我发了信息给一个Quora的回答,看有人回复没:

https://www.quora.com/How-do-I-convert-HTML-to-a-video

这个文章的作者我也联系了,等待回复:

https://www.linkedin.com/in/samdutton/?originalSubdomain=uk

【待验证】Puppeteer可以操控devtools的录屏功能!!!

https://zhuanlan.zhihu.com/p/172293774?hmsr=toutiao.io

我们知道puppeteer还支持 tracing 的 api,tracing 导出的结果是一个 json 文件里面是基于DevTools Protocol的,大家知道在性能测试阶段,DevTools 是支持截屏功能的,我们可以不可以利用这一点呢?

这个原理应该是通过tracing的api截屏,生成图片的Base64数据,然后将数据流合并为视频。这样其实还是截屏,但是可以在用户本地截屏!如果可以在本地将数据进行合并,就相当于本地生成视频。

这样应该可以解决非Canvas的截屏问题了吧??????

有问题:这是通过无头浏览器调用的,用户本地可没有无头浏览器。

先琢磨下Chrome DevTools Protocol的API:https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-startScreencast

https://blog.csdn.net/weixin_30570101/article/details/95014902

【经典】可以用vue-devtools的方式写扩展来实现录制:https://blog.csdn.net/LuckyWinty/article/details/105743305

awesome dev tools:https://github.com/ChromeDevTools/awesome-chrome-devtools#chrome-devtools-protocol

这个待试用:

https://github.com/checkly/headless-recorder

(TODO)浏览器扩展的方式

Chrome扩展也是JS开发的,按理说我们也能做的,关键是弄清楚其实现原理。

这个已经商业化了:

https://chrome.google.com/webstore/detail/loom-for-chrome/liecbddmkiiihnedobmlmillhodjkdmb/related?hl=zh-CN

https://chrome.google.com/webstore/detail/screencastify-screen-vide/mmeijimgabbpbgpdklnllpncmdofkcpn/related

https://www.techrepublic.com/article/how-to-record-your-browser-window-in-google-chrome/

2021.05.26:我基于这个开源的代码库进行调试:https://github.com/samdutton/rtcshare

项目代码位于git/rtcshare下面

screenity

https://github.com/alyssaxuu/screenity

(精)各种录屏插件的对比:

https://docs.google.com/spreadsheets/d/1juc1zWC2QBxYqlhpDZZUNHl3P6Tens6YiChchFcEJVw/edit#gid=0

录制方案

是采用了Chrome自带的API(popup.js):

1
2
3
4
5
6
7
8
9
10
11
12
// Start recording
function record(){
if (!recording) {
chrome.runtime.sendMessage({type: "record"});
$("#record").html(chrome.i18n.getMessage("starting_recording"));
} else {
recording = false;
$("#record").html(chrome.i18n.getMessage("start_recording"));
chrome.runtime.sendMessage({type: "stop-save"});
window.close();
}
}

支持三种录制范围:tab-only、desktop、camara-only:

1
2
chrome.runtime.sendMessage({type: "recording-type", recording:$(".type-active").attr("id")});
chrome.storage.sync.set({type:$(".type-active").attr("id")});

background.js

newRecording

getTab是具体的录制逻辑,看起来是可以进行自定义设置的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chrome.tabCapture.capture({
video: true,
audio: true,
videoConstraints: {
mandatory: {
chromeMediaSource: 'tab',
minWidth: width,
minHeight: height,
maxWidth: width,
maxHeight: height,
maxFrameRate: fps
},
},
}

用了这个API:

http://www.kkh86.com/it/chrome-extension-doc/extensions/tabCapture.html

https://crxdoc-zh.appspot.com/extensions/tabCapture#type-MediaStreamConstraint

官方文档:

https://developer.chrome.com/docs/extensions/reference/tabCapture/#method-capture

这个需要仔细阅读:

https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API/Constraints

看看支持哪些配置:

1
var supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
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
30
31
32
aspectRatio: true
autoGainControl: true
brightness: true
channelCount: true
colorTemperature: true
contrast: true
deviceId: true
echoCancellation: true
exposureCompensation: true
exposureMode: true
exposureTime: true
facingMode: true
focusDistance: true
focusMode: true
frameRate: true
groupId: true
height: true
iso: true
latency: true
noiseSuppression: true
pan: true
pointsOfInterest: true
resizeMode: true
sampleRate: true
sampleSize: true
saturation: true
sharpness: true
tilt: true
torch: true
whiteBalanceMode: true
width: true
zoom: true

思考:没事把MDN读一遍,可以知道浏览器的能力,非常值得!

7年前的共享屏幕的WebRTC的Demo:

https://github.com/samdutton/rtcshare

(精)讲解原理和关键代码:

https://developers.google.com/web/updates/2012/12/Screensharing-with-WebRTC

(精)WebRTC教程:

https://www.html5rocks.com/en/tutorials/webrtc/basics/

Chrome插件调试技巧

https://my.oschina.net/u/265943/blog/292904

几个注意事项:

1、manifest.json中,permissions下面必须添加对downloads的支持,否则无法下载文件

2、只有background.js可以执行下载操作,content-scripts没有支持下载功能

待解决的问题

测试URL:

http://datav.iwencai.com/micro-video/page/#/business/financialReport/preview?readConfig=true&api=http://datav.iwencai.com/micro-video/api/v1/process/11045&scale=0.5

(Done)录制时间控制

通过track.stop()控制

清晰度

用WebRTC的方式,清晰度设置似乎并未生效;因此我们只能考虑通过scale放大网页再录制的方式来进行测试了

https://blog.csdn.net/xyphf/article/details/107237085

黄色丢失问题很严重

清晰度参数:

https://blog.csdn.net/ihtml5/article/details/88658873

VP8 VS. H264:

https://www.cnblogs.com/mlj318/p/5723738.html

https://zhuanlan.zhihu.com/p/24813904?refer=codec

VP9 VS. H265:

https://www.zhihu.com/question/21067823?sort=created

H265目前Chrome还不支持

常用编码对比分析:

https://blog.csdn.net/owen7500/article/details/47334929

经测试,H264比vp8清晰,主要是色彩丢失没那么严重

(Warning)截屏受限于浏览器可视区域大小的问题
(Done)无头浏览器如何安装插件

这个文章提到,只有在headless=false下才能用,如果是这样,那我们这个就不能用了

http://www.sanfenzui.com/pyppeteer-install-chrome-extensions-and-test.html?imbgzw=6mxmx1

这个文章也提到了,无头模式不能开启插件:

https://www.it1352.com/1954944.html

这个文章注明了puppeteer如何开启插件:

https://juejin.cn/post/6844904132524900360

网页操作还原

https://github.com/oct16/TimeCat/blob/master/README.cn.md

这个是国人写的,功能很强大,文档很易用,似乎也能满足我们需求,牛逼!

Demo程序

前端HTML页面record.html:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="text/javascript" src="https://s.thsi.cn/js/jquery-2.0.0.min.js"></script>
<title>Document</title>
<style>
body {
display: flex;
}
#videoContainer {
position: absolute;
left: 600px;
width: 600px;
height: 600px;
background-color: #ccc;
}
h3 {
margin-left: 40%;
}
</style>
</head>
<body>
<div>
<h3>原始网页</h3>
<canvas width="600" height="600"></canvas>
</div>

<div id="videoContainer">
<h3>录制的视频</h3>
<video id="myvideo" autoplay muted></video>
</div>

<script>
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const { width, height } = canvas;

ctx.fillStyle = 'red';

// 画个正方形
function draw(rotation = 0) {
ctx.clearRect(0, 0, 1000, 1000);
ctx.save();
ctx.translate(width / 2, height / 2);
ctx.rotate(rotation);
ctx.translate(-width / 2, -height / 2);
ctx.beginPath();
ctx.rect(200, 200, 200, 200);
ctx.fill();
ctx.restore();
}

// 让正方形不停旋转
function update(t) {
draw(t / 500);
requestAnimationFrame(update);
}
update(0);

const stream = canvas.captureStream();
const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
const data = [];
recorder.ondataavailable = function (event) {
if (event.data && event.data.size) {
console.log('event.data', event.data);
data.push(event.data);
}
};

recorder.onstop = () => {
const url = URL.createObjectURL(new Blob(data, { type: 'video/webm' }));
document.querySelector('#videoContainer').style.display = 'block';
document.querySelector('#myvideo').src = url;

upload(data);
};

recorder.start();

setTimeout(() => {
recorder.stop();
}, 2000);

function upload(data) {
var fd = new FormData();
// data必须是个可迭代的对象,一般就是用数组
var blob = new Blob(data, { type: 'video/webm' });
var fileOfBlob = new File([blob], 'test.avi');
fd.append('filedata', fileOfBlob);
$.ajax({
type: 'POST',
url: '/',
data: fd,
processData: false,
contentType: false,
}).done(function (data) {
console.log(data);
});
}
</script>
</body>
</html>

后端Node服务server.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* Node上传文件的参考文章:https://segmentfault.com/a/1190000020654277
*/

// 1. 导入http模块
const http = require('http');
const fs = require('fs');
const formidable = require('formidable')
const { v4: uuidv4 } = require('uuid');

// 2. 创建一个web服务器对象
const server = http.createServer();

// 3. 监听请求事件
server.on('request', (req, res) => {
// console.log(req);
console.log('req.url', req.url);
// 通过响应头设置返回前台数据格式及编码。(解决中文乱码的问题)
res.setHeader('Content-Type', 'text/html;charset=utf-8');
if (req.url === '/record.html') {
const html = fs.readFileSync('./record.html');
res.write(html);
} else if (req.url === '/favicon.ico') {
res.writeHead(200, { 'Content-Type': 'text/html;charset=UTF8' });
res.end('上传成功!');
return;
} else {
const form = new formidable.IncomingForm();
// 指定解析规则
form.encoding = 'utf-8'; // 设置编码
form.uploadDir = './upload'; // 指定上传目录
form.keepExtensions = true; // 保留文件后缀
form.maxFieldsSize = 20 * 1024 * 1024; // 指定上传文件大小

form.parse(req, (err, fields, files) => {
if (err) throw err;
const oldPath = files.filedata.path;
const uniqueId = uuidv4();
const newPath = `${form.uploadDir}/${uniqueId}.avi`;
// 将上传的文件从临时位置写到目标位置进行存储;由于是测试,文件名就用uuid随机生成了
fs.rename(oldPath, newPath, (err) => {
if (err) throw err;
res.writeHead(200, { 'Content-Type': 'text/html;charset=UTF8' });
res.end('上传成功!');
})
})
return;
}
res.end();
})

// 4. 监听端口,为了避免端口冲突,这里给一个本机端口3000
server.listen(3000, () => {
console.log('服务器启动成功');
})

运行在docker中的启动无头浏览器的脚本multiProcess.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* eslint-disable no-await-in-loop */
/* eslint-disable no-continue */
const cluster = require('cluster');
const puppeteer = require('puppeteer');

// 最大子进程数
const maxChildProcessNumber = 4;

async function captureCanvas() {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
executablePath: '/opt/google/chrome/chrome',
});
const page = await browser.newPage();
// 这个地址需要改为你本机的局域网IP和你server.js启动的服务端口号
await page.goto('http://192.168.56.1:3000/record.html');
await page.setViewport({
width: 1920,
height: 1080,
});

page.on('console', (msg) => console.log(msg.text()));

setTimeout(async () => {
await browser.close();
}, 15000);
}

(async () => {
if (cluster.isMaster) {
for (let i = 0; i < maxChildProcessNumber; i += 1) {
cluster.fork();
}
console.log(`主进程 ${process.pid} 正在运行`);

cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出, code=${code}, signal=${signal}`);
});
} else {
console.log(`工作进程 ${process.pid} 已启动`);
await captureCanvas();
}
})();

各大产品的生成方案

数可视-花火

服务端生成,时间接近1:1,应该是跟我们一样,通过无头浏览器截屏生成的;帧数比较低,但是还挺快。我们低帧数也达不到1:1吧

感觉还是有些不一样的地方的,得琢磨下。

来画

可视化搭建也是基于DOM的。

他们有个时间轴的设计,很nice

33秒的视频,大约40秒生成,接近1:1了

他们也是服务端生成,分页生成,因此可以大幅度提速。从生成过程的进度请求来看,是拆分了几个进程去生成的。

但是他们生成的视频,分页衔接完全无缝,这是怎么做到的?

生成前面,有个排队,我感觉这个排队过程其实已经在生成了(不对,排队是真的在排队);他们的导出功能耗时很长,导出是否就是在合成?

来画支持gif图片,那么就一定不是服务端截屏再合成!一定是录屏!

元素组件

很多都是通过svg元素来实现的,估计是设计师设计出来,然后导出为svg元素,加进去的

预览功能

DOM页面还原的,不是Canvas

(TODO)生成视频

也是服务端生成,但是应该不是我们的截屏?

感觉可能是录制的形式,在服务端播放一次;可能是通过浏览器插件录制的?

配置信息

每一页有若干个json配置文件

参考资料

技术原理讲解:

https://mp.weixin.qq.com/s/LUfeTzUoA5IrrbOckONPIA

RecordRTC的Demo:

https://www.webrtc-experiment.com/RecordRTC/simple-demos/

在线测试页面:

https://www.webrtc-experiment.com/RecordRTC/

RecordRTC的github:

https://github.com/muaz-khan/RecordRTC

新华智云的速度:套餐有优先级;划分独立资源给高端用户;加水印5倍速度;虚拟主播1分钟,合成10分钟