@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.
- package/dist/index.js +771 -663
- package/dist/index.umd.cjs +22 -22
- package/package.json +1 -1
- package/src/audio/AudioNavigator.ts +181 -23
- package/src/audio/AudioPoolManager.ts +103 -82
- package/src/audio/engine/AudioEngine.ts +4 -14
- package/src/audio/engine/WebAudioEngine.ts +21 -137
- package/src/audio/protection/AudioNavigatorProtector.ts +38 -0
- package/src/epub/frame/FrameManager.ts +1 -1
- package/src/epub/fxl/FXLFrameManager.ts +1 -1
- package/src/preferences/Types.ts +2 -2
- package/src/protection/CopyProtector.ts +22 -0
- package/src/protection/DevToolsDetector.ts +1 -0
- package/src/protection/DragAndDropProtector.ts +34 -0
- package/src/protection/NavigatorProtector.ts +3 -3
- package/src/webpub/WebPubFrameManager.ts +1 -1
- package/src/webpub/WebPubNavigator.ts +6 -2
- package/types/src/audio/AudioNavigator.d.ts +42 -3
- package/types/src/audio/AudioPoolManager.d.ts +18 -39
- package/types/src/audio/engine/AudioEngine.d.ts +4 -12
- package/types/src/audio/engine/WebAudioEngine.d.ts +5 -12
- package/types/src/audio/protection/AudioNavigatorProtector.d.ts +8 -0
- package/types/src/protection/CopyProtector.d.ts +8 -0
- package/types/src/protection/DragAndDropProtector.d.ts +10 -0
- package/types/src/protection/NavigatorProtector.d.ts +1 -1
|
@@ -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
|
-
*
|
|
70
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/src/preferences/Types.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
132
|
-
|
|
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: (
|
|
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
|
|
5
|
+
private readonly pool;
|
|
5
6
|
private _audioEngine;
|
|
6
|
-
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
17
|
-
preload(href: string): void;
|
|
17
|
+
private ensure;
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
23
|
+
private update;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
62
|
-
*
|
|
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
|
-
|
|
56
|
+
changeSrc(href: string): void;
|
|
65
57
|
/**
|
|
66
58
|
* Plays the current audio resource.
|
|
67
59
|
*/
|