@readium/navigator 2.10.3 → 2.11.0

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 (86) hide show
  1. package/dist/audio/AudioNavigator.js +1 -1
  2. package/dist/audio/AudioPoolManager.js +1 -1
  3. package/dist/audio/engine/WebAudioEngine.js +1 -1
  4. package/dist/divina/DivinaNavigator.js +1 -0
  5. package/dist/divina/DivinaPageManager.js +1 -0
  6. package/dist/divina/DivinaPagedPresenter.js +1 -0
  7. package/dist/divina/DivinaPeripherals.js +1 -0
  8. package/dist/divina/DivinaScrolledPresenter.js +1 -0
  9. package/dist/divina/DivinaSpreader.js +1 -0
  10. package/dist/divina/DivinaVariantSelector.js +1 -0
  11. package/dist/divina/preferences/DivinaDefaults.js +1 -0
  12. package/dist/divina/preferences/DivinaPreferences.js +1 -0
  13. package/dist/divina/preferences/DivinaPreferencesEditor.js +1 -0
  14. package/dist/divina/preferences/DivinaSettings.js +1 -0
  15. package/dist/divina/protection/DivinaNavigatorProtector.js +1 -0
  16. package/dist/epub/EpubNavigator.js +1 -1
  17. package/dist/epub/frame/FrameBlobBuilder.js +3 -1
  18. package/dist/epub/frame/FrameManager.js +1 -1
  19. package/dist/epub/frame/FramePoolManager.js +1 -1
  20. package/dist/epub/fxl/FXLFrameManager.js +1 -1
  21. package/dist/epub/fxl/FXLFramePoolManager.js +1 -1
  22. package/dist/epub/fxl/FXLPeripherals.js +2 -2
  23. package/dist/index.js +1 -1
  24. package/dist/peripherals/KeyboardConditionBridge.js +1 -1
  25. package/dist/peripherals/KeyboardPeripherals.js +1 -1
  26. package/dist/webpub/WebPubFrameManager.js +1 -1
  27. package/dist/webpub/WebPubFramePoolManager.js +1 -1
  28. package/dist/webpub/WebPubNavigator.js +1 -1
  29. package/package.json +5 -5
  30. package/src/audio/AudioNavigator.ts +107 -5
  31. package/src/audio/AudioPoolManager.ts +14 -8
  32. package/src/audio/engine/AudioEngine.ts +34 -1
  33. package/src/audio/engine/WebAudioEngine.ts +48 -3
  34. package/src/divina/DivinaNavigator.ts +704 -0
  35. package/src/divina/DivinaPageManager.ts +322 -0
  36. package/src/divina/DivinaPagedPresenter.ts +508 -0
  37. package/src/divina/DivinaPeripherals.ts +630 -0
  38. package/src/divina/DivinaScrolledPresenter.ts +516 -0
  39. package/src/divina/DivinaSpreader.ts +202 -0
  40. package/src/divina/DivinaVariantSelector.ts +77 -0
  41. package/src/divina/index.ts +8 -0
  42. package/src/divina/preferences/DivinaDefaults.ts +37 -0
  43. package/src/divina/preferences/DivinaPreferences.ts +77 -0
  44. package/src/divina/preferences/DivinaPreferencesEditor.ts +96 -0
  45. package/src/divina/preferences/DivinaSettings.ts +38 -0
  46. package/src/divina/preferences/index.ts +4 -0
  47. package/src/divina/protection/DivinaNavigatorProtector.ts +44 -0
  48. package/src/epub/EpubNavigator.ts +73 -7
  49. package/src/epub/frame/FrameBlobBuilder.ts +144 -39
  50. package/src/epub/frame/FrameManager.ts +7 -9
  51. package/src/epub/frame/FramePoolManager.ts +106 -48
  52. package/src/epub/fxl/FXLFrameManager.ts +69 -26
  53. package/src/epub/fxl/FXLFramePoolManager.ts +98 -53
  54. package/src/epub/fxl/FXLPeripherals.ts +22 -1
  55. package/src/index.ts +1 -0
  56. package/src/peripherals/KeyboardConditionBridge.ts +15 -5
  57. package/src/peripherals/KeyboardPeripherals.ts +13 -7
  58. package/src/webpub/WebPubFrameManager.ts +7 -9
  59. package/src/webpub/WebPubFramePoolManager.ts +5 -2
  60. package/src/webpub/WebPubNavigator.ts +44 -0
  61. package/types/src/audio/AudioNavigator.d.ts +29 -0
  62. package/types/src/audio/engine/AudioEngine.d.ts +27 -1
  63. package/types/src/audio/engine/WebAudioEngine.d.ts +14 -2
  64. package/types/src/divina/DivinaNavigator.d.ts +119 -0
  65. package/types/src/divina/DivinaPageManager.d.ts +66 -0
  66. package/types/src/divina/DivinaPagedPresenter.d.ts +122 -0
  67. package/types/src/divina/DivinaPeripherals.d.ts +121 -0
  68. package/types/src/divina/DivinaScrolledPresenter.d.ts +125 -0
  69. package/types/src/divina/DivinaSpreader.d.ts +65 -0
  70. package/types/src/divina/DivinaVariantSelector.d.ts +30 -0
  71. package/types/src/divina/index.d.ts +8 -0
  72. package/types/src/divina/preferences/DivinaDefaults.d.ts +18 -0
  73. package/types/src/divina/preferences/DivinaPreferences.d.ts +33 -0
  74. package/types/src/divina/preferences/DivinaPreferencesEditor.d.ts +20 -0
  75. package/types/src/divina/preferences/DivinaSettings.d.ts +26 -0
  76. package/types/src/divina/preferences/index.d.ts +4 -0
  77. package/types/src/divina/protection/DivinaNavigatorProtector.d.ts +14 -0
  78. package/types/src/epub/EpubNavigator.d.ts +7 -1
  79. package/types/src/epub/frame/FrameBlobBuilder.d.ts +19 -3
  80. package/types/src/epub/frame/FramePoolManager.d.ts +5 -1
  81. package/types/src/epub/fxl/FXLFrameManager.d.ts +2 -0
  82. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +8 -2
  83. package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
  84. package/types/src/index.d.ts +1 -0
  85. package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -0
  86. package/types/src/webpub/WebPubNavigator.d.ts +2 -0
