diff --git a/src/replay/index.ts b/src/replay/index.ts index a3e3416c..9588734a 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -1,6 +1,6 @@ import { rebuild, buildNodeWithSN } from 'rrweb-snapshot'; import * as mittProxy from 'mitt'; -import { later, clear } from './timer'; +import Timer from './timer'; import { EventType, incrementalData, @@ -14,6 +14,7 @@ import { missingNodeMap, addedNodeMutation, missingNode, + actionWithDelay, } from '../types'; import { mirror } from '../utils'; import './styles/style.css'; @@ -36,13 +37,14 @@ export class Replayer { private iframe: HTMLIFrameElement; private mouse: HTMLDivElement; - private timerIds: number[] = []; private emitter: mitt.Emitter = mitt(); private baselineTime: number = 0; // record last played event timestamp when paused private lastPlayedEvent: eventWithTime; + private timer: Timer; + constructor(events: eventWithTime[], config?: Partial) { if (events.length < 2) { throw new Error('Replayer need at least 2 events.'); @@ -50,6 +52,7 @@ export class Replayer { this.events = events; this.handleResize = this.handleResize.bind(this); + this.timer = new Timer(this.config); this.setConfig(Object.assign({}, config)); this.setupDom(); this.emitter.on('resize', this.handleResize as mitt.Handler); @@ -84,22 +87,27 @@ export class Replayer { */ public play(timeOffset = 0) { this.baselineTime = this.events[0].timestamp + timeOffset; + const actions = new Array(); for (const event of this.events) { const isSync = event.timestamp < this.baselineTime; const castFn = this.getCastFn(event, isSync); if (isSync) { castFn(); } else { - this.later(castFn, this.getDelay(event)); + actions.push({ doAction: castFn, delay: this.getDelay(event) }); } } + this.timer.addActions(actions); + this.timer.start(); } public pause() { - this.timerIds.forEach(clear); + this.timer.clear(); } public resume(timeOffset = 0) { + this.timer.clear(); + const actions = new Array(); for (const event of this.events) { if ( event.timestamp < this.lastPlayedEvent.timestamp || @@ -109,8 +117,13 @@ export class Replayer { } const delayToBaseline = this.getDelay(event); const castFn = this.getCastFn(event); - this.later(castFn, delayToBaseline - timeOffset); + actions.push({ + doAction: castFn, + delay: delayToBaseline - timeOffset, + }); } + this.timer.addActions(actions); + this.timer.start(); } private setupDom() { @@ -131,11 +144,6 @@ export class Replayer { this.iframe.height = `${dimension.height}px`; } - private later(cb: () => void, delayMs: number) { - const id = later(cb, delayMs, this.config); - this.timerIds.push(id); - } - // TODO: add speed to mouse move timestamp calculation private getDelay(event: eventWithTime): number { // Mouse move events was recorded in a throttle function, @@ -147,13 +155,14 @@ export class Replayer { const firstOffset = event.data.positions[0].timeOffset; // timeOffset is a negative offset to event.timestamp const firstTimestamp = event.timestamp + firstOffset; + const delay = firstTimestamp - this.baselineTime event.data.positions = event.data.positions.map(p => { return { ...p, - timeOffset: p.timeOffset - firstOffset, + timeOffset: p.timeOffset - firstOffset + delay, }; }); - return firstTimestamp - this.baselineTime; + return delay; } return event.timestamp - this.baselineTime; } @@ -294,14 +303,18 @@ export class Replayer { // skip mouse move in sync mode if (!isSync) { d.positions.forEach(p => { - this.later(() => { - this.mouse.style.left = `${p.x}px`; - this.mouse.style.top = `${p.y}px`; - const target = mirror.getNode(p.id); - if (target) { - this.hoverElements((target as Node) as Element); - } - }, p.timeOffset); + const action = { + doAction: () => { + this.mouse.style.left = `${p.x}px`; + this.mouse.style.top = `${p.y}px`; + const target = mirror.getNode(p.id); + if (target) { + this.hoverElements((target as Node) as Element); + } + }, + delay: p.timeOffset, + }; + this.timer.addAction(action); }); } break; diff --git a/src/replay/timer.ts b/src/replay/timer.ts index 8059ccb3..d6db248b 100644 --- a/src/replay/timer.ts +++ b/src/replay/timer.ts @@ -1,38 +1,70 @@ -import { playerConfig } from '../types'; +import { playerConfig, actionWithDelay } from '../types'; -const FRAME_MS = 16; -let _id = 1; -const timerMap: Map = new Map(); +export default class Timer { + private actions: actionWithDelay[]; + private config: playerConfig; -export function later( - cb: () => void, - delayMs: number, - config: playerConfig, -): number { - const now = performance.now(); - let lastStep = now; - const id = _id++; - timerMap.set(id, true); - - function check(step: number) { - if (!timerMap.has(id)) { - return; - } - const stepDiff = step - lastStep; - lastStep = step; - delayMs -= config.speed * stepDiff; - if (delayMs < FRAME_MS) { - cb(); - clear(id); - } else { - requestAnimationFrame(check); - } + constructor(config: playerConfig, actions: actionWithDelay[] = []) { + this.actions = actions; + this.config = config; + } + /** + * Add an action after the timer starts. + * @param action + */ + public addAction(action: actionWithDelay) { + const index = this.findActionIndex(action); + this.actions.splice(index, 0, action); + } + /** + * Add all actions before the timer starts + * @param actions + */ + public addActions(actions: actionWithDelay[]) { + this.actions.push(...actions); } - requestAnimationFrame(check); - return id; -} + public start() { + this.actions.sort((a1, a2) => a1.delay - a2.delay); + let delayed = 0; + const start = performance.now(); + const { actions, config } = this; + function check(time: number) { + delayed = time - start; + while (actions.length) { + const action = actions[0]; + const delayNeeded = action.delay / config.speed; + if (delayed >= delayNeeded) { + actions.shift(); + action.doAction(); + } else { + break; + } + } + if (actions.length > 0) { + requestAnimationFrame(check); + } + } + requestAnimationFrame(check); + } -export function clear(id: number) { - timerMap.delete(id); + public clear() { + this.actions = []; + } + + private findActionIndex(action: actionWithDelay): number { + let start = 0; + let end = this.actions.length - 1; + while (start <= end) { + let mid = Math.floor((start + end) / 2); + if (this.actions[mid].delay < action.delay) { + start = mid + 1; + } else if (this.actions[mid].delay > action.delay) { + end = mid - 1; + } else { + return mid; + } + } + return start; + } } diff --git a/src/types.ts b/src/types.ts index 906c81d4..70337891 100644 --- a/src/types.ts +++ b/src/types.ts @@ -239,3 +239,8 @@ export type missingNode = { export type missingNodeMap = { [id: number]: missingNode; }; + +export type actionWithDelay = { + doAction: () => void; + delay: number; +};