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

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.4",
3
+ "version": "2.4.0-beta.6",
4
4
  "type": "module",
5
5
  "description": "Next generation SDK for publications in Web Apps",
6
6
  "author": "readium",
@@ -66,6 +66,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
66
66
  private _defaults: AudioDefaults;
67
67
  private _settings: AudioSettings;
68
68
  private _preferencesEditor: AudioPreferencesEditor | null = null;
69
+ private _mediaSessionEnabled: boolean = false;
69
70
  private pool: AudioPoolManager;
70
71
  private readonly _navigatorProtector: AudioNavigatorProtector | null = null;
71
72
  private readonly _keyboardPeripheralsManager: KeyboardPeripherals | null = null;
@@ -110,7 +111,6 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
110
111
  state: {
111
112
  currentTime: initialTime,
112
113
  duration: 0,
113
- volume: this._settings.volume
114
114
  } as PlaybackState,
115
115
  playWhenReady: false,
116
116
  index: trackIndex
@@ -154,10 +154,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
154
154
  }
155
155
 
156
156
  this.setupEventListeners();
157
-
158
- if (this._settings.enableMediaSession) {
159
- this.setupMediaSession();
160
- }
157
+ this.applyPreferences();
161
158
 
162
159
  this.pool.setCurrentAudio(trackIndex, "forward");
163
160
 
@@ -190,7 +187,6 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
190
187
  }
191
188
 
192
189
  private applyPreferences(): void {
193
- const oldSettings = this._settings;
194
190
  this._settings = new AudioSettings(this._preferences, this._defaults);
195
191
 
196
192
  if (this._preferencesEditor !== null) {
@@ -200,9 +196,11 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
200
196
  this.pool.audioEngine.setVolume(this._settings.volume);
201
197
  this.pool.audioEngine.setPlaybackRate(this._settings.playbackRate, this._settings.preservePitch);
202
198
 
203
- if (this._settings.enableMediaSession && !oldSettings.enableMediaSession) {
199
+ if (this._settings.enableMediaSession && !this._mediaSessionEnabled) {
200
+ this._mediaSessionEnabled = true;
204
201
  this.setupMediaSession();
205
- } else if (!this._settings.enableMediaSession && oldSettings.enableMediaSession) {
202
+ } else if (!this._settings.enableMediaSession && this._mediaSessionEnabled) {
203
+ this._mediaSessionEnabled = false;
206
204
  this.destroyMediaSession();
207
205
  }
208
206
  }
@@ -328,6 +326,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
328
326
  fragments: [`t=${this.duration}`]
329
327
  }));
330
328
  this.listeners.trackEnded(this.currentLocator);
329
+ if (!this.canGoForward) return;
331
330
  await this.nextTrack();
332
331
  if (this._settings.autoPlay) this.play();
333
332
  });
@@ -1,8 +1,11 @@
1
1
  import { Link, Publication } from "@readium/shared";
2
2
  import { WebAudioEngine } from "./engine/WebAudioEngine";
3
3
 
