implemented the play any offset feature

This commit is contained in:
Yanzhen Yu
2018-10-18 16:24:01 +08:00
parent 8186f05e1c
commit 6c8cf5c379
4 changed files with 68 additions and 36 deletions

View File

@@ -40,6 +40,6 @@
}, },
"dependencies": { "dependencies": {
"mitt": "^1.1.3", "mitt": "^1.1.3",
"rrweb-snapshot": "^0.4.2" "rrweb-snapshot": "^0.4.3"
} }
} }

View File

@@ -27,7 +27,7 @@ export class Replayer {
public wrapper: HTMLDivElement; public wrapper: HTMLDivElement;
private events: eventWithTime[] = []; private events: eventWithTime[] = [];
private config: playerConfig; private config: playerConfig = defaultConfig;
private iframe: HTMLIFrameElement; private iframe: HTMLIFrameElement;
private mouse: HTMLDivElement; private mouse: HTMLDivElement;
@@ -37,10 +37,13 @@ export class Replayer {
private emitter: mitt.Emitter = mitt(); private emitter: mitt.Emitter = mitt();
constructor(events: eventWithTime[], config?: Partial<playerConfig>) { constructor(events: eventWithTime[], config?: Partial<playerConfig>) {
if (events.length < 2) {
throw new Error('Replayer need at least 2 events.');
}
this.events = events; this.events = events;
this.handleResize = this.handleResize.bind(this); this.handleResize = this.handleResize.bind(this);
this.setConfig(Object.assign({}, defaultConfig, 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);
} }
@@ -50,18 +53,12 @@ export class Replayer {
} }
public setConfig(config: Partial<playerConfig>) { public setConfig(config: Partial<playerConfig>) {
this.config = { Object.keys(config).forEach((key: keyof playerConfig) => {
...this.config, this.config[key] = config[key]!;
...config, });
};
} }
public getMetaData(): playerMetaData { public getMetaData(): playerMetaData {
if (this.events.length < 2) {
return {
totalTime: 0,
};
}
const firstEvent = this.events[0]; const firstEvent = this.events[0];
const lastEvent = this.events[this.events.length - 1]; const lastEvent = this.events[this.events.length - 1];
return { return {
@@ -69,31 +66,51 @@ export class Replayer {
}; };
} }
public play() { /**
* This API was designed to be used as play at any time offset.
* Since we minimized the data collected from recorder, we do not
* have the ability of undo an event.
* So the implementation of play at any time offset will always iterate
* all of the events, cast event before the offset synchronously
* and cast event after the offset asynchronously with timer.
* @param timeOffset number
*/
public play(timeOffset = 0) {
this.startTime = this.events[0].timestamp + timeOffset;
for (const event of this.events) { for (const event of this.events) {
const isSync = event.timestamp < this.startTime;
let castFn: undefined | (() => void);
switch (event.type) { switch (event.type) {
case EventType.DomContentLoaded: case EventType.DomContentLoaded:
this.startTime = event.timestamp;
break; break;
case EventType.Load: case EventType.Load:
this.emitter.emit('resize', { castFn = () =>
width: event.data.width, this.emitter.emit('resize', {
height: event.data.height, width: event.data.width,
}); height: event.data.height,
});
break; break;
case EventType.FullSnapshot: case EventType.FullSnapshot:
this.later(() => { castFn = () => {
this.rebuildFullSnapshot(event); this.rebuildFullSnapshot(event);
this.iframe.contentWindow!.scrollTo(event.data.initialOffset); this.iframe.contentWindow!.scrollTo(event.data.initialOffset);
}, this.getDelay(event)); };
break; break;
case EventType.IncrementalSnapshot: case EventType.IncrementalSnapshot:
this.later(() => { castFn = () => {
this.applyIncremental(event.data); this.applyIncremental(event.data, isSync);
}, this.getDelay(event)); };
break; break;
default: default:
} }
if (!castFn) {
continue;
}
if (isSync) {
castFn();
} else {
this.later(castFn, this.getDelay(event));
}
} }
} }
@@ -120,7 +137,7 @@ export class Replayer {
} }
private later(cb: () => void, delayMs: number) { private later(cb: () => void, delayMs: number) {
const id = later(cb, delayMs, this.config.speed); const id = later(cb, delayMs, this.config);
this.timerIds.push(id); this.timerIds.push(id);
} }
@@ -166,7 +183,7 @@ export class Replayer {
} }
} }
private applyIncremental(d: incrementalData) { private applyIncremental(d: incrementalData, isSync: boolean) {
switch (d.source) { switch (d.source) {
case IncrementalSource.Mutation: { case IncrementalSource.Mutation: {
d.texts.forEach(mutation => { d.texts.forEach(mutation => {
@@ -215,12 +232,15 @@ export class Replayer {
break; break;
} }
case IncrementalSource.MouseMove: case IncrementalSource.MouseMove:
d.positions.forEach(p => { // skip mouse move in sync mode
this.later(() => { if (!isSync) {
this.mouse.style.left = `${p.x}px`; d.positions.forEach(p => {
this.mouse.style.top = `${p.y}px`; this.later(() => {
}, p.timeOffset); this.mouse.style.left = `${p.x}px`;
}); this.mouse.style.top = `${p.y}px`;
}, p.timeOffset);
});
}
break; break;
case IncrementalSource.MouseInteraction: { case IncrementalSource.MouseInteraction: {
const event = new Event(MouseInteractions[d.type].toLowerCase()); const event = new Event(MouseInteractions[d.type].toLowerCase());
@@ -241,7 +261,7 @@ export class Replayer {
this.iframe.contentWindow!.scrollTo({ this.iframe.contentWindow!.scrollTo({
top: d.y, top: d.y,
left: d.x, left: d.x,
behavior: 'smooth', behavior: isSync ? 'instant' : 'smooth',
}); });
} else { } else {
(target as Element).scrollTop = d.y; (target as Element).scrollTop = d.y;

View File

@@ -1,16 +1,27 @@
import { playerConfig } from '../types';
const FRAME_MS = 16; const FRAME_MS = 16;
let _id = 1;
const timerMap: Map<number, boolean> = new Map(); const timerMap: Map<number, boolean> = new Map();
export function later(cb: () => void, delayMs: number, speed = 1): number { export function later(
cb: () => void,
delayMs: number,
config: playerConfig,
): number {
const now = performance.now(); const now = performance.now();
const id = timerMap.size + 1; let lastStep = now;
const id = _id++;
timerMap.set(id, true); timerMap.set(id, true);
function check(step: number) { function check(step: number) {
if (!timerMap.has(id)) { if (!timerMap.has(id)) {
return; return;
} }
if (step - now > delayMs / speed - FRAME_MS) { const stepDiff = step - lastStep;
lastStep = step;
delayMs -= config.speed * stepDiff;
if (delayMs < FRAME_MS) {
cb(); cb();
clear(id); clear(id);
} else { } else {

View File

@@ -1,4 +1,4 @@
import { idNodeMap, NodeType, serializeNodeWithId } from 'rrweb-snapshot'; import { idNodeMap, NodeType, serializeNodeWithId, resetId } from 'rrweb-snapshot';
import { import {
Mirror, Mirror,
throttleOptions, throttleOptions,
@@ -32,6 +32,7 @@ export const mirror: Mirror = {
// TODO: transform this into the snapshot repo // TODO: transform this into the snapshot repo
export function getIdNodeMap(doc: Document) { export function getIdNodeMap(doc: Document) {
resetId();
const map: idNodeMap = {}; const map: idNodeMap = {};
function walk(n: Node) { function walk(n: Node) {