@readium/navigator 2.4.0-beta.1 → 2.4.0-beta.10

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.1",
3
+ "version": "2.4.0-beta.10",
4
4
  "type": "module",
5
5
  "description": "Next generation SDK for publications in Web Apps",
6
6
  "author": "readium",
@@ -1,5 +1,5 @@
1
- import { Link, Locator, LocatorLocations, Publication } from "@readium/shared";
2
- import { MediaNavigator } from "../Navigator";
1
+ import { Link, Locator, LocatorLocations, Publication, Timeline, TimelineItem } from "@readium/shared";
2
+ import { MediaNavigator, IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator";
3
3
  import { Configurable } from "../preferences";
4
4
  import { WebAudioEngine, PlaybackState } from "./engine";
5
5
  import {
@@ -11,10 +11,15 @@ import {
11
11
  IAudioDefaults
12
12
  } from "./preferences";
13
13
  import { AudioPoolManager } from "./AudioPoolManager";
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";
14
18
 
15
19
  export interface AudioNavigatorListeners {
16
20
  trackLoaded: (media: HTMLMediaElement) => void;
17
21
  positionChanged: (locator: Locator) => void;
22
+ timelineItemChanged: (item: TimelineItem | undefined) => void;
18
23
  error: (error: any, locator: Locator) => void;
19
24
  trackEnded: (locator: Locator) => void;
20
25
  play: (locator: Locator) => void;
@@ -23,17 +28,40 @@ export interface AudioNavigatorListeners {
23
28
  stalled: (isStalled: boolean) => void;
24
29
  seeking: (isSeeking: boolean) => void;
25
30
  seekable: (seekable: TimeRanges) => void;
31
+ contentProtection: (type: string, data: SuspiciousActivityEvent) => void;
32
+ peripheral: (data: KeyboardEventData) => void;
33
+ contextMenu: (data: ContextMenuEvent) => void;
26
34
  }
27
35
 
36
+ const defaultListeners = (listeners: Partial<AudioNavigatorListeners>): AudioNavigatorListeners => ({
37
+ trackLoaded: listeners.trackLoaded ?? (() => {}),
38
+ positionChanged: listeners.positionChanged ?? (() => {}),
39
+ timelineItemChanged: listeners.timelineItemChanged ?? (() => {}),
40
+ error: listeners.error ?? (() => {}),
41
+ trackEnded: listeners.trackEnded ?? (() => {}),
42
+ play: listeners.play ?? (() => {}),
43
+ pause: listeners.pause ?? (() => {}),
44
+ metadataLoaded: listeners.metadataLoaded ?? (() => {}),
45
+ stalled: listeners.stalled ?? (() => {}),
46
+ seeking: listeners.seeking ?? (() => {}),
47
+ seekable: listeners.seekable ?? (() => {}),
48
+ contentProtection: listeners.contentProtection ?? (() => {}),
49
+ peripheral: listeners.peripheral ?? (() => {}),
50
+ contextMenu: listeners.contextMenu ?? (() => {}),
51
+ });
52
+
28
53
  export interface AudioNavigatorConfiguration {
29
54
  preferences: IAudioPreferences;
30
55
  defaults: IAudioDefaults;
56
+ contentProtection?: IContentProtectionConfig;
57
+ keyboardPeripherals?: IKeyboardPeripheralsConfig;
31
58
  }
32
59
 
33
60
  export class AudioNavigator extends MediaNavigator implements Configurable<AudioSettings, AudioPreferences> {
34
61
  private readonly pub: Publication;
35
62
  private positionPollInterval: ReturnType<typeof setInterval> | null = null;
36
63
  private navigationId: number = 0;
64
+ private _playIntent: boolean = false;
37
65
  private listeners: AudioNavigatorListeners;
38
66
  private currentLocation!: Locator;
39
67
 
@@ -41,7 +69,13 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
41
69
  private _defaults: AudioDefaults;
42
70
  private _settings: AudioSettings;
43
71
  private _preferencesEditor: AudioPreferencesEditor | null = null;
72
+ private _mediaSessionEnabled: boolean = false;
44
73
  private pool: AudioPoolManager;
74
+ private readonly _navigatorProtector: AudioNavigatorProtector | null = null;
75
+ private _currentTimelineItem: TimelineItem | undefined;
76
+ private readonly _keyboardPeripheralsManager: KeyboardPeripherals | null = null;
77
+ private readonly _suspiciousActivityListener: ((event: Event) => void) | null = null;
78
+ private readonly _keyboardPeripheralListener: ((event: Event) => void) | null = null;
45
79
 
46
80
  constructor(publication: Publication, listeners: AudioNavigatorListeners, initialPosition?: Locator, configuration: AudioNavigatorConfiguration = {
47
81
  preferences: {},
@@ -49,11 +83,11 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
49
83
  }) {
50
84
  super();
51
85
  this.pub = publication;
86
+ this.listeners = defaultListeners(listeners);
52
87
 
53
88
  this._preferences = new AudioPreferences(configuration.preferences);
54
89
  this._defaults = new AudioDefaults(configuration.defaults);
55
90
  this._settings = new AudioSettings(this._preferences, this._defaults);
56
- this.listeners = listeners;
57
91
 
58
92
  if (initialPosition) {
59
93
  this.currentLocation = this.ensureLocatorLocations(initialPosition);
@@ -81,27 +115,59 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
81
115
  state: {
82
116
  currentTime: initialTime,
83
117
  duration: 0,
84
- volume: this._settings.volume
85
118
  } as PlaybackState,
86
119
  playWhenReady: false,
87
120
  index: trackIndex
88
121
  }
89
122
  });
90
123
 
91
- this.pool = new AudioPoolManager(audioEngine);
92
- this.setupEventListeners();
124
+ this.pool = new AudioPoolManager(audioEngine, publication);
125
+
126
+ // Initialize content protection
127
+ const contentProtection = configuration.contentProtection || {};
128
+ const keyboardPeripherals = this.mergeKeyboardPeripherals(
129
+ contentProtection,
130
+ configuration.keyboardPeripherals || []
131
+ );
132
+
133
+ if (contentProtection.disableContextMenu ||
134
+ contentProtection.checkAutomation ||
135
+ contentProtection.checkIFrameEmbedding ||
136
+ contentProtection.monitorDevTools ||
137
+ contentProtection.protectPrinting?.disable ||
138
+ contentProtection.disableDragAndDrop ||
139
+ contentProtection.protectCopy) {
140
+ this._navigatorProtector = new AudioNavigatorProtector(contentProtection);
141
+ this._suspiciousActivityListener = (event: Event) => {
142
+ const { type, ...detail } = (event as CustomEvent).detail;
143
+ if (type === "context_menu") {
144
+ this.listeners.contextMenu(detail as ContextMenuEvent);
145
+ } else {
146
+ this.listeners.contentProtection(type, detail as SuspiciousActivityEvent);
147
+ }
148
+ };
149
+ window.addEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
150
+ }
93
151
 
94
- if (this._settings.enableMediaSession) {
95
- this.setupMediaSession();
152
+ if (keyboardPeripherals.length > 0) {
153
+ this._keyboardPeripheralsManager = new KeyboardPeripherals({ keyboardPeripherals });
154
+ this._keyboardPeripheralListener = (event: Event) => {
155
+ this.listeners.peripheral((event as CustomEvent).detail);
156
+ };
157
+ window.addEventListener(NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT, this._keyboardPeripheralListener);
96
158
  }
97
159
 
98
- this.pool.setCurrentAudio(initialHref, this.pub, trackIndex, "forward");
160
+ this.setupEventListeners();
161
+ this.applyPreferences();
162
+
163
+ this.pool.setCurrentAudio(trackIndex, "forward");
99
164
 
100
165
  // Load and seek to initial position, then notify consumer.
101
166
  // No cancellation needed here — the constructor runs once.
102
167
  this.waitForLoadedAndSeeked(initialTime)
103
168
  .then(() => {
104
169
  this.listeners.trackLoaded(this.pool.audioEngine.getMediaElement());
170
+ this._notifyTimelineChange(this.currentLocator);
105
171
  this.listeners.positionChanged(this.currentLocator);
106
172
  })
107
173
  .catch(() => {
@@ -126,7 +192,6 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
126
192
  }
127
193
 
128
194
  private applyPreferences(): void {
129
- const oldSettings = this._settings;
130
195
  this._settings = new AudioSettings(this._preferences, this._defaults);
131
196
 
132
197
  if (this._preferencesEditor !== null) {
@@ -136,9 +201,11 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
136
201
  this.pool.audioEngine.setVolume(this._settings.volume);
137
202
  this.pool.audioEngine.setPlaybackRate(this._settings.playbackRate, this._settings.preservePitch);
138
203
 
139
- if (this._settings.enableMediaSession && !oldSettings.enableMediaSession) {
204
+ if (this._settings.enableMediaSession && !this._mediaSessionEnabled) {
205
+ this._mediaSessionEnabled = true;
140
206
  this.setupMediaSession();
141
- } else if (!this._settings.enableMediaSession && oldSettings.enableMediaSession) {
207
+ } else if (!this._settings.enableMediaSession && this._mediaSessionEnabled) {
208
+ this._mediaSessionEnabled = false;
142
209
  this.destroyMediaSession();
143
210
  }
144
211
  }
@@ -147,6 +214,18 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
147
214
  return this.pub;
148
215
  }
149
216
 
217
+ get timeline(): Timeline {
218
+ return this.pub.timeline;
219
+ }
220
+
221
+ private _notifyTimelineChange(locator: Locator): void {
222
+ const item = this.pub.timeline.locate(locator);
223
+ if (item !== this._currentTimelineItem) {
224
+ this._currentTimelineItem = item;
225
+ this.listeners.timelineItemChanged(item);
226
+ }
227
+ }
228
+
150
229
  private ensureLocatorLocations(locator: Locator): Locator {
151
230
  return new Locator({
152
231
  ...locator,
@@ -264,6 +343,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
264
343
  fragments: [`t=${this.duration}`]
265
344
  }));
266
345
  this.listeners.trackEnded(this.currentLocator);
346
+ if (!this.canGoForward) return;
267
347
  await this.nextTrack();
268
348
  if (this._settings.autoPlay) this.play();
269
349
  });
@@ -293,6 +373,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
293
373
  progression,
294
374
  fragments: [`t=${currentTime}`]
295
375
  }));
376
+ this._notifyTimelineChange(this.currentLocation);
296
377
  this.listeners.positionChanged(this.currentLocation);
297
378
  }