4
+ const UPPER_BOUNDARY = 1;
5
+ const LOWER_BOUNDARY = 1;
6
+
4
7
  export class AudioPoolManager {
5
- private preloadedElements: Map<string, HTMLAudioElement> = new Map();
8
+ private readonly pool: Map<string, HTMLAudioElement> = new Map();
6
9
  private _audioEngine: WebAudioEngine;
7
10
  private readonly _publication: Publication;
8
11
  private readonly _supportedAudioTypes: Map<string, "probably" | "maybe">;
@@ -48,102 +51,84 @@ export class AudioPoolManager {
48
51
  }
49
52
 
50
53
  /**
51
- * Sets the current audio by href, using preloaded element if available or loading otherwise,
52
- * and preloads adjacent tracks.
53
- * @param href The URL of the audio resource.
54
- * @param publication The publication containing the reading order.
55
- * @param currentIndex The current track index.
56
- * @param direction The navigation direction ('forward' or 'backward').
54
+ * Ensures an audio element exists in the pool for the given href.
55
+ * If one already exists, it is left untouched (preserving its buffered data).
57
56
  */
58
- setCurrentAudio(currentIndex: number, direction: 'forward' | 'backward'): void {
59
- const href = this.pickPlayableHref(this._publication.readingOrder.items[currentIndex]);
60
- // When Web Audio is active, preloaded elements lack crossOrigin="anonymous"
61
- // and cannot be connected to MediaElementAudioSourceNode, so bypass the pool.
62
- const preloadedElement = !this.audioEngine.isWebAudioActive ? this.get(href) : undefined;
63
- if (preloadedElement) {
64
- this.audioEngine.setMediaElement(preloadedElement);
65
- this.clear(href);
66
- } else {
67
- this.clear(href);
68
- this.audioEngine.loadAudio(href);
57
+ private ensure(href: string): HTMLAudioElement {
58
+ let element = this.pool.get(href);
59
+ if (!element) {
60
+ element = document.createElement("audio");
61
+ element.preload = "auto";
62
+ element.src = href;
63
+ element.load();
64
+ this.pool.set(href, element);
69
65
  }
70
- this.preloadAdjacent(currentIndex, direction);
71
- }
72
- preload(href: string): void {
73
- if (this.preloadedElements.has(href)) {
74
- return; // Already preloaded
75
- }
76
-
77
- const audioElement = document.createElement("audio");
78
- audioElement.preload = "auto";
79
- audioElement.src = href;
80
- audioElement.load(); // Start buffering
81
-
82
- this.preloadedElements.set(href, audioElement);
66
+ return element;
83
67
  }
84
68
 
85
69
  /**
86
- * Retrieves a preloaded audio element by URL.
87
- * @param href The URL of the audio resource.
88
- * @returns The preloaded HTMLAudioElement, or undefined if not preloaded.
70
+ * Updates the pool around the given index: ensures elements exist within
71
+ * the LOWER_BOUNDARY and disposes those beyond the UPPER_BOUNDARY.
89
72
  */
90
- get(href: string): HTMLAudioElement | undefined {
91
- return this.preloadedElements.get(href);
92
- }
73
+ private update(currentIndex: number): void {
74
+ const items = this._publication.readingOrder.items;
75
+ const keep = new Set<string>();
93
76
 
94
- /**
95
- * Removes a preloaded element from the pool.
96
- * @param href The URL of the audio resource.
97
- */
98
- clear(href: string): void {
99
- this.preloadedElements.delete(href);
100
- }
101
-
102
- /**
103
- * Preloads the next track in the reading order.
104
- * @param publication The publication containing the reading order.
105
- * @param currentIndex The current track index.
106
- */
107
- preloadNext(currentIndex: number): void {
108
- const nextIndex = currentIndex + 1;
109
- if (nextIndex < this._publication.readingOrder.items.length) {
110
- const nextLink = this._publication.readingOrder.items[nextIndex];
111
- this.preload(this.pickPlayableHref(nextLink));
77
+ for (let j = 0; j < items.length; j++) {
78
+ const href = this.pickPlayableHref(items[j]);
79
+ if (j >= currentIndex - LOWER_BOUNDARY && j <= currentIndex + LOWER_BOUNDARY) {
80
+ this.ensure(href);
81
+ keep.add(href);
82
+ } else if (j >= currentIndex - UPPER_BOUNDARY && j <= currentIndex + UPPER_BOUNDARY) {
83
+ // Between lower and upper: keep if already loaded, don't create
84
+ if (this.pool.has(href)) {
85
+ keep.add(href);
86
+ }
87
+ }
112
88
  }
113
- }
114
89
 
115
- /**
116
- * Preloads the previous track in the reading order.
117
- * @param currentIndex The current track index.
118
- */
119
- preloadPrevious(currentIndex: number): void {
120
- const prevIndex = currentIndex - 1;
121
- if (prevIndex >= 0) {
122
- const prevLink = this._publication.readingOrder.items[prevIndex];
123
- this.preload(this.pickPlayableHref(prevLink));
90
+ // Dispose elements beyond the upper boundary
91
+ for (const [href, element] of this.pool) {
92
+ if (!keep.has(href)) {
93
+ element.removeAttribute("src");
94
+ element.load(); // release network resources
95
+ this.pool.delete(href);
96
+ }
124
97
  }
125
98
  }
126
99
 
127
100
  /**
128
- * Preloads adjacent tracks (previous and next) for smoother navigation.
129
- * @param currentIndex The current track index.
130
- * @param direction The navigation direction ('forward' or 'backward').
101
+ * Sets the current audio for playback at the given track index.
102
+ * The element is always sourced from the pool — never loaded ad-hoc on the engine.
131
103
  */
132
- preloadAdjacent(currentIndex: number, direction: 'forward' | 'backward' = 'forward'): void {
133
- if (direction === 'forward') {
134
- this.preloadNext(currentIndex);
135
- this.preloadPrevious(currentIndex);
104
+ setCurrentAudio(currentIndex: number, _direction: 'forward' | 'backward'): void {
105
+ const href = this.pickPlayableHref(this._publication.readingOrder.items[currentIndex]);
106
+ const element = this.ensure(href);
107
+
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);
136
115
  } else {
137
- this.preloadPrevious(currentIndex);
138
- this.preloadNext(currentIndex);
116
+ this.audioEngine.setMediaElement(element);
139
117
  }
118
+
119
+ // Remove from pool so the engine fully owns it and we don't dispose it
120
+ this.pool.delete(href);
121
+
122
+ // Manage the pool around the new position
123
+ this.update(currentIndex);
140
124
  }
141
125
 
142
- /**
143
- * Destroys the pool by stopping the engine and clearing all preloaded elements.
144
- */
145
126
  destroy(): void {
146
127
  this.audioEngine.stop();
147
- this.preloadedElements.clear();
128
+ for (const [, element] of this.pool) {
129
+ element.removeAttribute("src");
130
+ element.load();
131
+ }
132
+ this.pool.clear();
148
133
  }
149
134
  }
@@ -11,11 +11,6 @@ export interface PlaybackState {
11
11
  * The duration of the audio resource.
12
12
  */
13
13
  duration: number;
14
-
15
- /**
16
- * The volume of the audio resource.
17
- */
18
- volume: number;
19
14
  }
20
15
 
21
16
  /**
@@ -16,6 +16,7 @@ export class WebAudioEngine implements AudioEngine {
16
16
  private sourceNode: MediaElementAudioSourceNode | null = null;
17
17
  private gainNode: GainNode | null = null;
18
18
  private listeners: { [event: string]: EventCallback[] } = {};
19
+ private currentVolume: number = 1;
19
20
  private currentPlaybackRate: number = 1;
20
21
  private isMutedValue: boolean = false;
21
22
  private isPlayingValue: boolean = false;
@@ -48,7 +49,6 @@ export class WebAudioEngine implements AudioEngine {
48
49
 
49
50
  // crossOrigin is set lazily in activateWebAudio() only when the worklet is needed
50
51
  this.mediaElement = document.createElement("audio");
51
- this.setVolume(this.playback.state.volume);
52
52
 
53
53
  // Event listeners (to report the client app about some async events)
54
54
  this.mediaElement.addEventListener("canplaythrough", this.boundOnCanPlayThrough);
@@ -100,6 +100,11 @@ export class WebAudioEngine implements AudioEngine {
100
100
  * @param url The URL of the audio resource.
101
101
  * */
102
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
+
103
108
  this.isLoadingValue = true;
104
109
  this.isLoadedValue = false;
105
110
  this.isPlayingValue = false;
@@ -109,6 +114,7 @@ export class WebAudioEngine implements AudioEngine {
109
114
  this.mediaElement.crossOrigin = "anonymous";
110
115
  this.mediaElement.src = url;
111
116
  this.mediaElement.load();
117
+ this.mediaElement.playbackRate = this.currentPlaybackRate;
112
118
 
113
119
  // If the server doesn't honour the CORS preflight, fall back to a
114
120
  // non-CORS load and tear down the Web Audio graph so the element
@@ -123,6 +129,7 @@ export class WebAudioEngine implements AudioEngine {
123
129
  this.mediaElement.removeAttribute("crossOrigin");
124
130
  this.mediaElement.src = url;
125
131
  this.mediaElement.load();
132
+ this.mediaElement.playbackRate = this.currentPlaybackRate;
126
133
  };
127
134
  const onCORSSuccess = () => cleanup();
128
135
  this.mediaElement.addEventListener("error", onCORSError);
@@ -130,6 +137,7 @@ export class WebAudioEngine implements AudioEngine {
130
137
  } else {
131
138
  this.mediaElement.src = url;
132
139
  this.mediaElement.load();
140
+ this.mediaElement.playbackRate = this.currentPlaybackRate;
133
141
  }
134
142
  }
135
143
 
@@ -203,7 +211,7 @@ export class WebAudioEngine implements AudioEngine {
203
211
  this.mediaElement.addEventListener("progress", this.boundOnProgress);
204
212
 
205
213
  // Re-apply current volume and playback rate to the new element
206
- this.mediaElement.volume = this.isMutedValue ? 0 : this.playback.state.volume;
214
+ this.mediaElement.volume = this.isMutedValue ? 0 : this.currentVolume;
207
215
  this.mediaElement.playbackRate = this.currentPlaybackRate;
208
216
 
209
217
  // Check if metadata is already loaded (common with preloaded elements)
@@ -370,23 +378,23 @@ export class WebAudioEngine implements AudioEngine {
370
378
  */
371
379
  public setVolume(volume: number): void {
372
380
  if (volume < 0) {
381
+ this.currentVolume = 0;
373
382
  this.mediaElement.volume = 0;
374
383
  if (this.gainNode) {
375
384
  this.gainNode.gain.value = 0;
376
385
  }
377
386
  this.isMutedValue = true;
378
- this.playback.state.volume = 0;
379
387
  return;
380
388
  }
381
389
  if (volume > 1) {
382
390
  this.setVolume(volume / 100);
383
391
  return;
384
392
  }
393
+ this.currentVolume = volume;
385
394
  this.mediaElement.volume = volume;
386
395
  if (this.gainNode) {
387
396
  this.gainNode.gain.value = volume;
388
397
  }
389
- this.playback.state.volume = volume;
390
398
  }
391
399
 
392
400
  /**
@@ -34,6 +34,7 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
34
34
  private _defaults;
35
35
  private _settings;
36
36
  private _preferencesEditor;
37
+ private _mediaSessionEnabled;
37
38
  private pool;
38
39
  private readonly _navigatorProtector;
39
40
  private readonly _keyboardPeripheralsManager;
@@ -1,7 +1,7 @@
1
1
  import { Publication } from "@readium/shared";
2
2
  import { WebAudioEngine } from "./engine/WebAudioEngine";
3
3
  export declare class AudioPoolManager {
4
- private preloadedElements;
4
+ private readonly pool;
5
5
  private _audioEngine;
6
6
  private readonly _publication;
7
7
  private readonly _supportedAudioTypes;
@@ -10,45 +10,19 @@ export declare class AudioPoolManager {
10
10
  private pickPlayableHref;
11
11
  get audioEngine(): WebAudioEngine;
12
12
  /**
13
- * Sets the current audio by href, using preloaded element if available or loading otherwise,
14
- * and preloads adjacent tracks.
15
- * @param href The URL of the audio resource.
16
- * @param publication The publication containing the reading order.
17
- * @param currentIndex The current track index.
18
- * @param direction The navigation direction ('forward' or 'backward').
13
+ * Ensures an audio element exists in the pool for the given href.
14
+ * If one already exists, it is left untouched (preserving its buffered data).
19
15
  */
20
- setCurrentAudio(currentIndex: number, direction: 'forward' | 'backward'): void;
21
- preload(href: string): void;
16
+ private ensure;
22
17
  /**
23
- * Retrieves a preloaded audio element by URL.
24
- * @param href The URL of the audio resource.
25
- * @returns The preloaded HTMLAudioElement, or undefined if not preloaded.
18
+ * Updates the pool around the given index: ensures elements exist within
19
+ * the LOWER_BOUNDARY and disposes those beyond the UPPER_BOUNDARY.
26
20
  */
27
- get(href: string): HTMLAudioElement | undefined;
21
+ private update;
28
22
  /**
29
- * Removes a preloaded element from the pool.
30
- * @param href The URL of the audio resource.
31
- */
32
- clear(href: string): void;
33
- /**
34
- * Preloads the next track in the reading order.
35
- * @param publication The publication containing the reading order.
36
- * @param currentIndex The current track index.
37
- */
38
- preloadNext(currentIndex: number): void;
39
- /**
40
- * Preloads the previous track in the reading order.
41
- * @param currentIndex The current track index.
42
- */
43
- preloadPrevious(currentIndex: number): void;
44
- /**
45
- * Preloads adjacent tracks (previous and next) for smoother navigation.
46
- * @param currentIndex The current track index.
47
- * @param direction The navigation direction ('forward' or 'backward').
48
- */
49
- preloadAdjacent(currentIndex: number, direction?: 'forward' | 'backward'): void;
50
- /**
51
- * Destroys the pool by stopping the engine and clearing all preloaded elements.
23
+ * Sets the current audio for playback at the given track index.
24
+ * The element is always sourced from the pool — never loaded ad-hoc on the engine.
52
25
  */
26
+ setCurrentAudio(currentIndex: number, _direction: 'forward' | 'backward'): void;
53
27
  destroy(): void;
54
28
  }
@@ -10,10 +10,6 @@ export interface PlaybackState {
10
10
  * The duration of the audio resource.
11
11
  */
12
12
  duration: number;
13
- /**
14
- * The volume of the audio resource.
15
- */
16
- volume: number;
17
13
  }
18
14
  /**
19
15
  * Playback interface for an audio engine state
@@ -7,6 +7,7 @@ export declare class WebAudioEngine implements AudioEngine {
7
7
  private sourceNode;
8
8
  private gainNode;
9
9
  private listeners;
10
+ private currentVolume;
10
11
  private currentPlaybackRate;
11
12
  private isMutedValue;
12
13
  private isPlayingValue;