@readium/navigator 2.4.0-beta.9 → 2.5.0-beta.1
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/ReadiumCSS-after-B_e3a-PY.js +592 -0
- package/dist/ReadiumCSS-after-C-T_0paD.js +530 -0
- package/dist/ReadiumCSS-after-lr-n3fz2.js +475 -0
- package/dist/ReadiumCSS-after-mXeKKPap.js +490 -0
- package/dist/ReadiumCSS-before-Bjd3POej.js +426 -0
- package/dist/ReadiumCSS-before-CfXPAGaQ.js +425 -0
- package/dist/ReadiumCSS-before-CrNWvuyE.js +425 -0
- package/dist/ReadiumCSS-before-KVen5ceo.js +425 -0
- package/dist/ReadiumCSS-default-BKAG5pGU.js +162 -0
- package/dist/ReadiumCSS-default-C63bYOYF.js +183 -0
- package/dist/ReadiumCSS-default-CclvbeNC.js +162 -0
- package/dist/ReadiumCSS-default-DnlgDaBu.js +180 -0
- package/dist/ReadiumCSS-ebpaj_fonts_patch-Dt2XliTg.js +82 -0
- package/dist/index.js +2642 -3430
- package/dist/index.umd.cjs +4407 -995
- package/package.json +2 -2
- package/src/audio/AudioNavigator.ts +155 -42
- package/src/audio/AudioPoolManager.ts +27 -14
- package/src/audio/engine/AudioEngine.ts +4 -3
- package/src/audio/engine/PreservePitchProcessor.js +166 -101
- package/src/audio/engine/PreservePitchWorklet.ts +2 -17
- package/src/audio/engine/WebAudioEngine.ts +138 -160
- package/src/audio/engine/index.ts +2 -2
- package/src/audio/index.ts +3 -3
- package/src/audio/preferences/AudioDefaults.ts +2 -2
- package/src/audio/preferences/AudioPreferences.ts +11 -11
- package/src/audio/preferences/AudioPreferencesEditor.ts +13 -13
- package/src/audio/preferences/AudioSettings.ts +3 -3
- package/src/audio/preferences/index.ts +4 -4
- package/src/audio/protection/AudioNavigatorProtector.ts +4 -4
- package/src/css/index.ts +1 -1
- package/src/epub/EpubNavigator.ts +113 -78
- package/src/epub/css/Properties.ts +15 -15
- package/src/epub/css/ReadiumCSS.ts +43 -43
- package/src/epub/css/index.ts +2 -2
- package/src/epub/frame/FrameBlobBuilder.ts +31 -31
- package/src/epub/frame/FrameComms.ts +1 -1
- package/src/epub/frame/FrameManager.ts +13 -9
- package/src/epub/frame/FramePoolManager.ts +13 -13
- package/src/epub/frame/index.ts +4 -4
- package/src/epub/fxl/FXLCoordinator.ts +3 -3
- package/src/epub/fxl/FXLFrameManager.ts +8 -8
- package/src/epub/fxl/FXLFramePoolManager.ts +18 -14
- package/src/epub/fxl/FXLPeripherals.ts +4 -4
- package/src/epub/fxl/index.ts +5 -5
- package/src/epub/helpers/scriptMode.ts +45 -0
- package/src/epub/index.ts +6 -5
- package/src/epub/preferences/EpubDefaults.ts +23 -23
- package/src/epub/preferences/EpubPreferences.ts +16 -16
- package/src/epub/preferences/EpubPreferencesEditor.ts +53 -53
- package/src/epub/preferences/EpubSettings.ts +101 -101
- package/src/epub/preferences/index.ts +4 -4
- package/src/helpers/index.ts +2 -2
- package/src/index.ts +8 -8
- package/src/injection/Injector.ts +42 -42
- package/src/injection/epubInjectables.ts +86 -17
- package/src/injection/index.ts +2 -2
- package/src/injection/webpubInjectables.ts +2 -2
- package/src/preferences/Configurable.ts +2 -2
- package/src/preferences/PreferencesEditor.ts +2 -2
- package/src/preferences/guards.ts +2 -2
- package/src/preferences/index.ts +5 -5
- package/src/protection/CopyProtector.ts +5 -1
- package/src/protection/DevToolsDetector.ts +16 -16
- package/src/protection/DragAndDropProtector.ts +14 -1
- package/src/protection/NavigatorProtector.ts +6 -6
- package/src/webpub/WebPubBlobBuilder.ts +1 -1
- package/src/webpub/WebPubFrameManager.ts +8 -8
- package/src/webpub/WebPubFramePoolManager.ts +7 -7
- package/src/webpub/WebPubNavigator.ts +27 -27
- package/src/webpub/css/Properties.ts +3 -3
- package/src/webpub/css/WebPubCSS.ts +11 -11
- package/src/webpub/css/index.ts +2 -2
- package/src/webpub/index.ts +6 -6
- package/src/webpub/preferences/WebPubDefaults.ts +12 -12
- package/src/webpub/preferences/WebPubPreferences.ts +8 -8
- package/src/webpub/preferences/WebPubPreferencesEditor.ts +31 -31
- package/src/webpub/preferences/WebPubSettings.ts +45 -45
- package/src/webpub/preferences/index.ts +4 -4
- package/types/src/audio/AudioNavigator.d.ts +34 -5
- package/types/src/audio/AudioPoolManager.d.ts +7 -4
- package/types/src/audio/engine/AudioEngine.d.ts +4 -3
- package/types/src/audio/engine/PreservePitchWorklet.d.ts +1 -4
- package/types/src/audio/engine/WebAudioEngine.d.ts +15 -9
- package/types/src/audio/engine/index.d.ts +2 -2
- package/types/src/audio/index.d.ts +3 -3
- package/types/src/audio/preferences/AudioPreferences.d.ts +9 -9
- package/types/src/audio/preferences/AudioPreferencesEditor.d.ts +4 -4
- package/types/src/audio/preferences/AudioSettings.d.ts +3 -3
- package/types/src/audio/preferences/index.d.ts +4 -4
- package/types/src/audio/protection/AudioNavigatorProtector.d.ts +2 -2
- package/types/src/css/index.d.ts +1 -1
- package/types/src/epub/EpubNavigator.d.ts +15 -14
- package/types/src/epub/css/Properties.d.ts +2 -2
- package/types/src/epub/css/ReadiumCSS.d.ts +3 -3
- package/types/src/epub/css/index.d.ts +2 -2
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +1 -1
- package/types/src/epub/frame/FrameComms.d.ts +1 -1
- package/types/src/epub/frame/FrameManager.d.ts +3 -2
- package/types/src/epub/frame/FramePoolManager.d.ts +3 -3
- package/types/src/epub/frame/index.d.ts +4 -4
- package/types/src/epub/fxl/FXLFrameManager.d.ts +3 -3
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +5 -5
- package/types/src/epub/fxl/FXLPeripherals.d.ts +2 -2
- package/types/src/epub/fxl/index.d.ts +5 -5
- package/types/src/epub/helpers/scriptMode.d.ts +16 -0
- package/types/src/epub/index.d.ts +6 -5
- package/types/src/epub/preferences/EpubDefaults.d.ts +1 -1
- package/types/src/epub/preferences/EpubPreferences.d.ts +2 -2
- package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +5 -5
- package/types/src/epub/preferences/EpubSettings.d.ts +4 -4
- package/types/src/epub/preferences/index.d.ts +4 -4
- package/types/src/helpers/index.d.ts +2 -2
- package/types/src/index.d.ts +8 -8
- package/types/src/injection/Injector.d.ts +1 -1
- package/types/src/injection/epubInjectables.d.ts +5 -3
- package/types/src/injection/index.d.ts +2 -2
- package/types/src/injection/webpubInjectables.d.ts +1 -1
- package/types/src/preferences/Configurable.d.ts +1 -1
- package/types/src/preferences/PreferencesEditor.d.ts +1 -1
- package/types/src/preferences/guards.d.ts +1 -1
- package/types/src/preferences/index.d.ts +5 -5
- package/types/src/protection/CopyProtector.d.ts +1 -0
- package/types/src/protection/DragAndDropProtector.d.ts +2 -0
- package/types/src/protection/NavigatorProtector.d.ts +1 -1
- package/types/src/webpub/WebPubBlobBuilder.d.ts +1 -1
- package/types/src/webpub/WebPubFrameManager.d.ts +2 -2
- package/types/src/webpub/WebPubFramePoolManager.d.ts +3 -3
- package/types/src/webpub/WebPubNavigator.d.ts +10 -10
- package/types/src/webpub/css/Properties.d.ts +2 -2
- package/types/src/webpub/css/WebPubCSS.d.ts +2 -2
- package/types/src/webpub/css/index.d.ts +2 -2
- package/types/src/webpub/index.d.ts +6 -6
- package/types/src/webpub/preferences/WebPubDefaults.d.ts +1 -1
- package/types/src/webpub/preferences/WebPubPreferences.d.ts +2 -2
- package/types/src/webpub/preferences/WebPubPreferencesEditor.d.ts +5 -5
- package/types/src/webpub/preferences/WebPubSettings.d.ts +4 -4
- package/types/src/webpub/preferences/index.d.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@readium/navigator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Next generation SDK for publications in Web Apps",
|
|
6
6
|
"author": "readium",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"generate:css-selector": "node scripts/generate-css-selector.js"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@readium/css": "^2.0.
|
|
53
|
+
"@readium/css": "^2.0.1",
|
|
54
54
|
"@readium/navigator-html-injectables": "workspace:*",
|
|
55
55
|
"@readium/shared": "workspace:*",
|
|
56
56
|
"@types/path-browserify": "^1.0.3",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Link, Locator, LocatorLocations, Publication, Timeline, TimelineItem } from "@readium/shared";
|
|
2
|
-
import { MediaNavigator, IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator";
|
|
3
|
-
import { Configurable } from "../preferences";
|
|
4
|
-
import { WebAudioEngine, PlaybackState } from "./engine";
|
|
2
|
+
import { MediaNavigator, IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator.ts";
|
|
3
|
+
import { Configurable } from "../preferences/Configurable.ts";
|
|
4
|
+
import { WebAudioEngine, PlaybackState } from "./engine/index.ts";
|
|
5
5
|
import {
|
|
6
6
|
AudioPreferences,
|
|
7
7
|
AudioDefaults,
|
|
@@ -9,12 +9,19 @@ import {
|
|
|
9
9
|
AudioPreferencesEditor,
|
|
10
10
|
IAudioPreferences,
|
|
11
11
|
IAudioDefaults
|
|
12
|
-
} from "./preferences";
|
|
13
|
-
import { AudioPoolManager } from "./AudioPoolManager";
|
|
12
|
+
} from "./preferences/index.ts";
|
|
13
|
+
import { AudioPoolManager } from "./AudioPoolManager.ts";
|
|
14
14
|
import { ContextMenuEvent, KeyboardEventData, SuspiciousActivityEvent } from "@readium/navigator-html-injectables";
|
|
15
|
-
import { AudioNavigatorProtector } from "./protection/AudioNavigatorProtector";
|
|
16
|
-
import { NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT } from "../protection/NavigatorProtector";
|
|
17
|
-
import { KeyboardPeripherals, NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT } from "../peripherals/KeyboardPeripherals";
|
|
15
|
+
import { AudioNavigatorProtector } from "./protection/AudioNavigatorProtector.ts";
|
|
16
|
+
import { NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT } from "../protection/NavigatorProtector.ts";
|
|
17
|
+
import { KeyboardPeripherals, NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT } from "../peripherals/KeyboardPeripherals.ts";
|
|
18
|
+
|
|
19
|
+
export interface AudioMetadata {
|
|
20
|
+
duration: number;
|
|
21
|
+
textTracks: TextTrackList;
|
|
22
|
+
readyState: number;
|
|
23
|
+
networkState: number;
|
|
24
|
+
}
|
|
18
25
|
|
|
19
26
|
export interface AudioNavigatorListeners {
|
|
20
27
|
trackLoaded: (media: HTMLMediaElement) => void;
|
|
@@ -24,13 +31,14 @@ export interface AudioNavigatorListeners {
|
|
|
24
31
|
trackEnded: (locator: Locator) => void;
|
|
25
32
|
play: (locator: Locator) => void;
|
|
26
33
|
pause: (locator: Locator) => void;
|
|
27
|
-
metadataLoaded: (
|
|
34
|
+
metadataLoaded: (metadata: AudioMetadata) => void;
|
|
28
35
|
stalled: (isStalled: boolean) => void;
|
|
29
36
|
seeking: (isSeeking: boolean) => void;
|
|
30
37
|
seekable: (seekable: TimeRanges) => void;
|
|
31
38
|
contentProtection: (type: string, data: SuspiciousActivityEvent) => void;
|
|
32
39
|
peripheral: (data: KeyboardEventData) => void;
|
|
33
40
|
contextMenu: (data: ContextMenuEvent) => void;
|
|
41
|
+
remotePlaybackStateChanged: (state: RemotePlaybackState) => void;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
const defaultListeners = (listeners: Partial<AudioNavigatorListeners>): AudioNavigatorListeners => ({
|
|
@@ -48,12 +56,18 @@ const defaultListeners = (listeners: Partial<AudioNavigatorListeners>): AudioNav
|
|
|
48
56
|
contentProtection: listeners.contentProtection ?? (() => {}),
|
|
49
57
|
peripheral: listeners.peripheral ?? (() => {}),
|
|
50
58
|
contextMenu: listeners.contextMenu ?? (() => {}),
|
|
59
|
+
remotePlaybackStateChanged: listeners.remotePlaybackStateChanged ?? (() => {}),
|
|
51
60
|
});
|
|
52
61
|
|
|
62
|
+
export interface IAudioContentProtectionConfig extends IContentProtectionConfig {
|
|
63
|
+
/** Prevents the media element from being cast to remote devices via the Remote Playback API. */
|
|
64
|
+
disableRemotePlayback?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
export interface AudioNavigatorConfiguration {
|
|
54
68
|
preferences: IAudioPreferences;
|
|
55
69
|
defaults: IAudioDefaults;
|
|
56
|
-
contentProtection?:
|
|
70
|
+
contentProtection?: IAudioContentProtectionConfig;
|
|
57
71
|
keyboardPeripherals?: IKeyboardPeripheralsConfig;
|
|
58
72
|
}
|
|
59
73
|
|
|
@@ -76,6 +90,12 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
76
90
|
private readonly _keyboardPeripheralsManager: KeyboardPeripherals | null = null;
|
|
77
91
|
private readonly _suspiciousActivityListener: ((event: Event) => void) | null = null;
|
|
78
92
|
private readonly _keyboardPeripheralListener: ((event: Event) => void) | null = null;
|
|
93
|
+
private readonly _contentProtection: IAudioContentProtectionConfig;
|
|
94
|
+
/** True while a track transition is in progress; suppresses spurious mid-navigation events. */
|
|
95
|
+
private _isNavigating: boolean = false;
|
|
96
|
+
private _isStalled: boolean = false;
|
|
97
|
+
private _stalledWatchdog: ReturnType<typeof setInterval> | null = null;
|
|
98
|
+
private _stalledCheckTime: number = 0;
|
|
79
99
|
|
|
80
100
|
constructor(publication: Publication, listeners: AudioNavigatorListeners, initialPosition?: Locator, configuration: AudioNavigatorConfiguration = {
|
|
81
101
|
preferences: {},
|
|
@@ -89,6 +109,10 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
89
109
|
this._defaults = new AudioDefaults(configuration.defaults);
|
|
90
110
|
this._settings = new AudioSettings(this._preferences, this._defaults);
|
|
91
111
|
|
|
112
|
+
if (publication.readingOrder.items.length === 0) {
|
|
113
|
+
throw new Error("AudioNavigator: publication has an empty reading order");
|
|
114
|
+
}
|
|
115
|
+
|
|
92
116
|
if (initialPosition) {
|
|
93
117
|
this.currentLocation = this.ensureLocatorLocations(initialPosition);
|
|
94
118
|
} else {
|
|
@@ -98,7 +122,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
98
122
|
type: firstLink.type || "audio/mpeg",
|
|
99
123
|
title: firstLink.title,
|
|
100
124
|
locations: new LocatorLocations({
|
|
101
|
-
position:
|
|
125
|
+
position: 1,
|
|
102
126
|
progression: 0,
|
|
103
127
|
totalProgression: 0,
|
|
104
128
|
fragments: ["t=0"]
|
|
@@ -108,6 +132,9 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
108
132
|
|
|
109
133
|
const initialHref = this.currentLocation.href.split("#")[0];
|
|
110
134
|
const trackIndex = this.hrefToTrackIndex(initialHref);
|
|
135
|
+
if (trackIndex === -1) {
|
|
136
|
+
throw new Error(`AudioNavigator: initial href "${ initialHref }" not found in reading order`);
|
|
137
|
+
}
|
|
111
138
|
const initialTime = this.currentLocation.locations?.time() || 0;
|
|
112
139
|
|
|
113
140
|
const audioEngine = new WebAudioEngine({
|
|
@@ -121,10 +148,11 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
121
148
|
}
|
|
122
149
|
});
|
|
123
150
|
|
|
124
|
-
this.pool = new AudioPoolManager(audioEngine, publication);
|
|
151
|
+
this.pool = new AudioPoolManager(audioEngine, publication, configuration.contentProtection);
|
|
125
152
|
|
|
126
153
|
// Initialize content protection
|
|
127
154
|
const contentProtection = configuration.contentProtection || {};
|
|
155
|
+
this._contentProtection = contentProtection;
|
|
128
156
|
const keyboardPeripherals = this.mergeKeyboardPeripherals(
|
|
129
157
|
contentProtection,
|
|
130
158
|
configuration.keyboardPeripherals || []
|
|
@@ -158,19 +186,27 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
158
186
|
}
|
|
159
187
|
|
|
160
188
|
this.setupEventListeners();
|
|
161
|
-
this.applyPreferences();
|
|
162
189
|
|
|
190
|
+
this._isNavigating = true;
|
|
163
191
|
this.pool.setCurrentAudio(trackIndex, "forward");
|
|
164
192
|
|
|
193
|
+
// applyPreferences() must come after setCurrentAudio() so that the src
|
|
194
|
+
// is already set on the media element when setPlaybackRate() tries to
|
|
195
|
+
// activate the Web Audio graph for the preservePitch polyfill path.
|
|
196
|
+
this.applyPreferences();
|
|
197
|
+
|
|
165
198
|
// Load and seek to initial position, then notify consumer.
|
|
166
199
|
// No cancellation needed here — the constructor runs once.
|
|
167
200
|
this.waitForLoadedAndSeeked(initialTime)
|
|
168
201
|
.then(() => {
|
|
202
|
+
this._isNavigating = false;
|
|
169
203
|
this.listeners.trackLoaded(this.pool.audioEngine.getMediaElement());
|
|
170
204
|
this._notifyTimelineChange(this.currentLocator);
|
|
171
205
|
this.listeners.positionChanged(this.currentLocator);
|
|
206
|
+
this._setupRemotePlayback();
|
|
172
207
|
})
|
|
173
208
|
.catch(() => {
|
|
209
|
+
this._isNavigating = false;
|
|
174
210
|
// Error already forwarded via the error event listener.
|
|
175
211
|
});
|
|
176
212
|
}
|
|
@@ -201,6 +237,8 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
201
237
|
this.pool.audioEngine.setVolume(this._settings.volume);
|
|
202
238
|
this.pool.audioEngine.setPlaybackRate(this._settings.playbackRate, this._settings.preservePitch);
|
|
203
239
|
|
|
240
|
+
if (this.positionPollInterval !== null) this.startPositionPolling();
|
|
241
|
+
|
|
204
242
|
if (this._settings.enableMediaSession && !this._mediaSessionEnabled) {
|
|
205
243
|
this._mediaSessionEnabled = true;
|
|
206
244
|
this.setupMediaSession();
|
|
@@ -223,6 +261,9 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
223
261
|
if (item !== this._currentTimelineItem) {
|
|
224
262
|
this._currentTimelineItem = item;
|
|
225
263
|
this.listeners.timelineItemChanged(item);
|
|
264
|
+
if (this._settings.enableMediaSession) {
|
|
265
|
+
this.updateMediaSessionMetadata();
|
|
266
|
+
}
|
|
226
267
|
}
|
|
227
268
|
}
|
|
228
269
|
|
|
@@ -279,7 +320,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
279
320
|
title: link.title,
|
|
280
321
|
locations: new LocatorLocations({
|
|
281
322
|
progression: duration > 0 ? timestamp / duration : 0,
|
|
282
|
-
position: trackIndex,
|
|
323
|
+
position: trackIndex + 1,
|
|
283
324
|
fragments: [`t=${timestamp}`]
|
|
284
325
|
})
|
|
285
326
|
});
|
|
@@ -338,7 +379,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
338
379
|
this.pool.audioEngine.on("ended", async () => {
|
|
339
380
|
this.stopPositionPolling();
|
|
340
381
|
this.currentLocation = this.currentLocation.copyWithLocations(new LocatorLocations({
|
|
341
|
-
position: this.currentTrackIndex(),
|
|
382
|
+
position: this.currentTrackIndex() + 1,
|
|
342
383
|
progression: 1,
|
|
343
384
|
fragments: [`t=${this.duration}`]
|
|
344
385
|
}));
|
|
@@ -349,46 +390,91 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
349
390
|
});
|
|
350
391
|
|
|
351
392
|
this.pool.audioEngine.on("play", () => {
|
|
393
|
+
if (this._isNavigating) return;
|
|
352
394
|
this.startPositionPolling();
|
|
353
395
|
this.listeners.play(this.currentLocator);
|
|
354
396
|
});
|
|
355
397
|
|
|
356
398
|
this.pool.audioEngine.on("playing", () => {
|
|
357
|
-
this.
|
|
399
|
+
if (this._isNavigating) return;
|
|
400
|
+
this._setStalled(false);
|
|
358
401
|
});
|
|
359
402
|
|
|
360
403
|
this.pool.audioEngine.on("pause", () => {
|
|
404
|
+
if (this._isNavigating) return;
|
|
361
405
|
this.stopPositionPolling();
|
|
362
406
|
this.listeners.pause(this.currentLocator);
|
|
363
407
|
});
|
|
364
408
|
|
|
365
409
|
this.pool.audioEngine.on("seeked", () => {
|
|
410
|
+
if (this._isNavigating) return;
|
|
366
411
|
this.listeners.seeking(false);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
412
|
+
const currentTime = this.currentTime;
|
|
413
|
+
const duration = this.duration;
|
|
414
|
+
const progression = duration > 0 ? currentTime / duration : 0;
|
|
415
|
+
this.currentLocation = this.currentLocation.copyWithLocations(new LocatorLocations({
|
|
416
|
+
position: this.currentTrackIndex() + 1,
|
|
417
|
+
progression,
|
|
418
|
+
fragments: [`t=${currentTime}`]
|
|
419
|
+
}));
|
|
420
|
+
// Always notify on seeked — don't defer to polling — so that a skip
|
|
421
|
+
// crossing a timeline item boundary fires timelineItemChanged immediately
|
|
422
|
+
// regardless of play state. _notifyTimelineChange deduplicates internally.
|
|
423
|
+
this._notifyTimelineChange(this.currentLocation);
|
|
424
|
+
this.listeners.positionChanged(this.currentLocation);
|
|
379
425
|
});
|
|
380
426
|
|
|
381
|
-
this.pool.audioEngine.on("seeking", () => this.listeners.seeking(true));
|
|
382
|
-
this.pool.audioEngine.on("waiting", () => this.listeners.seeking(true));
|
|
383
|
-
this.pool.audioEngine.on("stalled", () => this.
|
|
384
|
-
this.pool.audioEngine.on("canplaythrough", () => this.
|
|
385
|
-
this.pool.audioEngine.on("progress", (seekable: TimeRanges) => this.listeners.seekable(seekable));
|
|
386
|
-
|
|
427
|
+
this.pool.audioEngine.on("seeking", () => { if (!this._isNavigating) this.listeners.seeking(true); });
|
|
428
|
+
this.pool.audioEngine.on("waiting", () => { if (!this._isNavigating) this.listeners.seeking(true); });
|
|
429
|
+
this.pool.audioEngine.on("stalled", () => { if (!this._isNavigating) this._setStalled(true); });
|
|
430
|
+
this.pool.audioEngine.on("canplaythrough", () => { if (!this._isNavigating) this._setStalled(false); });
|
|
431
|
+
this.pool.audioEngine.on("progress", (seekable: TimeRanges) => { if (!this._isNavigating) this.listeners.seekable(seekable); });
|
|
432
|
+
|
|
387
433
|
this.pool.audioEngine.on("loadedmetadata", () => {
|
|
388
|
-
this.
|
|
434
|
+
const mediaElement = this.pool.audioEngine.getMediaElement();
|
|
435
|
+
const metadata: AudioMetadata = {
|
|
436
|
+
duration: this.pool.audioEngine.duration(),
|
|
437
|
+
textTracks: mediaElement.textTracks,
|
|
438
|
+
readyState: mediaElement.readyState,
|
|
439
|
+
networkState: mediaElement.networkState
|
|
440
|
+
};
|
|
441
|
+
this.listeners.metadataLoaded(metadata);
|
|
389
442
|
});
|
|
390
443
|
}
|
|
391
444
|
|
|
445
|
+
private _setStalled(isStalled: boolean): void {
|
|
446
|
+
if (this._isStalled === isStalled) return;
|
|
447
|
+
this._isStalled = isStalled;
|
|
448
|
+
this.listeners.stalled(isStalled);
|
|
449
|
+
if (isStalled) {
|
|
450
|
+
this._stalledCheckTime = this.currentTime;
|
|
451
|
+
this._startStalledWatchdog();
|
|
452
|
+
} else {
|
|
453
|
+
this._stopStalledWatchdog();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private _startStalledWatchdog(): void {
|
|
458
|
+
this._stalledWatchdog = setInterval(() => {
|
|
459
|
+
if (!this.isPlaying) {
|
|
460
|
+
this._setStalled(false);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const t = this.currentTime;
|
|
464
|
+
if (t !== this._stalledCheckTime) {
|
|
465
|
+
this._setStalled(false);
|
|
466
|
+
}
|
|
467
|
+
this._stalledCheckTime = t;
|
|
468
|
+
}, 500);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private _stopStalledWatchdog(): void {
|
|
472
|
+
if (this._stalledWatchdog !== null) {
|
|
473
|
+
clearInterval(this._stalledWatchdog);
|
|
474
|
+
this._stalledWatchdog = null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
392
478
|
private setupMediaSession(): void {
|
|
393
479
|
if (!("mediaSession" in navigator)) return;
|
|
394
480
|
navigator.mediaSession.setActionHandler("play", () => this.play());
|
|
@@ -411,7 +497,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
411
497
|
? this.pub.metadata.authors.items.map((a) => a.name.getTranslation()).join(", ")
|
|
412
498
|
: undefined,
|
|
413
499
|
album: this.pub.metadata.title.getTranslation(),
|
|
414
|
-
artwork: cover ? [{ src: cover.href, type: cover.type }] : undefined,
|
|
500
|
+
artwork: cover ? [{ src: cover.toURL(this.pub.baseURL) ?? cover.href, type: cover.type }] : undefined,
|
|
415
501
|
});
|
|
416
502
|
}
|
|
417
503
|
|
|
@@ -422,7 +508,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
422
508
|
const duration = this.duration;
|
|
423
509
|
const progression = duration > 0 ? currentTime / duration : 0;
|
|
424
510
|
this.currentLocation = this.currentLocation.copyWithLocations(new LocatorLocations({
|
|
425
|
-
position: this.currentTrackIndex(),
|
|
511
|
+
position: this.currentTrackIndex() + 1,
|
|
426
512
|
progression,
|
|
427
513
|
fragments: [`t=${currentTime}`]
|
|
428
514
|
}));
|
|
@@ -451,25 +537,27 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
451
537
|
}
|
|
452
538
|
|
|
453
539
|
const id = ++this.navigationId;
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
// engine's playing flag, so a rapid second go() would see false and
|
|
457
|
-
// never resume playback.
|
|
540
|
+
const previousTrackIndex = this.currentTrackIndex();
|
|
541
|
+
const direction: "forward" | "backward" = trackIndex >= previousTrackIndex ? "forward" : "backward";
|
|
458
542
|
const wasPlaying = this.isPlaying || this._playIntent;
|
|
459
543
|
this._playIntent = wasPlaying;
|
|
460
544
|
|
|
545
|
+
this._isNavigating = true;
|
|
461
546
|
this.stopPositionPolling();
|
|
462
547
|
this.pool.setCurrentAudio(trackIndex, direction);
|
|
463
548
|
this.currentLocation = locator.copyWithLocations(locator.locations);
|
|
464
549
|
|
|
465
550
|
await this.waitForLoadedAndSeeked(time, id);
|
|
551
|
+
this._isNavigating = false;
|
|
466
552
|
|
|
467
553
|
if (id !== this.navigationId) {
|
|
468
554
|
cb(false);
|
|
469
555
|
return;
|
|
470
556
|
}
|
|
471
557
|
|
|
472
|
-
|
|
558
|
+
if (trackIndex !== previousTrackIndex) {
|
|
559
|
+
this.listeners.trackLoaded(this.pool.audioEngine.getMediaElement());
|
|
560
|
+
}
|
|
473
561
|
this._notifyTimelineChange(this.currentLocator);
|
|
474
562
|
this.listeners.positionChanged(this.currentLocator);
|
|
475
563
|
|
|
@@ -481,6 +569,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
481
569
|
|
|
482
570
|
cb(true);
|
|
483
571
|
} catch (error) {
|
|
572
|
+
this._isNavigating = false;
|
|
484
573
|
console.error("Failed to go to locator:", error);
|
|
485
574
|
cb(false);
|
|
486
575
|
} finally {
|
|
@@ -574,6 +663,29 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
574
663
|
return this.currentTrackIndex() < this.pub.readingOrder.items.length - 1;
|
|
575
664
|
}
|
|
576
665
|
|
|
666
|
+
/**
|
|
667
|
+
* The RemotePlayback object for the primary media element.
|
|
668
|
+
* Because the element is never swapped, this reference is stable for the
|
|
669
|
+
* lifetime of the navigator — host apps can store it and call `.prompt()`,
|
|
670
|
+
* `.watchAvailability()`, etc. directly.
|
|
671
|
+
*/
|
|
672
|
+
get remotePlayback(): RemotePlayback | undefined {
|
|
673
|
+
const el = this.pool.audioEngine.getMediaElement();
|
|
674
|
+
return "remote" in el ? el.remote : undefined;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Wires up the optional remotePlaybackStateChanged listener. Called once after initial load. */
|
|
678
|
+
private _setupRemotePlayback(): void {
|
|
679
|
+
if (this._contentProtection.disableRemotePlayback) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const remote = this.remotePlayback;
|
|
683
|
+
if (!remote) return;
|
|
684
|
+
remote.onconnecting = () => this.listeners.remotePlaybackStateChanged("connecting");
|
|
685
|
+
remote.onconnect = () => this.listeners.remotePlaybackStateChanged("connected");
|
|
686
|
+
remote.ondisconnect = () => this.listeners.remotePlaybackStateChanged("disconnected");
|
|
687
|
+
}
|
|
688
|
+
|
|
577
689
|
private destroyMediaSession(): void {
|
|
578
690
|
if (!("mediaSession" in navigator)) return;
|
|
579
691
|
navigator.mediaSession.metadata = null;
|
|
@@ -587,6 +699,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
|
|
|
587
699
|
|
|
588
700
|
destroy(): void {
|
|
589
701
|
this.stopPositionPolling();
|
|
702
|
+
this._stopStalledWatchdog();
|
|
590
703
|
this.destroyMediaSession();
|
|
591
704
|
if (this._suspiciousActivityListener) {
|
|
592
705
|
window.removeEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Link, Publication } from "@readium/shared";
|
|
2
|
-
import { WebAudioEngine } from "./engine/WebAudioEngine";
|
|
2
|
+
import { WebAudioEngine } from "./engine/WebAudioEngine.ts";
|
|
3
|
+
import type { IAudioContentProtectionConfig } from "./AudioNavigator.ts";
|
|
3
4
|
|
|
4
5
|
const UPPER_BOUNDARY = 1;
|
|
5
6
|
const LOWER_BOUNDARY = 1;
|
|
@@ -10,10 +11,14 @@ export class AudioPoolManager {
|
|
|
10
11
|
private readonly _publication: Publication;
|
|
11
12
|
private readonly _supportedAudioTypes: Map<string, "probably" | "maybe">;
|
|
12
13
|
|
|
13
|
-
constructor(audioEngine: WebAudioEngine, publication: Publication) {
|
|
14
|
+
constructor(audioEngine: WebAudioEngine, publication: Publication, contentProtection: IAudioContentProtectionConfig = {}) {
|
|
14
15
|
this._audioEngine = audioEngine;
|
|
15
16
|
this._publication = publication;
|
|
16
17
|
this._supportedAudioTypes = this.detectSupportedAudioTypes();
|
|
18
|
+
|
|
19
|
+
if (contentProtection.disableRemotePlayback) {
|
|
20
|
+
this._audioEngine.getMediaElement().disableRemotePlayback = true;
|
|
21
|
+
}
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
private detectSupportedAudioTypes(): Map<string, "probably" | "maybe"> {
|
|
@@ -34,16 +39,18 @@ export class AudioPoolManager {
|
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
private pickPlayableHref(link: Link): string {
|
|
42
|
+
const base = this._publication.baseURL;
|
|
37
43
|
const candidates = [link, ...(link.alternates?.items ?? [])];
|
|
38
44
|
let best: { href: string; confidence: "probably" | "maybe" } | undefined;
|
|
39
45
|
for (const candidate of candidates) {
|
|
40
46
|
if (!candidate.type) continue;
|
|
41
47
|
const confidence = this._supportedAudioTypes.get(candidate.type);
|
|
42
48
|
if (!confidence) continue;
|
|
43
|
-
|
|
44
|
-
if (
|
|
49
|
+
const href = candidate.toURL(base) ?? candidate.href;
|
|
50
|
+
if (confidence === "probably") return href;
|
|
51
|
+
if (!best) best = { href, confidence };
|
|
45
52
|
}
|
|
46
|
-
return best?.href ?? link.href;
|
|
53
|
+
return best?.href ?? (link.toURL(base) ?? link.href);
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
get audioEngine(): WebAudioEngine {
|
|
@@ -59,8 +66,8 @@ export class AudioPoolManager {
|
|
|
59
66
|
if (!element) {
|
|
60
67
|
element = document.createElement("audio");
|
|
61
68
|
element.preload = "auto";
|
|
62
|
-
//
|
|
63
|
-
//
|
|
69
|
+
// Match the primary element's CORS mode so cached responses
|
|
70
|
+
// are reusable when changeSrc() loads this href on it.
|
|
64
71
|
if (this._audioEngine.isWebAudioActive) {
|
|
65
72
|
element.crossOrigin = "anonymous";
|
|
66
73
|
}
|
|
@@ -74,12 +81,14 @@ export class AudioPoolManager {
|
|
|
74
81
|
/**
|
|
75
82
|
* Updates the pool around the given index: ensures elements exist within
|
|
76
83
|
* the LOWER_BOUNDARY and disposes those beyond the UPPER_BOUNDARY.
|
|
84
|
+
* The current track is excluded — the primary engine element represents it.
|
|
77
85
|
*/
|
|
78
86
|
private update(currentIndex: number): void {
|
|
79
87
|
const items = this._publication.readingOrder.items;
|
|
80
88
|
const keep = new Set<string>();
|
|
81
89
|
|
|
82
90
|
for (let j = 0; j < items.length; j++) {
|
|
91
|
+
if (j === currentIndex) continue; // primary element handles the current track
|
|
83
92
|
const href = this.pickPlayableHref(items[j]);
|
|
84
93
|
if (j >= currentIndex - LOWER_BOUNDARY && j <= currentIndex + LOWER_BOUNDARY) {
|
|
85
94
|
this.ensure(href);
|
|
@@ -103,17 +112,21 @@ export class AudioPoolManager {
|
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
/**
|
|
106
|
-
* Sets the current audio for playback at the given track index
|
|
107
|
-
*
|
|
115
|
+
* Sets the current audio for playback at the given track index by changing
|
|
116
|
+
* the src on the persistent primary element. This preserves the RemotePlayback
|
|
117
|
+
* session and any Web Audio graph connections across track changes.
|
|
108
118
|
*/
|
|
109
119
|
setCurrentAudio(currentIndex: number, _direction: 'forward' | 'backward'): void {
|
|
110
120
|
const href = this.pickPlayableHref(this._publication.readingOrder.items[currentIndex]);
|
|
111
|
-
|
|
121
|
+
this.audioEngine.changeSrc(href);
|
|
112
122
|
|
|
113
|
-
this
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
// Discard any pool entry for this href — the primary element owns it now
|
|
124
|
+
if (this.pool.has(href)) {
|
|
125
|
+
const existing = this.pool.get(href)!;
|
|
126
|
+
existing.removeAttribute("src");
|
|
127
|
+
existing.load();
|
|
128
|
+
this.pool.delete(href);
|
|
129
|
+
}
|
|
117
130
|
|
|
118
131
|
// Manage the pool around the new position
|
|
119
132
|
this.update(currentIndex);
|
|
@@ -55,10 +55,11 @@ export interface AudioEngine {
|
|
|
55
55
|
off(event: string, callback: (data: any) => void): void;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
58
|
+
* Changes the src of the primary media element without swapping it,
|
|
59
|
+
* preserving the RemotePlayback session and all attached event listeners.
|
|
60
|
+
* @param href The URL of the new audio resource.
|
|
60
61
|
*/
|
|
61
|
-
|
|
62
|
+
changeSrc(href: string): void;
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
65
|
* Plays the current audio resource.
|