298
379
  });
@@ -300,6 +381,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
300
381
  this.pool.audioEngine.on("seeking", () => this.listeners.seeking(true));
301
382
  this.pool.audioEngine.on("waiting", () => this.listeners.seeking(true));
302
383
  this.pool.audioEngine.on("stalled", () => this.listeners.stalled(true));
384
+ this.pool.audioEngine.on("canplaythrough", () => this.listeners.stalled(false));
303
385
  this.pool.audioEngine.on("progress", (seekable: TimeRanges) => this.listeners.seekable(seekable));
304
386
 
305
387
  this.pool.audioEngine.on("loadedmetadata", () => {
@@ -322,12 +404,14 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
322
404
  if (!("mediaSession" in navigator)) return;
323
405
  const trackIndex = this.currentTrackIndex();
324
406
  const track = this.pub.readingOrder.items[trackIndex];
407
+ const cover = this.pub.getCover();
325
408
  navigator.mediaSession.metadata = new MediaMetadata({
326
409
  title: track?.title || `Track ${trackIndex + 1}`,
327
410
  artist: this.pub.metadata.authors
328
411
  ? this.pub.metadata.authors.items.map((a) => a.name.getTranslation()).join(", ")
329
412
  : undefined,
330
413
  album: this.pub.metadata.title.getTranslation(),
414
+ artwork: cover ? [{ src: cover.href, type: cover.type }] : undefined,
331
415
  });
332
416
  }
333
417
 
@@ -342,6 +426,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
342
426
  progression,
343
427
  fragments: [`t=${currentTime}`]
344
428
  }));
