我在腾讯主导了一个用于用户行为监控与回放的项目,其中,通过录制前端DOM的变化,实现用户界面的变化。和rrweb不同,我们没有使用MutationObserver方案进行录制,这个方案需要解决如何获取被移除节点的路径问题,非常复杂,且MutationObserver具有兼容性问题,低版本浏览器不支持,另外由于MutationObserver基于微任务进行响应,响应过快,导致一些性能问题,另外还有不可预测的丢帧问题。我们采用了另外一套更加缜密的DOM变化记录方案,但由于内部项目暂时不便公开。在除了web录屏与回放的研究之外,我还关注到滴滴内部开源了一套原生APP的录屏方案。另外,除了基于DOM的录屏方案外,我还在寻找另外的更高效的录屏方案,因为基于js的录屏方案,会带来不可避免的性能问题,以及丢失数据问题。在一次偶然冲浪过程中,我发现谷歌已经通过chrome开放了录屏接口,也就是getDisplayMedia,于是进行了一番研究,并通过本文记录下来。
什么是WebRTC?
getDisplayMedia是webrtc整体标准中的一个接口,要了解webrtc的入门信息,可以移步《WebRTC点对点应用架构研究》这篇2018年发表的博文,这篇文章可以作为入门了解webrtc及其基本开发步骤的知识文章。
在上述文章之上,我们再去理解webrtc,可以这么理解:webrtc是一套客户端点对点流式信息传播的技术方案。其中我的新认识在于“流式信息”这个点,以前,我把重点放在“点对点”上,但逐渐我发现,但凡基于流式信息的传播,都可以在webrtc标准下去讨论。这里的流式信息,主要是指基于音视轨的实时数据流。说白了,就是音频和视频实时数据。
而本文讨论的屏幕录制,就属于这一范畴。因此,录屏接口也被放在webrtc整体框架下面。
getDisplayMedia与直播
从接口名称可以看出,这是一个获取显示器实施音视频数据的接口。在我最早的文章中并没有介绍这个接口,当时并没有注意到它。我们看下它的兼容性。
可以看出来,它的兼容性并不好,IE直接pass,手机端无一支持。但考虑到实际场景,录屏基本上也只会在PC端有应用场景。
关于getDisplayMedia这个接口的具体用法,可以参考MDN。它的使用超级简单:
navigator.mediaDevices.getDisplayMedia(constraints).then((stream) => { // ... })
上面代码会执行webrtc惯用步骤,弹出与用户之间的交互对话框,只有用户允许之后,才能进行录屏。它返回一个promise,then里面接住一个stream,这个stream就是被录制的屏幕音视流。我们可以把这个流塞到一个video标签上,就可以看到录制的实时效果了。
document.querySelector('video#player').srcObject = stream
这里的stream是一个MediaStream类型的流,这种流的特征是,它拥有多个轨,也就是tracks。而在我文章开头的文章里面有代码演示,通过peer.addTrack就可以完成节点之间的点对点传输的对接工作。也就是说,我们将前文的peer相关的知识和这里的知识结合,就可以马上做一款录屏直播应用。
stream.getTracks().forEach((track) => { peer.addTrack(track, stream) })
这一切看上去都非常简单不是吗?
用户行为监控
由于webrtc在启动能力前,会有原生的chrome窗口弹出来询问用户,且在开启getDisplayMedia之后会有一个状态条,所以在整个的操作中会有一些生硬,用户有明显的知道自己的屏幕正在被录制。因此,利用webrtc进行录屏,是一种有感录制,和我之前一直在做的无感录屏稍有不同。
因此,此类用户行为监控更适合用在测试场景下,或者明确授权情况下。我们来看下具体的实现过程。
在通过getDisplayMedia获得stream之后,我们需要通过MediaRecorder对stream进行录制。MediaRecorder也是webrtc的接口,你可以通过MDN了解它的具体用法。它可以对一个stream进行录制,并生成blob格式的目标数据。但凡webrtc相关的stream,都可以被MediaRecorder录制出来。一旦理解了stream的本质,那么对录制也会理解不少:录制就是在流上创建截面序列,也就是采集音视频的过程,因此,我们需要配置一些采样信息,通过配置不同的采样信息,就可以得到不同画质、音质、流畅度的结果。
recorder = new MediaRecorder(stream, { mimeType: "video/webm; codecs=vp9" }) recorder.ondataavailable = handleRecord recorder.onerror = e => console.log(e) recorder.start()
其中,ondataavailable是一个回调,当需要产生一个视频数据时,它的回调函数会接受一个BlobEvent事件,而该事件中有一个data属性,内容为一个Blob。什么时候会触发这个事件呢?有两种情况,一种是调用recorder.requestData()方法,那么这个回调就被触发一次,此时会产生一个blob。另一种是调用recorder.stop()时,也会产生一个blob。每次产生的blob都是从上一次产生到当前这一时刻之间的录制片段,因此,如果中途不调用requestData方法,那么当你调用stop方法的时候,全部过程就会被记录下来。
function handleRecord(e) { if (event.data.size <= 0) { return } chunks.push(event.data) }
但作为用户行为监控的一个部分,很显然,我们更希望当用户的某个操作产生了错误的时候,才上报对应的录制视频。因此,我可能更倾向按照一定的策略来生成一段一段的视频,在需要的时候,通过前后端的交互,完成视频传输。
blob = new Blob(chunks, { type: "video/webm" })
由于我们拥有indexedDB这个利器,可以将blob存在本地,在需要的时候,通过webrtc点对点传输的特质,直接将视频传输给运维人员观看。这样可以解决服务端存储占用过多的问题。另外,ffmpeg已经出了webassembly版本,这让我们可以在前端浏览器中,对视频进行转化、裁剪、截图、修改、压缩等。
结语
Webrtc是web标准中关于流式数据实时传输的重要标准,而录屏能力,在直播、网课、用户行为监控等方面,都给我提供了一定的想象空间。我在最新一期的《Robust》中提到音视频应用、5G等基础设施层面、消费层面的相关逻辑,新的一年在webrtc这个点上,会给我们带来更多想象空间。如果你也对相关领域感兴趣,不妨一起来试试。