本文可以说是一篇调研学习文,是我自己感觉可行的一套方案,后续会去读读已经开源的一些类似的代码库,补足自己遗漏的一些细节,所以大家可以当作学习文,生产环境慎用。
录屏重现错误场景
如果你的应用有接入到web apm系统中,那么你可能就知道apm系统能帮你捕获到页面发生的未捕获错误,给出错误栈,帮助你定位到BUG。但是,有些时候,当你不知道用户的具体操作时,是没有办法重现这个错误的,这时候,如果有操作录屏,你就可以清楚地了解到用户的操作路径,从而复现这个BUG并且修复。(本次先录制,下次再回放)
一、先来看看音频可视化的实现思路:
思路
需要将音频文件解码成二进制流文件,这个流文件即为音频资源buffer播放源,之后需要将buffer播放源与分析器相连,分析器与扬声器相连;
audioContext.destination:返回AudioDestinationNode对象,表示当前audio context中所有节点的最终节点,一般表示音频渲染设备。
二、录制视频的实现
简单了解了音频可视化以后咱们就来研究一下录屏的实现。
关于录屏和摄像对比了两种方法:
- 使用HTML5的api实现
摄像:mediaDevices(获取设备)+ getUserMedia(获取流) + MediaRecorder(存储)
录屏:getDisplayMedia(获取流) + MediaRecorder(存储) - ffmpeg + node:FFmpeg是一套非常强大的音视频处理的开源工具,不多介绍,而Electron基于node和chromium,它允许使用node的API以及几乎所有的node模块,这意味这着我们可以调用cmd命令来操作ffmpeg实现录屏和摄像录制,当然ffmpeg功能绝不止这点。
HTML5实现
mediaDevices
- 用于收集系统上可用的多媒体输入和输出设备的信息
- 该方法调用成功返回设备列表,并传入带有devceID的MediaStreamConstraints对象可以指定设备获取流媒体来源
navigator.mediaDevices.enumerateDevices().then(devicelist => {
// audiooutput 扬声器
// audioinput 麦克风
// audiooutput 摄像
console.log(devicelist)
}).catch(err => console.log(err))
getUserMedia
- 用户提供访问硬件设备媒体(摄像头、视频、音频、地理位置等)的接口,基于该接口,开发者可以在不依赖任何浏览器插件的条件下访问硬件媒体设备。
- 该方法返回视频流,将获取到的流赋给video标签可实现边录边看
navigator.mediaDevices.getUserMedia(MediaStreamConstraints).then(stream => {
videoElement.srcObject = stream; //
}, error => console.log(error));
MediaRecorder
- 记录和捕获媒体,也就是视频和音频
- getDisplayMedia 和 getUserMedia 获取到的流都需要使用MediaRecorder存储起来,并且可以保存成文件
let herf
this.recorder = new MediaRecorder(stream);
this.recorder.ondataavailable = e => {
herf = e.data;
download.href = URL.createObjectURL(herf);
};
this.recorder.start();
以上是我在网上找了找HTML5实现的一些方法,希望能给大家带来一点思路。
使用ffmpeg
一些基本参数:
- -formats 输出所有可用格式
- -f fmt 指定格式(音频或视频格式)
- -i filename 指定输入文件名,在linux下当然也能指定:0.0(屏幕录制)或摄像头
- -y 覆盖已有文件
- -t duration 记录时长为t
- -fs limit_size 设置文件大小上限
- -itsoffset time_off 设置时间偏移(s),该选项影响所有后面的输入文件。该偏移被加到输入文件的时戳,定义一个正偏移意味着相应的流被延迟了 offset秒。
- [-]hh:mm:ss* [.xxx]的格式也支持 音 频
- -ab bitrate 设置音频码率
- -ar freq 设置音频采样率
- -ac channels 设置通道 缺省为1 视 频
- -b bitrate 设置比特率,缺省200kb/s
- -r fps 设置帧频 缺省25
- -s size 设置帧大小 格式为WXH 缺省160X128.下面的简写也可以直接使用:
录屏相关命令
列出可用的设备包括音频和摄像等等
ffmpeg -list_devices true -f dshow -i dummy
录屏,你也可以加入关于视频的一些基本参数来获得你想要的文件
ffmpeg -f gdigrab -i desktop captrue.mkv -y
node调用
d进入bin文件夹后执行录屏相关命令
关于停止录制,虽然ffmpeg按 Q 可以停止录制,但是我们通过代码调用是看不到cmd命令行的而且他在录制过程中是一直占用这个进程什么命令也无法输入 所以这个地方我只想到一个办法就是强制停止该进程
这个属于是一个录制视频的组件库,基本方法就是调用一些特定的方法和参数就可以实现。
接下来是我最近看的一篇文章看到的一些思路,也写给大家参考一下
思路一:利用Canvas截图
这个思路比较简单,就是利用canvas去画网页内容,比较有名的库有: html2canvas ,这个库的简单原理是:
- 收集所有的DOM,存入一个queue中;
- 根据zIndex按照顺序将DOM一个个通过一定规则,把DOM和其CSS样式一起画到Canvas上。
这个实现是比较复杂的,但是我们可以直接使用,所以我们可以获取到我们想要的网页截图。为了使得生成的视频较为流畅,我们一秒中需要生成大约25帧,也就是需要25张截图,思路流程图如下:
但是,这个思路有个最致命的不足:为了视频流畅,一秒中我们需要25张图,一张图300KB,当我们需要30秒的视频时,图的大小总共为220M,这么大的网络开销明显不行。
思路二:记录所有操作重现
为了降低网络开销,我们换个思路,我们在最开始的页面基础上,记录下一步步操作,在我们需要"播放"的时候,按照顺序应用这些操作,这样我们就能看到页面的变化了。这个思路把鼠标操作和DOM变化分开:
鼠标变化:
- 监听mouseover事件,记录鼠标的clientX和clientY。
- 重放的时候使用js画出一个假的鼠标,根据坐标记录来更改"鼠标"的位置。
DOM变化:
- 对页面DOM进行一次全量快照。包括样式的收集、JS脚本去除,并通过一定的规则给当前的每个DOM元素标记一个id。
- 监听所有可能对界面产生影响的事件,例如各类鼠标事件、输入事件、滚动事件、缩放事件等等,每个事件都记录参数和目标元素,目标元素可以是刚才记录的id,这样的每一次变化事件可以记录为一次增量的快照。
- 将一定量的快照发送给后端。
- 在后台根据快照和操作链进行播放。
当然这个说明是比较简略的,鼠标的记录比较简单,我们不展开讲,主要说明一下DOM监控的实现思路。
思路:
首先你可能会想到,要实现页面全量快照,可以直接使用 outerHTML
const content = document.documentElement.outerHTML;
这样就简单记录了页面的所有DOM,你只需要首先给DOM增加标记id,然后得到outerHTML,然后去除JS脚本。
但是,这里有个问题,使用 outerHTML 记录的DOM会将把临近的两个TextNode合并为一个节点,而我们后续监控DOM变化时会使用 MutationObserver ,此时你需要大量的处理来兼容这种TextNode的合并,不然你在还原操作的时候无法定位到操作的目标节点。
那么,我们有办法保持页面DOM的原有结构吗?
答案是肯定的,在这里我们使用Virtual DOM来记录DOM结构,把documentElement变成Virtual DOM,记录下来,后面还原的时候重新生成DOM即可。
DOM转化为Virtual DOM
这里只需要关心两种Node类型: Node.TEXT_NODE 和 Node.ELEMENT_NODE 。 同时,要注意,SVG和SVG子元素的创建需要使用API: createElementNS,所以,我们在记录Virtual DOM的时候,需要注意namespace的记录
我们可以将整个documentElement转化为Virtual DOM,其中__flow用来记录一些参数,包括标记ID等,Virtual Node记录了: type、attributes、children、namespace。
Virtual DOM还原为DOM
将Virtual DOM还原为DOM的时候就比较简单了,只需要递归创建DOM即可,其中nodeFilter是为了过滤script元素,因为我们不需要JS脚本的执行。
以上就是实现一个录制的过程和一些方法,希望能给你带来一点点灵感
接下来我会研究一些录制完成后实现录制回放的功能,后续也写出来供大家参考。