import { createMachine , interpret , assign , StateMachine } from '@xstate/fsm' ; import { playerConfig , eventWithTime , actionWithDelay , ReplayerEvents , EventType , Emitter , IncrementalSource , } from '../types' ; import { Timer , addDelay } from './timer' ; export type PlayerContext = { events : eventWithTime [ ] ; timer : Timer ; timeOffset : number ; baselineTime : number ; lastPlayedEvent : eventWithTime | null ; } ; export type PlayerEvent = { type : 'PLAY' ; payload : { timeOffset : number ; } ; } | { type : 'CAST_EVENT' ; payload : { event : eventWithTime ; } ; } | { type : 'PAUSE' ; } | { type : 'TO_LIVE' ; payload : { baselineTime ? : number ; } ; } | { type : 'ADD_EVENT' ; payload : { event : eventWithTime ; } ; } | { type : 'END' ; } ; export type PlayerState = { value : 'playing' ; context : PlayerContext ; } | { value : 'paused' ; context : PlayerContext ; } | { value : 'live' ; context : PlayerContext ; } ; /**
 * If the array have multiple meta and fullsnapshot events,
 * return the events from last meta to the end.
 */ export function discardPriorSnapshots ( events , baselineTime ) { for ( let idx = events . length - 1 ; idx >= 0 ; idx -- ) { const event = events [ idx ] ; if ( event . type === EventType . Meta ) { if ( event . timestamp <= baselineTime ) { return events . slice ( idx ) ; } } } return events ; } type PlayerAssets = { emitter : Emitter ; applyEventsSynchronously ( events ) : void ; getCastFn ( event , isSync ) : ( ) => void ; } ; export function createPlayerService ( context , { getCastFn , applyEventsSynchronously , emitter } ) { const playerMachine = createMachine < PlayerContext , PlayerEvent , PlayerState > ( { id : 'player' , context , initial : 'paused' , states : { playing : { on : { PAUSE : { target : 'paused' , actions : [ 'pause' ] , } , CAST_EVENT : { target : 'playing' , actions : 'castEvent' , } , END : { target : 'paused' , actions : [ 'resetLastPlayedEvent' , 'pause' ] , } , ADD_EVENT : { target : 'playing' , actions : [ 'addEvent' ] , } , } , } , paused : { on : { PLAY : { target : 'playing' , actions : [ 'recordTimeOffset' , 'play' ] , } , CAST_EVENT : { target : 'paused' , actions : 'castEvent' , } , TO_LIVE : { target : 'live' , actions : [ 'startLive' ] , } , ADD_EVENT : { target : 'paused' , actions : [ 'addEvent' ] , } , } , } , live : { on : { ADD_EVENT : { target : 'live' , actions : [ 'addEvent' ] , } , CAST_EVENT : { target : 'live' , actions : [ 'castEvent' ] , } , } , } , } , } , { actions : { castEvent : assign ( { lastPlayedEvent : ( ctx , event ) => { if ( event . type === 'CAST_EVENT' ) { return event . payload . event ; } return ctx . lastPlayedEvent ; } , } ) , recordTimeOffset : assign ( ( ctx , event ) => { let timeOffset = ctx . timeOffset ; if ( 'payload' in event && 'timeOffset' in event . payload ) { timeOffset = event . payload . timeOffset ; } return { ... ctx , timeOffset , baselineTime : ctx . events [ 0 ] . timestamp + timeOffset , } ; } ) , play ( ctx ) { const { timer , events , baselineTime , lastPlayedEvent } = ctx ; timer . clear ( ) ; for ( const event of events ) { addDelay ( event , baselineTime ) ; } const neededEvents = discardPriorSnapshots ( events , baselineTime ) ; let lastPlayedTimestamp = lastPlayedEvent ?. timestamp ; if ( lastPlayedEvent ?. type === EventType . IncrementalSnapshot && lastPlayedEvent . data . source === IncrementalSource . MouseMove ) { lastPlayedTimestamp = lastPlayedEvent . timestamp + lastPlayedEvent . data . positions [ 0 ] ?. timeOffset ; } if ( baselineTime < ( lastPlayedTimestamp || 0 ) ) { emitter . emit ( ReplayerEvents . PlayBack ) ; } const syncEvents = new Array < eventWithTime > ( ) ; const actions = new Array < actionWithDelay > ( ) ; for ( const event of neededEvents ) { if ( lastPlayedTimestamp && lastPlayedTimestamp < baselineTime && ( event . timestamp <= lastPlayedTimestamp || event === lastPlayedEvent ) ) { continue ; } if ( event . timestamp < baselineTime ) { syncEvents . push ( event ) ; } else { const castFn = getCastFn ( event , false ) ; actions . push ( { doAction : ( ) => { castFn ( ) ; emitter . emit ( ReplayerEvents . EventCast , event ) ; } , delay : event . delay ! , } ) ; } } applyEventsSynchronously ( syncEvents ) ; emitter . emit ( ReplayerEvents . Flush ) ; timer . addActions ( actions ) ; timer . start ( ) ; } , 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 ; } return Date . now ( ) ; } , } ) , addEvent : assign ( ( ctx , machineEvent ) => { const { baselineTime , timer , events } = ctx ; if ( machineEvent . type === 'ADD_EVENT' ) { const { event } = machineEvent . payload ; addDelay ( event , baselineTime ) ; let end = events . length - 1 ; if ( ! events [ end ] || events [ end ] . timestamp <= event . timestamp ) { events . push ( event ) ; } else { let insertionIndex = - 1 ; let start = 0 ; while ( start <= end ) { let mid = Math . floor ( ( start + end ) / 2 ) ; if ( events [ mid ] . timestamp <= event . timestamp ) { start = mid + 1 ; } else { end = mid - 1 ; } } if ( insertionIndex === - 1 ) { insertionIndex = start ; } events . splice ( insertionIndex , 0 , event ) ; } const isSync = event . timestamp < baselineTime ; const castFn = getCastFn ( event , isSync ) ; if ( isSync ) { castFn ( ) ; } else if ( timer . isActive ( ) ) { timer . addAction ( { doAction : ( ) => { castFn ( ) ; emitter . emit ( ReplayerEvents . EventCast , event ) ; } , delay : event . delay ! , } ) ; } } return { ... ctx , events } ; } ) , } , } ) ; 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 ) { 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 > ;