close #274 implement the new state management proposal

This commit is contained in:
Yanzhen Yu
2020-08-08 17:07:37 +08:00
parent 6e53410e2b
commit a90999d96e
8 changed files with 259 additions and 144 deletions

View File

@@ -1,4 +1,4 @@
import { createMachine, interpret, assign } from '@xstate/fsm';
import { createMachine, interpret, assign, StateMachine } from '@xstate/fsm';
import {
playerConfig,
eventWithTime,
@@ -7,13 +7,12 @@ import {
EventType,
Emitter,
} from '../types';
import { Timer, getDelay } from './timer';
import { Timer, addDelay } from './timer';
import { needCastInSyncMode } from '../utils';
export type PlayerContext = {
events: eventWithTime[];
timer: Timer;
speed: playerConfig['speed'];
timeOffset: number;
baselineTime: number;
lastPlayedEvent: eventWithTime | null;
@@ -32,28 +31,17 @@ export type PlayerEvent =
};
}
| { 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: 'ADD_EVENT';
payload: {
event: eventWithTime;
};
}
| {
type: 'END';
};
export type PlayerState =
| {
value: 'inited';
context: PlayerContext;
}
| {
value: 'playing';
context: PlayerContext;
@@ -62,14 +50,6 @@ export type PlayerState =
value: 'paused';
context: PlayerContext;
}
| {
value: 'ended';
context: PlayerContext;
}
| {
value: 'skipping';
context: PlayerContext;
}
| {
value: 'live';
context: PlayerContext;
@@ -106,37 +86,27 @@ export function createPlayerService(
{
id: 'player',
context,
initial: 'inited',
initial: 'paused',
states: {
inited: {
on: {
PLAY: {
target: 'playing',
actions: ['recordTimeOffset', 'play'],
},
TO_LIVE: {
target: 'live',
actions: ['startLive'],
},
},
},
playing: {
on: {
PAUSE: {
target: 'paused',
actions: ['pause'],
},
END: 'ended',
FAST_FORWARD: 'skipping',
CAST_EVENT: {
target: 'playing',
actions: 'castEvent',
},
END: {
target: 'paused',
actions: ['resetLastPlayedEvent', 'pause'],
},
},
},
paused: {
on: {
RESUME: {
PLAY: {
target: 'playing',
actions: ['recordTimeOffset', 'play'],
},
@@ -146,16 +116,6 @@ export function createPlayerService(
},
},
},
skipping: {
on: {
BACK_TO_NORMAL: 'playing',
},
},
ended: {
on: {
REPLAY: 'playing',
},
},
live: {
on: {
ADD_EVENT: {
@@ -173,7 +133,7 @@ export function createPlayerService(
if (event.type === 'CAST_EVENT') {
return event.payload.event;
}
return context.lastPlayedEvent;
return ctx.lastPlayedEvent;
},
}),
recordTimeOffset: assign((ctx, event) => {
@@ -190,13 +150,17 @@ export function createPlayerService(
play(ctx) {
const { timer, events, baselineTime, lastPlayedEvent } = ctx;
timer.clear();
for (const event of events) {
// TODO: improve this API
addDelay(event, baselineTime);
}
const neededEvents = discardPriorSnapshots(events, baselineTime);
const actions = new Array<actionWithDelay>();
for (const event of neededEvents) {
if (
lastPlayedEvent &&
lastPlayedEvent.timestamp > baselineTime &&
lastPlayedEvent.timestamp < baselineTime &&
(event.timestamp <= lastPlayedEvent.timestamp ||
event === lastPlayedEvent)
) {
@@ -215,7 +179,7 @@ export function createPlayerService(
castFn();
emitter.emit(ReplayerEvents.EventCast, event);
},
delay: getDelay(event, baselineTime),
delay: event.delay!,
});
}
}
@@ -226,8 +190,15 @@ export function createPlayerService(
pause(ctx) {
ctx.timer.clear();
},
resetLastPlayedEvent: assign((ctx) => {
return {
...ctx,
lastPlayedEvent: null,
};
}),
startLive: assign({
baselineTime: (ctx, event) => {
ctx.timer.toggleLiveMode(true);
ctx.timer.start();
if (event.type === 'TO_LIVE' && event.payload.baselineTime) {
return event.payload.baselineTime;
@@ -239,6 +210,7 @@ export function createPlayerService(
const { baselineTime, timer, events } = ctx;
if (machineEvent.type === 'ADD_EVENT') {
const { event } = machineEvent.payload;
addDelay(event, baselineTime);
events.push(event);
const isSync = event.timestamp < baselineTime;
const castFn = getCastFn(event, isSync);
@@ -250,7 +222,7 @@ export function createPlayerService(
castFn();
emitter.emit(ReplayerEvents.EventCast, event);
},
delay: getDelay(event, baselineTime),
delay: event.delay!,
});
}
}
@@ -261,3 +233,96 @@ export function createPlayerService(
);
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
>;