Prechádzať zdrojové kódy

Switch to event model for track

This allows us to handle pause/buffering in the game
Thomas Dy 4 rokov pred
rodič
commit
0608163c87
3 zmenil súbory, kde vykonal 99 pridanie a 68 odobranie
  1. 77 50
      src/audio.ts
  2. 2 7
      src/background.ts
  3. 20 11
      src/game/typing.ts

+ 77 - 50
src/audio.ts

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

+ 2 - 7
src/background.ts

@@ -29,13 +29,7 @@ namespace background {
     }
 
     hideVideo() {
-      if (this.last != null) {
-        this.last.classList.add('show');
-        this.last.addEventListener('transitionend', () => {
-          this.video.classList.remove('show');
-          this.video.innerHTML = '';
-        });
-      }
+      this.last?.classList.add('show');
     }
 
     setVideo(element: HTMLElement) {
@@ -67,6 +61,7 @@ namespace background {
         this.last.classList.remove('show');
         this.next.addEventListener('transitionend', () => {
           this.element.removeChild(toRemove);
+          this.video.classList.remove('show');
           this.video.innerHTML = '';
         }, { once: true });
       }

+ 20 - 11
src/game/typing.ts

@@ -100,7 +100,7 @@ namespace game {
         const progressListener = this.fnContext.wrap((percentage: number) => {
           this.barElement!.style.width = `${percentage}%`;
         });
-        let trackPromise;
+        let trackPromise: Promise<audio.Track>;
         if (videoId !== null) {
           const ytElement = document.createElement('div');
           trackPromise = this.context.audioManager.loadTrackFromYoutube(
@@ -111,12 +111,14 @@ namespace game {
           this.context.bgManager.setVideo(ytElement);
           if (this.context.level.background == undefined) {
             trackPromise.then((track) => {
-              track.playPromise.then(track.fnContext.wrap(() => {
-                this.context.bgManager.showVideo();
-              }));
-              track.finishPromise.then(track.fnContext.wrap(() => {
-                this.context.bgManager.hideVideo();
-              }));
+              track.addListener((_, state) => {
+                if (state === audio.PlayState.PLAYING) {
+                  this.context.bgManager.showVideo();
+                }
+                if (state === audio.PlayState.STOPPED) {
+                  this.context.bgManager.hideVideo();
+                }
+              });
             });
           }
         } else {
@@ -198,16 +200,24 @@ namespace game {
 
     enter(): void {
       let progressElement: HTMLElement = this.gameContainer.querySelector<HTMLElement>('.track-progress')!;
-      if (this.context.level.audio == null) {
+      if (this.context.track == null) {
         progressElement.style.visibility = 'hidden';
         this.lines = this.context.level.lines.filter(line => line.kana != "@");
       } else {
         progressElement.style.visibility = 'visible';
-        this.progressController = new display.TrackProgressController(
+        const progressController = new display.TrackProgressController(
           progressElement,
           this.lines
         );
-        this.progressController.setListener(event => this.onIntervalEnd());
+        progressController.setListener(_ => this.onIntervalEnd());
+        this.context.track.addListener((track, state) => {
+          if (state === audio.PlayState.PLAYING) {
+            progressController.start(track.getTime());
+          } else {
+            progressController.pause();
+          }
+        });
+        this.progressController = progressController;
       }
       this.onStart();
     }
@@ -220,7 +230,6 @@ namespace game {
     onStart(): void {
       this.nextLine();
       if (this.context.track !== null) {
-        this.progressController!.start();
         this.context.track.play();
       }