@readium/navigator 2.4.0-alpha.8 → 2.4.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.
Files changed (54) hide show
  1. package/dist/index.js +1809 -1055
  2. package/dist/index.umd.cjs +170 -23
  3. package/package.json +10 -10
  4. package/src/Navigator.ts +55 -1
  5. package/src/audio/AudioNavigator.ts +497 -0
  6. package/src/audio/AudioPoolManager.ts +120 -0
  7. package/src/audio/engine/AudioEngine.ts +26 -10
  8. package/src/audio/engine/PreservePitchProcessor.js +149 -0
  9. package/src/audio/engine/PreservePitchWorklet.ts +79 -0
  10. package/src/audio/engine/WebAudioEngine.ts +558 -259
  11. package/src/audio/index.ts +3 -1
  12. package/src/audio/preferences/AudioDefaults.ts +43 -0
  13. package/src/audio/preferences/AudioPreferences.ts +54 -0
  14. package/src/audio/preferences/AudioPreferencesEditor.ts +123 -0
  15. package/src/audio/preferences/AudioSettings.ts +36 -0
  16. package/src/audio/preferences/index.ts +4 -0
  17. package/src/epub/EpubNavigator.ts +2 -2
  18. package/src/epub/frame/FrameBlobBuilder.ts +33 -84
  19. package/src/epub/frame/FramePoolManager.ts +23 -18
  20. package/src/epub/fxl/FXLFrameManager.ts +4 -11
  21. package/src/epub/fxl/FXLFramePoolManager.ts +22 -26
  22. package/src/epub/preferences/EpubPreferences.ts +4 -4
  23. package/src/injection/Injector.ts +5 -5
  24. package/src/preferences/Configurable.ts +2 -3
  25. package/src/preferences/PreferencesEditor.ts +1 -1
  26. package/src/preferences/Types.ts +19 -0
  27. package/src/webpub/WebPubNavigator.ts +1 -2
  28. package/src/webpub/preferences/WebPubPreferences.ts +3 -3
  29. package/types/src/Navigator.d.ts +46 -0
  30. package/types/src/audio/AudioNavigator.d.ts +79 -0
  31. package/types/src/audio/AudioPoolManager.d.ts +52 -0
  32. package/types/src/audio/engine/AudioEngine.d.ts +21 -7
  33. package/types/src/audio/engine/PreservePitchWorklet.d.ts +18 -0
  34. package/types/src/audio/engine/WebAudioEngine.d.ts +52 -7
  35. package/types/src/audio/index.d.ts +2 -0
  36. package/types/src/audio/preferences/AudioDefaults.d.ts +21 -0
  37. package/types/src/audio/preferences/AudioPreferences.d.ts +23 -0
  38. package/types/src/audio/preferences/AudioPreferencesEditor.d.ts +19 -0
  39. package/types/src/audio/preferences/AudioSettings.d.ts +24 -0
  40. package/types/src/audio/preferences/index.d.ts +4 -0
  41. package/types/src/epub/EpubNavigator.d.ts +2 -2
  42. package/types/src/epub/frame/FrameBlobBuilder.d.ts +3 -6
  43. package/types/src/epub/fxl/FXLFrameManager.d.ts +0 -2
  44. package/types/src/epub/preferences/EpubPreferences.d.ts +2 -2
  45. package/types/src/preferences/Configurable.d.ts +2 -3
  46. package/types/src/preferences/PreferencesEditor.d.ts +1 -1
  47. package/types/src/preferences/Types.d.ts +3 -0
  48. package/types/src/webpub/WebPubNavigator.d.ts +2 -2
  49. package/types/src/webpub/preferences/WebPubPreferences.d.ts +2 -2
  50. package/LICENSE +0 -28
  51. package/src/divina/DivinaNavigator.ts +0 -0
  52. package/src/divina/index.ts +0 -0
  53. package/types/src/divina/DivinaNavigator.d.ts +0 -0
  54. package/types/src/divina/index.d.ts +0 -0