429
+ this._notifyTimelineChange(this.currentLocation);
345
430
  this.listeners.positionChanged(this.currentLocation);
346
431
  }, this._settings.pollInterval);
347
432
  }
@@ -367,17 +452,25 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
367
452
 
368
453
  const id = ++this.navigationId;
369
454
  const direction: "forward" | "backward" = trackIndex >= this.currentTrackIndex() ? "forward" : "backward";
370
- const wasPlaying = this.isPlaying;
455
+ // Use _playIntent rather than isPlaying — setMediaElement resets the
456
+ // engine's playing flag, so a rapid second go() would see false and
457
+ // never resume playback.
458
+ const wasPlaying = this.isPlaying || this._playIntent;
459
+ this._playIntent = wasPlaying;
371
460
 
372
461
  this.stopPositionPolling();
373
- this.pool.setCurrentAudio(href, this.pub, trackIndex, direction);
462
+ this.pool.setCurrentAudio(trackIndex, direction);
374
463
  this.currentLocation = locator.copyWithLocations(locator.locations);
375
464
 
376
465
  await this.waitForLoadedAndSeeked(time, id);
377
466
 
378
- if (id !== this.navigationId) return;
467
+ if (id !== this.navigationId) {
468
+ cb(false);
469
+ return;
470
+ }
379
471
 
