# TimeCat **Repository Path**: sujunmin/TimeCat ## Basic Information - **Project Name**: TimeCat - **Description**: A Magical Web Recorder 🖥 (WIP) - **Primary Language**: Unknown - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-08-05 - **Last Updated**: 2022-02-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README

TimeCat

A Magical Web Recorder 🖥 网页录屏器
![GitHub issues](https://img.shields.io/github/issues-raw/oct16/TimeCat) ![GitHub last commit](https://img.shields.io/github/last-commit/oct16/timecat) ![npm (tag)](https://img.shields.io/npm/v/timecatjs/latest)
### 项目简介 中文 | [English](./README.md) TimeCat 是一套网页录屏的解决方案,利用其独特的算法,提供超高性能,超高压缩比的网页无损录制。可以广泛运作于监控系统,行为分析,案例复盘,远程教育,低流量视频协同等场景 [🖥 DEMO](https://oct16.github.io/TimeCat) Chrome浏览器 ### Progress 06.07 Support Audio 05.24 Beta 1.0.0 Released 04.26 Live Mode 03.31 Add Chrome Plugin ### Version ![npm (tag)](https://img.shields.io/npm/v/timecatjs/latest) ###### Browsers Support | [Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | | - | - | - | - | ### Installation ###### Using [NPM](https://www.npmjs.com/package/timecatjs) ```shell $ npm i timecatjs -D ``` ###### Import in Browser Add script tags in your browser and use the global variable ``timecat`` - [jsDelivr](https://cdn.jsdelivr.net/npm/timecatjs@latest/lib/timecatjs.min.js) - [UNPKG](https://unpkg.com/timecatjs) ### Usage ###### Import SDK ```ts // from module import { record, replay } from 'timecatjs'; // from cdn const { record, replay } = window.timecat ``` ###### Record Data ```ts // record page interface RecordOptions { audio?: boolean // if your want record audio emitter?: (data: RecordData, db: IndexedDBOperator) => void } // default use IndexedDB to save records const ctrl = record(RecordOptions) // if you wanna send the records to server const ctrl = record({ emitter: (data, db) => fetch(, { body: JSON.stringify(data), method: 'POST', ContentType: 'application/json' }) }) // if you want stop record ctrl.unsubscribe() ``` - [Record Example](https://github.com/oct16/TimeCat/blob/073c467afc644ce37e4f51937c28eb5000b2a92c/examples/todo.html#L258) ###### Replay ```ts // replay record interface ReplayOptions { socketUrl?: string // if live mode proxy?: string // if cross domain autoplay?: boolean // autoplay when data loaded } replay(ReplayOptions) ``` - [Replay example](https://github.com/oct16/TimeCat/blob/4c91fe2e9dc3786921cd23288e26b421f6ea0848/examples/player.html#L14) ###### Export ```ts // export html file interface ExportOptions { scripts?: ScriptItem[] // inject script in html autoplay?: boolean // autoplay when data loaded audioExternal?: boolean // export audio as a file, default is inline dataExternal?: boolean // export data json as a file, default is inline } exportReplay(ExportOptions) ``` ### API Documentation [TYPEDOC](https://oct16.github.io/TimeCat/docs/globals.html) ### TimeCat -- 不可思议的Web录屏器 如果你爱打游戏,一定打过魔兽争霸3(暴露年纪🤣),你也许会游戏导出的录像文件感到好奇,明明打了一个小时游戏,为什么录像才几百KB而已。不过很快你又发现另一个问题,在每次导入录像的时候需要重新加载一次地图,否则就不能播放。 录像记录的数据不是一个视频文件,而是带着时间戳的一系列动作,导入地图的时候,实际相当于初始了一个状态,在这个状态的基础上,只需要对之前的动作进行还原,也就还原了之前的游戏过程,这就是repl的基本原理了 > 相关问题:[《魔兽争霸》的录像,为什么长达半小时的录像大小只有几百 KB?](https://www.zhihu.com/question/25431134) 但是这样有什么好处呢? 首先是对于一个录像,这样的方式极大程度的减小了体积,假设需要录一个小时的1080p24f视频,在视频未压缩的情况下 ``` 总帧数 = 3600s * 24 = 86400frame 假设每个逻辑像素用RGB三基色表示,每个基色8bits(256色) 帧大小 = (1920 * 1080)pixels * 8bits * 3 = 49766400bits 换算成KB是 49766400bits / 8 / 1024 = 6075KB 总视频体积 = 6075KB * 86400 = 524880000KB ≈ 500GB ``` 所以对比传统的视频录像方法,假设录像是500KB,那么理论上体积上缩小了大约 524880000KB / 500KB ≈ 1000000倍 Web录屏器其实也借鉴这样的一种思路,工程上一般称之为Operations Log, 本质上他的实现也是通过记录一系列的浏览器事件数据,利用浏览器引擎重新渲染,还原了之前的操作过程,也就达到了“录屏器”的效果 从实际来看,即使对比采用H.265压缩比达到几百倍的压缩视频,体积上至少也能节省200倍以上 对比传统的视频流,它的优势也是显而易见的 1. 极大程度减小录像文件体积 2. 极低的CPU与内存占用比率 3. 无损显示,可以进行无极缩放,窗口自适应等 4. 非常灵活的时间跳转,几乎无法感知的缓冲时间 5. 所有信息都是活的,文本图片可以复制,链接可以点击,鼠标可以滚动 6. 可以方便的录制声音,并让声音和画面同步,还以类似YouTube那样把声音翻译成字幕 7. 方便进行视频细节的修改,例如显示的内容进行脱敏,生成热力图等 8. 记录的序列化数据,十分利于知乎进行数据分析 ... 那么利用这样的技术,有哪些应用场景呢? 主要有以下几个方面 1. 异常监控系统,例如[LogRocket](https://logrocket.com/),可以理解他是一个整合了Sentry + 录屏器的工具,能回放网页错误时的图形界面与数据日志,从而帮助Debug 2. 记录用户的行为进行分析,例如[MouseFlow](https://mouseflow.com/)。甚至还可以是直播的方式[LiveSession](https://livesession.io/),“连接”到用户的浏览中,看看用户是怎么使用网站的 3. 对客服人员的监控,例如阿里的有十万级别的客服小二人员分散在全国各地,需要对他们的服务过程进行7x24小时的录屏,在这个数量级上的对监控的性能要求就非常高了,阿里内部的工具叫`XReplay` 4. 协同工具,Web直播等,也会涉及类似的技术 5. RPA .... --- ### Web录屏器的技术细节 ![Architecture](./assets/architecture.png) #### 对DOM进行快照 通过DOM的API可以很轻易的拿到页面的节点数据,但是对于录屏的需求而言,显而DOM Node提供的数据太冗余了,这一步通过参考VirtualDom的设计,把信息精简一下 ```ts interface VNode { type: number id: number tag: string attrs: Attrs children: VNode[] extra: Extra } ``` 对DOM进行深度遍历后,DOM被映射成了VNode类型节点,需要记录的 Node 主要是三种类型 `ELEMENT_NODE`,`COMMENT_NODE`和 `TEXT_NODE`,之后在播放时,只需要对VNode进行解析,就可以还原成记录时的状态了 在这过程中,有一些节点和属性需要特殊处理,例如 - `InputElement`等类型的`value` `checked`是无法从DOM获取的,需要从节点中对象中获取 - `script`标签的内容由于之后不会去执行,所以可以直接`skip`或者标记为`noscript` - `SVG`可以直接获取,但是它本身以及它的子元素重新转换为DOM的时候需要使用`createElementNS("http://www.w3.org/2000/svg", tagName)`的方法创建元素 - `src`或`href`属性如果是相对路径,需要把他们转换为绝对路径 ...... #### 记录影响页面元素变化的Action DOM的变化可以使用`MutationObserver`, 监听到`attributes`,`characterData`,`childList` 三种类型的变化 ```ts const observer = new MutationObserver((mutationRecords, observer) => { // Record the data }) observer.observe(target, options) ``` 再借助`WindowEventHandlers` `addEventListener` 等的能力组合,就可以监听到页面一系列的操作事件了 - Add Node Action - Delete Node Action - Change Attribute Action - Scroll Action - Change Location Action ... 通过 `mouseMove` 和 `click` 事件记录鼠标动作 对于 `mouseMove` 事件,在移动的过程中会频繁的触发,产生很容冗余的数据,这样的数据会浪费很多的空间,因此对于鼠标的轨迹,只采集少量的关键点即可。 最简单的办法是使用节流来减小事件产生的数据量,但是也有一些缺点: - 截流的间隔中可能会丢失关键的鼠标坐标数据 - 即时通过截流在移动距离足够长的时候任然会产生巨大的数据量,更好的办法是通过 `Spline Curves(样条曲线)` 函数来计算得出移动轨迹、抖动、加速度等生成一条路径曲线用来控制鼠标的移动 Input的变换可以通过`Node.addEventListener` 的 `input` `blur` `focus` 事件监听,不过这只能监听到用户的行为,如果是通过JavaScript对标签进行赋值,这样是监听不到数据的变化的,这时可以通过`Object.defineProperty`来对一些表单对象的特殊属性进行劫持,在不影响目标赋值的情况下,把value新值转发到自定的handle上,统一处理状态变化 ```ts const elementList: [HTMLElement, string][] = [ [HTMLInputElement.prototype, 'value'], [HTMLInputElement.prototype, 'checked'], [HTMLSelectElement.prototype, 'value'], [HTMLTextAreaElement.prototype, 'value'] ] elementList.forEach(item => { const [target, key] = item const original = Object.getOwnPropertyDescriptor(target, key) Object.defineProperty(target, key, { set: function(value: string | boolean) { setTimeout(() => { handleEvent.call(this, key, value) }) if (original && original.set) { original.set.call(this, value) } } }) }) ``` #### MutationObserver ###### 优化改进 由于 DOM 的 Diff Patch 是借助 MutationObserver 来实现的,需要对发生更变的记录进行收集处理,这涉及到一些关键问题:例如DOM更变的时序是有先后的,Mutation只归纳为新增和删除,但是在调用insertBefore或者appendChild的时候,会造成移动,要对这些节点进行处理,标记为移动,否则节点的引用丢失就可能会导致渲染错误 ###### 兼容性处理 [Can I Use MutationObserver](https://caniuse.com/#search=mutationObserver) 表示只在IE11及以上,安卓4.4及以上可以使用,对于老浏览器的兼容可以通过[mutationobserver-shim](https://www.npmjs.com/package/mutationobserver-shim)的方式来支持,但是使用shim可能会因为收集的数据致精度不足从而产生一些致命Bug,另外还有一种情况是某些网站可能会屏蔽的掉MutationObserver这个API,遇到这种情况可以通过创建Iframe的方式来还原``Native Code`` #### Canvas、Iframe、Video等元素的处理 - Canvas:通过猴子补丁的方式去扩展或修改相应的API,从而获取到对应的动作 - Iframe:在非跨域的状态下,也可以直接访问内部的节点进行录制,类似的还有Shadow Dom - Video:利用HTMLVideoElement获取并且记录视频的状态信息 - Flash:通过截屏的方式进行录制 #### 外链的处理 加载HTML以后会引用很多外界的资源,通常会有多种形式 例如: - 绝对路径 ```` - 相对路径 ```` - 相对当前path的路径 ```` - 协议相对URL (The Protocol-relative URL)```` - srcset响应式图片 [Responsive images](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images) ``src="www.xxx.png" srcset="www.xxx.png 1x, www.xxx.png 2x"`` ... 以上就需要一个转换器来处理路径问题,在``Deserialize``阶段,把他们转成原域名下的绝对路径才能在跨域下正常加载 还有一种情况是对于第三方资源加载第三方资源的问题,这就需要借助服务器来解决了 ###### CORS Error 问题 通常是由于被录制的网站对资源进行了限制, 开启了CORS Policy,解决方案是,如果资源可控,可以添加白名单或者忽略,另外就是使用代理服务器, > 参考文章:[3 Ways to Fix the CORS Error](https://medium.com/@dtkatz/3-ways-to-fix-the-cors-error-and-how-access-control-allow-origin-works-d97d55946d9) #### SPA网页的渲染时间 在开始播放前,需要把之前的存储的数据还原成真实的DOM,这个过程中会占用一定的加载时间产生白屏,这取决于你的浏览器性能以及录制网页资源情况,参考FMP(First Meaningful Paint)的实现,加载过程中可以通过之前映射的数据动态生成骨架图,等待FMP发出Ready信号之后再进行播放 > 参考文章: [Time to First Meaningful Paint](https://docs.google.com/document/d/1BR94tJdZLsin5poeet0XoTW60M0SjvOJQttKT-JK8HI/view#) #### 通过样条曲线模拟鼠标轨迹 用户在网页中移动鼠标会产生很多`mouseMove`事件,通过 `const { x, y } = event.target` 获取到了轨迹的坐标与时间戳 假如在页面上用鼠标划过一个💖的轨迹,可能会得到下图这样的坐标点 但是对于录屏这个业务场景来说,大部分场合并不要求100%还原精确的鼠标轨迹,通常只会关心两种情况: 1. 鼠标在哪里点击? 2. 鼠标在哪里停留? 那么通过这个两个策略对鼠标轨迹进行精简后,画一个💖大约只需要6个点,通过样条曲线来模拟鼠标的虚拟轨迹,当 t = 0.2 的时候,就可以得到一个下图这样带着弧度的轨迹了 通过规则筛选出关键点后,利用B样条曲线计算函数,按照最小间隔进行取样并插入鼠标路径队执行列里,当渲染时重绘鼠标位置的时候,就可以得到一个近似曲线的鼠标轨迹了 #### 通过Diff字符串优化数据长度 当在一个输入框中不断的敲击内容时,Watcher函数会源源不断的事件响应,通过`Event.target.value`可以拿到当前`HTMLInputElement`最新的值,利用节流函数可以过滤掉一些冗余的响应,但是还不够,例如在一个TextArea中的文本会非常的长长长长长,假设文本的长度是n,当在文本后面添加了10个字符,每次输入响应1次,那么响应的长度是: > 10n + ∑(k=1, n=10) 可见会产生大量的数据 通过 Diff Patch 之后,把字符串`abcd` 修改 `bcde`可以表达为: >

abcde

```ts const patches = [ { type: 'delete', index: 0, count: 1 }, { type: 'add', index: 3, value: 'e' } ] ``` #### 通过鼠标数据生成热力图 之前已经通过鼠标事件记录了完整的坐标信息,通过[heatmap.js](https://www.patrick-wied.at/static/heatmapjs/)可以很方便的生成热力图,用于对用户的行为数据进行分析。 这里需要注意的地方是当页面切换的时候需要重置热力图,如果是单页应用,通过 `History` 的 `popstate` 与 `hashchange` 可以监听页面的变化 ![heatmap](./assets/heatmap.png) #### 对于用户隐私的脱敏 对于一些客户个人隐私数据,通过在开发时对DOM进行标注的 `Node.COMMENT_NODE`(例如: ``)信息申明,这是可以获取并加工的。通过约定好的声明对需要脱敏的DOM块按业务的需求进行处理即可,例如在项目的DEMO中,若需要在回放的时候把```