@@ -1,7 +1,7 @@
1
1
  import { ModuleName } from "@readium/navigator-html-injectables";
2
2
  import { Locator, Publication, ReadingProgression, Page, Link } from "@readium/shared";
3
3
  import { FrameCommsListener } from "../frame";
4
- import FrameBlobBuilder from "../frame/FrameBlobBuilder";
4
+ import FrameBlobBuider from "../frame/FrameBlobBuilder";
5
5
  import { FXLFrameManager } from "./FXLFrameManager";
6
6
  import { FXLPeripherals } from "./FXLPeripherals";
7
7
  import { FXLSpreader, Orientation, Spread } from "./FXLSpreader";
@@ -21,7 +21,7 @@ export class FXLFramePoolManager {
21
21
  private readonly container: HTMLElement;
22
22
  private readonly positions: Locator[];
23
23
  private readonly pool: Map<string, FXLFrameManager> = new Map();
24
- private readonly blobs: Map<string, FrameBlobBuilder> = new Map();
24
+ private readonly blobs: Map<string, string> = new Map();
25
25
  private readonly inprogress: Map<string, Promise<void>> = new Map();
26
26
  private readonly delayedShow: Map<string, Promise<void>> = new Map();
27
27
  private readonly delayedTimeout: Map<string, number> = new Map();
@@ -86,18 +86,16 @@ export class FXLFramePoolManager {
86
86
 
87
87
  this.peripherals = new FXLPeripherals(this);
88
88
 
89
- const fragment = document.createDocumentFragment();
90
89
  this.pub.readingOrder.items.forEach((link) => {
91
90
  // Create <iframe>
92
91
  const fm = new FXLFrameManager(this.peripherals, this.pub.metadata.effectiveReadingProgression, link.href, this.contentProtectionConfig, this.keyboardPeripheralsConfig);
92
+ this.spineElement.appendChild(fm.element);
93
93
 
94
+ // this.pages.push(fm);
94
95
  this.pool.set(link.href, fm);
95
96
  fm.width = 100 / this.length * (link.properties?.otherProperties["orientation"] === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
96
97
  fm.height = this.height;
97
- fragment.appendChild(fm.wrapper);
98
98
  });
99
- this.spineElement.appendChild(fragment);
100
-
101
99
  }
102
100
 
103
101
  private _listener!: FrameCommsListener;
@@ -136,18 +134,15 @@ export class FXLFramePoolManager {
136
134
 
137
135
  clearTimeout(this.resizeTimeout);
138
136
  this.resizeTimeout = window.setTimeout(() => {
139
- const baseWidthFactor = 100 / this.length;
140
- const itemLookup = new Map(this.pub.readingOrder.items.map(item => [item.href, item]));
137
+ // TODO optimize this expensive set of loops and operations
141
138
  this.pool.forEach((frm, linkHref) => {
142
- const link = itemLookup.get(linkHref);
143
- if(!link) return;
144
- requestAnimationFrame(() => {
145
- frm.width = baseWidthFactor * (link.properties?.otherProperties["orientation"] === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
146
- frm.height = this.height;
147
- if(!frm.loaded) return;
148
- const spread = this.spreader.findByLink(link)!;
149
- frm.update(this.spreadPosition(spread, link));
150
- });
139
+ let i = this.pub.readingOrder.items.findIndex(l => l.href === linkHref);
140
+ const link = this.pub.readingOrder.items[i];
141
+ frm.width = 100 / this.length * (link.properties?.otherProperties["orientation"] === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
142
+ frm.height = this.height;
143
+ if(!frm.loaded) return;
144
+ const spread = this.spreader.findByLink(link)!;
145
+ frm.update(this.spreadPosition(spread, link));
151
146
  });
152
147
  }, RESIZE_UPDATE_TIMEOUT);
153
148
  }
@@ -410,8 +405,7 @@ export class FXLFramePoolManager {
410
405
  this.pool.clear();
411
406
 
412
407
  // Revoke all blobs
413
- this.blobs.forEach(v => v.reset());
414
- this.blobs.clear();
408
+ this.blobs.forEach(v => URL.revokeObjectURL(v));
415
409
 
416
410
  // Clean up injector if it exists
417
411
  this.injector?.dispose();
@@ -502,13 +496,13 @@ export class FXLFramePoolManager {
502
496
  if(!this.pool.has(href)) return;
503
497
  this.cancelShowing(href);
504
498
  await this.pool.get(href)?.unload();
505
- this.blobs.get(href)?.reset();
499
+ // this.pool.delete(href);
506
500
  });
507
501
 
508
502
  // Check if base URL of publication has changed
509
503
  if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
510
504
  // Revoke all blobs
511
- this.blobs.forEach(v => v.reset());
505
+ this.blobs.forEach(v => URL.revokeObjectURL(v));
512
506
  this.blobs.clear();
513
507
  }
514
508
  this.currentBaseURL = pub.baseURL;
@@ -518,14 +512,16 @@ export class FXLFramePoolManager {
518
512
  const itm = pub.readingOrder.items[index];
519
513
  if(!itm) return; // TODO throw?
520
514
  if(!this.blobs.has(href)) {
521
- this.blobs.set(href, new FrameBlobBuilder(
515
+ const blobBuilder = new FrameBlobBuider(
522
516
  pub,
523
517
  this.currentBaseURL || "",
524
518
  itm,
525
519
  {
526
520
  injector: this.injector
527
521
  }
528
- ));
522
+ );
523
+ const blobURL = await blobBuilder.build(true);
524
+ this.blobs.set(href, blobURL);
529
525
  }
530
526
 
531
527
  // Show future offscreen frame in advance after a delay
@@ -539,7 +535,7 @@ export class FXLFramePoolManager {
539
535
  const spread = this.makeSpread(this.reAlign(index));
540
536
  const page = this.spreadPosition(spread, itm);
541
537
  const fm = this.pool.get(href)!;
542
- await fm.load(modules, await this.blobs.get(href)!.build(true));
538
+ await fm.load(modules, this.blobs.get(href)!);
543
539
  if(!this.peripherals.isScaled) // When scaled, positioning is screwed up, so wait to show
544
540
  await fm.show(page); // Show/activate new frame
545
541
  this.delayedShow.delete(href);
@@ -563,10 +559,10 @@ export class FXLFramePoolManager {
563
559
  for (const s of spread) {
564
560
  const newFrame = this.pool.get(s.href)!;
565
561
  const source = this.blobs.get(s.href);
566
- if(!source) continue; // Thfis can get destroyed
562
+ if(!source) continue; // This can get destroyed
567
563
 
568
564
  this.cancelShowing(s.href);
569
- await newFrame.load(modules, await source.build(true)); // In order to ensure modules match the latest configuration
565
+ await newFrame.load(modules, source); // In order to ensure modules match the latest configuration
570
566
  await newFrame.show(this.spreadPosition(spread, s)); // Show/activate new frame
571
567
  this.previousFrames.push(newFrame);
572
568
  await newFrame.activate();
@@ -59,7 +59,7 @@ export interface IEpubPreferences {
59
59
  wordSpacing?: number | null
60
60
  }
61
61
 
62
- export class EpubPreferences implements ConfigurablePreferences {
62
+ export class EpubPreferences implements ConfigurablePreferences<EpubPreferences> {
63
63
  backgroundColor?: string | null;
64
64
  blendFilter?: boolean | null;
65
65
  constraint?: number | null;
@@ -160,7 +160,7 @@ export class EpubPreferences implements ConfigurablePreferences {
160
160
  }
161
161
  }
162
162
 
163
- merging(other: ConfigurablePreferences): ConfigurablePreferences {
163
+ merging(other: EpubPreferences): EpubPreferences {
164
164
  const merged: IEpubPreferences = { ...this };
165
165
  for (const key of Object.keys(other) as (keyof IEpubPreferences)[]) {
166
166
  if (
@@ -172,11 +172,11 @@ export class EpubPreferences implements ConfigurablePreferences {
172
172
  ) &&
173
173
  (
174
174
  key !== "minimalLineLength" ||
175
- other[key] === null ||
175
+ other[key] === null ||
176
176
  (other[key] <= (other.optimalLineLength ?? merged.optimalLineLength ?? 65))
177
177
  )
178
178
  ) {
179
- merged[key] = other[key];
179
+ (merged as Record<string, unknown>)[key] = other[key];
180
180
  }
181
181
  }
182
182
  return new EpubPreferences(merged);
@@ -155,7 +155,7 @@ export class Injector implements IInjector {
155
155
  return [...this.allowedDomains]; // Return a copy to prevent external modification
156
156
  }
157
157
 
158
- public async injectForDocument(doc: Document, link: Link): Promise<void> {
158
+ public async injectForDocument(doc: Document, link: Link): Promise<void> {
159
159
  for (const rule of this.rules) {
160
160
  if (this.matchesRule(rule, link)) {
161
161
  await this.applyRule(doc, rule);
@@ -178,7 +178,7 @@ export class Injector implements IInjector {
178
178
  private async getOrCreateBlobUrl(resource: IInjectable): Promise<string> {
179
179
  // Use the injectable ID as the cache key
180
180
  const cacheKey = resource.id!; // ID is guaranteed to exist after constructor
181
-
181
+
182
182
  if (this.blobStore.has(cacheKey)) {
183
183
  const entry = this.blobStore.get(cacheKey)!;
184
184
  entry.refCount++;
@@ -191,7 +191,7 @@ export class Injector implements IInjector {
191
191
  this.createdBlobUrls.add(url);
192
192
  return url;
193
193
  }
194
-
194
+
195
195
  throw new Error("Resource must have a blob property");
196
196
  }
197
197
 
@@ -304,13 +304,13 @@ export class Injector implements IInjector {
304
304
  let url: string | null = null;
305
305
  try {
306
306
  url = await this.getResourceUrl(resource, doc);
307
-
307
+
308
308
  if (resource.rel === "preload" && "url" in resource) {
309
309
  this.createPreloadLink(doc, resource, url);
310
310
  } else {
311
311
  const element = this.createElement(doc, resource, url);
312
312
  createdElements.push({ element, url });
313
-
313
+
314
314
  if (position === "prepend") {
315
315
  target.prepend(element);
316
316
  } else {
@@ -4,9 +4,8 @@ export interface ConfigurableSettings {
4
4
  [key: string]: any;
5
5
  }
6
6
 
7
- export interface ConfigurablePreferences {
8
- [key: string]: any;
9
- merging(other: ConfigurablePreferences): ConfigurablePreferences;
7
+ export interface ConfigurablePreferences<T> {
8
+ merging(other: T): T;
10
9
  }
11
10
 
12
11
  export interface Configurable<ConfigurableSettings, ConfigurablePreferences> {
@@ -1,6 +1,6 @@
1
1
  import { ConfigurablePreferences } from "./Configurable";
2
2
 
3
3
  export interface IPreferencesEditor {
4
- preferences: ConfigurablePreferences;
4
+ preferences: ConfigurablePreferences<unknown>;
5
5
  clear(): void;
6
6
  }
@@ -16,6 +16,8 @@ export type RangeConfig = {
16
16
  step: number
17
17
  }
18
18
 
19
+ // ReadiumCSS preferences
20
+
19
21
  export const filterRangeConfig: RangeConfig = {
20
22
  range: [0, 100],
21
23
  step: 1
@@ -69,4 +71,21 @@ export const wordSpacingRangeConfig: RangeConfig = {
69
71
  export const zoomRangeConfig: RangeConfig = {
70
72
  range: [0.7, 4],
71
73
  step: 0.05
74
+ }
75
+
76
+ // Audio preferences
77
+
78
+ export const volumeRangeConfig: RangeConfig = {
79
+ range: [0, 1],
80
+ step: 0.1
81
+ }
82
+
83
+ export const playbackRateRangeConfig: RangeConfig = {
84
+ range: [0.5, 2],
85
+ step: 0.25
86
+ }
87
+
88
+ export const skipIntervalRangeConfig: RangeConfig = {
89
+ range: [5, 60],
90
+ step: 5
72
91
  }
@@ -12,7 +12,6 @@ import { WebUserProperties, WebRSProperties } from "./css/Properties";
12
12
  import { IWebPubPreferences, WebPubPreferences } from "./preferences/WebPubPreferences";
13
13
  import { IWebPubDefaults, WebPubDefaults } from "./preferences/WebPubDefaults";
14
14
  import { WebPubSettings } from "./preferences/WebPubSettings";
15
- import { IPreferencesEditor } from "../preferences/PreferencesEditor";
16
15
  import { WebPubPreferencesEditor } from "./preferences/WebPubPreferencesEditor";
17
16
  import { Injector } from "../injection/Injector";
18
17
  import { createReadiumWebPubRules } from "../injection/webpubInjectables";
@@ -181,7 +180,7 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
181
180
  return Object.freeze({ ...this._settings });
182
181
  }
183
182
 
184
- public get preferencesEditor(): IPreferencesEditor {
183
+ public get preferencesEditor(): WebPubPreferencesEditor {
185
184
  if (this._preferencesEditor === null) {
186
185
  this._preferencesEditor = new WebPubPreferencesEditor(this._preferences, this.settings, this.pub.metadata);
187
186
  }
@@ -32,7 +32,7 @@ export interface IWebPubPreferences {
32
32
  zoom?: number | null
33
33
  }
34
34
 
35
- export class WebPubPreferences implements ConfigurablePreferences {
35
+ export class WebPubPreferences implements ConfigurablePreferences<WebPubPreferences> {
36
36
  fontFamily?: string | null;
37
37
  fontWeight?: number | null;
38
38
  hyphens?: boolean | null;
@@ -82,11 +82,11 @@ export class WebPubPreferences implements ConfigurablePreferences {
82
82
  }
83
83
  }
84
84
 
85
- merging(other: ConfigurablePreferences): ConfigurablePreferences {
85
+ merging(other: WebPubPreferences): WebPubPreferences {
86
86
  const merged: IWebPubPreferences = { ...this };
87
87
  for (const key of Object.keys(other) as (keyof IWebPubPreferences)[]) {
88
88
  if (other[key] !== undefined) {
89
- merged[key] = other[key];
89
+ (merged as Record<string, unknown>)[key] = other[key];
90
90
  }
91
91
  }
92
92
  return new WebPubPreferences(merged);
@@ -61,4 +61,50 @@ export declare abstract class VisualNavigator extends Navigator {
61
61
  */
62
62
  goRight(animated: boolean | undefined, completion: cbb): void;
63
63
  }
64
+ export declare abstract class MediaNavigator extends Navigator {
65
+ /**
66
+ * Current playback state - is media currently playing?
67
+ */
68
+ abstract get isPlaying(): boolean;
69
+ /**
70
+ * Current playback state - is media currently paused?
71
+ */
72
+ abstract get isPaused(): boolean;
73
+ /**
74
+ * Duration of current media resource in seconds
75
+ */
76
+ abstract get duration(): number;
77
+ /**
78
+ * Current time in seconds within the media resource
79
+ */
80
+ abstract get currentTime(): number;
81
+ /**
82
+ * Play the current media resource
83
+ */
84
+ abstract play(): void;
85
+ /**
86
+ * Pause the currently playing media
87
+ */
88
+ abstract pause(): void;
89
+ /**
90
+ * Stop playback and reset to beginning
91
+ */
92
+ abstract stop(): void;
93
+ /**
94
+ * Seek to specific time in seconds
95
+ */
96
+ abstract seek(time: number): void;
97
+ /**
98
+ * Jump forward or backward by specified seconds
99
+ */
100
+ abstract jump(seconds: number): void;
101
+ /**
102
+ * Skip forward by the configured interval
103
+ */
104
+ abstract skipForward(): void;
105
+ /**
106
+ * Skip backward by the configured interval
107
+ */
108
+ abstract skipBackward(): void;
109
+ }
64
110
  export {};
@@ -0,0 +1,79 @@
1
+ import { Link, Locator, Publication } from "@readium/shared";
2
+ import { MediaNavigator } from "../Navigator";
3
+ import { Configurable } from "../preferences";
4
+ import { AudioPreferences, AudioSettings, AudioPreferencesEditor, IAudioPreferences, IAudioDefaults } from "./preferences";
5
+ export interface AudioNavigatorListeners {
6
+ trackLoaded: (media: HTMLMediaElement) => void;
7
+ positionChanged: (locator: Locator) => void;
8
+ error: (error: any, locator: Locator) => void;
9
+ trackEnded: (locator: Locator) => void;
10
+ play: (locator: Locator) => void;
11
+ pause: (locator: Locator) => void;
12
+ metadataLoaded: (duration: number) => void;
13
+ stalled: (isStalled: boolean) => void;
14
+ seeking: (isSeeking: boolean) => void;
15
+ seekable: (seekable: TimeRanges) => void;
16
+ }
17
+ export interface AudioNavigatorConfiguration {
18
+ preferences: IAudioPreferences;
19
+ defaults: IAudioDefaults;
20
+ }
21
+ export declare class AudioNavigator extends MediaNavigator implements Configurable<AudioSettings, AudioPreferences> {
22
+ private readonly pub;
23
+ private positionPollInterval;
24
+ private navigationId;
25
+ private listeners;
26
+ private currentLocation;
27
+ private _preferences;
28
+ private _defaults;
29
+ private _settings;
30
+ private _preferencesEditor;
31
+ private pool;
32
+ constructor(publication: Publication, listeners: AudioNavigatorListeners, initialPosition?: Locator, configuration?: AudioNavigatorConfiguration);
33
+ get settings(): AudioSettings;
34
+ get preferencesEditor(): AudioPreferencesEditor;
35
+ submitPreferences(preferences: AudioPreferences): Promise<void>;
36
+ private applyPreferences;
37
+ get publication(): Publication;
38
+ private ensureLocatorLocations;
39
+ /** Resolves a bare href (no fragment) to its index in the reading order. Returns -1 if not found. */
40
+ private hrefToTrackIndex;
41
+ /** Current track index derived from the current location's href. */
42
+ private currentTrackIndex;
43
+ get currentLocator(): Locator;
44
+ get isPlaying(): boolean;
45
+ get isPaused(): boolean;
46
+ get duration(): number;
47
+ get currentTime(): number;
48
+ private createLocator;
49
+ /**
50
+ * Waits for the current audio to be ready to play, then seeks to seekTime if > 0.
51
+ * Rejects if an error event fires before the audio is ready.
52
+ * When navId is provided, skips the seek if that navigation has been superseded.
53
+ */
54
+ private waitForLoadedAndSeeked;
55
+ private setupEventListeners;
56
+ private setupMediaSession;
57
+ private updateMediaSessionMetadata;
58
+ private startPositionPolling;
59
+ private stopPositionPolling;
60
+ go(locator: Locator, _animated: boolean, cb: (ok: boolean) => void): Promise<void>;
61
+ goLink(link: Link, _animated: boolean, cb: (ok: boolean) => void): Promise<void>;
62
+ goForward(_animated: boolean, cb: (ok: boolean) => void): Promise<void>;
63
+ goBackward(_animated: boolean, cb: (ok: boolean) => void): Promise<void>;
64
+ play(): void;
65
+ pause(): void;
66
+ stop(): void;
67
+ private nextTrack;
68
+ private previousTrack;
69
+ seek(time: number): void;
70
+ jump(seconds: number): void;
71
+ skipForward(): void;
72
+ skipBackward(): void;
73
+ get isTrackStart(): boolean;
74
+ get isTrackEnd(): boolean;
75
+ get canGoBackward(): boolean;
76
+ get canGoForward(): boolean;
77
+ private destroyMediaSession;
78
+ destroy(): void;
79
+ }
@@ -0,0 +1,52 @@
1
+ import { Publication } from "@readium/shared";
2
+ import { WebAudioEngine } from "./engine/WebAudioEngine";
3
+ export declare class AudioPoolManager {
4
+ private preloadedElements;
5
+ private _audioEngine;
6
+ constructor(audioEngine: WebAudioEngine);
7
+ get audioEngine(): WebAudioEngine;
8
+ /**
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').
15
+ */
16
+ setCurrentAudio(href: string, publication: Publication, currentIndex: number, direction: 'forward' | 'backward'): void;
17
+ preload(href: string): void;
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.
22
+ */
23
+ get(href: string): HTMLAudioElement | undefined;
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.
50
+ */
51
+ destroy(): void;
52
+ }
@@ -1,5 +1,3 @@
1
- import { Locator } from '@readium/shared';
2
- import { Publication } from '@readium/shared';
3
1
  /**
4
2
  * Initial state of the audio engine playback.
5
3
  */
@@ -43,9 +41,10 @@ export interface AudioEngine {
43
41
  */
44
42
  playback: Playback;
45
43
  /**
46
- * Plays the audio resource at the given locator.
44
+ * Loads the audio resource at the given URL.
45
+ * @param url The URL of the audio resource.
47
46
  */
48
- playLocator(publication: Publication, locator: Locator): Promise<void>;
47
+ loadAudio(url: string): void;
49
48
  /**
50
49
  * Adds an event listener to the audio engine.
51
50
  * @param event The event name to listen.
@@ -59,10 +58,10 @@ export interface AudioEngine {
59
58
  */
60
59
  off(event: string, callback: (data: any) => void): void;
61
60
  /**
62
- * Loads the audio resource at the given URL.
63
- * @param url The URL of the audio resource.
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.
64
63
  */
65
- loadAudio(url: string): void;
64
+ setMediaElement(element: HTMLAudioElement): void;
66
65
  /**
67
66
  * Plays the current audio resource.
68
67
  */
@@ -83,6 +82,17 @@ export interface AudioEngine {
83
82
  * Returns the duration of the audio resource.
84
83
  */
85
84
  duration(): number;
85
+ /**
86
+ * Sets the volume of the audio resource.
87
+ * @param volume The volume to set, in the range [0, 1].
88
+ */
89
+ setVolume(volume: number): void;
90
+ /**
91
+ * Sets the playback rate of the audio resource.
92
+ * @param rate The playback rate to set.
93
+ * @param preservePitch Whether to preserve pitch when changing playback rate.
94
+ */
95
+ setPlaybackRate(rate: number, preservePitch: boolean): void;
86
96
  /**
87
97
  * Returns whether the audio resource is currently playing.
88
98
  */
@@ -91,6 +101,10 @@ export interface AudioEngine {
91
101
  * Returns whether the audio resource is currently paused.
92
102
  */
93
103
  isPaused(): boolean;
104
+ /**
105
+ * Returns the HTML media element used for playback.
106
+ */
107
+ getMediaElement(): HTMLMediaElement;
94
108
  /**
95
109
  * Returns whether the audio resource is currently stopped.
96
110
  */
@@ -0,0 +1,18 @@
1
+ interface PreservePitchWorkletOptions {
2
+ ctx: AudioContext;
3
+ mediaElement?: HTMLMediaElement;
4
+ pitchFactor?: number;
5
+ modulePath?: string;
6
+ }
7
+ export declare class PreservePitchWorklet {
8
+ mediaElement: HTMLMediaElement | null;
9
+ source: MediaElementAudioSourceNode | null;
10
+ ctx: AudioContext;
11
+ workletNode: AudioWorkletNode | null;
12
+ url: string | null;
13
+ static createWorklet(options: PreservePitchWorkletOptions): Promise<PreservePitchWorklet>;
14
+ constructor(ctx: AudioContext);
15
+ updatePitchFactor(factor: number): void;
16
+ destroy(): void;
17
+ }
18
+ export {};
@@ -1,6 +1,4 @@
1
1
  import { AudioEngine, Playback } from "./AudioEngine";
2
- import { Publication } from "@readium/shared";
3
- import { Locator } from "@readium/shared";
4
2
  type EventCallback = (data: any) => void;
5
3
  export declare class WebAudioEngine implements AudioEngine {
6
4
  readonly playback: Playback;
@@ -9,15 +7,33 @@ export declare class WebAudioEngine implements AudioEngine {
9
7
  private sourceNode;
10
8
  private gainNode;
11
9
  private listeners;
10
+ private currentPlaybackRate;
12
11
  private isMutedValue;
13
12
  private isPlayingValue;
14
13
  private isPausedValue;
15
14
  private isLoadingValue;
16
15
  private isLoadedValue;
17
16
  private isEndedValue;
17
+ private isStoppedValue;
18
+ private worklet;
19
+ private webAudioActive;
20
+ private readonly boundOnCanPlayThrough;
21
+ private readonly boundOnTimeUpdate;
22
+ private readonly boundOnError;
23
+ private readonly boundOnEnded;
24
+ private readonly boundOnStalled;
25
+ private readonly boundOnEmptied;
26
+ private readonly boundOnSuspend;
27
+ private readonly boundOnWaiting;
28
+ private readonly boundOnLoadedMetadata;
29
+ private readonly boundOnSeeking;
30
+ private readonly boundOnSeeked;
31
+ private readonly boundOnPlay;
32
+ private readonly boundOnPlaying;
33
+ private readonly boundOnPause;
34
+ private readonly boundOnProgress;
18
35
  constructor(values: {
19
36
  playback: Playback;
20
- audioContext: AudioContext;
21
37
  });
22
38
  /**
23
39
  * Adds an event listener to the audio engine.
@@ -36,16 +52,30 @@ export declare class WebAudioEngine implements AudioEngine {
36
52
  * @param url The URL of the audio resource.
37
53
  * */
38
54
  loadAudio(url: string): void;
55
+ private deactivateWebAudio;
56
+ /**
57
+ * Sets the media element for playback.
58
+ * @param element The HTML audio element to use.
59
+ */
60
+ setMediaElement(element: HTMLAudioElement): void;
39
61
  private ensureAudioContextRunning;
62
+ private getOrCreateAudioContext;
40
63
  private onTimeUpdate;
41
64
  private onCanPlayThrough;
42
65
  private onError;
43
66
  private onEnded;
67
+ private onStalled;
68
+ private onEmptied;
69
+ private onSuspend;
70
+ private onWaiting;
71
+ private onLoadedMetadata;
72
+ private onSeeking;
73
+ private onSeeked;
74
+ private onPlay;
75
+ private onPlaying;
76
+ private onPause;
77
+ private onProgress;
44
78
  private emit;
45
- /**
46
- * Plays the audio resource at the given locator.
47
- */
48
- playLocator(_publication: Publication, _locator: Locator): Promise<void>;
49
79
  /**
50
80
  * Plays the current audio resource.
51
81
  */
@@ -103,5 +133,20 @@ export declare class WebAudioEngine implements AudioEngine {
103
133
  * Returns whether the audio resource is currently muted.
104
134
  */
105
135
  isMuted(): boolean;
136
+ /**
137
+ * Sets the playback rate of the audio resource with pitch preservation.
138
+ */
139
+ setPlaybackRate(rate: number, preservePitch: boolean): void;
140
+ /**
141
+ * Activates the Web Audio graph for the current media element.
142
+ * Sets crossOrigin = "anonymous" and reloads so MediaElementAudioSourceNode can be used.
143
+ * No-ops if Web Audio is already active.
144
+ */
145
+ private activateWebAudio;
146
+ get isWebAudioActive(): boolean;
147
+ /**
148
+ * Returns the HTML media element used for playback.
149
+ */
150
+ getMediaElement(): HTMLMediaElement;
106
151
  }
107
152
  export {};
@@ -1 +1,3 @@
1
1
  export * from './engine';
2
+ export * from './preferences';
3
+ export * from './AudioNavigator';