380
472
  this.listeners.trackLoaded(this.pool.audioEngine.getMediaElement());
473
+ this._notifyTimelineChange(this.currentLocator);
381
474
  this.listeners.positionChanged(this.currentLocator);
382
475
 
383
476
  if (this._settings.enableMediaSession) {
@@ -390,6 +483,8 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
390
483
  } catch (error) {
391
484
  console.error("Failed to go to locator:", error);
392
485
  cb(false);
486
+ } finally {
487
+ this._playIntent = false;
393
488
  }
394
489
  }
395
490
 
@@ -399,7 +494,8 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
399
494
  cb(false);
400
495
  return;
401
496
  }
402
- const locator = this.createLocator(trackIndex, 0);
497
+ const time = link.locator.locations?.time() ?? 0;
498
+ const locator = this.createLocator(trackIndex, time);
403
499
  await this.go(locator, _animated, cb);
404
500
  }
405
501
 
@@ -492,6 +588,14 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
492
588
  destroy(): void {
493
589
  this.stopPositionPolling();
494
590
  this.destroyMediaSession();
591
+ if (this._suspiciousActivityListener) {
592
+ window.removeEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
593
+ }
594
+ if (this._keyboardPeripheralListener) {
595
+ window.removeEventListener(NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT, this._keyboardPeripheralListener);
596
+ }
597
+ this._navigatorProtector?.destroy();
598
+ this._keyboardPeripheralsManager?.destroy();
495
599
  this.pool.destroy();
496
600
  }
497
601
  }
@@ -1,120 +1,130 @@
1
- import { Publication } from "@readium/shared";
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;
10
+ private readonly _publication: Publication;
11
+ private readonly _supportedAudioTypes: Map<string, "probably" | "maybe">;
7
12
 
