背景

最近在做保险相关的项目,由于医保局的监管要求,用户购买保险的流程必须可以回溯。这样在用户与保险公司之间产生纠纷时,就可以有迹可循。
比如用户说,当时为自己和妻子二人投了保,但是保险公司后台只有一笔订单,这时如果只是把后台数据给用户看,用户肯定不会信服。

最好的手段就是把用户投保的具体操作过程录制成视频,在发生纠纷时,直接以视频为证,这样才更有说服力。

DOM 快照

当我们想要查看用户在投保过程中某一时刻的页面状态时,我们只需要将那一刻的页面 dom 结构,以及页面中的 css 样式记录下来,然后在浏览器中重新渲染出来就能达到回溯的效果了。

const cloneDoc  = document.documentElement.cloneNode(true); // 录制
document.replaceChild(cloneDoc, document.documentElement); // 回放

这样我们就实现了某一时刻 DOM 快照的功能。但是这个录制的 cloneDoc 还只是内存中的一个对象,并没有实现远程录制。

序列化

为了实现远程录制,我们需要将 cloneDoc 这个对象序列化成字符串,保存到服务端,然后在回放的时候从服务器上取出来,交给浏览器重新渲染。

const serializer = new XMLSerializer(); // XMLSerializer 是浏览器自带的 api,可以将 dom 对象序列化成 string
const str = serializer.serializeToString(cloneDoc);
document.documentElement.innerHTML = str;

至此,我们就完成了对用户界面某一时刻的远程录制功能。

定时快照

但是我们的目的是录制视频,只有一个 dom 快照显然是不够的。了解动画的同学都应该知道,动画是由每秒至少 24 帧的画面按顺序播放而产生的。在这里顺便科普一下这块的知识,当我们人眼观察到一个物体之后,这个画面会在我们的视网膜中停留16.7ms左右的的时间,专业名词叫做视觉停留,那么具体到给我们的感觉就是这个画面是“渐渐”消失的。

那么当我们在播放动画的时候,当第一帧画面在我们的视网膜中刚刚消失的时候,把第二帧放出来,那么给人的感觉就是画面是连续的,是在动的。但是动画里的人物动作给人的感觉还是有点卡顿、有点不自然的,为什么呢?我们来算一下: 1秒/24帧 = 41.7毫秒,远远低于人眼可分辨的16ms的间隔,所以我们会觉得有点卡卡的。

为了达到更加流畅的画面,很多游戏和电影都会采用 60 帧/秒的速度来放映画面,因为 1秒/60帧 = 16.7ms,和人眼视觉停留的时间差不多,所以会感觉到画面很流畅。可以看一下你的电脑屏幕,一般的刷新率也是60帧。


扯远了,我们回归正题。由上面的知识我们知道,既然我们想要录制视频,那么至少每秒需要 24 帧的数据,也就是说 1000ms/24帧 = 41.7毫秒要 clone 一遍网页内容。

setInterval(() => {const cloneDoc  = document.documentElement.cloneNode(true)const str = serializer.serializeToString(cloneDoc);axios.post(address,str); // 保存到服务端
}, 41.7)

现在我们可以让画面动起来了,但是稍微细想便可知道这种方法根本行不通,原因有一下几点:

  • 每秒 clone 24 次整个页面内容,对性能损耗巨大,严重影响用户体验
  • 每秒要将 24 帧的页面内容上传到服务端,对网络开销也是巨大的
  • 回放时,每秒要渲染 24 个完整的 html 内容,浏览器根本做不到这么快
  • 还有,要是页面没变动,那么 24 帧的数据可能是完全一样的,根本没必要clone 这个多次。

增量快照