@@ -38,19 +38,20 @@ export class AudioPoolManager {
38
38
  return supported;
39
39
  }
40
40
 
41
- private pickPlayableHref(link: Link): string {
41
+ private pickPlayableHref(link: Link): { href: string, type?: string } {
42
42
  const base = this._publication.baseURL;
43
43
  const candidates = [link, ...(link.alternates?.items ?? [])];
44
- let best: { href: string; confidence: "probably" | "maybe" } | undefined;
44
+ let best: { href: string; type?: string; confidence: "probably" | "maybe" } | undefined;
45
45
  for (const candidate of candidates) {
46
46
  if (!candidate.type) continue;
47
47
  const confidence = this._supportedAudioTypes.get(candidate.type);
48
48
  if (!confidence) continue;
49
49
  const href = candidate.toURL(base) ?? candidate.href;
50
- if (confidence === "probably") return href;
51
- if (!best) best = { href, confidence };
50
+ if (confidence === "probably") return { href, type: candidate.type };
51
+ if (!best) best = { href, type: candidate.type, confidence };
52
52
  }
53
- return best?.href ?? (link.toURL(base) ?? link.href);
53
+ if (best) return { href: best.href, type: best.type };
54
+ return { href: link.toURL(base) ?? link.href, type: link.type };
54
55
  }
55
56
 
56
57
  get audioEngine(): WebAudioEngine {
@@ -84,12 +85,16 @@ export class AudioPoolManager {
84
85
  * The current track is excluded — the primary engine element represents it.
85
86
  */
86
87
  private update(currentIndex: number): void {
88
+ // Progressive preload elements would feed encrypted media through the
89
+ // broken src= demuxer path — the MSE loader owns fetching instead
90
+ if (this._audioEngine.usesMse) return;
91
+
87
92
  const items = this._publication.readingOrder.items;
88
93
  const keep = new Set<string>();
89
94
 
90
95
  for (let j = 0; j < items.length; j++) {
91
96
  if (j === currentIndex) continue; // primary element handles the current track
92
- const href = this.pickPlayableHref(items[j]);
97
+ const { href } = this.pickPlayableHref(items[j]);
93
98
  if (j >= currentIndex - LOWER_BOUNDARY && j <= currentIndex + LOWER_BOUNDARY) {
94
99
  this.ensure(href);
95
100
  keep.add(href);
@@ -117,8 +122,8 @@ export class AudioPoolManager {
117
122
  * session and any Web Audio graph connections across track changes.
118
123
  */
119
124
  setCurrentAudio(currentIndex: number, _direction: 'forward' | 'backward'): void {
120
- const href = this.pickPlayableHref(this._publication.readingOrder.items[currentIndex]);
121
- this.audioEngine.changeSrc(href);
125
+ const { href, type } = this.pickPlayableHref(this._publication.readingOrder.items[currentIndex]);
126
+ this.audioEngine.changeSrc(href, type);
122
127
 
123
128
  // Discard any pool entry for this href — the primary element owns it now
124
129
  if (this.pool.has(href)) {
@@ -134,6 +139,7 @@ export class AudioPoolManager {
134
139
 
135
140
  destroy(): void {
136
141
  this.audioEngine.stop();
142
+ this.audioEngine.destroy();
137
143
  for (const [, element] of this.pool) {
138
144
  element.removeAttribute("src");
139
145
  element.load();
@@ -30,6 +30,37 @@ export interface Playback {
30
30
  buffered?: number;
31
31
  }
32
32
 
33
+ /**
34
+ * A consumer-supplied loader that feeds one resource to the playback element
35
+ * through Media Source Extensions. The engine constructs one per track via
36
+ * the [AudioMseLoaderFactory] and calls [start] immediately; the loader owns
37
+ * the element's `src` (a MediaSource object URL), all fetching, buffering and
38
+ * seek servicing until [destroy] is called. Container specifics (WebM, fMP4,
39
+ * byte ranges vs. segments…) are entirely up to the implementation.
40
+ */
41
+ export interface AudioMseLoader {
42
+ /** Attaches to the element and begins streaming. Called exactly once. */
43
+ start(): void;
44
+
45
+ /**
46
+ * Cancels in-flight fetches, detaches element listeners and releases the
47
+ * MediaSource object URL. The element itself must be left intact — the
48
+ * engine reuses it for the next track.
49
+ */
50
+ destroy(): void;
51
+ }
52
+
53
+ /**
54
+ * Creates the [AudioMseLoader] for a resource. [mimeType] is the resource's
55
+ * media type from the manifest, when known — needed to configure the
56
+ * SourceBuffer.
57
+ */
58
+ export type AudioMseLoaderFactory = (
59
+ element: HTMLMediaElement,
60
+ href: string,
61
+ mimeType?: string,
62
+ ) => AudioMseLoader;
63
+
33
64
  /**
34
65
  * An audio engine that plays audio resources from a publication.
35
66
  * @playback - The current [Playback] state.
@@ -58,8 +89,10 @@ export interface AudioEngine {
58
89
  * Changes the src of the primary media element without swapping it,
59
90
  * preserving the RemotePlayback session and all attached event listeners.
60
91
  * @param href The URL of the new audio resource.
92
+ * @param mimeType The media type of the resource, when known. Required for
93
+ * MSE-based loading to configure the SourceBuffer.
61
94
  */
62
- changeSrc(href: string): void;
95
+ changeSrc(href: string, mimeType?: string): void;
63
96
 
64
97
  /**
65
98
  * Plays the current audio resource.
@@ -2,6 +2,8 @@
2
2
 
3
3
  import {
4
4
  AudioEngine,
5
+ AudioMseLoader,
6
+ AudioMseLoaderFactory,
5
7
  Playback,
6
8
  } from "./AudioEngine.ts";
7
9
  import { PreservePitchWorklet } from "./PreservePitchWorklet.ts";
@@ -25,6 +27,10 @@ export class WebAudioEngine implements AudioEngine {
25
27
  private isStoppedValue: boolean = false;
26
28
  private worklet: PreservePitchWorklet | null = null;
27
29
  private webAudioActive: boolean = false;
30
+ private readonly mseLoaderFactory: AudioMseLoaderFactory | null;
31
+ private mseLoader: AudioMseLoader | null = null;
32
+ /** The resource href currently loaded (element src is a blob: URL in MSE mode). */
33
+ private currentHref: string = "";
28
34
 
29
35
  private readonly boundOnCanPlayThrough = this.onCanPlayThrough.bind(this);
30
36
  private readonly boundOnTimeUpdate = this.onTimeUpdate.bind(this);
@@ -42,8 +48,9 @@ export class WebAudioEngine implements AudioEngine {
42
48
  private readonly boundOnPause = this.onPause.bind(this);
43
49
  private readonly boundOnProgress = this.onProgress.bind(this);
44
50
 
45
- constructor(values: { playback: Playback }) {
51
+ constructor(values: { playback: Playback, mseLoaderFactory?: AudioMseLoaderFactory }) {
46
52
  this.playback = values.playback;
53
+ this.mseLoaderFactory = values.mseLoaderFactory ?? null;
47
54
 
48
55
  // crossOrigin is set lazily in activateWebAudio() only when the worklet is needed
49
56
  this.mediaElement = document.createElement("audio");
@@ -386,6 +393,19 @@ export class WebAudioEngine implements AudioEngine {
386
393
  private async activateWebAudio(): Promise<void> {
387
394
  if (this.webAudioActive) return;
388
395
 
396
+ if (this.usesMse) {
397
+ // blob: MediaSource URLs are same-origin — no CORS reload required
398
+ this.sourceNode = new MediaElementAudioSourceNode(this.getOrCreateAudioContext(), { mediaElement: this.mediaElement });
399
+ const audioContext = this.getOrCreateAudioContext();
400
+ this.gainNode = audioContext.createGain();
401
+ this.gainNode.gain.value = this.mediaElement.volume;
402
+ this.mediaElement.volume = 1;
403
+ this.sourceNode.connect(this.gainNode);
404
+ this.gainNode.connect(audioContext.destination);
405
+ this.webAudioActive = true;
406
+ return;
407
+ }
408
+
389
409
  const src = this.mediaElement.src;
390
410
  if (!src) return;
391
411
 
@@ -469,6 +489,11 @@ export class WebAudioEngine implements AudioEngine {
469
489
  return this.webAudioActive;
470
490
  }
471
491
 
492
+ /** True when this engine streams media through Media Source Extensions. */
493
+ public get usesMse(): boolean {
494
+ return this.mseLoaderFactory !== null;
495
+ }
496
+
472
497
  /**
473
498
  * Tears down the Web Audio graph and restores the media element to standalone
474
499
  * playback. Safe to call even if Web Audio was never activated.
@@ -499,10 +524,11 @@ export class WebAudioEngine implements AudioEngine {
499
524
  * the required headers), the graph is torn down and the src is reloaded
500
525
  * without CORS so playback continues — just without pitch correction.
501
526
  */
502
- public changeSrc(href: string): void {
503
- if (this.mediaElement.src === href) {
527
+ public changeSrc(href: string, mimeType?: string): void {
528
+ if (this.currentHref === href || this.mediaElement.src === href) {
504
529
  return;
505
530
  }
531
+ this.currentHref = href;
506
532
  this.mediaElement.pause();
507
533
  this.isPlayingValue = false;
508
534
  this.isPausedValue = false;
@@ -510,6 +536,16 @@ export class WebAudioEngine implements AudioEngine {
510
536
  this.isLoadingValue = true;
511
537
  this.isEndedValue = false;
512
538
 
539
+ if (this.mseLoaderFactory) {
540
+ // MSE path: swap loaders on the persistent element. The blob: object
541
+ // URL is same-origin, so none of the crossOrigin reload handling below
542
+ // applies here — the Web Audio graph (if active) keeps working.
543
+ this.mseLoader?.destroy();
544
+ this.mseLoader = this.mseLoaderFactory(this.mediaElement, href, mimeType);
545
+ this.mseLoader.start();
546
+ return;
547
+ }
548
+
513
549
  if (this.webAudioActive) {
514
550
  this.mediaElement.crossOrigin = "anonymous";
515
551
  this.mediaElement.src = href;
@@ -542,4 +578,13 @@ export class WebAudioEngine implements AudioEngine {
542
578
  public getMediaElement(): HTMLMediaElement {
543
579
  return this.mediaElement;
544
580
  }
581
+
582
+ /**
583
+ * Releases loader resources (in-flight fetches, MediaSource object URL).
584
+ * The media element itself is left intact.
585
+ */
586
+ public destroy(): void {
587
+ this.mseLoader?.destroy();
588
+ this.mseLoader = null;
589
+ }
545
590
  }