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

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.
@@ -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.
@@ -66,10 +55,11 @@ export interface AudioEngine {
66
55
  off(event: string, callback: (data: any) => void): void;
67
56
 
68
57
  /**
69
- * Sets the media element for playback, enabling use of preloaded elements from the pool.
70
- * @param element The HTML audio element to use for playback.
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.
71
61
  */
72
- setMediaElement(element: HTMLAudioElement): void;
62
+ changeSrc(href: string): void;
73
63
 
74
64
  /**
75
65
  * Plays the current audio resource.
@@ -16,7 +16,6 @@ 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 currentPlaybackRate: number = 1;
20
19
  private isMutedValue: boolean = false;
21
20
  private isPlayingValue: boolean = false;
22
21
  private isPausedValue: boolean = false;
@@ -48,7 +47,6 @@ export class WebAudioEngine implements AudioEngine {
48
47
 
49
48
  // crossOrigin is set lazily in activateWebAudio() only when the worklet is needed
50
49
  this.mediaElement = document.createElement("audio");
51
- this.setVolume(this.playback.state.volume);
52
50
 
53
51
  // Event listeners (to report the client app about some async events)
54
52
  this.mediaElement.addEventListener("canplaythrough", this.boundOnCanPlayThrough);
@@ -95,138 +93,6 @@ export class WebAudioEngine implements AudioEngine {
95
93
  );
96
94
  }
97
95
 
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
- this.isLoadingValue = true;
104
- this.isLoadedValue = false;
105
- this.isPlayingValue = false;
106
- this.isPausedValue = false;
107
-
108
- if (this.webAudioActive) {
109
- this.mediaElement.crossOrigin = "anonymous";
110
- this.mediaElement.src = url;
111
- this.mediaElement.load();
112
-
113
- // If the server doesn't honour the CORS preflight, fall back to a
114
- // non-CORS load and tear down the Web Audio graph so the element
115
- // is never passed to MediaElementAudioSourceNode in a tainted state.
116
- const cleanup = () => {
117
- this.mediaElement.removeEventListener("error", onCORSError);
118
- this.mediaElement.removeEventListener("canplaythrough", onCORSSuccess);
119
- };
120
- const onCORSError = () => {
121
- cleanup();
122
- this.deactivateWebAudio();
123
- this.mediaElement.removeAttribute("crossOrigin");
124
- this.mediaElement.src = url;
125
- this.mediaElement.load();
126
- };
127
- const onCORSSuccess = () => cleanup();
128
- this.mediaElement.addEventListener("error", onCORSError);
129
- this.mediaElement.addEventListener("canplaythrough", onCORSSuccess);
130
- } else {
131
- this.mediaElement.src = url;
132
- this.mediaElement.load();
133
- }
134
- }
135
-
136
- private deactivateWebAudio(): void {
137
- if (this.worklet) {
138
- this.worklet.destroy();
139
- this.worklet = null;
140
- }
141
- if (this.sourceNode) {
142
- this.sourceNode.disconnect();
143
- this.sourceNode = null;
144
- }
145
- if (this.gainNode) {
146
- this.gainNode.disconnect();
147
- this.gainNode = null;
148
- }
149
- this.webAudioActive = false;
150
- }
151
-
152
- /**
153
- * Sets the media element for playback.
154
- * @param element The HTML audio element to use.
155
- */
156
- public setMediaElement(element: HTMLAudioElement): void {
157
- // Pause the outgoing element before replacing it
158
- this.mediaElement.pause();
159
- this.isPlayingValue = false;
160
- this.isPausedValue = false;
161
-
162
- // Disconnect old source node if it exists
163
- if (this.sourceNode) {
164
- this.sourceNode.disconnect();
165
- this.sourceNode = null;
166
- }
167
-
168
- // Remove old event listeners from current mediaElement
169
- this.mediaElement.removeEventListener("canplaythrough", this.boundOnCanPlayThrough);
170
- this.mediaElement.removeEventListener("timeupdate", this.boundOnTimeUpdate);
171
- this.mediaElement.removeEventListener("error", this.boundOnError);
172
- this.mediaElement.removeEventListener("ended", this.boundOnEnded);
173
- this.mediaElement.removeEventListener("stalled", this.boundOnStalled);
174
- this.mediaElement.removeEventListener("emptied", this.boundOnEmptied);
175
- this.mediaElement.removeEventListener("suspend", this.boundOnSuspend);
176
- this.mediaElement.removeEventListener("waiting", this.boundOnWaiting);
177
- this.mediaElement.removeEventListener("loadedmetadata", this.boundOnLoadedMetadata);
178
- this.mediaElement.removeEventListener("seeking", this.boundOnSeeking);
179
- this.mediaElement.removeEventListener("seeked", this.boundOnSeeked);
180
- this.mediaElement.removeEventListener("play", this.boundOnPlay);
181
- this.mediaElement.removeEventListener("playing", this.boundOnPlaying);
182
- this.mediaElement.removeEventListener("pause", this.boundOnPause);
183
- this.mediaElement.removeEventListener("progress", this.boundOnProgress);
184
-
185
- // Set new media element
186
- this.mediaElement = element;
187
-
188
- // Add event listeners to new element
189
- this.mediaElement.addEventListener("canplaythrough", this.boundOnCanPlayThrough);
190
- this.mediaElement.addEventListener("timeupdate", this.boundOnTimeUpdate);
191
- this.mediaElement.addEventListener("error", this.boundOnError);
192
- this.mediaElement.addEventListener("ended", this.boundOnEnded);
193
- this.mediaElement.addEventListener("stalled", this.boundOnStalled);
194
- this.mediaElement.addEventListener("emptied", this.boundOnEmptied);
195
- this.mediaElement.addEventListener("suspend", this.boundOnSuspend);
196
- this.mediaElement.addEventListener("waiting", this.boundOnWaiting);
197
- this.mediaElement.addEventListener("loadedmetadata", this.boundOnLoadedMetadata);
198
- this.mediaElement.addEventListener("seeking", this.boundOnSeeking);
199
- this.mediaElement.addEventListener("seeked", this.boundOnSeeked);
200
- this.mediaElement.addEventListener("play", this.boundOnPlay);
201
- this.mediaElement.addEventListener("playing", this.boundOnPlaying);
202
- this.mediaElement.addEventListener("pause", this.boundOnPause);
203
- this.mediaElement.addEventListener("progress", this.boundOnProgress);
204
-
205
- // Re-apply current volume and playback rate to the new element
206
- this.mediaElement.volume = this.isMutedValue ? 0 : this.playback.state.volume;
207
- this.mediaElement.playbackRate = this.currentPlaybackRate;
208
-
209
- // Check if metadata is already loaded (common with preloaded elements)
210
- if (this.mediaElement.readyState >= 1) {
211
- this.onLoadedMetadata(new Event('loadedmetadata'));
212
- }
213
-
214
- // Preloaded elements may have already buffered data before being swapped in,
215
- // so progress events would have fired before we were listening. Emit now if
216
- // seekable ranges are already available.
217
- if (this.mediaElement.seekable.length > 0) {
218
- this.onProgress();
219
- }
220
-
221
- // Check if the element is already loaded and trigger appropriate events
222
- if (this.mediaElement.readyState >= 4) {
223
- this.onCanPlayThrough();
224
- } else {
225
- this.isLoadingValue = true;
226
- this.isLoadedValue = false;
227
- }
228
- }
229
-
230
96
  // Ensure AudioContext is running
231
97
  private async ensureAudioContextRunning() {
232
98
  if (!this.audioContext) {
@@ -375,7 +241,6 @@ export class WebAudioEngine implements AudioEngine {
375
241
  this.gainNode.gain.value = 0;
376
242
  }
377
243
  this.isMutedValue = true;
378
- this.playback.state.volume = 0;
379
244
  return;
380
245
  }
381
246
  if (volume > 1) {
@@ -386,7 +251,6 @@ export class WebAudioEngine implements AudioEngine {
386
251
  if (this.gainNode) {
387
252
  this.gainNode.gain.value = volume;
388
253
  }
389
- this.playback.state.volume = volume;
390
254
  }
391
255
 
392
256
  /**
@@ -470,7 +334,6 @@ export class WebAudioEngine implements AudioEngine {
470
334
  * Sets the playback rate of the audio resource with pitch preservation.
471
335
  */
472
336
  public setPlaybackRate(rate: number, preservePitch: boolean): void {
473
- this.currentPlaybackRate = rate;
474
337
  this.mediaElement.playbackRate = rate;
475
338
  if (preservePitch) {
476
339
  if ('preservesPitch' in this.mediaElement) {
@@ -576,6 +439,27 @@ export class WebAudioEngine implements AudioEngine {
576
439
  return this.webAudioActive;
577
440
  }
578
441
 
442
+ /**
443
+ * Changes the src of the primary media element without swapping the element.
444
+ * Preserves the RemotePlayback session and all attached event listeners.
445
+ */
446
+ public changeSrc(href: string): void {
447
+ if (this.mediaElement.src === href) {
448
+ return;
449
+ }
450
+ this.mediaElement.pause();
451
+ this.isPlayingValue = false;
452
+ this.isPausedValue = false;
453
+ this.isLoadedValue = false;
454
+ this.isLoadingValue = true;
455
+ this.isEndedValue = false;
456
+ if (this.webAudioActive) {
457
+ this.mediaElement.crossOrigin = "anonymous";
458
+ }
459
+ this.mediaElement.src = href;
460
+ this.mediaElement.load();
461
+ }
462
+
579
463
  /**
580
464
  * Returns the HTML media element used for playback.
581
465
  */
@@ -0,0 +1,38 @@
1
+ import { NavigatorProtector } from "../../protection/NavigatorProtector";
2
+ import { DragAndDropProtector } from "../../protection/DragAndDropProtector";
3
+ import { CopyProtector } from "../../protection/CopyProtector";
4
+ import { IContentProtectionConfig } from "../../Navigator";
5
+
6
+ export class AudioNavigatorProtector extends NavigatorProtector {
7
+ private dragAndDropProtector?: DragAndDropProtector;
8
+ private copyProtector?: CopyProtector;
9
+
10
+ constructor(config: IContentProtectionConfig = {}) {
11
+ super(config);
12
+
13
+ if (config.disableDragAndDrop) {
14
+ this.dragAndDropProtector = new DragAndDropProtector({
15
+ onDragDetected: (dataTransferTypes) => {
16
+ this.dispatchSuspiciousActivity("drag_detected", { dataTransferTypes, targetFrameSrc: "" });
17
+ },
18
+ onDropDetected: (dataTransferTypes, fileCount) => {
19
+ this.dispatchSuspiciousActivity("drop_detected", { dataTransferTypes, fileCount, targetFrameSrc: "" });
20
+ }
21
+ });
22
+ }
23
+
24
+ if (config.protectCopy) {
25
+ this.copyProtector = new CopyProtector({
26
+ onCopyBlocked: () => {
27
+ this.dispatchSuspiciousActivity("bulk_copy", { targetFrameSrc: "" });
28
+ }
29
+ });
30
+ }
31
+ }
32
+
33
+ public override destroy() {
34
+ super.destroy();
35
+ this.dragAndDropProtector?.destroy();
36
+ this.copyProtector?.destroy();
37
+ }
38
+ }
@@ -85,7 +85,7 @@ export class FrameManager {
85
85
  }
86
86
 
87
87
  // Apply print protection if configured
88
- if (this.contentProtectionConfig.protectPrinting) {
88
+ if (this.contentProtectionConfig.protectPrinting?.disable) {
89
89
  this.comms!.send("print_protection", this.contentProtectionConfig.protectPrinting);
90
90
  }
91
91
  }
@@ -219,7 +219,7 @@ export class FXLFrameManager {
219
219
  }
220
220
 
221
221
  // Apply print protection if configured
222
- if (this.contentProtectionConfig.protectPrinting) {
222
+ if (this.contentProtectionConfig.protectPrinting?.disable) {
223
223
  this.comms!.send("print_protection", this.contentProtectionConfig.protectPrinting);
224
224
  }
225
225
  }
@@ -81,8 +81,8 @@ export const volumeRangeConfig: RangeConfig = {
81
81
  }
82
82
 
83
83
  export const playbackRateRangeConfig: RangeConfig = {
84
- range: [0.5, 2],
85
- step: 0.25
84
+ range: [0.5, 4],
85
+ step: 0.1
86
86
  }
87
87
 
88
88
  export const skipIntervalRangeConfig: RangeConfig = {
@@ -0,0 +1,22 @@
1
+ export interface CopyProtectionOptions {
2
+ onCopyBlocked?: () => void;
3
+ }
4
+
5
+ export class CopyProtector {
6
+ private copyHandler: (event: ClipboardEvent) => void;
7
+
8
+ constructor(options: CopyProtectionOptions = {}) {
9
+ this.copyHandler = (event: ClipboardEvent) => {
10
+ event.preventDefault();
11
+ event.stopPropagation();
12
+ options.onCopyBlocked?.();
13
+ };
14
+
15
+ document.addEventListener("copy", this.copyHandler, true);
16
+ window.addEventListener("unload", () => this.destroy());
17
+ }
18
+
19
+ public destroy() {
20
+ document.removeEventListener("copy", this.copyHandler, true);
21
+ }
22
+ }
@@ -282,6 +282,7 @@ export class DevToolsDetector {
282
282
  // Cleanup Web Worker
283
283
  if (this.workerConsole) {
284
284
  this.workerConsole.destroy();
285
+ this.workerConsole = undefined;
285
286
  }
286
287
 
287
288
  this.isOpen = false;
@@ -0,0 +1,34 @@
1
+ export interface DragAndDropProtectionOptions {
2
+ onDragDetected?: (dataTransferTypes: readonly string[]) => void;
3
+ onDropDetected?: (dataTransferTypes: readonly string[], fileCount: number) => void;
4
+ }
5
+
6
+ export class DragAndDropProtector {
7
+ private dragstartHandler: (event: DragEvent) => void;
8
+ private dropHandler: (event: DragEvent) => void;
9
+
10
+ constructor(options: DragAndDropProtectionOptions = {}) {
11
+ this.dragstartHandler = (event: DragEvent) => {
12
+ event.preventDefault();
13
+ event.stopPropagation();
14
+ options.onDragDetected?.(Array.from(event.dataTransfer?.types ?? []));
15
+ };
16
+
17
+ this.dropHandler = (event: DragEvent) => {
18
+ event.preventDefault();
19
+ event.stopPropagation();
20
+ const types = Array.from(event.dataTransfer?.types ?? []);
21
+ const fileCount = event.dataTransfer?.files.length ?? 0;
22
+ options.onDropDetected?.(types, fileCount);
23
+ };
24
+
25
+ document.addEventListener("dragstart", this.dragstartHandler, true);
26
+ document.addEventListener("drop", this.dropHandler, true);
27
+ window.addEventListener("unload", () => this.destroy());
28
+ }
29
+
30
+ public destroy() {
31
+ document.removeEventListener("dragstart", this.dragstartHandler, true);
32
+ document.removeEventListener("drop", this.dropHandler, true);
33
+ }
34
+ }
@@ -15,7 +15,7 @@ export class NavigatorProtector {
15
15
  private printProtector?: PrintProtector;
16
16
  private contextMenuProtector?: ContextMenuProtector;
17
17
 
18
- private dispatchSuspiciousActivity(type: string, detail: Record<string, unknown>) {
18
+ protected dispatchSuspiciousActivity(type: string, detail: Record<string, unknown>) {
19
19
  const event = new CustomEvent(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, {
20
20
  detail: {
21
21
  type,
@@ -55,7 +55,7 @@ export class NavigatorProtector {
55
55
  }
56
56
  });
57
57
  }
58
-
58
+
59
59
  // Enable iframe embedding detection if explicitly enabled in config
60
60
  if (config.checkIFrameEmbedding) {
61
61
  this.iframeEmbeddingDetector = new IframeEmbeddingDetector({
@@ -74,7 +74,7 @@ export class NavigatorProtector {
74
74
  }
75
75
  });
76
76
  }
77
-
77
+
78
78
  // Enable context menu protection if configured
79
79
  if (config.disableContextMenu) {
80
80
  this.contextMenuProtector = new ContextMenuProtector({
@@ -83,7 +83,7 @@ export class WebPubFrameManager {
83
83
  }
84
84
 
85
85
  // Apply print protection if configured
86
- if (this.contentProtectionConfig.protectPrinting) {
86
+ if (this.contentProtectionConfig.protectPrinting?.disable) {
87
87
  this.comms!.send("print_protection", this.contentProtectionConfig.protectPrinting);
88
88
  }
89
89
  }
@@ -128,8 +128,12 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
128
128
 
129
129
  // Listen for custom events from NavigatorProtector
130
130
  this._suspiciousActivityListener = (event: Event) => {
131
- const customEvent = event as CustomEvent;
132
- this.listeners.contentProtection(customEvent.detail.type, customEvent.detail);
131
+ const { type, ...activity } = (event as CustomEvent).detail;
132
+ if (type === "context_menu") {
133
+ this.listeners.contextMenu(activity as ContextMenuEvent);
134
+ } else {
135
+ this.listeners.contentProtection(type, activity);
136
+ }
133
137
  };
134
138
  window.addEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
135
139
  }
@@ -1,40 +1,70 @@
1
- import { Link, Locator, Publication } from "@readium/shared";
2
- import { MediaNavigator } from "../Navigator";
1
+ import { Link, Locator, Publication, Timeline, TimelineItem } from "@readium/shared";
2
+ import { MediaNavigator, IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator";
3
3
  import { Configurable } from "../preferences";
4
4
  import { AudioPreferences, AudioSettings, AudioPreferencesEditor, IAudioPreferences, IAudioDefaults } from "./preferences";
5
+ import { ContextMenuEvent, KeyboardEventData, SuspiciousActivityEvent } from "@readium/navigator-html-injectables";
6
+ export interface AudioMetadata {
7
+ duration: number;
8
+ textTracks: TextTrackList;
9
+ readyState: number;
10
+ networkState: number;
11
+ }
5
12
  export interface AudioNavigatorListeners {
6
13
  trackLoaded: (media: HTMLMediaElement) => void;
7
14
  positionChanged: (locator: Locator) => void;
15
+ timelineItemChanged: (item: TimelineItem | undefined) => void;
8
16
  error: (error: any, locator: Locator) => void;
9
17
  trackEnded: (locator: Locator) => void;
10
18
  play: (locator: Locator) => void;
11
19
  pause: (locator: Locator) => void;
12
- metadataLoaded: (duration: number) => void;
20
+ metadataLoaded: (metadata: AudioMetadata) => void;
13
21
  stalled: (isStalled: boolean) => void;
14
22
  seeking: (isSeeking: boolean) => void;
15
23
  seekable: (seekable: TimeRanges) => void;
24
+ contentProtection: (type: string, data: SuspiciousActivityEvent) => void;
25
+ peripheral: (data: KeyboardEventData) => void;
26
+ contextMenu: (data: ContextMenuEvent) => void;
27
+ remotePlaybackStateChanged?: (state: RemotePlaybackState) => void;
28
+ }
29
+ export interface IAudioContentProtectionConfig extends IContentProtectionConfig {
30
+ /** Prevents the media element from being cast to remote devices via the Remote Playback API. */
31
+ disableRemotePlayback?: boolean;
16
32
  }
17
33
  export interface AudioNavigatorConfiguration {
18
34
  preferences: IAudioPreferences;
19
35
  defaults: IAudioDefaults;
36
+ contentProtection?: IAudioContentProtectionConfig;
37
+ keyboardPeripherals?: IKeyboardPeripheralsConfig;
20
38
  }
21
39
  export declare class AudioNavigator extends MediaNavigator implements Configurable<AudioSettings, AudioPreferences> {
22
40
  private readonly pub;
23
41
  private positionPollInterval;
24
42
  private navigationId;
43
+ private _playIntent;
25
44
  private listeners;
26
45
  private currentLocation;
27
46
  private _preferences;
28
47
  private _defaults;
29
48
  private _settings;
30
49
  private _preferencesEditor;
50
+ private _mediaSessionEnabled;
31
51
  private pool;
52
+ private readonly _navigatorProtector;
53
+ private _currentTimelineItem;
54
+ private readonly _keyboardPeripheralsManager;
55
+ private readonly _suspiciousActivityListener;
56
+ private readonly _keyboardPeripheralListener;
57
+ private readonly _contentProtection;
58
+ /** True while a track transition is in progress; suppresses spurious mid-navigation events. */
59
+ private _isNavigating;
32
60
  constructor(publication: Publication, listeners: AudioNavigatorListeners, initialPosition?: Locator, configuration?: AudioNavigatorConfiguration);
33
61
  get settings(): AudioSettings;
34
62
  get preferencesEditor(): AudioPreferencesEditor;
35
63
  submitPreferences(preferences: AudioPreferences): Promise<void>;
36
64
  private applyPreferences;
37
65
  get publication(): Publication;
66
+ get timeline(): Timeline;
67
+ private _notifyTimelineChange;
38
68
  private ensureLocatorLocations;
39
69
  /** Resolves a bare href (no fragment) to its index in the reading order. Returns -1 if not found. */
40
70
  private hrefToTrackIndex;
@@ -74,6 +104,15 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
74
104
  get isTrackEnd(): boolean;
75
105
  get canGoBackward(): boolean;
76
106
  get canGoForward(): boolean;
107
+ /**
108
+ * The RemotePlayback object for the primary media element.
109
+ * Because the element is never swapped, this reference is stable for the
110
+ * lifetime of the navigator — host apps can store it and call `.prompt()`,
111
+ * `.watchAvailability()`, etc. directly.
112
+ */
113
+ get remotePlayback(): RemotePlayback;
114
+ /** Wires up the optional remotePlaybackStateChanged listener. Called once after initial load. */
115
+ private _setupRemotePlayback;
77
116
  private destroyMediaSession;
78
117
  destroy(): void;
79
118
  }
@@ -1,52 +1,31 @@
1
1
  import { Publication } from "@readium/shared";
2
2
  import { WebAudioEngine } from "./engine/WebAudioEngine";
3
+ import type { IAudioContentProtectionConfig } from "./AudioNavigator";
3
4
  export declare class AudioPoolManager {
4
- private preloadedElements;
5
+ private readonly pool;
5
6
  private _audioEngine;
6
- constructor(audioEngine: WebAudioEngine);
7
+ private readonly _publication;
8
+ private readonly _supportedAudioTypes;
9
+ constructor(audioEngine: WebAudioEngine, publication: Publication, contentProtection?: IAudioContentProtectionConfig);
10
+ private detectSupportedAudioTypes;
11
+ private pickPlayableHref;
7
12
  get audioEngine(): WebAudioEngine;
8
13
  /**
9
- * Sets the current audio by href, using preloaded element if available or loading otherwise,
10
- * and preloads adjacent tracks.
11
- * @param href The URL of the audio resource.
12
- * @param publication The publication containing the reading order.
13
- * @param currentIndex The current track index.
14
- * @param direction The navigation direction ('forward' or 'backward').
14
+ * Ensures an audio element exists in the pool for the given href.
15
+ * If one already exists, it is left untouched (preserving its buffered data).
15
16
  */
16
- setCurrentAudio(href: string, publication: Publication, currentIndex: number, direction: 'forward' | 'backward'): void;
17
- preload(href: string): void;
17
+ private ensure;
18
18
  /**
19
- * Retrieves a preloaded audio element by URL.
20
- * @param href The URL of the audio resource.
21
- * @returns The preloaded HTMLAudioElement, or undefined if not preloaded.
19
+ * Updates the pool around the given index: ensures elements exist within
20
+ * the LOWER_BOUNDARY and disposes those beyond the UPPER_BOUNDARY.
21
+ * The current track is excluded the primary engine element represents it.
22
22
  */
23
- get(href: string): HTMLAudioElement | undefined;
23
+ private update;
24
24
  /**
25
- * Removes a preloaded element from the pool.
26
- * @param href The URL of the audio resource.
27
- */
28
- clear(href: string): void;
29
- /**
30
- * Preloads the next track in the reading order.
31
- * @param publication The publication containing the reading order.
32
- * @param currentIndex The current track index.
33
- */
34
- preloadNext(publication: Publication, currentIndex: number): void;
35
- /**
36
- * Preloads the previous track in the reading order.
37
- * @param publication The publication containing the reading order.
38
- * @param currentIndex The current track index.
39
- */
40
- preloadPrevious(publication: Publication, currentIndex: number): void;
41
- /**
42
- * Preloads adjacent tracks (previous and next) for smoother navigation.
43
- * @param publication The publication containing the reading order.
44
- * @param currentIndex The current track index.
45
- * @param direction The navigation direction ('forward' or 'backward').
46
- */
47
- preloadAdjacent(publication: Publication, currentIndex: number, direction?: 'forward' | 'backward'): void;
48
- /**
49
- * Destroys the pool by stopping the engine and clearing all preloaded elements.
25
+ * Sets the current audio for playback at the given track index by changing
26
+ * the src on the persistent primary element. This preserves the RemotePlayback
27
+ * session and any Web Audio graph connections across track changes.
50
28
  */
29
+ setCurrentAudio(currentIndex: number, _direction: 'forward' | 'backward'): void;
51
30
  destroy(): void;
52
31
  }
@@ -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
@@ -40,11 +36,6 @@ export interface AudioEngine {
40
36
  * The current playback state.
41
37
  */
42
38
  playback: Playback;
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
39
  /**
49
40
  * Adds an event listener to the audio engine.
50
41
  * @param event The event name to listen.
@@ -58,10 +49,11 @@ export interface AudioEngine {
58
49
  */
59
50
  off(event: string, callback: (data: any) => void): void;
60
51
  /**
61
- * Sets the media element for playback, enabling use of preloaded elements from the pool.
62
- * @param element The HTML audio element to use for playback.
52
+ * Changes the src of the primary media element without swapping it,
53
+ * preserving the RemotePlayback session and all attached event listeners.
54
+ * @param href The URL of the new audio resource.
63
55
  */
64
- setMediaElement(element: HTMLAudioElement): void;
56
+ changeSrc(href: string): void;
65
57
  /**
66
58
  * Plays the current audio resource.
67
59
  */