@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/dist/index.js +357 -393
- package/dist/index.umd.cjs +18 -18
- package/package.json +1 -1
- package/src/audio/AudioNavigator.ts +7 -8
- package/src/audio/AudioPoolManager.ts +63 -78
- package/src/audio/engine/AudioEngine.ts +0 -5
- package/src/audio/engine/WebAudioEngine.ts +12 -4
- package/types/src/audio/AudioNavigator.d.ts +1 -0
- package/types/src/audio/AudioPoolManager.d.ts +10 -36
- package/types/src/audio/engine/AudioEngine.d.ts +0 -4
- package/types/src/audio/engine/WebAudioEngine.d.ts +1 -0
package/package.json
CHANGED
|
@@ -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 && !
|
|
199
|
+
if (this._settings.enableMediaSession && !this._mediaSessionEnabled) {
|
|
200
|
+
this._mediaSessionEnabled = true;
|
|
204
201
|
this.setupMediaSession();
|
|
205
|
-
} else if (!this._settings.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
|
|
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
|
-
*
|
|
52
|
-
*
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.
|
|
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
|
-
|
|
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
|
-
*
|
|
87
|
-
*
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
73
|
+
private update(currentIndex: number): void {
|
|
74
|
+
const items = this._publication.readingOrder.items;
|
|
75
|
+
const keep = new Set<string>();
|
|
93
76
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
*
|
|
129
|
-
*
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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.
|
|
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.
|
|
128
|
+
for (const [, element] of this.pool) {
|
|
129
|
+
element.removeAttribute("src");
|
|
130
|
+
element.load();
|
|
131
|
+
}
|
|
132
|
+
this.pool.clear();
|
|
148
133
|
}
|
|
149
134
|
}
|
|
@@ -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.
|
|
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
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
21
|
-
preload(href: string): void;
|
|
16
|
+
private ensure;
|
|
22
17
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
|
|
21
|
+
private update;
|
|
28
22
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
}
|