浏览器页面录制及转视频方案

100 篇文章 7 订阅
4 篇文章 0 订阅

背景

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

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

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 转视频工具。这里附上源码地址 (https://github.com/gumuqi/rrweb-to-video)。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java可以通过使用第三方插件来录制浏览器中的视频。其中一个常用的插件是Selenium WebDriver。Selenium WebDriver是一个用于自动化浏览器的工具,它可以通过编写Java代码来实现录制和控制浏览器视频。 首先,我们需要下载并配置Selenium WebDriver。在Java开发环境中,可以使用Maven或Gradle来导入相关的依赖项。然后,我们需要下载并配置适用于所需浏览器WebDriver驱动程序,如ChromeDriver或GeckoDriver。 接下来,我们可以使用Java代码编写录制视频的逻辑。首先,我们需要启动一个浏览器会话,示例如下: WebDriver driver = new ChromeDriver(); 然后,我们可以使用WebDriver对象打开所需的网页: driver.get("http://example.com"); 接下来,我们可以使用Java的图形库来截取浏览器的屏幕。这可以通过调用WebDriver对象的getScreenshotAs方法来完成: File screenshotFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE); 最后,我们可以将截取到的屏幕保存为视频文件。为了将多个截图文件合并为一个视频文件,我们可以使用一些第三方库,如FFmpeg或Xuggler。 需要注意的是,录制浏览器视频可能涉及一些安全和隐私问题。在实际应用中,我们应该确保获取用户的明确许可,并严格遵守相关法律法规。 总结起来,通过使用Selenium WebDriver和一些第三方库,我们可以使用Java来调用插件录制浏览器视频。这样可以实现自动化测试、模拟用户行为等功能。但在实际使用时,需要注意安全和隐私问题,并遵守相关法律法规。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值