|  | @@ -66,34 +66,59 @@ namespace audio {
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  export interface Track {
 | 
	
		
			
				|  |  | -    play(): void;
 | 
	
		
			
				|  |  | -    start(fromTime?: number, duration?: number): void;
 | 
	
		
			
				|  |  | -    pause(): void;
 | 
	
		
			
				|  |  | -    stop(): void;
 | 
	
		
			
				|  |  | -    exit(): void;
 | 
	
		
			
				|  |  | -    isPlaying(): boolean;
 | 
	
		
			
				|  |  | -    getTime(): number;
 | 
	
		
			
				|  |  | -    getDuration(): number;
 | 
	
		
			
				|  |  | +  export enum PlayState {
 | 
	
		
			
				|  |  | +    UNSTARTED ='unstarted',
 | 
	
		
			
				|  |  | +    PLAYING = 'playing',
 | 
	
		
			
				|  |  | +    PAUSED = 'paused',
 | 
	
		
			
				|  |  | +    STOPPED = 'stopped'
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  export class FileTrack implements Track {
 | 
	
		
			
				|  |  | +  export type TrackListener = (track: Track, state: PlayState) => void;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  export abstract class Track {
 | 
	
		
			
				|  |  | +    private listeners: TrackListener[] = [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    addListener(listener: TrackListener) {
 | 
	
		
			
				|  |  | +      this.listeners.push(listener);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    clearListeners() {
 | 
	
		
			
				|  |  | +      this.listeners = [];
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    emit(state: PlayState) {
 | 
	
		
			
				|  |  | +      this.listeners.forEach(l => l(this, state));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    exit(): void {
 | 
	
		
			
				|  |  | +      this.clearListeners();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    abstract play(): void;
 | 
	
		
			
				|  |  | +    abstract start(fromTime?: number, duration?: number): void;
 | 
	
		
			
				|  |  | +    abstract pause(): void;
 | 
	
		
			
				|  |  | +    abstract stop(): void;
 | 
	
		
			
				|  |  | +    abstract getState(): PlayState;
 | 
	
		
			
				|  |  | +    abstract getTime(): number;
 | 
	
		
			
				|  |  | +    abstract getDuration(): number;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  export class FileTrack extends Track {
 | 
	
		
			
				|  |  |      manager: AudioManager;
 | 
	
		
			
				|  |  |      buffer: AudioBuffer;
 | 
	
		
			
				|  |  |      source: AudioBufferSourceNode | null;
 | 
	
		
			
				|  |  |      playStartTime: number;
 | 
	
		
			
				|  |  |      resumeTime: number;
 | 
	
		
			
				|  |  | -    hasStarted: boolean;
 | 
	
		
			
				|  |  | -    isFinished: boolean;
 | 
	
		
			
				|  |  | +    state: PlayState;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      constructor(manager: AudioManager, buffer: AudioBuffer) {
 | 
	
		
			
				|  |  | +      super();
 | 
	
		
			
				|  |  |        this.manager = manager;
 | 
	
		
			
				|  |  |        this.buffer = buffer;
 | 
	
		
			
				|  |  |        this.source = null;
 | 
	
		
			
				|  |  |        this.playStartTime = 0;
 | 
	
		
			
				|  |  |        this.resumeTime = 0;
 | 
	
		
			
				|  |  | -      this.hasStarted = false;
 | 
	
		
			
				|  |  | -      this.isFinished = true;
 | 
	
		
			
				|  |  | +      this.state = PlayState.UNSTARTED;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      play(): void {
 | 
	
	
		
			
				|  | @@ -101,8 +126,7 @@ namespace audio {
 | 
	
		
			
				|  |  |        this.source.buffer = this.buffer;
 | 
	
		
			
				|  |  |        this.source.connect(this.manager.output);
 | 
	
		
			
				|  |  |        this.playStartTime = this.manager.getTime();
 | 
	
		
			
				|  |  | -      this.isFinished = false;
 | 
	
		
			
				|  |  | -      this.hasStarted = true;
 | 
	
		
			
				|  |  | +      this.setState(PlayState.PLAYING);
 | 
	
		
			
				|  |  |        this.source.start();
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -115,49 +139,51 @@ namespace audio {
 | 
	
		
			
				|  |  |        this.source.connect(this.manager.output);
 | 
	
		
			
				|  |  |        this.source.onended = (event) => {
 | 
	
		
			
				|  |  |          if (this.source == event.target) {
 | 
	
		
			
				|  |  | -          this.isFinished = true;
 | 
	
		
			
				|  |  |            this.resumeTime = this.manager.getTime() - this.playStartTime;
 | 
	
		
			
				|  |  |            if (this.resumeTime > this.getDuration()) {
 | 
	
		
			
				|  |  |              this.resumeTime = 0;
 | 
	
		
			
				|  |  | +            this.setState(PlayState.STOPPED);
 | 
	
		
			
				|  |  | +          } else {
 | 
	
		
			
				|  |  | +            this.setState(PlayState.PAUSED);
 | 
	
		
			
				|  |  |            }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | -      this.isFinished = false;
 | 
	
		
			
				|  |  | -      this.hasStarted = true;
 | 
	
		
			
				|  |  |        this.playStartTime = this.manager.getTime() - this.resumeTime;
 | 
	
		
			
				|  |  | +      this.setState(PlayState.PLAYING);
 | 
	
		
			
				|  |  |        this.source.start(0, this.resumeTime, duration);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      pause(): void {
 | 
	
		
			
				|  |  | -      if (this.isFinished) return;
 | 
	
		
			
				|  |  | +      if (this.state === PlayState.PAUSED || this.state === PlayState.STOPPED) return;
 | 
	
		
			
				|  |  |        this.resumeTime = this.manager.getTime() - this.playStartTime;
 | 
	
		
			
				|  |  | -      this.isFinished = true;
 | 
	
		
			
				|  |  |        if (this.source) {
 | 
	
		
			
				|  |  |          this.source.stop();
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | +      this.setState(PlayState.PAUSED);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      stop(): void {
 | 
	
		
			
				|  |  |        this.resumeTime = 0;
 | 
	
		
			
				|  |  | -      this.isFinished = true;
 | 
	
		
			
				|  |  |        if (this.source) {
 | 
	
		
			
				|  |  |          this.source.stop();
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | +      this.setState(PlayState.STOPPED);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      exit(): void {
 | 
	
		
			
				|  |  | +      super.exit();
 | 
	
		
			
				|  |  |        this.stop();
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    isPlaying(): boolean {
 | 
	
		
			
				|  |  | -      return this.hasStarted && !this.isFinished;
 | 
	
		
			
				|  |  | +    getState(): PlayState {
 | 
	
		
			
				|  |  | +      return this.state;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      getTime(): number {
 | 
	
		
			
				|  |  | -      if (!this.hasStarted) {
 | 
	
		
			
				|  |  | +      if (this.state === PlayState.UNSTARTED) {
 | 
	
		
			
				|  |  |          return 0;
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | -      else if (this.isFinished) {
 | 
	
		
			
				|  |  | +      else if (this.state === PlayState.PAUSED || this.state === PlayState.STOPPED) {
 | 
	
		
			
				|  |  |          if (this.resumeTime > 0) {
 | 
	
		
			
				|  |  |            return this.resumeTime;
 | 
	
		
			
				|  |  |          } else {
 | 
	
	
		
			
				|  | @@ -171,25 +197,18 @@ namespace audio {
 | 
	
		
			
				|  |  |      getDuration(): number {
 | 
	
		
			
				|  |  |        return this.buffer.duration;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private setState(state: PlayState): void {
 | 
	
		
			
				|  |  | +      this.state = state;
 | 
	
		
			
				|  |  | +      this.emit(state);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  export class YoutubeTrack implements Track {
 | 
	
		
			
				|  |  | +  export class YoutubeTrack extends Track {
 | 
	
		
			
				|  |  |      private timeoutHandle?: number;
 | 
	
		
			
				|  |  | -    private playDeferred: util.Deferred;
 | 
	
		
			
				|  |  | -    private finishDeferred: util.Deferred;
 | 
	
		
			
				|  |  | -    readonly fnContext: util.FnContext = new util.FnContext();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      constructor(readonly player: YT.Player, readonly id: string) {
 | 
	
		
			
				|  |  | -      this.playDeferred = util.makeDeferred();
 | 
	
		
			
				|  |  | -      this.finishDeferred = util.makeDeferred();
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    get playPromise(): Promise<void> {
 | 
	
		
			
				|  |  | -      return this.playDeferred.promise;
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    get finishPromise(): Promise<void> {
 | 
	
		
			
				|  |  | -      return this.finishDeferred.promise;
 | 
	
		
			
				|  |  | +      super();
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      preload(): Promise<void> {
 | 
	
	
		
			
				|  | @@ -204,9 +223,8 @@ namespace audio {
 | 
	
		
			
				|  |  |                this.player.unMute();
 | 
	
		
			
				|  |  |                resolve();
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  | -          } else if (data === YT.PlayerState.ENDED) {
 | 
	
		
			
				|  |  | -            this.finishDeferred.resolve();
 | 
	
		
			
				|  |  |            }
 | 
	
		
			
				|  |  | +          this.emit(this.mapState(data));
 | 
	
		
			
				|  |  |          };
 | 
	
		
			
				|  |  |          this.player.addEventListener('onStateChange', onStateChange);
 | 
	
		
			
				|  |  |          this.player.mute();
 | 
	
	
		
			
				|  | @@ -216,7 +234,6 @@ namespace audio {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      play(): void {
 | 
	
		
			
				|  |  |        this.clearTimeout();
 | 
	
		
			
				|  |  | -      this.playDeferred.resolve();
 | 
	
		
			
				|  |  |        this.player.playVideo();
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -243,13 +260,8 @@ namespace audio {
 | 
	
		
			
				|  |  |        this.player.stopVideo();
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    exit(): void {
 | 
	
		
			
				|  |  | -      // the video will be removed from the background and stop immediately
 | 
	
		
			
				|  |  | -      this.fnContext.invalidate();
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    isPlaying(): boolean {
 | 
	
		
			
				|  |  | -      return this.player.getPlayerState() === YT.PlayerState.PLAYING;
 | 
	
		
			
				|  |  | +    getState(): PlayState {
 | 
	
		
			
				|  |  | +      return this.mapState(this.player.getPlayerState());
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      getTime(): number {
 | 
	
	
		
			
				|  | @@ -265,5 +277,20 @@ namespace audio {
 | 
	
		
			
				|  |  |          clearTimeout(this.timeoutHandle);
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private mapState(ytState: YT.PlayerState): PlayState {
 | 
	
		
			
				|  |  | +      switch (ytState) {
 | 
	
		
			
				|  |  | +        case YT.PlayerState.PLAYING:
 | 
	
		
			
				|  |  | +          return PlayState.PLAYING;
 | 
	
		
			
				|  |  | +        case YT.PlayerState.ENDED:
 | 
	
		
			
				|  |  | +          return PlayState.STOPPED;
 | 
	
		
			
				|  |  | +        case YT.PlayerState.UNSTARTED:
 | 
	
		
			
				|  |  | +        case YT.PlayerState.CUED:
 | 
	
		
			
				|  |  | +          return PlayState.UNSTARTED;
 | 
	
		
			
				|  |  | +        case YT.PlayerState.BUFFERING:
 | 
	
		
			
				|  |  | +        case YT.PlayerState.PAUSED:
 | 
	
		
			
				|  |  | +          return PlayState.PAUSED;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  }
 |