8
- constructor(audioEngine: WebAudioEngine) {
13
+ constructor(audioEngine: WebAudioEngine, publication: Publication) {
9
14
  this._audioEngine = audioEngine;
15
+ this._publication = publication;
16
+ this._supportedAudioTypes = this.detectSupportedAudioTypes();
10
17
  }
11
18
 
12
- get audioEngine(): WebAudioEngine {
13
- return this._audioEngine;
19
+ private detectSupportedAudioTypes(): Map<string, "probably" | "maybe"> {
20
+ const audio = document.createElement("audio");
21
+ const unique = new Set<string>();
22
+ for (const link of this._publication.readingOrder.items) {
23
+ if (link.type) unique.add(link.type);
24
+ for (const alt of link.alternates?.items ?? []) {
25
+ if (alt.type) unique.add(alt.type);
26
+ }
27
+ }
28
+ const supported = new Map<string, "probably" | "maybe">();
29
+ for (const type of unique) {
30
+ const result = audio.canPlayType(type);
31
+ if (result !== "") supported.set(type, result as "probably" | "maybe");
32
+ }
33
+ return supported;
14
34
  }
15
35
 
16
- /**
17
- * Sets the current audio by href, using preloaded element if available or loading otherwise,
18
- * and preloads adjacent tracks.
19
- * @param href The URL of the audio resource.
20
- * @param publication The publication containing the reading order.
21
- * @param currentIndex The current track index.
22
- * @param direction The navigation direction ('forward' or 'backward').
23
- */
24
- setCurrentAudio(href: string, publication: Publication, currentIndex: number, direction: 'forward' | 'backward'): void {
25
- // When Web Audio is active, preloaded elements lack crossOrigin="anonymous"
26
- // and cannot be connected to MediaElementAudioSourceNode, so bypass the pool.
27
- const preloadedElement = !this.audioEngine.isWebAudioActive ? this.get(href) : undefined;
28
- if (preloadedElement) {
29
- this.audioEngine.setMediaElement(preloadedElement);
30
- this.clear(href);
31
- } else {
32
- this.clear(href);
33
- this.audioEngine.loadAudio(href);
36
+ private pickPlayableHref(link: Link): string {
37
+ const candidates = [link, ...(link.alternates?.items ?? [])];
38
+ let best: { href: string; confidence: "probably" | "maybe" } | undefined;
39
+ for (const candidate of candidates) {
40
+ if (!candidate.type) continue;
41
+ const confidence = this._supportedAudioTypes.get(candidate.type);
42
+ if (!confidence) continue;
43
+ if (confidence === "probably") return candidate.href;
44
+ if (!best) best = { href: candidate.href, confidence };
34
45
  }
35
- this.preloadAdjacent(publication, currentIndex, direction);
46
+ return best?.href ?? link.href;
36
47
  }
37
- preload(href: string): void {
38
- if (this.preloadedElements.has(href)) {
39
- return; // Already preloaded
40
- }
41
48
 
42
- const audioElement = document.createElement("audio");
43
- audioElement.preload = "auto";
44
- audioElement.src = href;
45
- audioElement.load(); // Start buffering
46
-
47
- this.preloadedElements.set(href, audioElement);
49
+ get audioEngine(): WebAudioEngine {
50
+ return this._audioEngine;
48
51
  }
49
52
 
50
53
  /**
51
- * Retrieves a preloaded audio element by URL.
52
- * @param href The URL of the audio resource.
53
- * @returns The preloaded HTMLAudioElement, or undefined if not preloaded.
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).
54
56
  */
55
- get(href: string): HTMLAudioElement | undefined {
56
- return this.preloadedElements.get(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
+ // 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
+ }
67
+ element.src = href;
68
+ element.load();
69
+ this.pool.set(href, element);
70
+ }
71
+ return element;
57
72
  }
58
73
 
59
74
  /**
60
- * Removes a preloaded element from the pool.
61
- * @param href The URL of the audio resource.
75
+ * Updates the pool around the given index: ensures elements exist within
76
+ * the LOWER_BOUNDARY and disposes those beyond the UPPER_BOUNDARY.
62
77
  */
63
- clear(href: string): void {
64
- this.preloadedElements.delete(href);
65
- }
78
+ private update(currentIndex: number): void {
79
+ const items = this._publication.readingOrder.items;
80
+ const keep = new Set<string>();
66
81
 
67
- /**
68
- * Preloads the next track in the reading order.
69
- * @param publication The publication containing the reading order.
70
- * @param currentIndex The current track index.
71
- */
72
- preloadNext(publication: Publication, currentIndex: number): void {
73
- const nextIndex = currentIndex + 1;
74
- if (nextIndex < publication.readingOrder.items.length) {
75
- const nextLink = publication.readingOrder.items[nextIndex];
76
- if (nextLink.href) {
77
- this.preload(nextLink.href);
82
+ for (let j = 0; j < items.length; j++) {
83
+ const href = this.pickPlayableHref(items[j]);
84
+ if (j >= currentIndex - LOWER_BOUNDARY && j <= currentIndex + LOWER_BOUNDARY) {
85
+ this.ensure(href);
86
+ keep.add(href);
87
+ } else if (j >= currentIndex - UPPER_BOUNDARY && j <= currentIndex + UPPER_BOUNDARY) {
88
+ // Between lower and upper: keep if already loaded, don't create
89
+ if (this.pool.has(href)) {
90
+ keep.add(href);
91
+ }
78
92
  }
79
93
  }
80
- }
81
94
 
82
- /**
83
- * Preloads the previous track in the reading order.
84
- * @param publication The publication containing the reading order.
85
- * @param currentIndex The current track index.
86
- */
87
- preloadPrevious(publication: Publication, currentIndex: number): void {
88
- const prevIndex = currentIndex - 1;
89
- if (prevIndex >= 0) {
90
- const prevLink = publication.readingOrder.items[prevIndex];
91
- if (prevLink.href) {
92
- this.preload(prevLink.href);
95
+ // Dispose elements beyond the upper boundary
96
+ for (const [href, element] of this.pool) {
97
+ if (!keep.has(href)) {
98
+ element.removeAttribute("src");
99
+ element.load(); // release network resources
100
+ this.pool.delete(href);
93
101
  }
94
102
  }
95
103
  }
96
104
 
97
105
  /**
98
- * Preloads adjacent tracks (previous and next) for smoother navigation.
99
- * @param publication The publication containing the reading order.
100
- * @param currentIndex The current track index.
101
- * @param direction The navigation direction ('forward' or 'backward').
106
+ * Sets the current audio for playback at the given track index.
107
+ * The element is always sourced from the pool — never loaded ad-hoc on the engine.
102
108
  */
103
- preloadAdjacent(publication: Publication, currentIndex: number, direction: 'forward' | 'backward' = 'forward'): void {
104
- if (direction === 'forward') {
105
- this.preloadNext(publication, currentIndex);
106
- this.preloadPrevious(publication, currentIndex);
107
- } else {
108
- this.preloadPrevious(publication, currentIndex);
109
- this.preloadNext(publication, currentIndex);
110
- }
109
+ setCurrentAudio(currentIndex: number, _direction: 'forward' | 'backward'): void {
110
+ const href = this.pickPlayableHref(this._publication.readingOrder.items[currentIndex]);
111
+ const element = this.ensure(href);
112
+
113
+ this.audioEngine.setMediaElement(element);
114
+
115
+ // Remove from pool so the engine fully owns it and we don't dispose it
116
+ this.pool.delete(href);
117
+
118
+ // Manage the pool around the new position
119
+ this.update(currentIndex);
111
120
  }
112
121
 
113
- /**
114
- * Destroys the pool by stopping the engine and clearing all preloaded elements.
115
- */
116
122
  destroy(): void {
117
123
  this.audioEngine.stop();
118
- this.preloadedElements.clear();
124
+ for (const [, element] of this.pool) {
125
+ element.removeAttribute("src");
126
+ element.load();
127
+ }
128
+ this.pool.clear();
119
129
  }
120
130
  }
@@ -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
  /**
@@ -45,12 +40,6 @@ export interface AudioEngine {
45
40
  */
46
41
  playback: Playback;
47
42
 
48
- /**
49
- * Loads the audio resource at the given URL.
50
- * @param url The URL of the audio resource.
51
- */
52
- loadAudio(url: string): void;
53
-
54
43
  /**
55
44
  * Adds an event listener to the audio engine.
56
45
  * @param event The event name to listen.