Skip to content

Commit

Permalink
feat: add eventbus
Browse files Browse the repository at this point in the history
  • Loading branch information
oct16 committed Mar 9, 2020
1 parent 08fca40 commit c5c9f27
Show file tree
Hide file tree
Showing 14 changed files with 204 additions and 110 deletions.
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

如果你爱打游戏,一定打过魔兽争霸3(暴露年纪🤣),你也许会游戏导出的录像文件感到好奇,明明打了一个小时游戏,为什么录像才几百KB而已。不过很快你又发现另一个问题,在每次导入录像的时候需要重新加载一次地图,否则就不能播放。

突然恍然大悟,原来如此啊!
好像明白了什么...

录像记录的数据不是一个视频文件,而是带着时间戳的一系列动作,导入地图的时候,实际相当于初始了一个状态,在这个状态的基础上,只需要对之前的动作进行还原,也就还原的之前的游戏过程。
> 相关问题:[《魔兽争霸》的录像,为什么长达半小时的录像大小只有几百 KB?](https://www.zhihu.com/question/25431134/answer/30917935)
Expand All @@ -25,7 +25,7 @@
所以对比传统的视频录像方法,假设录像是500KB,那么理论上体积上缩小了大约 500GB / 500KB = **1000000倍**

Web录屏器其实也借鉴这样的一种思路,工程上一般称之为Operations Log, 本质上他的实现也是通过记录一系列的浏览器事件数据,通过浏览器引擎重新渲染,还原了之前的操作过程,也就达到了“录屏器”的效果
从实际来看,对比采用最新H265压缩比达到300倍的视频,体积上至少也能节省200倍以上
从实际来看,即使对比采用H.265压缩比达到300倍的有损压缩视频,体积上至少也能节省200倍以上

对比传统的视频流,它的优势也是显而易见的

Expand Down Expand Up @@ -64,7 +64,7 @@ interface VNode{
}
```

对DOM进行深度遍历后,DOM被映射成了VNode类型节点,需要记录的 Node 主要是两种类型 ``ELEMENT_NODE`` ``TEXT_NODE``,之后在播放时,只需要对VNode进行解析,就很轻松的还原成记录时的状态了
对DOM进行深度遍历后,DOM被映射成了VNode类型节点,需要记录的 Node 主要是三种类型 ``ELEMENT_NODE```COMMENT_NODE```TEXT_NODE``,之后在播放时,只需要对VNode进行解析,就很轻松的还原成记录时的状态了

在这过程中,有一些节点和属性需要特殊处理,例如

Expand Down Expand Up @@ -135,6 +135,17 @@ function listenInputElement() {
那么通过这个两个策略对鼠标轨迹进行精简后,画一个💖大约只需要6个点,通过样条曲线来模拟鼠标的虚拟轨迹,当 t = 0.2 的时候,就可以得到一个下图这样带着弧度的轨迹了
![heart2](./heart2.png)

##### 通过鼠标数据生成热力图

之前已经通过鼠标事件记录了完整的坐标信息,通过[heatmap.js](https://www.patrick-wied.at/static/heatmapjs/)可以很方便的生成热力图,用于对用户的行为数据进行分析。

这里需要注意的地方是当页面切换的时候我们需要重置热力图,如果是单页应用,通过 `History``popstate``hashchange` 可以监听页面的变化

![heatmap](./heatmap.png)

##### 对于用户隐私的脱敏

对于一些客户个人隐私数据,通过在开发时DOM进行标注的 `Node.COMMENT_NODE`(例如: `<!-- ... -->`)信息申明,我们是可以获取并加工的。通过约定好的声明对需要脱敏的DOM块进行处理,在渲染的时候通过CSS打上马赛克或模糊处理即可

##### 沙箱化提升安全

Expand All @@ -149,9 +160,9 @@ function listenInputElement() {
- 不能执行自动播放的tricky. 比如: autofocused, autoplay
```

##### 数据块的压缩

##### 在客户端进行的Gzip压缩

在客户端可以进行基于 `Gzip` 的数据包压缩,Gzip是基于哈夫曼二叉树的,具体原来可以看看这里[How gzip uses Huffman coding](https://jvns.ca/blog/2015/02/22/how-gzip-uses-huffman-coding/)

##### 播放、跳转与快进

Expand Down
2 changes: 1 addition & 1 deletion global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ declare module '*.css' {


declare interface EventTarget {
result: IDBDatabase;
result: any;
transaction: IDBTransaction

}
Expand Down
Binary file added heatmap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 3 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { record } from '@WebReplay/record'
import { replay } from '@WebReplay/player'
import { dbPromise, SnapshotData } from '@WebReplay/snapshot'
import { dbPromise } from '@WebReplay/snapshot'

async function start() {
const indexDB = await dbPromise
Expand All @@ -10,12 +10,11 @@ async function start() {
indexDB.add(data)
}
})

const replayButton = document.getElementById('replay')
if (replayButton) {
replayButton.onclick = () => {
indexDB.readAll((data: SnapshotData[]) => {
replay(data)
})
replay()
}
}
}
Expand Down
70 changes: 5 additions & 65 deletions packages/player/src/container.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { VNode, diffNode } from '@WebReplay/virtual-dom'
import UI from './ui.html'
import HTML from './ui.html'
import STYLE from './ui.css'
import { Pointer } from './pointer'
import { Player } from './player'
import { SnapshotData } from '@WebReplay/snapshot'

export class Container {
data: SnapshotData[]
player: Player
container: HTMLElement
controller: HTMLElement
snapshotHTML: string
sandBox: HTMLIFrameElement
pointer: Pointer
Expand All @@ -18,25 +13,17 @@ export class Container {
width: number
height: number

constructor(params: { vNode: VNode; width: number; height: number; data: SnapshotData[] }) {
constructor(params: { vNode: VNode; width: number; height: number }) {
this.vNode = params.vNode
this.width = params.width
this.height = params.height
this.data = params.data
this.init()
}

init() {
this.renderHTML()
this.initCtrlPanel()
this.initTemplate()
this.initSandbox()
this.initPlayer()
}

initPlayer() {
const player = new Player(this.data)
this.player = player
player.play()
}

initSandbox() {
Expand All @@ -54,61 +41,14 @@ export class Container {
return diffNode(this.vNode, null)
}

initCtrlPanel() {
initTemplate() {
document.head.appendChild(this.createStyle())
document.body.appendChild(this.createContainer())

this.controller = this.container.querySelector('.controller') as HTMLElement
this.container.addEventListener('click', (e: MouseEvent) => {
const target = e.target
if (target) {
const command = (target as HTMLElement).getAttribute('command')
this.command(command as any)
}
})
}

command(c: 'play' | 'pause' | 'x1' | 'x4' | 'x8') {
const pauseBtn = this.container.querySelector('.pause') as HTMLButtonElement
const playBtn = this.container.querySelector('.play') as HTMLButtonElement
const [x1, x4, x8] = this.container.querySelectorAll('.speed') as NodeListOf<HTMLButtonElement>
switch (c) {
case 'play':
pauseBtn.removeAttribute('disabled')
playBtn.setAttribute('disabled', '')
this.player.play()
break
case 'pause':
playBtn.removeAttribute('disabled')
pauseBtn.setAttribute('disabled', '')
this.player.pause()
break
case 'x1':
x8.removeAttribute('disabled')
x4.removeAttribute('disabled')
x1.setAttribute('disabled', '')
this.player.setSpeed(1)
break
case 'x4':
x8.removeAttribute('disabled')
x1.removeAttribute('disabled')
x4.setAttribute('disabled', '')
this.player.setSpeed(4)
break
case 'x8':
x1.removeAttribute('disabled')
x4.removeAttribute('disabled')
x8.setAttribute('disabled', '')
this.player.setSpeed(8)
break
default:
break
}
}

createContainer() {
const parser = new DOMParser()
const element = parser.parseFromString(UI, 'text/html').body.firstChild as HTMLElement
const element = parser.parseFromString(HTML, 'text/html').body.firstChild as HTMLElement
element.style.width = this.width + 'px'
element.style.height = this.height + 'px'
return (this.container = element)
Expand Down
39 changes: 39 additions & 0 deletions packages/player/src/eventbus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export class EventBus {
eventTopics: { [key: string]: Function[] } = {}

listen(event: string, listener: Function) {

if (!this.eventTopics[event] || this.eventTopics[event].length < 1) {
this.eventTopics[event] = []
}
this.eventTopics[event].push(listener)
console.log(this.eventTopics[event])

}

emit(event: string, params?: any) {


if (!this.eventTopics[event] || this.eventTopics[event].length < 1) {
return
}

console.log(event)

this.eventTopics[event].forEach(listener => {
listener(!!params ? params : {})
})
}

remove(event: string) {
if (!this.eventTopics[event] || this.eventTopics[event].length < 1) {
return
}
// delete listener by event name
delete this.eventTopics[event]
}

getListener(event: string) {
return this.eventTopics[event]
}
}
27 changes: 21 additions & 6 deletions packages/player/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import { SnapshotData, WindowSnapshotData, DOMSnapshotData } from '@WebReplay/snapshot'
import { dbPromise } from '@WebReplay/snapshot'
import { Container } from './container'
import { Player } from './player'
import { Pointer } from './pointer'
import { Panel } from './panel'

export async function replay() {
const indexDB = await dbPromise
const { width, height, vNode, data } = await indexDB.getData()

export function replay(data: SnapshotData[]) {
const [{ width, height }, { vNode }] = data.splice(0, 2).map(_ => _.data) as [WindowSnapshotData, DOMSnapshotData]
document.documentElement.innerHTML = ''
new Container({

const contain = new Container({
vNode,
width,
height,
data
height
})

const panel = new Panel(contain.container)

const player = new Player(data, new Pointer())
panel.listenCommand(command => {
panel.command(command)
player.command(command)
})

panel.control.play()
}
69 changes: 69 additions & 0 deletions packages/player/src/panel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { EventBus } from './eventbus'

export class Panel {
container: HTMLElement
controller: HTMLElement

event = new EventBus()

constructor(container: HTMLElement) {
this.container = container
this.initCtrlPanel()
}

control = {
play: () => {
this.event.emit('command', 'play')
}
}

initCtrlPanel() {
this.controller = this.container.querySelector('.wr-panel') as HTMLElement
this.container.addEventListener('click', (e: MouseEvent) => {
const target = e.target
if (target) {
const command = (target as HTMLElement).getAttribute('command')
// this.command(command as any)
this.event.emit('command', command)
}
})
}

listenCommand(callback: (command: string) => void) {
return this.event.listen('command', callback)
}

command(c: string) {
const pauseBtn = this.container.querySelector('.pause') as HTMLButtonElement
const playBtn = this.container.querySelector('.play') as HTMLButtonElement
const [x1, x4, x8] = this.container.querySelectorAll('.speed') as NodeListOf<HTMLButtonElement>
switch (c) {
case 'play':
pauseBtn.removeAttribute('disabled')
playBtn.setAttribute('disabled', '')
break
case 'pause':
playBtn.removeAttribute('disabled')
pauseBtn.setAttribute('disabled', '')
break
case 'x1':
x8.removeAttribute('disabled')
x4.removeAttribute('disabled')
x1.setAttribute('disabled', '')
break
case 'x4':
x8.removeAttribute('disabled')
x1.removeAttribute('disabled')
x4.setAttribute('disabled', '')
break
case 'x8':
x1.removeAttribute('disabled')
x4.removeAttribute('disabled')
x8.setAttribute('disabled', '')
// this.player.setSpeed(8)
break
default:
break
}
}
}
27 changes: 25 additions & 2 deletions packages/player/src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ export class Player {
index = 0
requestID: number
startTime: number
pointer = new Pointer('wr-player')
constructor(data: SnapshotData[]) {
pointer: Pointer
constructor(data: SnapshotData[], pointer: Pointer) {
this.data = data
this.pointer = pointer
}

play() {
Expand All @@ -32,6 +33,28 @@ export class Player {
this.requestID = window.requestAnimationFrame(loop.bind(this))
}

command(c: string) {
switch (c) {
case 'play':
this.play()
break
case 'pause':
this.pause()
break
case 'x1':
this.setSpeed(1)
break
case 'x4':
this.setSpeed(4)
break
case 'x8':
this.setSpeed(8)
break
default:
break
}
}

pause() {
cancelAnimationFrame(this.requestID)
}
Expand Down
Loading

0 comments on commit c5c9f27

Please sign in to comment.