@readium/navigator 2.4.0-beta.6 → 2.4.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readium/navigator",
3
- "version": "2.4.0-beta.6",
3
+ "version": "2.4.0-beta.7",
4
4
  "type": "module",
5
5
  "description": "Next generation SDK for publications in Web Apps",
6
6
  "author": "readium",
@@ -59,6 +59,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
59
59
  private readonly pub: Publication;
60
60
  private positionPollInterval: ReturnType<typeof setInterval> | null = null;
61
61
  private navigationId: number = 0;
62
+ private _playIntent: boolean = false;
62
63
  private listeners: AudioNavigatorListeners;
63
64
  private currentLocation!: Locator;
64
65
 
@@ -432,7 +433,11 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
432
433
 
433
434
  const id = ++this.navigationId;
434
435
  const direction: "forward" | "backward" = trackIndex >= this.currentTrackIndex() ? "forward" : "backward";
435
- const wasPlaying = this.isPlaying;
436
+ // Use _playIntent rather than isPlaying — setMediaElement resets the
437
+ // engine's playing flag, so a rapid second go() would see false and
438
+ // never resume playback.
439
+ const wasPlaying = this.isPlaying || this._playIntent;
440
+ this._playIntent = wasPlaying;
436
441
 
437
442
  this.stopPositionPolling();
438
443
  this.pool.setCurrentAudio(trackIndex, direction);
@@ -450,6 +455,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
450
455
  }
451
456
 
452
457
  if (wasPlaying) this.play();
458
+ this._playIntent = false;
453
459
 
454
460
  cb(true);
