尚待尝试的一些想法
原则:对于高时效性要求的场景,从并行执行角度去优化;对于大批量生成的场景,从资源复用角度去优化。
音频生成阶段
根据文本长度和语速,评估语音时长
可以找语音部门的同事问一下他们的语音时长逻辑,然后我们自己可以在没生成语音的情况下,直接通过文本+语速算出时长,这样就省掉了用语音回填网页时长的这一步。
然后语音的生成、网页截屏,可以同步进行,极大缩短单个视频的生成时间。
应用场景:量不大,但是对于生成时效性有很高要求的场景,比如新闻。
语音复用
同一个模板里面,有一些描述性的语音其实是固定不变、或者在有限的几个逻辑中变动的,这部分我们可以提前生成好语音,存起来进行复用。
应用场景:大批量生成的场景,比如个股收评。
原始视频生成阶段
通过浏览器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
| 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(); 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
|
const http = require('http'); const fs = require('fs'); const formidable = require('formidable') const { v4: uuidv4 } = require('uuid');
const server = http.createServer();
server.on('request', (req, res) => { 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`; fs.rename(oldPath, newPath, (err) => { if (err) throw err; res.writeHead(200, { 'Content-Type': 'text/html;charset=UTF8' }); res.end('上传成功!'); }) }) return; } res.end(); })
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
|
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(); 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分钟