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