close #274 implement the new state management proposal
This commit is contained in:
@@ -2,7 +2,7 @@ import { rebuild, buildNodeWithSN, INode, NodeType } from 'rrweb-snapshot';
|
|||||||
import * as mittProxy from 'mitt';
|
import * as mittProxy from 'mitt';
|
||||||
import * as smoothscroll from 'smoothscroll-polyfill';
|
import * as smoothscroll from 'smoothscroll-polyfill';
|
||||||
import { Timer } from './timer';
|
import { Timer } from './timer';
|
||||||
import { createPlayerService } from './machine';
|
import { createPlayerService, createSpeedService } from './machine';
|
||||||
import {
|
import {
|
||||||
EventType,
|
EventType,
|
||||||
IncrementalSource,
|
IncrementalSource,
|
||||||
@@ -56,24 +56,23 @@ export class Replayer {
|
|||||||
public wrapper: HTMLDivElement;
|
public wrapper: HTMLDivElement;
|
||||||
public iframe: HTMLIFrameElement;
|
public iframe: HTMLIFrameElement;
|
||||||
|
|
||||||
|
public service: ReturnType<typeof createPlayerService>;
|
||||||
|
public speedService: ReturnType<typeof createSpeedService>;
|
||||||
public get timer() {
|
public get timer() {
|
||||||
return this.service.state.context.timer;
|
return this.service.state.context.timer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private config: playerConfig;
|
public config: playerConfig;
|
||||||
|
|
||||||
private mouse: HTMLDivElement;
|
private mouse: HTMLDivElement;
|
||||||
|
|
||||||
private emitter: Emitter = mitt();
|
private emitter: Emitter = mitt();
|
||||||
|
|
||||||
private nextUserInteractionEvent: eventWithTime | null;
|
private nextUserInteractionEvent: eventWithTime | null;
|
||||||
private noramlSpeed: number = -1;
|
|
||||||
|
|
||||||
// tslint:disable-next-line: variable-name
|
// tslint:disable-next-line: variable-name
|
||||||
private legacy_missingNodeRetryMap: missingNodeMap = {};
|
private legacy_missingNodeRetryMap: missingNodeMap = {};
|
||||||
|
|
||||||
private service!: ReturnType<typeof createPlayerService>;
|
|
||||||
|
|
||||||
private treeIndex!: TreeIndex;
|
private treeIndex!: TreeIndex;
|
||||||
private fragmentParentMap!: Map<INode, INode>;
|
private fragmentParentMap!: Map<INode, INode>;
|
||||||
|
|
||||||
@@ -124,6 +123,7 @@ export class Replayer {
|
|||||||
this.fragmentParentMap.clear();
|
this.fragmentParentMap.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const timer = new Timer([], config?.speed || defaultConfig.speed);
|
||||||
this.service = createPlayerService(
|
this.service = createPlayerService(
|
||||||
{
|
{
|
||||||
events: events.map((e) => {
|
events: events.map((e) => {
|
||||||
@@ -132,8 +132,7 @@ export class Replayer {
|
|||||||
}
|
}
|
||||||
return e as eventWithTime;
|
return e as eventWithTime;
|
||||||
}),
|
}),
|
||||||
timer: new Timer(this.config),
|
timer,
|
||||||
speed: config?.speed || defaultConfig.speed,
|
|
||||||
timeOffset: 0,
|
timeOffset: 0,
|
||||||
baselineTime: 0,
|
baselineTime: 0,
|
||||||
lastPlayedEvent: null,
|
lastPlayedEvent: null,
|
||||||
@@ -145,25 +144,37 @@ export class Replayer {
|
|||||||
);
|
);
|
||||||
this.service.start();
|
this.service.start();
|
||||||
this.service.subscribe((state) => {
|
this.service.subscribe((state) => {
|
||||||
if (!state.changed) {
|
this.emitter.emit(ReplayerEvents.StateChange, {
|
||||||
return;
|
player: state,
|
||||||
}
|
});
|
||||||
// publish via emitter
|
});
|
||||||
|
this.speedService = createSpeedService({
|
||||||
|
normalSpeed: -1,
|
||||||
|
timer,
|
||||||
|
});
|
||||||
|
this.speedService.start();
|
||||||
|
this.speedService.subscribe((state) => {
|
||||||
|
this.emitter.emit(ReplayerEvents.StateChange, {
|
||||||
|
speed: state,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// rebuild first full snapshot as the poster of the player
|
// rebuild first full snapshot as the poster of the player
|
||||||
// maybe we can cache it for performance optimization
|
// maybe we can cache it for performance optimization
|
||||||
const { events: contextEvents } = this.service.state.context;
|
const firstMeta = this.service.state.context.events.find(
|
||||||
const firstMeta = contextEvents.find((e) => e.type === EventType.Meta);
|
(e) => e.type === EventType.Meta,
|
||||||
const firstFullsnapshot = contextEvents.find(
|
);
|
||||||
|
const firstFullsnapshot = this.service.state.context.events.find(
|
||||||
(e) => e.type === EventType.FullSnapshot,
|
(e) => e.type === EventType.FullSnapshot,
|
||||||
);
|
);
|
||||||
if (firstMeta) {
|
if (firstMeta) {
|
||||||
const { width, height } = firstMeta.data as metaEvent['data'];
|
const { width, height } = firstMeta.data as metaEvent['data'];
|
||||||
this.emitter.emit(ReplayerEvents.Resize, {
|
setTimeout(() => {
|
||||||
width,
|
this.emitter.emit(ReplayerEvents.Resize, {
|
||||||
height,
|
width,
|
||||||
});
|
height,
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
if (firstFullsnapshot) {
|
if (firstFullsnapshot) {
|
||||||
this.rebuildFullSnapshot(
|
this.rebuildFullSnapshot(
|
||||||
@@ -182,14 +193,15 @@ export class Replayer {
|
|||||||
this.config[key] = config[key];
|
this.config[key] = config[key];
|
||||||
});
|
});
|
||||||
if (!this.config.skipInactive) {
|
if (!this.config.skipInactive) {
|
||||||
this.noramlSpeed = -1;
|
this.backToNormal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMetaData(): playerMetaData {
|
public getMetaData(): playerMetaData {
|
||||||
const { events } = this.service.state.context;
|
const firstEvent = this.service.state.context.events[0];
|
||||||
const firstEvent = events[0];
|
const lastEvent = this.service.state.context.events[
|
||||||
const lastEvent = events[events.length - 1];
|
this.service.state.context.events.length - 1
|
||||||
|
];
|
||||||
return {
|
return {
|
||||||
startTime: firstEvent.timestamp,
|
startTime: firstEvent.timestamp,
|
||||||
endTime: lastEvent.timestamp,
|
endTime: lastEvent.timestamp,
|
||||||
@@ -216,25 +228,31 @@ export class Replayer {
|
|||||||
* @param timeOffset number
|
* @param timeOffset number
|
||||||
*/
|
*/
|
||||||
public play(timeOffset = 0) {
|
public play(timeOffset = 0) {
|
||||||
if (this.service.state.value === 'ended') {
|
if (this.service.state.matches('paused')) {
|
||||||
this.service.state.context.lastPlayedEvent = null;
|
this.service.send({ type: 'PLAY', payload: { timeOffset } });
|
||||||
this.service.send({ type: 'REPLAY' });
|
|
||||||
}
|
|
||||||
if (this.service.state.value === 'paused') {
|
|
||||||
this.service.send({ type: 'RESUME', payload: { timeOffset } });
|
|
||||||
} else {
|
} else {
|
||||||
|
this.service.send({ type: 'PAUSE' });
|
||||||
this.service.send({ type: 'PLAY', payload: { timeOffset } });
|
this.service.send({ type: 'PLAY', payload: { timeOffset } });
|
||||||
}
|
}
|
||||||
this.emitter.emit(ReplayerEvents.Start);
|
this.emitter.emit(ReplayerEvents.Start);
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause() {
|
public pause(timeOffset?: number) {
|
||||||
this.service.send({ type: 'PAUSE' });
|
if (timeOffset === undefined && this.service.state.matches('playing')) {
|
||||||
|
this.service.send({ type: 'PAUSE' });
|
||||||
|
}
|
||||||
|
if (typeof timeOffset === 'number') {
|
||||||
|
this.play(timeOffset);
|
||||||
|
this.service.send({ type: 'PAUSE' });
|
||||||
|
}
|
||||||
this.emitter.emit(ReplayerEvents.Pause);
|
this.emitter.emit(ReplayerEvents.Pause);
|
||||||
}
|
}
|
||||||
|
|
||||||
public resume(timeOffset = 0) {
|
public resume(timeOffset = 0) {
|
||||||
this.service.send({ type: 'RESUME', payload: { timeOffset } });
|
console.warn(
|
||||||
|
`The 'resume' will be departed in 1.0. Please use 'play' method which has the same interface.`,
|
||||||
|
);
|
||||||
|
this.play(timeOffset);
|
||||||
this.emitter.emit(ReplayerEvents.Resume);
|
this.emitter.emit(ReplayerEvents.Resume);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +300,6 @@ export class Replayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getCastFn(event: eventWithTime, isSync = false) {
|
private getCastFn(event: eventWithTime, isSync = false) {
|
||||||
const { events } = this.service.state.context;
|
|
||||||
let castFn: undefined | (() => void);
|
let castFn: undefined | (() => void);
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case EventType.DomContentLoaded:
|
case EventType.DomContentLoaded:
|
||||||
@@ -314,19 +331,24 @@ export class Replayer {
|
|||||||
case EventType.IncrementalSnapshot:
|
case EventType.IncrementalSnapshot:
|
||||||
castFn = () => {
|
castFn = () => {
|
||||||
this.applyIncremental(event, isSync);
|
this.applyIncremental(event, isSync);
|
||||||
|
if (isSync) {
|
||||||
|
// do not check skip in sync
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event === this.nextUserInteractionEvent) {
|
if (event === this.nextUserInteractionEvent) {
|
||||||
this.nextUserInteractionEvent = null;
|
this.nextUserInteractionEvent = null;
|
||||||
this.restoreSpeed();
|
this.backToNormal();
|
||||||
}
|
}
|
||||||
if (this.config.skipInactive && !this.nextUserInteractionEvent) {
|
if (this.config.skipInactive && !this.nextUserInteractionEvent) {
|
||||||
for (const _event of events) {
|
for (const _event of this.service.state.context.events) {
|
||||||
if (_event.timestamp! <= event.timestamp!) {
|
if (_event.timestamp! <= event.timestamp!) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (this.isUserInteraction(_event)) {
|
if (this.isUserInteraction(_event)) {
|
||||||
if (
|
if (
|
||||||
_event.delay! - event.delay! >
|
_event.delay! - event.delay! >
|
||||||
SKIP_TIME_THRESHOLD * this.config.speed
|
SKIP_TIME_THRESHOLD *
|
||||||
|
this.speedService.state.context.timer.speed
|
||||||
) {
|
) {
|
||||||
this.nextUserInteractionEvent = _event;
|
this.nextUserInteractionEvent = _event;
|
||||||
}
|
}
|
||||||
@@ -334,13 +356,12 @@ export class Replayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.nextUserInteractionEvent) {
|
if (this.nextUserInteractionEvent) {
|
||||||
this.noramlSpeed = this.config.speed;
|
|
||||||
const skipTime =
|
const skipTime =
|
||||||
this.nextUserInteractionEvent.delay! - event.delay!;
|
this.nextUserInteractionEvent.delay! - event.delay!;
|
||||||
const payload = {
|
const payload = {
|
||||||
speed: Math.min(Math.round(skipTime / SKIP_TIME_INTERVAL), 360),
|
speed: Math.min(Math.round(skipTime / SKIP_TIME_INTERVAL), 360),
|
||||||
};
|
};
|
||||||
this.setConfig(payload);
|
this.speedService.send({ type: 'FAST_FORWARD', payload });
|
||||||
this.emitter.emit(ReplayerEvents.SkipStart, payload);
|
this.emitter.emit(ReplayerEvents.SkipStart, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,8 +374,13 @@ export class Replayer {
|
|||||||
castFn();
|
castFn();
|
||||||
}
|
}
|
||||||
this.service.send({ type: 'CAST_EVENT', payload: { event } });
|
this.service.send({ type: 'CAST_EVENT', payload: { event } });
|
||||||
if (event === events[events.length - 1]) {
|
if (
|
||||||
this.restoreSpeed();
|
event ===
|
||||||
|
this.service.state.context.events[
|
||||||
|
this.service.state.context.events.length - 1
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
this.backToNormal();
|
||||||
this.service.send('END');
|
this.service.send('END');
|
||||||
this.emitter.emit(ReplayerEvents.Finish);
|
this.emitter.emit(ReplayerEvents.Finish);
|
||||||
}
|
}
|
||||||
@@ -438,7 +464,6 @@ export class Replayer {
|
|||||||
e: incrementalSnapshotEvent & { timestamp: number },
|
e: incrementalSnapshotEvent & { timestamp: number },
|
||||||
isSync: boolean,
|
isSync: boolean,
|
||||||
) {
|
) {
|
||||||
const { baselineTime } = this.service.state.context;
|
|
||||||
const { data: d } = e;
|
const { data: d } = e;
|
||||||
switch (d.source) {
|
switch (d.source) {
|
||||||
case IncrementalSource.Mutation: {
|
case IncrementalSource.Mutation: {
|
||||||
@@ -461,7 +486,10 @@ export class Replayer {
|
|||||||
doAction: () => {
|
doAction: () => {
|
||||||
this.moveAndHover(d, p.x, p.y, p.id);
|
this.moveAndHover(d, p.x, p.y, p.id);
|
||||||
},
|
},
|
||||||
delay: p.timeOffset + e.timestamp - baselineTime,
|
delay:
|
||||||
|
p.timeOffset +
|
||||||
|
e.timestamp -
|
||||||
|
this.service.state.context.baselineTime,
|
||||||
};
|
};
|
||||||
this.timer.addAction(action);
|
this.timer.addAction(action);
|
||||||
});
|
});
|
||||||
@@ -863,14 +891,15 @@ export class Replayer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private restoreSpeed() {
|
private backToNormal() {
|
||||||
if (this.noramlSpeed === -1) {
|
this.nextUserInteractionEvent = null;
|
||||||
|
if (this.speedService.state.matches('normal')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = { speed: this.noramlSpeed };
|
this.speedService.send({ type: 'BACK_TO_NORMAL' });
|
||||||
this.setConfig(payload);
|
this.emitter.emit(ReplayerEvents.SkipEnd, {
|
||||||
this.emitter.emit(ReplayerEvents.SkipEnd, payload);
|
speed: this.speedService.state.context.normalSpeed,
|
||||||
this.noramlSpeed = -1;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private warnNodeNotFound(d: incrementalData, id: number) {
|
private warnNodeNotFound(d: incrementalData, id: number) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createMachine, interpret, assign } from '@xstate/fsm';
|
import { createMachine, interpret, assign, StateMachine } from '@xstate/fsm';
|
||||||
import {
|
import {
|
||||||
playerConfig,
|
playerConfig,
|
||||||
eventWithTime,
|
eventWithTime,
|
||||||
@@ -7,13 +7,12 @@ import {
|
|||||||
EventType,
|
EventType,
|
||||||
Emitter,
|
Emitter,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { Timer, getDelay } from './timer';
|
import { Timer, addDelay } from './timer';
|
||||||
import { needCastInSyncMode } from '../utils';
|
import { needCastInSyncMode } from '../utils';
|
||||||
|
|
||||||
export type PlayerContext = {
|
export type PlayerContext = {
|
||||||
events: eventWithTime[];
|
events: eventWithTime[];
|
||||||
timer: Timer;
|
timer: Timer;
|
||||||
speed: playerConfig['speed'];
|
|
||||||
timeOffset: number;
|
timeOffset: number;
|
||||||
baselineTime: number;
|
baselineTime: number;
|
||||||
lastPlayedEvent: eventWithTime | null;
|
lastPlayedEvent: eventWithTime | null;
|
||||||
@@ -32,28 +31,17 @@ export type PlayerEvent =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
| { type: 'PAUSE' }
|
| { type: 'PAUSE' }
|
||||||
| {
|
|
||||||
type: 'RESUME';
|
|
||||||
payload: {
|
|
||||||
timeOffset: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| { type: 'END' }
|
|
||||||
| { type: 'REPLAY' }
|
|
||||||
| { type: 'FAST_FORWARD' }
|
|
||||||
| { type: 'BACK_TO_NORMAL' }
|
|
||||||
| { type: 'TO_LIVE'; payload: { baselineTime?: number } }
|
| { type: 'TO_LIVE'; payload: { baselineTime?: number } }
|
||||||
| {
|
| {
|
||||||
type: 'ADD_EVENT';
|
type: 'ADD_EVENT';
|
||||||
payload: {
|
payload: {
|
||||||
event: eventWithTime;
|
event: eventWithTime;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'END';
|
||||||
};
|
};
|
||||||
export type PlayerState =
|
export type PlayerState =
|
||||||
| {
|
|
||||||
value: 'inited';
|
|
||||||
context: PlayerContext;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
value: 'playing';
|
value: 'playing';
|
||||||
context: PlayerContext;
|
context: PlayerContext;
|
||||||
@@ -62,14 +50,6 @@ export type PlayerState =
|
|||||||
value: 'paused';
|
value: 'paused';
|
||||||
context: PlayerContext;
|
context: PlayerContext;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
value: 'ended';
|
|
||||||
context: PlayerContext;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
value: 'skipping';
|
|
||||||
context: PlayerContext;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
value: 'live';
|
value: 'live';
|
||||||
context: PlayerContext;
|
context: PlayerContext;
|
||||||
@@ -106,37 +86,27 @@ export function createPlayerService(
|
|||||||
{
|
{
|
||||||
id: 'player',
|
id: 'player',
|
||||||
context,
|
context,
|
||||||
initial: 'inited',
|
initial: 'paused',
|
||||||
states: {
|
states: {
|
||||||
inited: {
|
|
||||||
on: {
|
|
||||||
PLAY: {
|
|
||||||
target: 'playing',
|
|
||||||
actions: ['recordTimeOffset', 'play'],
|
|
||||||
},
|
|
||||||
TO_LIVE: {
|
|
||||||
target: 'live',
|
|
||||||
actions: ['startLive'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
playing: {
|
playing: {
|
||||||
on: {
|
on: {
|
||||||
PAUSE: {
|
PAUSE: {
|
||||||
target: 'paused',
|
target: 'paused',
|
||||||
actions: ['pause'],
|
actions: ['pause'],
|
||||||
},
|
},
|
||||||
END: 'ended',
|
|
||||||
FAST_FORWARD: 'skipping',
|
|
||||||
CAST_EVENT: {
|
CAST_EVENT: {
|
||||||
target: 'playing',
|
target: 'playing',
|
||||||
actions: 'castEvent',
|
actions: 'castEvent',
|
||||||
},
|
},
|
||||||
|
END: {
|
||||||
|
target: 'paused',
|
||||||
|
actions: ['resetLastPlayedEvent', 'pause'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
paused: {
|
paused: {
|
||||||
on: {
|
on: {
|
||||||
RESUME: {
|
PLAY: {
|
||||||
target: 'playing',
|
target: 'playing',
|
||||||
actions: ['recordTimeOffset', 'play'],
|
actions: ['recordTimeOffset', 'play'],
|
||||||
},
|
},
|
||||||
@@ -146,16 +116,6 @@ export function createPlayerService(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
skipping: {
|
|
||||||
on: {
|
|
||||||
BACK_TO_NORMAL: 'playing',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ended: {
|
|
||||||
on: {
|
|
||||||
REPLAY: 'playing',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
live: {
|
live: {
|
||||||
on: {
|
on: {
|
||||||
ADD_EVENT: {
|
ADD_EVENT: {
|
||||||
@@ -173,7 +133,7 @@ export function createPlayerService(
|
|||||||
if (event.type === 'CAST_EVENT') {
|
if (event.type === 'CAST_EVENT') {
|
||||||
return event.payload.event;
|
return event.payload.event;
|
||||||
}
|
}
|
||||||
return context.lastPlayedEvent;
|
return ctx.lastPlayedEvent;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
recordTimeOffset: assign((ctx, event) => {
|
recordTimeOffset: assign((ctx, event) => {
|
||||||
@@ -190,13 +150,17 @@ export function createPlayerService(
|
|||||||
play(ctx) {
|
play(ctx) {
|
||||||
const { timer, events, baselineTime, lastPlayedEvent } = ctx;
|
const { timer, events, baselineTime, lastPlayedEvent } = ctx;
|
||||||
timer.clear();
|
timer.clear();
|
||||||
|
for (const event of events) {
|
||||||
|
// TODO: improve this API
|
||||||
|
addDelay(event, baselineTime);
|
||||||
|
}
|
||||||
const neededEvents = discardPriorSnapshots(events, baselineTime);
|
const neededEvents = discardPriorSnapshots(events, baselineTime);
|
||||||
|
|
||||||
const actions = new Array<actionWithDelay>();
|
const actions = new Array<actionWithDelay>();
|
||||||
for (const event of neededEvents) {
|
for (const event of neededEvents) {
|
||||||
if (
|
if (
|
||||||
lastPlayedEvent &&
|
lastPlayedEvent &&
|
||||||
lastPlayedEvent.timestamp > baselineTime &&
|
lastPlayedEvent.timestamp < baselineTime &&
|
||||||
(event.timestamp <= lastPlayedEvent.timestamp ||
|
(event.timestamp <= lastPlayedEvent.timestamp ||
|
||||||
event === lastPlayedEvent)
|
event === lastPlayedEvent)
|
||||||
) {
|
) {
|
||||||
@@ -215,7 +179,7 @@ export function createPlayerService(
|
|||||||
castFn();
|
castFn();
|
||||||
emitter.emit(ReplayerEvents.EventCast, event);
|
emitter.emit(ReplayerEvents.EventCast, event);
|
||||||
},
|
},
|
||||||
delay: getDelay(event, baselineTime),
|
delay: event.delay!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,8 +190,15 @@ export function createPlayerService(
|
|||||||
pause(ctx) {
|
pause(ctx) {
|
||||||
ctx.timer.clear();
|
ctx.timer.clear();
|
||||||
},
|
},
|
||||||
|
resetLastPlayedEvent: assign((ctx) => {
|
||||||
|
return {
|
||||||
|
...ctx,
|
||||||
|
lastPlayedEvent: null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
startLive: assign({
|
startLive: assign({
|
||||||
baselineTime: (ctx, event) => {
|
baselineTime: (ctx, event) => {
|
||||||
|
ctx.timer.toggleLiveMode(true);
|
||||||
ctx.timer.start();
|
ctx.timer.start();
|
||||||
if (event.type === 'TO_LIVE' && event.payload.baselineTime) {
|
if (event.type === 'TO_LIVE' && event.payload.baselineTime) {
|
||||||
return event.payload.baselineTime;
|
return event.payload.baselineTime;
|
||||||
@@ -239,6 +210,7 @@ export function createPlayerService(
|
|||||||
const { baselineTime, timer, events } = ctx;
|
const { baselineTime, timer, events } = ctx;
|
||||||
if (machineEvent.type === 'ADD_EVENT') {
|
if (machineEvent.type === 'ADD_EVENT') {
|
||||||
const { event } = machineEvent.payload;
|
const { event } = machineEvent.payload;
|
||||||
|
addDelay(event, baselineTime);
|
||||||
events.push(event);
|
events.push(event);
|
||||||
const isSync = event.timestamp < baselineTime;
|
const isSync = event.timestamp < baselineTime;
|
||||||
const castFn = getCastFn(event, isSync);
|
const castFn = getCastFn(event, isSync);
|
||||||
@@ -250,7 +222,7 @@ export function createPlayerService(
|
|||||||
castFn();
|
castFn();
|
||||||
emitter.emit(ReplayerEvents.EventCast, event);
|
emitter.emit(ReplayerEvents.EventCast, event);
|
||||||
},
|
},
|
||||||
delay: getDelay(event, baselineTime),
|
delay: event.delay!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,3 +233,96 @@ export function createPlayerService(
|
|||||||
);
|
);
|
||||||
return interpret(playerMachine);
|
return interpret(playerMachine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SpeedContext = {
|
||||||
|
normalSpeed: playerConfig['speed'];
|
||||||
|
timer: Timer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpeedEvent =
|
||||||
|
| {
|
||||||
|
type: 'FAST_FORWARD';
|
||||||
|
payload: { speed: playerConfig['speed'] };
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'BACK_TO_NORMAL';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_SPEED';
|
||||||
|
payload: { speed: playerConfig['speed'] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpeedState =
|
||||||
|
| {
|
||||||
|
value: 'normal';
|
||||||
|
context: SpeedContext;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
value: 'skipping';
|
||||||
|
context: SpeedContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createSpeedService(context: SpeedContext) {
|
||||||
|
const speedMachine = createMachine<SpeedContext, SpeedEvent, SpeedState>(
|
||||||
|
{
|
||||||
|
id: 'speed',
|
||||||
|
context,
|
||||||
|
initial: 'normal',
|
||||||
|
states: {
|
||||||
|
normal: {
|
||||||
|
on: {
|
||||||
|
FAST_FORWARD: {
|
||||||
|
target: 'skipping',
|
||||||
|
actions: ['recordSpeed', 'setSpeed'],
|
||||||
|
},
|
||||||
|
SET_SPEED: {
|
||||||
|
target: 'normal',
|
||||||
|
actions: ['setSpeed'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
skipping: {
|
||||||
|
on: {
|
||||||
|
BACK_TO_NORMAL: {
|
||||||
|
target: 'normal',
|
||||||
|
actions: ['restoreSpeed'],
|
||||||
|
},
|
||||||
|
SET_SPEED: {
|
||||||
|
target: 'normal',
|
||||||
|
actions: ['setSpeed'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: {
|
||||||
|
setSpeed: (ctx, event) => {
|
||||||
|
if ('payload' in event) {
|
||||||
|
ctx.timer.setSpeed(event.payload.speed);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recordSpeed: assign({
|
||||||
|
normalSpeed: (ctx) => ctx.timer.speed,
|
||||||
|
}),
|
||||||
|
restoreSpeed: (ctx) => {
|
||||||
|
ctx.timer.setSpeed(ctx.normalSpeed);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return interpret(speedMachine);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlayerMachineState = StateMachine.State<
|
||||||
|
PlayerContext,
|
||||||
|
PlayerEvent,
|
||||||
|
PlayerState
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type SpeedMachineState = StateMachine.State<
|
||||||
|
SpeedContext,
|
||||||
|
SpeedEvent,
|
||||||
|
SpeedState
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
playerConfig,
|
|
||||||
actionWithDelay,
|
actionWithDelay,
|
||||||
eventWithTime,
|
eventWithTime,
|
||||||
EventType,
|
EventType,
|
||||||
@@ -8,14 +7,15 @@ import {
|
|||||||
|
|
||||||
export class Timer {
|
export class Timer {
|
||||||
public timeOffset: number = 0;
|
public timeOffset: number = 0;
|
||||||
|
public speed: number;
|
||||||
|
|
||||||
private actions: actionWithDelay[];
|
private actions: actionWithDelay[];
|
||||||
private config: playerConfig;
|
|
||||||
private raf: number;
|
private raf: number;
|
||||||
|
private liveMode: boolean;
|
||||||
|
|
||||||
constructor(config: playerConfig, actions: actionWithDelay[] = []) {
|
constructor(actions: actionWithDelay[] = [], speed: number) {
|
||||||
this.actions = actions;
|
this.actions = actions;
|
||||||
this.config = config;
|
this.speed = speed;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Add an action after the timer starts.
|
* Add an action after the timer starts.
|
||||||
@@ -37,10 +37,10 @@ export class Timer {
|
|||||||
this.actions.sort((a1, a2) => a1.delay - a2.delay);
|
this.actions.sort((a1, a2) => a1.delay - a2.delay);
|
||||||
this.timeOffset = 0;
|
this.timeOffset = 0;
|
||||||
let lastTimestamp = performance.now();
|
let lastTimestamp = performance.now();
|
||||||
const { actions, config } = this;
|
const { actions } = this;
|
||||||
const self = this;
|
const self = this;
|
||||||
function check(time: number) {
|
function check(time: number) {
|
||||||
self.timeOffset += (time - lastTimestamp) * config.speed;
|
self.timeOffset += (time - lastTimestamp) * self.speed;
|
||||||
lastTimestamp = time;
|
lastTimestamp = time;
|
||||||
while (actions.length) {
|
while (actions.length) {
|
||||||
const action = actions[0];
|
const action = actions[0];
|
||||||
@@ -51,7 +51,7 @@ export class Timer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (actions.length > 0 || self.config.liveMode) {
|
if (actions.length > 0 || self.liveMode) {
|
||||||
self.raf = requestAnimationFrame(check);
|
self.raf = requestAnimationFrame(check);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,14 @@ export class Timer {
|
|||||||
this.actions.length = 0;
|
this.actions.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setSpeed(speed: number) {
|
||||||
|
this.speed = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleLiveMode(mode: boolean) {
|
||||||
|
this.liveMode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
private findActionIndex(action: actionWithDelay): number {
|
private findActionIndex(action: actionWithDelay): number {
|
||||||
let start = 0;
|
let start = 0;
|
||||||
let end = this.actions.length - 1;
|
let end = this.actions.length - 1;
|
||||||
@@ -83,7 +91,7 @@ export class Timer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add speed to mouse move timestamp calculation
|
// TODO: add speed to mouse move timestamp calculation
|
||||||
export function getDelay(event: eventWithTime, baselineTime: number): number {
|
export function addDelay(event: eventWithTime, baselineTime: number): number {
|
||||||
// Mouse move events was recorded in a throttle function,
|
// Mouse move events was recorded in a throttle function,
|
||||||
// so we need to find the real timestamp by traverse the time offsets.
|
// so we need to find the real timestamp by traverse the time offsets.
|
||||||
if (
|
if (
|
||||||
@@ -97,5 +105,5 @@ export function getDelay(event: eventWithTime, baselineTime: number): number {
|
|||||||
return firstTimestamp - baselineTime;
|
return firstTimestamp - baselineTime;
|
||||||
}
|
}
|
||||||
event.delay = event.timestamp - baselineTime;
|
event.delay = event.timestamp - baselineTime;
|
||||||
return event.timestamp - baselineTime;
|
return event.delay;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -410,4 +410,5 @@ export enum ReplayerEvents {
|
|||||||
EventCast = 'event-cast',
|
EventCast = 'event-cast',
|
||||||
CustomEvent = 'custom-event',
|
CustomEvent = 'custom-event',
|
||||||
Flush = 'flush',
|
Flush = 'flush',
|
||||||
|
StateChange = 'state-change',
|
||||||
}
|
}
|
||||||
|
|||||||
11
typings/replay/index.d.ts
vendored
11
typings/replay/index.d.ts
vendored
@@ -1,17 +1,18 @@
|
|||||||
import { Timer } from './timer';
|
import { Timer } from './timer';
|
||||||
|
import { createPlayerService, createSpeedService } from './machine';
|
||||||
import { eventWithTime, playerConfig, playerMetaData, Handler } from '../types';
|
import { eventWithTime, playerConfig, playerMetaData, Handler } from '../types';
|
||||||
import './styles/style.css';
|
import './styles/style.css';
|
||||||
export declare class Replayer {
|
export declare class Replayer {
|
||||||
wrapper: HTMLDivElement;
|
wrapper: HTMLDivElement;
|
||||||
iframe: HTMLIFrameElement;
|
iframe: HTMLIFrameElement;
|
||||||
|
service: ReturnType<typeof createPlayerService>;
|
||||||
|
speedService: ReturnType<typeof createSpeedService>;
|
||||||
get timer(): Timer;
|
get timer(): Timer;
|
||||||
private config;
|
config: playerConfig;
|
||||||
private mouse;
|
private mouse;
|
||||||
private emitter;
|
private emitter;
|
||||||
private nextUserInteractionEvent;
|
private nextUserInteractionEvent;
|
||||||
private noramlSpeed;
|
|
||||||
private legacy_missingNodeRetryMap;
|
private legacy_missingNodeRetryMap;
|
||||||
private service;
|
|
||||||
private treeIndex;
|
private treeIndex;
|
||||||
private fragmentParentMap;
|
private fragmentParentMap;
|
||||||
constructor(events: Array<eventWithTime | string>, config?: Partial<playerConfig>);
|
constructor(events: Array<eventWithTime | string>, config?: Partial<playerConfig>);
|
||||||
@@ -21,7 +22,7 @@ export declare class Replayer {
|
|||||||
getCurrentTime(): number;
|
getCurrentTime(): number;
|
||||||
getTimeOffset(): number;
|
getTimeOffset(): number;
|
||||||
play(timeOffset?: number): void;
|
play(timeOffset?: number): void;
|
||||||
pause(): void;
|
pause(timeOffset?: number): void;
|
||||||
resume(timeOffset?: number): void;
|
resume(timeOffset?: number): void;
|
||||||
startLive(baselineTime?: number): void;
|
startLive(baselineTime?: number): void;
|
||||||
addEvent(rawEvent: eventWithTime | string): void;
|
addEvent(rawEvent: eventWithTime | string): void;
|
||||||
@@ -40,7 +41,7 @@ export declare class Replayer {
|
|||||||
private moveAndHover;
|
private moveAndHover;
|
||||||
private hoverElements;
|
private hoverElements;
|
||||||
private isUserInteraction;
|
private isUserInteraction;
|
||||||
private restoreSpeed;
|
private backToNormal;
|
||||||
private warnNodeNotFound;
|
private warnNodeNotFound;
|
||||||
private debugNodeNotFound;
|
private debugNodeNotFound;
|
||||||
}
|
}
|
||||||
|
|||||||
55
typings/replay/machine.d.ts
vendored
55
typings/replay/machine.d.ts
vendored
@@ -1,9 +1,9 @@
|
|||||||
|
import { StateMachine } from '@xstate/fsm';
|
||||||
import { playerConfig, eventWithTime, Emitter } from '../types';
|
import { playerConfig, eventWithTime, Emitter } from '../types';
|
||||||
import { Timer } from './timer';
|
import { Timer } from './timer';
|
||||||
export declare type PlayerContext = {
|
export declare type PlayerContext = {
|
||||||
events: eventWithTime[];
|
events: eventWithTime[];
|
||||||
timer: Timer;
|
timer: Timer;
|
||||||
speed: playerConfig['speed'];
|
|
||||||
timeOffset: number;
|
timeOffset: number;
|
||||||
baselineTime: number;
|
baselineTime: number;
|
||||||
lastPlayedEvent: eventWithTime | null;
|
lastPlayedEvent: eventWithTime | null;
|
||||||
@@ -20,19 +20,6 @@ export declare type PlayerEvent = {
|
|||||||
};
|
};
|
||||||
} | {
|
} | {
|
||||||
type: 'PAUSE';
|
type: 'PAUSE';
|
||||||
} | {
|
|
||||||
type: 'RESUME';
|
|
||||||
payload: {
|
|
||||||
timeOffset: number;
|
|
||||||
};
|
|
||||||
} | {
|
|
||||||
type: 'END';
|
|
||||||
} | {
|
|
||||||
type: 'REPLAY';
|
|
||||||
} | {
|
|
||||||
type: 'FAST_FORWARD';
|
|
||||||
} | {
|
|
||||||
type: 'BACK_TO_NORMAL';
|
|
||||||
} | {
|
} | {
|
||||||
type: 'TO_LIVE';
|
type: 'TO_LIVE';
|
||||||
payload: {
|
payload: {
|
||||||
@@ -43,22 +30,15 @@ export declare type PlayerEvent = {
|
|||||||
payload: {
|
payload: {
|
||||||
event: eventWithTime;
|
event: eventWithTime;
|
||||||
};
|
};
|
||||||
|
} | {
|
||||||
|
type: 'END';
|
||||||
};
|
};
|
||||||
export declare type PlayerState = {
|
export declare type PlayerState = {
|
||||||
value: 'inited';
|
|
||||||
context: PlayerContext;
|
|
||||||
} | {
|
|
||||||
value: 'playing';
|
value: 'playing';
|
||||||
context: PlayerContext;
|
context: PlayerContext;
|
||||||
} | {
|
} | {
|
||||||
value: 'paused';
|
value: 'paused';
|
||||||
context: PlayerContext;
|
context: PlayerContext;
|
||||||
} | {
|
|
||||||
value: 'ended';
|
|
||||||
context: PlayerContext;
|
|
||||||
} | {
|
|
||||||
value: 'skipping';
|
|
||||||
context: PlayerContext;
|
|
||||||
} | {
|
} | {
|
||||||
value: 'live';
|
value: 'live';
|
||||||
context: PlayerContext;
|
context: PlayerContext;
|
||||||
@@ -68,5 +48,32 @@ declare type PlayerAssets = {
|
|||||||
emitter: Emitter;
|
emitter: Emitter;
|
||||||
getCastFn(event: eventWithTime, isSync: boolean): () => void;
|
getCastFn(event: eventWithTime, isSync: boolean): () => void;
|
||||||
};
|
};
|
||||||
export declare function createPlayerService(context: PlayerContext, { getCastFn, emitter }: PlayerAssets): import("@xstate/fsm").StateMachine.Service<PlayerContext, PlayerEvent, PlayerState>;
|
export declare function createPlayerService(context: PlayerContext, { getCastFn, emitter }: PlayerAssets): StateMachine.Service<PlayerContext, PlayerEvent, PlayerState>;
|
||||||
|
export declare type SpeedContext = {
|
||||||
|
normalSpeed: playerConfig['speed'];
|
||||||
|
timer: Timer;
|
||||||
|
};
|
||||||
|
export declare type SpeedEvent = {
|
||||||
|
type: 'FAST_FORWARD';
|
||||||
|
payload: {
|
||||||
|
speed: playerConfig['speed'];
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'BACK_TO_NORMAL';
|
||||||
|
} | {
|
||||||
|
type: 'SET_SPEED';
|
||||||
|
payload: {
|
||||||
|
speed: playerConfig['speed'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export declare type SpeedState = {
|
||||||
|
value: 'normal';
|
||||||
|
context: SpeedContext;
|
||||||
|
} | {
|
||||||
|
value: 'skipping';
|
||||||
|
context: SpeedContext;
|
||||||
|
};
|
||||||
|
export declare function createSpeedService(context: SpeedContext): StateMachine.Service<SpeedContext, SpeedEvent, SpeedState>;
|
||||||
|
export declare type PlayerMachineState = StateMachine.State<PlayerContext, PlayerEvent, PlayerState>;
|
||||||
|
export declare type SpeedMachineState = StateMachine.State<SpeedContext, SpeedEvent, SpeedState>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
9
typings/replay/timer.d.ts
vendored
9
typings/replay/timer.d.ts
vendored
@@ -1,14 +1,17 @@
|
|||||||
import { playerConfig, actionWithDelay, eventWithTime } from '../types';
|
import { actionWithDelay, eventWithTime } from '../types';
|
||||||
export declare class Timer {
|
export declare class Timer {
|
||||||
timeOffset: number;
|
timeOffset: number;
|
||||||
|
speed: number;
|
||||||
private actions;
|
private actions;
|
||||||
private config;
|
|
||||||
private raf;
|
private raf;
|
||||||
constructor(config: playerConfig, actions?: actionWithDelay[]);
|
private liveMode;
|
||||||
|
constructor(actions: actionWithDelay[] | undefined, speed: number);
|
||||||
addAction(action: actionWithDelay): void;
|
addAction(action: actionWithDelay): void;
|
||||||
addActions(actions: actionWithDelay[]): void;
|
addActions(actions: actionWithDelay[]): void;
|
||||||
start(): void;
|
start(): void;
|
||||||
clear(): void;
|
clear(): void;
|
||||||
|
setSpeed(speed: number): void;
|
||||||
|
toggleLiveMode(mode: boolean): void;
|
||||||
private findActionIndex;
|
private findActionIndex;
|
||||||
}
|
}
|
||||||
export declare function getDelay(event: eventWithTime, baselineTime: number): number;
|
export declare function getDelay(event: eventWithTime, baselineTime: number): number;
|
||||||
|
|||||||
3
typings/types.d.ts
vendored
3
typings/types.d.ts
vendored
@@ -308,6 +308,7 @@ export declare enum ReplayerEvents {
|
|||||||
MouseInteraction = "mouse-interaction",
|
MouseInteraction = "mouse-interaction",
|
||||||
EventCast = "event-cast",
|
EventCast = "event-cast",
|
||||||
CustomEvent = "custom-event",
|
CustomEvent = "custom-event",
|
||||||
Flush = "flush"
|
Flush = "flush",
|
||||||
|
StateChange = "state-change"
|
||||||
}
|
}
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Reference in New Issue
Block a user