From 5f5ea22d656c40eae2e764e8b70641c6721dbbee Mon Sep 17 00:00:00 2001
From: Jacob Spizziri <jspizziri@weare5stones.com>
Date: Thu, 27 Apr 2023 11:34:40 -0400
Subject: [PATCH] feat(web): prevent exceptions during SSR due to inability to
 import shaka player

---
 tsconfig.json                          |  1 +
 web/TrackPlayer/Player.ts              | 40 +++++++++++++++++++-------
 web/TrackPlayer/SetupNotCalledError.ts |  5 ++++
 web/TrackPlayerModule.ts               | 11 +++----
 4 files changed, 41 insertions(+), 16 deletions(-)
 create mode 100644 web/TrackPlayer/SetupNotCalledError.ts

diff --git a/tsconfig.json b/tsconfig.json
index 003c897c9..fc88689d2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,6 +11,7 @@
     "outDir": "./lib",
     "strict": true,
     "target": "esnext",
+    "module": "esnext",
     "skipLibCheck": true
   },
   "include": ["src"],
diff --git a/web/TrackPlayer/Player.ts b/web/TrackPlayer/Player.ts
index 53b97e01d..bf0d90358 100644
--- a/web/TrackPlayer/Player.ts
+++ b/web/TrackPlayer/Player.ts
@@ -1,12 +1,10 @@
-// @ts-ignore
-import shaka from 'shaka-player/dist/shaka-player.ui';
-
 import { State } from '../../src/constants/State';
 import type { Track, Progress, PlaybackState } from '../../src/interfaces';
