@readium/navigator 2.10.2 → 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.
- package/dist/audio/AudioNavigator.js +1 -1
- package/dist/audio/AudioPoolManager.js +1 -1
- package/dist/audio/engine/WebAudioEngine.js +1 -1
- package/dist/divina/DivinaNavigator.js +1 -0
- package/dist/divina/DivinaPageManager.js +1 -0
- package/dist/divina/DivinaPagedPresenter.js +1 -0
- package/dist/divina/DivinaPeripherals.js +1 -0
- package/dist/divina/DivinaScrolledPresenter.js +1 -0
- package/dist/divina/DivinaSpreader.js +1 -0
- package/dist/divina/DivinaVariantSelector.js +1 -0
- package/dist/divina/preferences/DivinaDefaults.js +1 -0
- package/dist/divina/preferences/DivinaPreferences.js +1 -0
- package/dist/divina/preferences/DivinaPreferencesEditor.js +1 -0
- package/dist/divina/preferences/DivinaSettings.js +1 -0
- package/dist/divina/protection/DivinaNavigatorProtector.js +1 -0
- package/dist/epub/EpubNavigator.js +1 -1
- package/dist/epub/frame/FrameBlobBuilder.js +3 -1
- package/dist/epub/frame/FrameManager.js +1 -1
- package/dist/epub/frame/FramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLFrameManager.js +1 -1
- package/dist/epub/fxl/FXLFramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLPeripherals.js +2 -2
- package/dist/index.js +1 -1
- package/dist/peripherals/KeyboardConditionBridge.js +1 -1
- package/dist/peripherals/KeyboardPeripherals.js +1 -1
- package/dist/webpub/WebPubFrameManager.js +1 -1
- package/dist/webpub/WebPubFramePoolManager.js +1 -1
- package/dist/webpub/WebPubNavigator.js +1 -1
- package/package.json +5 -5
- package/src/audio/AudioNavigator.ts +107 -5
- package/src/audio/AudioPoolManager.ts +14 -8
- package/src/audio/engine/AudioEngine.ts +34 -1
- package/src/audio/engine/WebAudioEngine.ts +48 -3
- package/src/divina/DivinaNavigator.ts +704 -0
- package/src/divina/DivinaPageManager.ts +322 -0
- package/src/divina/DivinaPagedPresenter.ts +508 -0
- package/src/divina/DivinaPeripherals.ts +630 -0
- package/src/divina/DivinaScrolledPresenter.ts +516 -0
- package/src/divina/DivinaSpreader.ts +202 -0
- package/src/divina/DivinaVariantSelector.ts +77 -0
- package/src/divina/index.ts +8 -0
- package/src/divina/preferences/DivinaDefaults.ts +37 -0
- package/src/divina/preferences/DivinaPreferences.ts +77 -0
- package/src/divina/preferences/DivinaPreferencesEditor.ts +96 -0
- package/src/divina/preferences/DivinaSettings.ts +38 -0
- package/src/divina/preferences/index.ts +4 -0
- package/src/divina/protection/DivinaNavigatorProtector.ts +44 -0
- package/src/epub/EpubNavigator.ts +73 -7
- package/src/epub/frame/FrameBlobBuilder.ts +144 -39
- package/src/epub/frame/FrameManager.ts +7 -9
- package/src/epub/frame/FramePoolManager.ts +106 -48
- package/src/epub/fxl/FXLFrameManager.ts +69 -26
- package/src/epub/fxl/FXLFramePoolManager.ts +98 -53
- package/src/epub/fxl/FXLPeripherals.ts +22 -1
- package/src/index.ts +1 -0
- package/src/peripherals/KeyboardConditionBridge.ts +15 -5
- package/src/peripherals/KeyboardPeripherals.ts +13 -7
- package/src/webpub/WebPubFrameManager.ts +7 -9
- package/src/webpub/WebPubFramePoolManager.ts +5 -2
- package/src/webpub/WebPubNavigator.ts +44 -0
- package/types/src/audio/AudioNavigator.d.ts +29 -0
- package/types/src/audio/engine/AudioEngine.d.ts +27 -1
- package/types/src/audio/engine/WebAudioEngine.d.ts +14 -2
- package/types/src/divina/DivinaNavigator.d.ts +119 -0
- package/types/src/divina/DivinaPageManager.d.ts +66 -0
- package/types/src/divina/DivinaPagedPresenter.d.ts +122 -0
- package/types/src/divina/DivinaPeripherals.d.ts +121 -0
- package/types/src/divina/DivinaScrolledPresenter.d.ts +125 -0
- package/types/src/divina/DivinaSpreader.d.ts +65 -0
- package/types/src/divina/DivinaVariantSelector.d.ts +30 -0
- package/types/src/divina/index.d.ts +8 -0
- package/types/src/divina/preferences/DivinaDefaults.d.ts +18 -0
- package/types/src/divina/preferences/DivinaPreferences.d.ts +33 -0
- package/types/src/divina/preferences/DivinaPreferencesEditor.d.ts +20 -0
- package/types/src/divina/preferences/DivinaSettings.d.ts +26 -0
- package/types/src/divina/preferences/index.d.ts +4 -0
- package/types/src/divina/protection/DivinaNavigatorProtector.d.ts +14 -0
- package/types/src/epub/EpubNavigator.d.ts +7 -1
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +19 -3
- package/types/src/epub/frame/FramePoolManager.d.ts +5 -1
- package/types/src/epub/fxl/FXLFrameManager.d.ts +2 -0
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +8 -2
- package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -0
- 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
|
|
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
|
}
|