export interface Clock { readonly currentTime : number ; readonly play : ( ) => void ; readonly pause : ( ) => void ; } const DEFAULT_TOLERANCE = 10 ; /**
 * A scheduler that can decide when to execute a certain
 * timestamped callback so that it happens in sync with a video
 * element.
 *
 * To use it:
 *
 * (1) Initialize a new Scheduler with a clock (to synchronize
 * against) and a callback (to be called when a message is in
 * sync with the video). The clock can be a HTMLVideoElement,
 * or anything that has a `currentTime` property which gives
 * the current presentation time in seconds, and a `pause` and
 * `play` method to control playback.
 *
 * (2) Call the `run` method every time a new message arrives
 * that you want to schedule (it needs to have an ntpTimestamp).
 * As soon at the presentation time is known, call the `init`
 * method and pass in that time, so that the scheduler can
 * start to schedule the callbacks. From then on, whenever
 * a message in the queue has a timestamp that matches the
 * current presentation time of the video, your callback will
 * fire.
 */ export class Scheduler < T extends { readonly ntpTimestamp ? : number ; } > { private readonly _clock : Clock ; private readonly _handler : ( msg : T ) => void ; private readonly _tolerance : number ; private _nextRun : number ; private _nextPlay : number ; private _fifo : T [ ] ; private _ntpPresentationTime : number ; private _suspended : boolean ; param clock param handler param tolerance constructor ( clock , handler , tolerance = DEFAULT_TOLERANCE ) { this . _clock = clock ; this . _handler = handler ; this . _tolerance = tolerance ; this . _nextRun = 0 ; this . _nextPlay = 0 ; this . _fifo = [ ] ; this . _ntpPresentationTime = 0 ; this . _suspended = false ; } /**
     * Bring the scheduler back to it's initial state.
     */ public reset ( ) { clearTimeout ( this . _nextRun ) ; clearTimeout ( this . _nextPlay ) ; this . _fifo = [ ] ; this . _ntpPresentationTime = 0 ; this . _suspended = false ; } param ntpPresentationTime public init ( ntpPresentationTime ) { this . _ntpPresentationTime = ntpPresentationTime ; } /**
     * Suspend the scheduler.
     *
     * This releases control of the clock and stops any scheduling activity.
     * Note that this doesn't mean the clock will be in a particular state
     * (could be started or stopped), just that the scheduler will no longer
     * control it.
     */ public suspend ( ) { clearTimeout ( this . _nextPlay ) ; this . _suspended = true ; } /**
     * Resume the scheduler.
     *
     * This gives back control of the clock and the ability
     * to schedule messages. The scheduler will immediately
     * try to do that on resume.
     */ public resume ( ) { this . _suspended = false ; this . run ( undefined ) ; } param newMessage public run ( newMessage ? ) { clearTimeout ( this . _nextRun ) ; if ( typeof this . _ntpPresentationTime === 'undefined' ) { return ; } if ( typeof newMessage !== 'undefined' ) { this . _fifo . push ( newMessage ) ; } if ( this . _suspended ) { return ; } if ( this . _fifo . length === 0 ) { return ; } let timeToPresent = 0 ; let currentMessage ; do { const msg = this . _fifo . shift ( ) ; if ( msg === undefined ) { throw new Error ( 'internal error: message should never be undefined' ) ; } currentMessage = msg ; const ntpTimestamp = currentMessage . ntpTimestamp ; if ( ntpTimestamp === undefined ) { continue ; } const presentationTime = ntpTimestamp - this . _ntpPresentationTime ; timeToPresent = presentationTime - this . _clock . currentTime * 1000 ; if ( Math . abs ( timeToPresent ) < this . _tolerance ) { this . _handler && this . _handler ( currentMessage ) ; } } while ( timeToPresent < this . _tolerance && this . _fifo . length > 0 ) ; if ( timeToPresent < - this . _tolerance ) { clearTimeout ( this . _nextPlay ) ; this . _clock . pause ( ) ; this . _nextPlay = window . setTimeout ( ( ) => this . _clock . play ( ) , - timeToPresent ) ; } else if ( timeToPresent > this . _tolerance ) { this . _fifo . unshift ( currentMessage ) ; this . _nextRun = window . setTimeout ( ( ) => this . run ( undefined ) , timeToPresent ) ; } } }