+import { SetupNotCalledError } from './SetupNotCalledError';
 
 export class Player {
-  protected element: HTMLMediaElement;
-  protected player: shaka.Player;
+  protected element?: HTMLMediaElement;
+  protected player?: shaka.Player;
   protected _current?: Track = undefined;
   protected _playWhenReady: boolean = false;
   protected _state: PlaybackState = { state: State.None };
@@ -35,7 +33,12 @@ export class Player {
     this._playWhenReady = pwr;
   }
 
-  constructor() {
+  async setupPlayer() {
+    // shaka only runs in a browser
+    if (typeof window === 'undefined') return;
+
+    // @ts-ignore
+    const shaka = await import('shaka-player/dist/shaka-player.ui');
     // Install built-in polyfills to patch browser incompatibilities.
     shaka.polyfill.installAll();
     // Check to see if the browser supports the basic APIs Shaka needs.
@@ -57,16 +60,16 @@ export class Player {
     this.player = new shaka.Player(this.element);
 
     // Listen for relevant events events.
-    this.player.addEventListener('error', (error: any) => {
+    this.player!.addEventListener('error', (error: any) => {
       // Extract the shaka.util.Error object from the event.
       this.onError(error.detail);
     });
     this.element.addEventListener('ended', () => this.onTrackEnded());
     this.element.addEventListener('playing', () => this.onTrackPlaying());
     this.element.addEventListener('pause', () => this.onTrackPaused());
-    this.player.addEventListener('loading', () => this.onTrackLoading());
-    this.player.addEventListener('loaded', () => this.onTrackLoaded());
-    this.player.addEventListener('buffering', ({ buffering }: any) => {
+    this.player!.addEventListener('loading', () => this.onTrackLoading());
+    this.player!.addEventListener('loaded', () => this.onTrackLoaded());
+    this.player!.addEventListener('buffering', ({ buffering }: any) => {
       if (buffering === true) {
         this.onTrackBuffering();
       }
@@ -115,62 +118,76 @@ export class Player {
    * player control
    */
   public async load(track: Track) {
-    await this.player.load(track.url);
+    if (!this.player) throw new SetupNotCalledError();
+    await this.player.load(track.url as string);
     this.current = track;
   }
 
   public async retry() {
+    if (!this.player) throw new SetupNotCalledError();
     this.player.retryStreaming();
   }
 
   public async stop() {
+    if (!this.player) throw new SetupNotCalledError();
     this.current = undefined;
     await this.player.unload()
   }
 
   public play() {
+    if (!this.element) throw new SetupNotCalledError();
     this.playWhenReady = true;
     return this.element.play();
   }
 
   public pause() {
+    if (!this.element) throw new SetupNotCalledError();
     this.playWhenReady = false;
     return this.element.pause();
   }
 
   public setRate(rate: number) {
+    if (!this.element) throw new SetupNotCalledError();
     return this.element.playbackRate = rate;
   }
 
   public getRate() {
+    if (!this.element) throw new SetupNotCalledError();
     return this.element.playbackRate;
   }
 
   public seekBy(offset: number) {
+    if (!this.element) throw new SetupNotCalledError();
     this.element.currentTime += offset;
   }
 
   public seekTo(seconds: number) {
+    if (!this.element) throw new SetupNotCalledError();
     this.element.currentTime = seconds;
   }
 
   public setVolume(volume: number) {
+    if (!this.element) throw new SetupNotCalledError();
     this.element.volume = volume;
   }
 
   public getVolume() {
+    if (!this.element) throw new SetupNotCalledError();
     return this.element.volume;
   }
 
   public getDuration() {
+    if (!this.element) throw new SetupNotCalledError();
     return this.element.duration
   }
 
   public getPosition() {
+    if (!this.element) throw new SetupNotCalledError();
     return this.element.currentTime
   }
 
   public getProgress(): Progress {
+    if (!this.element) throw new SetupNotCalledError();
     return {
       position: this.element.currentTime,
       duration: this.element.duration || 0,
@@ -179,6 +196,7 @@ export class Player {
   }
 
   public getBufferedPosition() {
+    if (!this.element) throw new SetupNotCalledError();
     return this.element.buffered.end;
   }
 }
diff --git a/web/TrackPlayer/SetupNotCalledError.ts b/web/TrackPlayer/SetupNotCalledError.ts
new file mode 100644
index 000000000..8f1e55530
--- /dev/null
+++ b/web/TrackPlayer/SetupNotCalledError.ts
@@ -0,0 +1,5 @@
+export class SetupNotCalledError extends Error {
+  constructor() {
+    super('You must call `setupPlayer` prior to interacting with the player.');
+  }
+}
diff --git a/web/TrackPlayerModule.ts b/web/TrackPlayerModule.ts
index 190c581c2..65022f64e 100644
--- a/web/TrackPlayerModule.ts
+++ b/web/TrackPlayerModule.ts
@@ -3,6 +3,7 @@ import { DeviceEventEmitter } from 'react-native';
 import { Event, PlaybackState, State } from '../src';
 import type { Track, UpdateOptions } from '../src';
 import { PlaylistPlayer, RepeatMode } from './TrackPlayer';
+import { SetupNotCalledError } from './TrackPlayer/SetupNotCalledError';
 
 export class TrackPlayerModule extends PlaylistPlayer {
   protected emitter = DeviceEventEmitter;
@@ -61,9 +62,6 @@ export class TrackPlayerModule extends PlaylistPlayer {
     this.emitter.emit(Event.PlaybackState, newState);
   }
 
-  // TODO: this probably does nothing
-  public setupPlayer(options: any) {}
-
   public async updateOptions(options: UpdateOptions) {
     // clear and reset interval
     this.clearUpdateEventInterval();
@@ -91,7 +89,7 @@ export class TrackPlayerModule extends PlaylistPlayer {
   }
 
   protected async onTrackEnded() {
-    const position = this.element.currentTime;
+    const position = this.element!.currentTime;
     await super.onTrackEnded();
 
     this.emitter.emit(Event.PlaybackTrackChanged, {
@@ -105,7 +103,7 @@ export class TrackPlayerModule extends PlaylistPlayer {
     await super.onPlaylistEnded();
     this.emitter.emit(Event.PlaybackQueueEnded, {
       track: this.currentIndex,
-      position: this.element.currentTime,
+      position: this.element!.currentTime,
     });
   }
 
@@ -132,6 +130,7 @@ export class TrackPlayerModule extends PlaylistPlayer {
   }
 
   public async load(track: Track) {
+    if (!this.element) throw new SetupNotCalledError();
     const lastTrack = this.current;
     const lastPosition = this.element.currentTime;
     await super.load(track);
@@ -154,6 +153,8 @@ export class TrackPlayerModule extends PlaylistPlayer {
   }
 
   public getActiveTrackIndex(): number | undefined {
+    // per the existing spec, this should throw if setup hasn't been called
+    if (!this.element || !this.player) throw new SetupNotCalledError();
     return this.currentIndex;
   }