implement pause which has a better performance than play at some time offset

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 47467ff692
commit 97ed5eb229

View File

@@ -31,11 +31,14 @@ export class Replayer {
private iframe: HTMLIFrameElement; private iframe: HTMLIFrameElement;
private mouse: HTMLDivElement; private mouse: HTMLDivElement;
private startTime: number = 0; private baselineTime: number = 0;
private timerIds: number[] = []; private timerIds: number[] = [];
private emitter: mitt.Emitter = mitt(); private emitter: mitt.Emitter = mitt();
// record last played event timestamp when paused
private lastPlayedEvent: eventWithTime;
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.');
@@ -76,37 +79,10 @@ export class Replayer {
* @param timeOffset number * @param timeOffset number
*/ */
public play(timeOffset = 0) { public play(timeOffset = 0) {
this.startTime = this.events[0].timestamp + timeOffset; this.baselineTime = this.events[0].timestamp + timeOffset;
for (const event of this.events) { for (const event of this.events) {
const isSync = event.timestamp < this.startTime; const isSync = event.timestamp < this.baselineTime;
let castFn: undefined | (() => void); const castFn = this.getCastFn(event, isSync);
switch (event.type) {
case EventType.DomContentLoaded:
case EventType.Load:
break;
case EventType.Meta:
castFn = () =>
this.emitter.emit('resize', {
width: event.data.width,
height: event.data.height,
});
break;
case EventType.FullSnapshot:
castFn = () => {
this.rebuildFullSnapshot(event);
this.iframe.contentWindow!.scrollTo(event.data.initialOffset);
};
break;
case EventType.IncrementalSnapshot:
castFn = () => {
this.applyIncremental(event.data, isSync);
};
break;
default:
}
if (!castFn) {
continue;
}
if (isSync) { if (isSync) {
castFn(); castFn();
} else { } else {
@@ -119,6 +95,20 @@ export class Replayer {
this.timerIds.forEach(clear); this.timerIds.forEach(clear);
} }
public resume(timeOffset = 0) {
for (const event of this.events) {
if (
event.timestamp < this.lastPlayedEvent.timestamp ||
event === this.lastPlayedEvent
) {
continue;
}
const delayToBaseline = this.getDelay(event);
const castFn = this.getCastFn(event);
this.later(castFn, delayToBaseline - timeOffset);
}
}
private setupDom() { private setupDom() {
this.wrapper = document.createElement('div'); this.wrapper = document.createElement('div');
this.wrapper.classList.add('replayer-wrapper'); this.wrapper.classList.add('replayer-wrapper');
@@ -151,7 +141,7 @@ export class Replayer {
event.data.source === IncrementalSource.MouseMove event.data.source === IncrementalSource.MouseMove
) { ) {
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;
event.data.positions = event.data.positions.map(p => { event.data.positions = event.data.positions.map(p => {
return { return {
@@ -159,12 +149,49 @@ export class Replayer {
timeOffset: p.timeOffset - firstOffset, timeOffset: p.timeOffset - firstOffset,
}; };
}); });
return firstTimestamp - this.startTime; return firstTimestamp - this.baselineTime;
} }
return event.timestamp - this.startTime; return event.timestamp - this.baselineTime;
} }
private rebuildFullSnapshot(event: fullSnapshotEvent) { private getCastFn(event: eventWithTime, isSync = false) {
let castFn: undefined | (() => void);
switch (event.type) {
case EventType.DomContentLoaded:
case EventType.Load:
break;
case EventType.Meta:
castFn = () =>
this.emitter.emit('resize', {
width: event.data.width,
height: event.data.height,
});
break;
case EventType.FullSnapshot:
castFn = () => {
this.rebuildFullSnapshot(event);
this.iframe.contentWindow!.scrollTo(event.data.initialOffset);
};
break;
case EventType.IncrementalSnapshot:
castFn = () => {
this.applyIncremental(event.data, isSync);
};
break;
default:
}
const wrappedCastFn = () => {
if (castFn) {
castFn();
}
this.lastPlayedEvent = event;
};
return wrappedCastFn;
}
private rebuildFullSnapshot(
event: fullSnapshotEvent & { timestamp: number },
) {
mirror.map = rebuild(event.data.node, this.iframe.contentDocument!)[1]; mirror.map = rebuild(event.data.node, this.iframe.contentDocument!)[1];
// avoid form submit to refresh the iframe // avoid form submit to refresh the iframe
this.iframe.contentDocument!.querySelectorAll('form').forEach(form => { this.iframe.contentDocument!.querySelectorAll('form').forEach(form => {