基于以上定时快照的缺点,其实我们可以只在页面初始化完成之后 clone 一次完整的页面内容,等到页面有变动的时候,只记录变化的部分。这样一来,好处就显而易见了:

  • 只记录变化的部分,比起记录整个网页要小的多。这样对网页的性能、网络的开销都会小很多。
  • 我们只在页面有变动的时候才记录,这样一来,大量重复数据的问题也给解决了。
  • 回放时,我们只需要首先将第一帧(完整的页面内容)先渲染出来,然后在按照记录的时间,按顺序将变化的部分渲染到页面。这样就可以像看视频一样来回溯用户的操作流程了。

    举个例子,如上图所示,页面中一共有 4 个 div。页面有两次变化,第一次 dom2 变成了红色,第二次变化 dom4 变成了绿色。那么我们记录的数据大致是这个样子:
var events = [{完整的 html 内容},{id: 'dom2',type: '#fff -> red'},{id: 'dom4',type: '#fff -> green'}
]

记录的数据是一个数组,数组中有 3 个原始,第一个元素是完整的 html 内容,第二个元素描述的是 dom2 变成了红色,第三个元素描述的是 dom4 变成了绿色。
然后我们根据上诉记录的数据,就可以首先将 events[0] 渲染出来,然后执行 events[1] 将 dom2 变成红色,再将 dom4 变成绿色。
这样我们在理论上就完成了从页面的录制,到保存到远程服务器,再到最后回放,形成了功能上的完整的闭环。

MutationObserver

在上一步中,我们已经从理论上实现了录制和回放的功能。但是具体实现呢?我们怎么才能知道页面什么时候变化呢?变化了哪些东西呢?
实际上浏览器已经为我们提供了非常强大的 API,叫做 MutationObserver。它会以批量的方式返回 dom 的更新记录。
还是拿上面的例子来说明,改变一下 dom2 和 dom4 的背景色

setTimeout(() => {let dom2 = document.getElementById("dom2");dom2.style.background = "red";let dom4 = document.getElementById("dom4");dom4.style.background = "green";
}, 5000);const callback = function (mutationsList, observer) {for (const mutation of mutationsList) {if (mutation.type === "childList") {console.log("子元素增加或者删除.");} else if (mutation.type === "attributes") {console.log("元素属性发生改变");}}
};document.addEventListener("DOMContentLoaded", function () {const observer = new MutationObserver(callback);observer.observe(document.body, {attributes: true,childList: true,subtree: true,});
});

得到的回调数据是这样的

可以看到,MutationObserver 只记录了变化的 dom 元素(target),和变化的类型(type)。如此一来,我们便可以利用 MutationObserver 实现增量快照的思路。

可交互元素

利用 MutationObserver 我们可以记录元素的增加、删除、属性的更改,但是它没法跟踪像 input、textarea、select 这类可交互元素的输入。
对于这种可交互的元素,我们就需要通过监听 input 和 change 来记录输入的过程,这样我们就解决了用户手动输入的场景。
但是有些元素的值是通过程序直接设置的,这样是不会出发 input 和 change 事件的。这种情况下我们可以通过劫持对应属性的 setter 来达到监听的目的。

const input = document.getElementById("input");
Object.defineProperty(input, "value", {get: function () {console.log("获取input的值");},set: function (val) {console.log("input 的值更新了");},
});
input.value = 123;

以上就是浏览器录制和回放的大体思路,也是开源工具 rrweb(record replay web)的核心思想。当然 rrweb 中还记录了鼠标的移动轨迹、浏览器窗口的大小,增加了回放时的沙盒环境、时间校准等等,在这里不再赘述,有兴趣的同学可以自行查阅 rrweb 官网的介绍。

rrweb

以上篇幅主要介绍了 rrweb 录制和回放的核心思想,这里大致介绍一下它的使用方法。更多使用姿势请查看 rrweb 使用指南。
通过 npm 引入

npm install --save rrweb

录制

