close #274 implement the new state management proposal

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 9362baac3e
commit a31efdc169
8 changed files with 259 additions and 144 deletions

View File

@@ -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) {

View File

@@ -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
>;

View File

@@ -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;
} }

View File

@@ -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',
} }

View File

@@ -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;
} }

View File

@@ -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 {};

View File

@@ -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
View File

@@ -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 {};