455
461
  } catch (error) {
@@ -59,6 +59,11 @@ export class AudioPoolManager {
59
59
  if (!element) {
60
60
  element = document.createElement("audio");
61
61
  element.preload = "auto";
62
+ // When Web Audio is active CORS already succeeded, so preload
63
+ // with crossOrigin to avoid a destructive reload at swap time.
64
+ if (this._audioEngine.isWebAudioActive) {
65
+ element.crossOrigin = "anonymous";
66
+ }
62
67
  element.src = href;
63
68
  element.load();
64
69
  this.pool.set(href, element);
@@ -105,16 +110,7 @@ export class AudioPoolManager {
105
110
  const href = this.pickPlayableHref(this._publication.readingOrder.items[currentIndex]);
106
111
  const element = this.ensure(href);
107
112
 
108
- // Hand the element to the engine. When Web Audio is active, the pooled
109
- // element doesn't have crossOrigin set (it would break non-CORS servers
110
- // during preload), so we swap in the fresh element and let loadAudio
111
- // handle CORS setup + fallback on the engine's own mediaElement.
112
- if (this.audioEngine.isWebAudioActive) {
113
- this.audioEngine.setMediaElement(element);
114
- this.audioEngine.loadAudio(href);
115
- } else {
116
- this.audioEngine.setMediaElement(element);
117
- }
113
+ this.audioEngine.setMediaElement(element);
118
114
 
119
115
  // Remove from pool so the engine fully owns it and we don't dispose it
120
116
  this.pool.delete(href);
@@ -40,12 +40,6 @@ export interface AudioEngine {
40
40
  */
41
41
  playback: Playback;
42
42
 
43
- /**
44
- * Loads the audio resource at the given URL.
45
- * @param url The URL of the audio resource.
46
- */
47
- loadAudio(url: string): void;
48
-
49
43
  /**
50
44
  * Adds an event listener to the audio engine.
51
45
  * @param event The event name to listen.
@@ -95,52 +95,6 @@ export class WebAudioEngine implements AudioEngine {
95
95
  );
96
96
  }
97
97
 
98
- /**
99
- * Load the audio resource at the given URL.
100
- * @param url The URL of the audio resource.
101
- * */
102
- public loadAudio(url: string): void {
103
- // Abort any in-progress load before starting a new one.
104
- this.mediaElement.pause();
105
- this.mediaElement.removeAttribute("src");
106
- this.mediaElement.load();
107
-
108
- this.isLoadingValue = true;
109
- this.isLoadedValue = false;
110
- this.isPlayingValue = false;
111
- this.isPausedValue = false;
112
-
113
- if (this.webAudioActive) {
114
- this.mediaElement.crossOrigin = "anonymous";
115
- this.mediaElement.src = url;
116
- this.mediaElement.load();
117
- this.mediaElement.playbackRate = this.currentPlaybackRate;
118
-
119
- // If the server doesn't honour the CORS preflight, fall back to a
120
- // non-CORS load and tear down the Web Audio graph so the element
121
- // is never passed to MediaElementAudioSourceNode in a tainted state.
122
- const cleanup = () => {
123
- this.mediaElement.removeEventListener("error", onCORSError);
124
- this.mediaElement.removeEventListener("canplaythrough", onCORSSuccess);
125
- };
126
- const onCORSError = () => {
127
- cleanup();
128
- this.deactivateWebAudio();
129
- this.mediaElement.removeAttribute("crossOrigin");
130
- this.mediaElement.src = url;
131
- this.mediaElement.load();
132
- this.mediaElement.playbackRate = this.currentPlaybackRate;
133
- };
134
- const onCORSSuccess = () => cleanup();
135
- this.mediaElement.addEventListener("error", onCORSError);
136
- this.mediaElement.addEventListener("canplaythrough", onCORSSuccess);
137
- } else {
138
- this.mediaElement.src = url;
139
- this.mediaElement.load();
140
- this.mediaElement.playbackRate = this.currentPlaybackRate;
141
- }
142
- }
143
-
144
98
  private deactivateWebAudio(): void {
145
99
  if (this.worklet) {
146
100
  this.worklet.destroy();
@@ -162,18 +116,7 @@ export class WebAudioEngine implements AudioEngine {
162
116
  * @param element The HTML audio element to use.
163
117
  */
164
118
  public setMediaElement(element: HTMLAudioElement): void {
165
- // Pause the outgoing element before replacing it
166
- this.mediaElement.pause();
167
- this.isPlayingValue = false;
168
- this.isPausedValue = false;
169
-
170
- // Disconnect old source node if it exists
171
- if (this.sourceNode) {
172
- this.sourceNode.disconnect();
173
- this.sourceNode = null;
174
- }
175
-
176
- // Remove old event listeners from current mediaElement
119
+ // Remove listeners BEFORE pausing so the pause doesn't leak through
177
120
  this.mediaElement.removeEventListener("canplaythrough", this.boundOnCanPlayThrough);
178
121
  this.mediaElement.removeEventListener("timeupdate", this.boundOnTimeUpdate);
179
122
  this.mediaElement.removeEventListener("error", this.boundOnError);
@@ -190,6 +133,17 @@ export class WebAudioEngine implements AudioEngine {
190
133
  this.mediaElement.removeEventListener("pause", this.boundOnPause);
191
134
  this.mediaElement.removeEventListener("progress", this.boundOnProgress);
192
135
 
136
+ // Now safe to pause the outgoing element
137
+ this.mediaElement.pause();
138
+ this.isPlayingValue = false;
139
+ this.isPausedValue = false;
140
+
141
+ // Disconnect old source node if it exists
142
+ if (this.sourceNode) {
143
+ this.sourceNode.disconnect();
144
+ this.sourceNode = null;
145
+ }
146
+
193
147
  // Set new media element
194
148
  this.mediaElement = element;
195
149
 
@@ -214,6 +168,26 @@ export class WebAudioEngine implements AudioEngine {
214
168
  this.mediaElement.volume = this.isMutedValue ? 0 : this.currentVolume;
215
169
  this.mediaElement.playbackRate = this.currentPlaybackRate;
216
170
 
171
+ // Reconnect the Web Audio graph to the new element
172
+ if (this.webAudioActive) {
173
+ try {
174
+ const ctx = this.getOrCreateAudioContext();
175
+ this.sourceNode = new MediaElementAudioSourceNode(ctx, { mediaElement: this.mediaElement });
176
+ if (!this.gainNode) {
177
+ this.gainNode = ctx.createGain();
178
+ this.gainNode.connect(ctx.destination);
179
+ }
180
+ if (this.worklet?.workletNode) {
181
+ this.sourceNode.connect(this.worklet.workletNode);
182
+ } else {
183
+ this.sourceNode.connect(this.gainNode);
184
+ }
185
+ } catch {
186
+ // CORS failed on this element — deactivate Web Audio gracefully
187
+ this.deactivateWebAudio();
188
+ }
189
+ }
190
+
217
191
  // Check if metadata is already loaded (common with preloaded elements)
218
192
  if (this.mediaElement.readyState >= 1) {
219
193
  this.onLoadedMetadata(new Event('loadedmetadata'));
@@ -28,6 +28,7 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
28
28
  private readonly pub;
29
29
  private positionPollInterval;
30
30
  private navigationId;
31
+ private _playIntent;
31
32
  private listeners;
32
33
  private currentLocation;
33
34
  private _preferences;
@@ -36,11 +36,6 @@ export interface AudioEngine {
36
36
  * The current playback state.
37
37
  */
38
38
  playback: Playback;
39
- /**
40
- * Loads the audio resource at the given URL.
41
- * @param url The URL of the audio resource.
42
- */
43
- loadAudio(url: string): void;
44
39
  /**
45
40
  * Adds an event listener to the audio engine.
46
41
  * @param event The event name to listen.
@@ -48,11 +48,6 @@ export declare class WebAudioEngine implements AudioEngine {
48
48
  * @param callback - callback function to be removed.
49
49
  */
50
50
  off(event: string, callback: EventCallback): void;
51
- /**
52
- * Load the audio resource at the given URL.
53
- * @param url The URL of the audio resource.
54
- * */
55
- loadAudio(url: string): void;
56
51
  private deactivateWebAudio;
57
52
  /**
58
53
  * Sets the media element for playback.