const events = []
let stopFn = rrweb.record({emit(event) {if (events.length > 100) {// 当事件数量大于 100 时停止录制stopFn();// 将 events 序列化成字符串,并保持到服务器}},
});

回放

const events = []; //从服务端取出记录并反序列化成数组
const replayer = new rrweb.Replayer(events);
replayer.play();

静态资源时效问题

下面是我截取的一段录制数据

可以看到录制的数据中存在外链的图片,也就是说在我们利用录制的数据进行回放的时候,需要依赖这张图片。但是随着项目的迭代,这张图片很可能早已不在,这时我们再回放时,页面中的图片就会加载不出来。
其实不只是图片,外链的 css、字体文件等等都有这个问题。再回到文章开头提到的保险场景,保额信息就在网站内的一张海报上,客户可能会说:“我当时看到的保额明明是150万,怎么现在变成100万了?”,这时你要怎么证明当时海报上写的就是 100 万保额呢?

json 转视频

所以最稳妥的方案还是将 rrweb 录制的原始数据转换成视频,这样一来,不管网站怎么变化,迭代了多少版本,视频是不受影响的。
我的做法是通过 puppeteer 在服务端运行无头浏览器,在无头浏览器中回放录制的数据,然后每秒截取一定数量的图片,最后通过 ffmpeg 合成视频。下面是大致的流程图

帧率
我这里是一秒 50 帧,也就是说每隔 20ms 要截一张图。
截图时机
这里有个坑,puppeteer 截一张图的时间大概需要 300ms,假设页面在回放的过程中,我们使用 setInterval 每隔 20ms 执行一次截图,那么两次截图动作之间其实相隔了一次截图的时间,差了接近 300ms。第二帧我们想要截取的是视频地 20ms 的数据,可是回放页面已经播放到 320ms 处了。

暂停播放
为解决截图耗时所带来的影响,在每次截图之前,我将回放视频暂停到对应的时间点,这样截取到的就是我们想要的画面了。

 updateCanvas () {if (this.imgIndex * 20 >= this.timeLength) {this.stopCut(); // 事先计算整个视频需要截多少帧,截满了就结束return;}// 截图this.iframe.screenshot({type: 'png',encoding: 'binary',}).then(buffer => {this.readAble.push(buffer) //保存截图数据到可读流中this.page.evaluate((data) => {window.chromePlayer.pause(data * 20); // 将回放页中的视频暂停到对应时间点}, this.imgIndex)this.updateCanvas(this.imgIndex++) })
}

输出视频

  stopCut () {this.readAble.push(null) // 截图完成后,需要给可读流一个 null,表示没有数据了this.ffmpeg.videoCodec('mpeg4')  // 视频格式,这里我输出的是 mp4.videoBitrate('1000k') // 每秒钟视频所占用的大小,这个是视频清晰度的关键指标.inputFPS(50) // 帧率,这个是视频流畅度的关键指标,需要和每秒截图的数量保持一致.on('end', () => {console.log('\n视频转换成功')}).on('error', (e) => {console.log('error happend:' + e)}).save('./res.mp4') // 输出视频}

结语

由于 puppeteer 截图性能的问题,目前转 1 秒中的 rrweb 视频,需要 15 秒的时间,性能上是远远不够的。如果你有什么好的想法,欢迎加入到这个项目中来,一起实现更加稳定、高效、强大的 rrweb 转视频工具。
这里附上 源码地址

rrweb 浏览器录制及转视频方案相关推荐

  1. 用浏览器轻松录制音频、视频—— MediaRecorder API

    原文链接 浏览器有个强大而且简单的API -- MediaRecorder,顾名思义,可以用来录制音频和视频. 闲话不说,先上demo -- #demo1: 录制语音 < 微信(66)哈哈哈·· ...

  2. 最近录制了一些视频,搭建和测试了一下视频平台

    最近录制了一些视频,讲解非常详细和细致,但也随之带来一个问题,对于已经会了一些的朋友来说,就显得有点罗嗦了.对于已经会的人,总是只想看到自己正好不会和没注意的地方,可每个人不会和没注意的地方都不同,我 ...

  3. 拍乐云推出业内首个「线上美术教学音视频方案」,打造极致互动体验

    在线教育因为其上课的时间地点便捷.名师资源共享和强大的教研能力,获得了越来越多学生和家长的青睐,教学生如何创造美的美术教育也被滚滚浪潮推向了线上.但无法面授,笔墨丹青如何一线牵?线上美术教学效果能不能 ...

  4. esp32cam 服务端远程视频方案

    esp32cam 服务端远程视频方案 说明 本方案为esp32cam 服务端 浏览器 三端联合使用.将服务端部署在公网即可远程使用,没有远程需求,可以直接在局域网使用.代码无需修改. 本文取缔了esp ...

  5. html5视频录制,在HTML5视频录制方面,我们为什么选WebRTC而不选Media Recorder API

    作者:Pipe(原文链接) 翻译:刘通 原标题:Why we chose WebRTC over Media Recorder API for HTML5 Video Recording 通过这篇文章 ...

  6. 计算机桌面视频录制,录制电脑屏幕 如何录制电脑屏幕视频?录制电脑屏幕软件...

    今天又是周五了,时间过得真快,此刻小编心情是十分激动的,因为小编这周末打算肥家,哈哈哈,想想就觉得好鸡冻呢,没事要多肥家看看吖.不过再鸡冻的心情,小编也会认真的完成今天教程方案的介绍的.在这个信息化的 ...

  7. 视频点播系统助力政府企业私有云视频方案

    为什么政府企业要建立私有云视频方案? 1.面对头部云厂商不断侵犯的隐私问题,把政务信息和企业内容放到云厂商真的安全吗?真的有隐私吗?以前可能是毫无疑问,而在经历一些事情以后,首先是政府对这些商业厂家的 ...

  8. H5页面在微信浏览器中自动播放视频

    H5页面在微信浏览器中自动播放视频 安卓和IOS不同 h5在安卓微信浏览器上的视频不能自动播放 h5在iOS微信浏览器上的视频可以自动播放 iOS的实现方案

  9. 音视频方案,音视频扩展内容(RTMP,FFMpeg/H.26*/mpeg*/AVC等标准与协议)(笔记)1,视频格式

    视频方案,雷霄骅的专栏- http://blog.csdn.net/leixiaohua1020  > SI, TI   ITU-R BT.1788建议使用时间信息(TI,Temporal pe ...

最新文章

  1. Servlet(一)
  2. postman无法获得响应_【原创翻译】POSTMAN从入门到精通系列(二):发送第一个请求...
  3. 打开PDF文件弹出阅读未加标签文档的解决方法
  4. doctype的三种类型
  5. Java - concurrent包详解
  6. 电脑任务管理器快捷键_电脑知识小常识
  7. 抱歉(HDU-1418)
  8. 依赖反转原理,IoC容器和依赖注入:第2部分
  9. 安装qt5后编译运行后有关Qt at-spi的警告
  10. 视频日志之android的总结与思考
  11. 区块链 以太坊 solidity require revert assert
  12. Android面试问题收集总结
  13. 惠普z800工作站bios设置_HP工作站BIOS说明书适用Z228Z440Z230Z640Z840Z800Z620Z420Z820主板设置.doc...
  14. Arduino程序笔记(一) - 串口调试助手
  15. CentOS6 的yum源配置
  16. Permutation 和 Combination
  17. A19,A2,A12 字符排序问题!
  18. 微软远程桌面0x104_如何处理WIN10上的远程桌面错误0x104教程
  19. 基于QT+Halcon实现拟合圆形
  20. 不要再使用TCHAR和_T了

热门文章

  1. 为什么我选择Ant Design Pro脚手架
  2. 云计算初认识 +阿里云服务器免费领取教程
  3. Oracle 数据排序——按照 IN 列表位置
  4. java仿qq 界面_界面--仿qq登录界面
  5. 读取SIM卡中联系人流程
  6. 前端用vue实现一个滚动数字时钟
  7. 快递查询 快递查询.htm?dh=快递单号
  8. 谷歌浏览器右下角弹窗提醒
  9. minSdkVersion、API level 以及兼容包appcompat三者之间的关系
  10. 应聘Morgan Stanley(转)