implemented the play any offset feature
This commit is contained in:
@@ -40,6 +40,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mitt": "^1.1.3",
|
"mitt": "^1.1.3",
|
||||||
"rrweb-snapshot": "^0.4.2"
|
"rrweb-snapshot": "^0.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user