@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/dist/index.js +72 -81
- package/dist/index.umd.cjs +8 -8
- package/package.json +1 -1
- package/src/audio/AudioNavigator.ts +7 -1
- package/src/audio/AudioPoolManager.ts +6 -10
- package/src/audio/engine/AudioEngine.ts +0 -6
- package/src/audio/engine/WebAudioEngine.ts +32 -58
- package/types/src/audio/AudioNavigator.d.ts +1 -0
- package/types/src/audio/engine/AudioEngine.d.ts +0 -5
- package/types/src/audio/engine/WebAudioEngine.d.ts +0 -5
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|