@readium/navigator 2.4.0-beta.9 → 2.5.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.
- package/dist/ReadiumCSS-after-B_e3a-PY.js +592 -0
- package/dist/ReadiumCSS-after-C-T_0paD.js +530 -0
- package/dist/ReadiumCSS-after-lr-n3fz2.js +475 -0
- package/dist/ReadiumCSS-after-mXeKKPap.js +490 -0
- package/dist/ReadiumCSS-before-Bjd3POej.js +426 -0
- package/dist/ReadiumCSS-before-CfXPAGaQ.js +425 -0
- package/dist/ReadiumCSS-before-CrNWvuyE.js +425 -0
- package/dist/ReadiumCSS-before-KVen5ceo.js +425 -0
- package/dist/ReadiumCSS-default-BKAG5pGU.js +162 -0
- package/dist/ReadiumCSS-default-C63bYOYF.js +183 -0
- package/dist/ReadiumCSS-default-CclvbeNC.js +162 -0
- package/dist/ReadiumCSS-default-DnlgDaBu.js +180 -0
- package/dist/ReadiumCSS-ebpaj_fonts_patch-Dt2XliTg.js +82 -0
- package/dist/index.js +2642 -3430
- package/dist/index.umd.cjs +4407 -995
- package/package.json +2 -2
- package/src/audio/AudioNavigator.ts +155 -42
- package/src/audio/AudioPoolManager.ts +27 -14
- package/src/audio/engine/AudioEngine.ts +4 -3
- package/src/audio/engine/PreservePitchProcessor.js +166 -101
- package/src/audio/engine/PreservePitchWorklet.ts +2 -17
- package/src/audio/engine/WebAudioEngine.ts +138 -160
- package/src/audio/engine/index.ts +2 -2
- package/src/audio/index.ts +3 -3
- package/src/audio/preferences/AudioDefaults.ts +2 -2
- package/src/audio/preferences/AudioPreferences.ts +11 -11
- package/src/audio/preferences/AudioPreferencesEditor.ts +13 -13
- package/src/audio/preferences/AudioSettings.ts +3 -3
- package/src/audio/preferences/index.ts +4 -4
- package/src/audio/protection/AudioNavigatorProtector.ts +4 -4
- package/src/css/index.ts +1 -1
- package/src/epub/EpubNavigator.ts +113 -78
- package/src/epub/css/Properties.ts +15 -15
- package/src/epub/css/ReadiumCSS.ts +43 -43
- package/src/epub/css/index.ts +2 -2
- package/src/epub/frame/FrameBlobBuilder.ts +31 -31
- package/src/epub/frame/FrameComms.ts +1 -1
- package/src/epub/frame/FrameManager.ts +13 -9
- package/src/epub/frame/FramePoolManager.ts +13 -13
- package/src/epub/frame/index.ts +4 -4
- package/src/epub/fxl/FXLCoordinator.ts +3 -3
- package/src/epub/fxl/FXLFrameManager.ts +8 -8
- package/src/epub/fxl/FXLFramePoolManager.ts +18 -14
- package/src/epub/fxl/FXLPeripherals.ts +4 -4
- package/src/epub/fxl/index.ts +5 -5
- package/src/epub/helpers/scriptMode.ts +45 -0
- package/src/epub/index.ts +6 -5
- package/src/epub/preferences/EpubDefaults.ts +23 -23
- package/src/epub/preferences/EpubPreferences.ts +16 -16
- package/src/epub/preferences/EpubPreferencesEditor.ts +53 -53
- package/src/epub/preferences/EpubSettings.ts +101 -101
- package/src/epub/preferences/index.ts +4 -4
- package/src/helpers/index.ts +2 -2
- package/src/index.ts +8 -8
- package/src/injection/Injector.ts +42 -42
- package/src/injection/epubInjectables.ts +86 -17
- package/src/injection/index.ts +2 -2
- package/src/injection/webpubInjectables.ts +2 -2
- package/src/preferences/Configurable.ts +2 -2
- package/src/preferences/PreferencesEditor.ts +2 -2
- package/src/preferences/guards.ts +2 -2
- package/src/preferences/index.ts +5 -5
- package/src/protection/CopyProtector.ts +5 -1
- package/src/protection/DevToolsDetector.ts +16 -16
- package/src/protection/DragAndDropProtector.ts +14 -1
- package/src/protection/NavigatorProtector.ts +6 -6
- package/src/webpub/WebPubBlobBuilder.ts +1 -1
- package/src/webpub/WebPubFrameManager.ts +8 -8
- package/src/webpub/WebPubFramePoolManager.ts +7 -7
- package/src/webpub/WebPubNavigator.ts +27 -27
- package/src/webpub/css/Properties.ts +3 -3
- package/src/webpub/css/WebPubCSS.ts +11 -11
- package/src/webpub/css/index.ts +2 -2
- package/src/webpub/index.ts +6 -6
- package/src/webpub/preferences/WebPubDefaults.ts +12 -12
- package/src/webpub/preferences/WebPubPreferences.ts +8 -8
- package/src/webpub/preferences/WebPubPreferencesEditor.ts +31 -31
- package/src/webpub/preferences/WebPubSettings.ts +45 -45
- package/src/webpub/preferences/index.ts +4 -4
- package/types/src/audio/AudioNavigator.d.ts +34 -5
- package/types/src/audio/AudioPoolManager.d.ts +7 -4
- package/types/src/audio/engine/AudioEngine.d.ts +4 -3
- package/types/src/audio/engine/PreservePitchWorklet.d.ts +1 -4
- package/types/src/audio/engine/WebAudioEngine.d.ts +15 -9
- package/types/src/audio/engine/index.d.ts +2 -2
- package/types/src/audio/index.d.ts +3 -3
- package/types/src/audio/preferences/AudioPreferences.d.ts +9 -9
- package/types/src/audio/preferences/AudioPreferencesEditor.d.ts +4 -4
- package/types/src/audio/preferences/AudioSettings.d.ts +3 -3
- package/types/src/audio/preferences/index.d.ts +4 -4
- package/types/src/audio/protection/AudioNavigatorProtector.d.ts +2 -2
- package/types/src/css/index.d.ts +1 -1
- package/types/src/epub/EpubNavigator.d.ts +15 -14
- package/types/src/epub/css/Properties.d.ts +2 -2
- package/types/src/epub/css/ReadiumCSS.d.ts +3 -3
- package/types/src/epub/css/index.d.ts +2 -2
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +1 -1
- package/types/src/epub/frame/FrameComms.d.ts +1 -1
- package/types/src/epub/frame/FrameManager.d.ts +3 -2
- package/types/src/epub/frame/FramePoolManager.d.ts +3 -3
- package/types/src/epub/frame/index.d.ts +4 -4
- package/types/src/epub/fxl/FXLFrameManager.d.ts +3 -3
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +5 -5
- package/types/src/epub/fxl/FXLPeripherals.d.ts +2 -2
- package/types/src/epub/fxl/index.d.ts +5 -5
- package/types/src/epub/helpers/scriptMode.d.ts +16 -0
- package/types/src/epub/index.d.ts +6 -5
- package/types/src/epub/preferences/EpubDefaults.d.ts +1 -1
- package/types/src/epub/preferences/EpubPreferences.d.ts +2 -2
- package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +5 -5
- package/types/src/epub/preferences/EpubSettings.d.ts +4 -4
- package/types/src/epub/preferences/index.d.ts +4 -4
- package/types/src/helpers/index.d.ts +2 -2
- package/types/src/index.d.ts +8 -8
- package/types/src/injection/Injector.d.ts +1 -1
- package/types/src/injection/epubInjectables.d.ts +5 -3
- package/types/src/injection/index.d.ts +2 -2
- package/types/src/injection/webpubInjectables.d.ts +1 -1
- package/types/src/preferences/Configurable.d.ts +1 -1
- package/types/src/preferences/PreferencesEditor.d.ts +1 -1
- package/types/src/preferences/guards.d.ts +1 -1
- package/types/src/preferences/index.d.ts +5 -5
- package/types/src/protection/CopyProtector.d.ts +1 -0
- package/types/src/protection/DragAndDropProtector.d.ts +2 -0
- package/types/src/protection/NavigatorProtector.d.ts +1 -1
- package/types/src/webpub/WebPubBlobBuilder.d.ts +1 -1
- package/types/src/webpub/WebPubFrameManager.d.ts +2 -2
- package/types/src/webpub/WebPubFramePoolManager.d.ts +3 -3
- package/types/src/webpub/WebPubNavigator.d.ts +10 -10
- package/types/src/webpub/css/Properties.d.ts +2 -2
- package/types/src/webpub/css/WebPubCSS.d.ts +2 -2
- package/types/src/webpub/css/index.d.ts +2 -2
- package/types/src/webpub/index.d.ts +6 -6
- package/types/src/webpub/preferences/WebPubDefaults.d.ts +1 -1
- package/types/src/webpub/preferences/WebPubPreferences.d.ts +2 -2
- package/types/src/webpub/preferences/WebPubPreferencesEditor.d.ts +5 -5
- package/types/src/webpub/preferences/WebPubSettings.d.ts +4 -4
- package/types/src/webpub/preferences/index.d.ts +4 -4
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import {
|
|
4
4
|
AudioEngine,
|
|
5
5
|
Playback,
|
|
6
|
-
} from "./AudioEngine";
|
|
7
|
-
import { PreservePitchWorklet } from "./PreservePitchWorklet";
|
|
6
|
+
} from "./AudioEngine.ts";
|
|
7
|
+
import { PreservePitchWorklet } from "./PreservePitchWorklet.ts";
|
|
8
8
|
|
|
9
9
|
type EventCallback = (data: any) => void;
|
|
10
10
|
|
|
@@ -16,8 +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 currentVolume: number = 1;
|
|
20
|
-
private currentPlaybackRate: number = 1;
|
|
21
19
|
private isMutedValue: boolean = false;
|
|
22
20
|
private isPlayingValue: boolean = false;
|
|
23
21
|
private isPausedValue: boolean = false;
|
|
@@ -95,120 +93,6 @@ export class WebAudioEngine implements AudioEngine {
|
|
|
95
93
|
);
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
private deactivateWebAudio(): void {
|
|
99
|
-
if (this.worklet) {
|
|
100
|
-
this.worklet.destroy();
|
|
101
|
-
this.worklet = null;
|
|
102
|
-
}
|
|
103
|
-
if (this.sourceNode) {
|
|
104
|
-
this.sourceNode.disconnect();
|
|
105
|
-
this.sourceNode = null;
|
|
106
|
-
}
|
|
107
|
-
if (this.gainNode) {
|
|
108
|
-
this.gainNode.disconnect();
|
|
109
|
-
this.gainNode = null;
|
|
110
|
-
}
|
|
111
|
-
this.webAudioActive = false;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Sets the media element for playback.
|
|
116
|
-
* @param element The HTML audio element to use.
|
|
117
|
-
*/
|
|
118
|
-
public setMediaElement(element: HTMLAudioElement): void {
|
|
119
|
-
// Remove listeners BEFORE pausing so the pause doesn't leak through
|
|
120
|
-
this.mediaElement.removeEventListener("canplaythrough", this.boundOnCanPlayThrough);
|
|
121
|
-
this.mediaElement.removeEventListener("timeupdate", this.boundOnTimeUpdate);
|
|
122
|
-
this.mediaElement.removeEventListener("error", this.boundOnError);
|
|
123
|
-
this.mediaElement.removeEventListener("ended", this.boundOnEnded);
|
|
124
|
-
this.mediaElement.removeEventListener("stalled", this.boundOnStalled);
|
|
125
|
-
this.mediaElement.removeEventListener("emptied", this.boundOnEmptied);
|
|
126
|
-
this.mediaElement.removeEventListener("suspend", this.boundOnSuspend);
|
|
127
|
-
this.mediaElement.removeEventListener("waiting", this.boundOnWaiting);
|
|
128
|
-
this.mediaElement.removeEventListener("loadedmetadata", this.boundOnLoadedMetadata);
|
|
129
|
-
this.mediaElement.removeEventListener("seeking", this.boundOnSeeking);
|
|
130
|
-
this.mediaElement.removeEventListener("seeked", this.boundOnSeeked);
|
|
131
|
-
this.mediaElement.removeEventListener("play", this.boundOnPlay);
|
|
132
|
-
this.mediaElement.removeEventListener("playing", this.boundOnPlaying);
|
|
133
|
-
this.mediaElement.removeEventListener("pause", this.boundOnPause);
|
|
134
|
-
this.mediaElement.removeEventListener("progress", this.boundOnProgress);
|
|
135
|
-
|
|
136
|
-
// Now safe to pause the outgoing element
|
|
137
|
-
this.mediaElement.pause();
|
|
138
|
-
this.isPlayingValue = false;
|
|
139
|
-
this.isPausedValue = false;
|
|
140
|
-
|
|
141
|
-
// Disconnect old source node if it exists
|
|
142
|
-
if (this.sourceNode) {
|
|
143
|
-
this.sourceNode.disconnect();
|
|
144
|
-
this.sourceNode = null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Set new media element
|
|
148
|
-
this.mediaElement = element;
|
|
149
|
-
|
|
150
|
-
// Add event listeners to new element
|
|
151
|
-
this.mediaElement.addEventListener("canplaythrough", this.boundOnCanPlayThrough);
|
|
152
|
-
this.mediaElement.addEventListener("timeupdate", this.boundOnTimeUpdate);
|
|
153
|
-
this.mediaElement.addEventListener("error", this.boundOnError);
|
|
154
|
-
this.mediaElement.addEventListener("ended", this.boundOnEnded);
|
|
155
|
-
this.mediaElement.addEventListener("stalled", this.boundOnStalled);
|
|
156
|
-
this.mediaElement.addEventListener("emptied", this.boundOnEmptied);
|
|
157
|
-
this.mediaElement.addEventListener("suspend", this.boundOnSuspend);
|
|
158
|
-
this.mediaElement.addEventListener("waiting", this.boundOnWaiting);
|
|
159
|
-
this.mediaElement.addEventListener("loadedmetadata", this.boundOnLoadedMetadata);
|
|
160
|
-
this.mediaElement.addEventListener("seeking", this.boundOnSeeking);
|
|
161
|
-
this.mediaElement.addEventListener("seeked", this.boundOnSeeked);
|
|
162
|
-
this.mediaElement.addEventListener("play", this.boundOnPlay);
|
|
163
|
-
this.mediaElement.addEventListener("playing", this.boundOnPlaying);
|
|
164
|
-
this.mediaElement.addEventListener("pause", this.boundOnPause);
|
|
165
|
-
this.mediaElement.addEventListener("progress", this.boundOnProgress);
|
|
166
|
-
|
|
167
|
-
// Re-apply current volume and playback rate to the new element
|
|
168
|
-
this.mediaElement.volume = this.isMutedValue ? 0 : this.currentVolume;
|
|
169
|
-
this.mediaElement.playbackRate = this.currentPlaybackRate;
|
|
170
|
-
|
|
171
|
-
// Reconnect the Web Audio graph to the new element
|
|
172
|
-
if (this.webAudioActive) {
|
|
173
|
-
try {
|
|
174
|
-
const ctx = this.getOrCreateAudioContext();
|
|
175
|
-
this.sourceNode = new MediaElementAudioSourceNode(ctx, { mediaElement: this.mediaElement });
|
|
176
|
-
if (!this.gainNode) {
|
|
177
|
-
this.gainNode = ctx.createGain();
|
|
178
|
-
this.gainNode.connect(ctx.destination);
|
|
179
|
-
}
|
|
180
|
-
if (this.worklet?.workletNode) {
|
|
181
|
-
this.sourceNode.connect(this.worklet.workletNode);
|
|
182
|
-
} else {
|
|
183
|
-
this.sourceNode.connect(this.gainNode);
|
|
184
|
-
}
|
|
185
|
-
} catch {
|
|
186
|
-
// CORS failed on this element — deactivate Web Audio gracefully
|
|
187
|
-
this.deactivateWebAudio();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Check if metadata is already loaded (common with preloaded elements)
|
|
192
|
-
if (this.mediaElement.readyState >= 1) {
|
|
193
|
-
this.onLoadedMetadata(new Event('loadedmetadata'));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Preloaded elements may have already buffered data before being swapped in,
|
|
197
|
-
// so progress events would have fired before we were listening. Emit now if
|
|
198
|
-
// seekable ranges are already available.
|
|
199
|
-
if (this.mediaElement.seekable.length > 0) {
|
|
200
|
-
this.onProgress();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Check if the element is already loaded and trigger appropriate events
|
|
204
|
-
if (this.mediaElement.readyState >= 4) {
|
|
205
|
-
this.onCanPlayThrough();
|
|
206
|
-
} else {
|
|
207
|
-
this.isLoadingValue = true;
|
|
208
|
-
this.isLoadedValue = false;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
96
|
// Ensure AudioContext is running
|
|
213
97
|
private async ensureAudioContextRunning() {
|
|
214
98
|
if (!this.audioContext) {
|
|
@@ -313,6 +197,9 @@ export class WebAudioEngine implements AudioEngine {
|
|
|
313
197
|
}
|
|
314
198
|
|
|
315
199
|
try {
|
|
200
|
+
if (this.audioContext) {
|
|
201
|
+
await this.ensureAudioContextRunning();
|
|
202
|
+
}
|
|
316
203
|
await this.mediaElement.play();
|
|
317
204
|
this.isPlayingValue = true;
|
|
318
205
|
this.isPausedValue = false;
|
|
@@ -351,24 +238,17 @@ export class WebAudioEngine implements AudioEngine {
|
|
|
351
238
|
* @volume The volume to set, in the range [0, 1].
|
|
352
239
|
*/
|
|
353
240
|
public setVolume(volume: number): void {
|
|
354
|
-
|
|
355
|
-
this.currentVolume = 0;
|
|
356
|
-
this.mediaElement.volume = 0;
|
|
357
|
-
if (this.gainNode) {
|
|
358
|
-
this.gainNode.gain.value = 0;
|
|
359
|
-
}
|
|
360
|
-
this.isMutedValue = true;
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
if (volume > 1) {
|
|
364
|
-
this.setVolume(volume / 100);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
this.currentVolume = volume;
|
|
368
|
-
this.mediaElement.volume = volume;
|
|
241
|
+
const clamped = Math.max(0, Math.min(1, volume));
|
|
369
242
|
if (this.gainNode) {
|
|
370
|
-
|
|
243
|
+
// When the Web Audio graph is active, keep the media element at full
|
|
244
|
+
// volume and control level exclusively through the GainNode to avoid
|
|
245
|
+
// double-attenuation (e.g. 0.5 × 0.5 = 0.25).
|
|
246
|
+
this.mediaElement.volume = 1;
|
|
247
|
+
this.gainNode.gain.value = clamped;
|
|
248
|
+
} else {
|
|
249
|
+
this.mediaElement.volume = clamped;
|
|
371
250
|
}
|
|
251
|
+
this.isMutedValue = clamped === 0;
|
|
372
252
|
}
|
|
373
253
|
|
|
374
254
|
/**
|
|
@@ -452,7 +332,6 @@ export class WebAudioEngine implements AudioEngine {
|
|
|
452
332
|
* Sets the playback rate of the audio resource with pitch preservation.
|
|
453
333
|
*/
|
|
454
334
|
public setPlaybackRate(rate: number, preservePitch: boolean): void {
|
|
455
|
-
this.currentPlaybackRate = rate;
|
|
456
335
|
this.mediaElement.playbackRate = rate;
|
|
457
336
|
if (preservePitch) {
|
|
458
337
|
if ('preservesPitch' in this.mediaElement) {
|
|
@@ -461,16 +340,14 @@ export class WebAudioEngine implements AudioEngine {
|
|
|
461
340
|
// Activate Web Audio graph first, then attach the worklet
|
|
462
341
|
this.activateWebAudio().then(() => {
|
|
463
342
|
if (!this.worklet) {
|
|
464
|
-
if (this.sourceNode) {
|
|
465
|
-
this.sourceNode.disconnect();
|
|
466
|
-
this.sourceNode = null;
|
|
467
|
-
}
|
|
468
343
|
PreservePitchWorklet.createWorklet({
|
|
469
344
|
ctx: this.getOrCreateAudioContext(),
|
|
470
|
-
mediaElement: this.mediaElement,
|
|
471
345
|
pitchFactor: 1.0
|
|
472
346
|
}).then(worklet => {
|
|
347
|
+
// Rewire: sourceNode → workletNode → gainNode
|
|
348
|
+
if (this.sourceNode) this.sourceNode.disconnect();
|
|
473
349
|
this.worklet = worklet;
|
|
350
|
+
this.sourceNode?.connect(this.worklet.workletNode!);
|
|
474
351
|
this.worklet.workletNode!.connect(this.gainNode!);
|
|
475
352
|
this.worklet.updatePitchFactor(1 / rate);
|
|
476
353
|
}).catch(err => {
|
|
@@ -484,12 +361,17 @@ export class WebAudioEngine implements AudioEngine {
|
|
|
484
361
|
});
|
|
485
362
|
}
|
|
486
363
|
} else {
|
|
364
|
+
// Disable native pitch preservation (mirrors the check in the true branch)
|
|
365
|
+
if ('preservesPitch' in this.mediaElement) {
|
|
366
|
+
(this.mediaElement as any).preservesPitch = false;
|
|
367
|
+
}
|
|
368
|
+
|
|
487
369
|
if (this.worklet) {
|
|
488
370
|
this.worklet.destroy();
|
|
489
371
|
this.worklet = null;
|
|
490
|
-
//
|
|
491
|
-
if (this.webAudioActive) {
|
|
492
|
-
this.sourceNode
|
|
372
|
+
// Restore: sourceNode → gainNode
|
|
373
|
+
if (this.webAudioActive && this.sourceNode) {
|
|
374
|
+
this.sourceNode.disconnect();
|
|
493
375
|
this.sourceNode.connect(this.gainNode!);
|
|
494
376
|
}
|
|
495
377
|
}
|
|
@@ -519,28 +401,57 @@ export class WebAudioEngine implements AudioEngine {
|
|
|
519
401
|
this.mediaElement.src = src;
|
|
520
402
|
this.mediaElement.load();
|
|
521
403
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
404
|
+
try {
|
|
405
|
+
await new Promise<void>((resolve, reject) => {
|
|
406
|
+
const onReady = () => {
|
|
407
|
+
this.mediaElement.removeEventListener("canplaythrough", onReady);
|
|
408
|
+
this.mediaElement.removeEventListener("error", onFail);
|
|
409
|
+
resolve();
|
|
410
|
+
};
|
|
411
|
+
const onFail = () => {
|
|
412
|
+
this.mediaElement.removeEventListener("canplaythrough", onReady);
|
|
413
|
+
this.mediaElement.removeEventListener("error", onFail);
|
|
414
|
+
reject(new Error("Audio reload with CORS failed — server may not send Access-Control-Allow-Origin"));
|
|
415
|
+
};
|
|
416
|
+
this.mediaElement.addEventListener("canplaythrough", onReady);
|
|
417
|
+
this.mediaElement.addEventListener("error", onFail);
|
|
418
|
+
});
|
|
419
|
+
} catch (err) {
|
|
420
|
+
// Roll back to non-CORS mode so the element remains playable.
|
|
421
|
+
// crossOrigin = "" still maps to "anonymous"; remove the attribute entirely
|
|
422
|
+
// to disable CORS on the reload.
|
|
423
|
+
this.mediaElement.removeAttribute("crossorigin");
|
|
424
|
+
this.mediaElement.src = src;
|
|
425
|
+
this.mediaElement.load();
|
|
426
|
+
if (wasPlaying) {
|
|
427
|
+
await new Promise<void>(resolve => {
|
|
428
|
+
const onReady = () => {
|
|
429
|
+
this.mediaElement.removeEventListener("canplaythrough", onReady);
|
|
430
|
+
resolve();
|
|
431
|
+
};
|
|
432
|
+
this.mediaElement.addEventListener("canplaythrough", onReady);
|
|
433
|
+
});
|
|
434
|
+
this.mediaElement.currentTime = currentTime;
|
|
435
|
+
await this.mediaElement.play();
|
|
436
|
+
this.isPlayingValue = true;
|
|
437
|
+
this.isPausedValue = false;
|
|
438
|
+
} else {
|
|
439
|
+
this.mediaElement.currentTime = currentTime;
|
|
440
|
+
}
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
536
443
|
|
|
537
444
|
this.mediaElement.currentTime = currentTime;
|
|
538
445
|
|
|
539
446
|
this.sourceNode = new MediaElementAudioSourceNode(this.getOrCreateAudioContext(), { mediaElement: this.mediaElement });
|
|
540
|
-
|
|
541
|
-
// Create gainNode lazily when Web Audio is activated
|
|
447
|
+
|
|
448
|
+
// Create gainNode lazily when Web Audio is activated.
|
|
449
|
+
// Seed its gain from the current element volume, then reset the element to
|
|
450
|
+
// 1.0 so volume isn't applied twice once setVolume routes through the node.
|
|
542
451
|
const audioContext = this.getOrCreateAudioContext();
|
|
543
452
|
this.gainNode = audioContext.createGain();
|
|
453
|
+
this.gainNode.gain.value = this.mediaElement.volume;
|
|
454
|
+
this.mediaElement.volume = 1;
|
|
544
455
|
this.sourceNode.connect(this.gainNode);
|
|
545
456
|
this.gainNode.connect(audioContext.destination);
|
|
546
457
|
|
|
@@ -558,10 +469,77 @@ export class WebAudioEngine implements AudioEngine {
|
|
|
558
469
|
return this.webAudioActive;
|
|
559
470
|
}
|
|
560
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Tears down the Web Audio graph and restores the media element to standalone
|
|
474
|
+
* playback. Safe to call even if Web Audio was never activated.
|
|
475
|
+
*/
|
|
476
|
+
private tearDownWebAudio(): void {
|
|
477
|
+
if (this.worklet) {
|
|
478
|
+
this.worklet.destroy();
|
|
479
|
+
this.worklet = null;
|
|
480
|
+
}
|
|
481
|
+
if (this.sourceNode) {
|
|
482
|
+
this.sourceNode.disconnect();
|
|
483
|
+
this.sourceNode = null;
|
|
484
|
+
}
|
|
485
|
+
if (this.gainNode) {
|
|
486
|
+
// Restore the volume that was previously managed by the GainNode.
|
|
487
|
+
this.mediaElement.volume = this.gainNode.gain.value;
|
|
488
|
+
this.gainNode.disconnect();
|
|
489
|
+
this.gainNode = null;
|
|
490
|
+
}
|
|
491
|
+
this.webAudioActive = false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Changes the src of the primary media element without swapping the element.
|
|
496
|
+
* Preserves the RemotePlayback session and all attached event listeners.
|
|
497
|
+
* When the Web Audio graph is active, the new src is loaded with
|
|
498
|
+
* crossOrigin="anonymous". If the CORS request fails (server does not send
|
|
499
|
+
* the required headers), the graph is torn down and the src is reloaded
|
|
500
|
+
* without CORS so playback continues — just without pitch correction.
|
|
501
|
+
*/
|
|
502
|
+
public changeSrc(href: string): void {
|
|
503
|
+
if (this.mediaElement.src === href) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
this.mediaElement.pause();
|
|
507
|
+
this.isPlayingValue = false;
|
|
508
|
+
this.isPausedValue = false;
|
|
509
|
+
this.isLoadedValue = false;
|
|
510
|
+
this.isLoadingValue = true;
|
|
511
|
+
this.isEndedValue = false;
|
|
512
|
+
|
|
513
|
+
if (this.webAudioActive) {
|
|
514
|
+
this.mediaElement.crossOrigin = "anonymous";
|
|
515
|
+
this.mediaElement.src = href;
|
|
516
|
+
this.mediaElement.load();
|
|
517
|
+
|
|
518
|
+
const onReady = () => { cleanup(); };
|
|
519
|
+
const onFail = () => {
|
|
520
|
+
cleanup();
|
|
521
|
+
console.warn("CORS reload failed for new track — disabling Web Audio graph:", href);
|
|
522
|
+
this.tearDownWebAudio();
|
|
523
|
+
this.mediaElement.removeAttribute("crossorigin");
|
|
524
|
+
this.mediaElement.src = href;
|
|
525
|
+
this.mediaElement.load();
|
|
526
|
+
};
|
|
527
|
+
const cleanup = () => {
|
|
528
|
+
this.mediaElement.removeEventListener("canplaythrough", onReady);
|
|
529
|
+
this.mediaElement.removeEventListener("error", onFail);
|
|
530
|
+
};
|
|
531
|
+
this.mediaElement.addEventListener("canplaythrough", onReady);
|
|
532
|
+
this.mediaElement.addEventListener("error", onFail);
|
|
533
|
+
} else {
|
|
534
|
+
this.mediaElement.src = href;
|
|
535
|
+
this.mediaElement.load();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
561
539
|
/**
|
|
562
540
|
* Returns the HTML media element used for playback.
|
|
563
541
|
*/
|
|
564
542
|
public getMediaElement(): HTMLMediaElement {
|
|
565
543
|
return this.mediaElement;
|
|
566
544
|
}
|
|
567
|
-
}
|
|
545
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './AudioEngine';
|
|
2
|
-
export * from './WebAudioEngine';
|
|
1
|
+
export * from './AudioEngine.ts';
|
|
2
|
+
export * from './WebAudioEngine.ts';
|
package/src/audio/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from './engine';
|
|
2
|
-
export * from './preferences';
|
|
3
|
-
export * from './AudioNavigator';
|
|
1
|
+
export * from './engine/index.ts';
|
|
2
|
+
export * from './preferences/index.ts';
|
|
3
|
+
export * from './AudioNavigator.ts';
|
|
@@ -2,12 +2,12 @@ import {
|
|
|
2
2
|
ensureBoolean,
|
|
3
3
|
ensureValueInRange,
|
|
4
4
|
ensureNonNegative
|
|
5
|
-
} from "../../preferences/guards";
|
|
5
|
+
} from "../../preferences/guards.ts";
|
|
6
6
|
import {
|
|
7
7
|
volumeRangeConfig,
|
|
8
8
|
playbackRateRangeConfig,
|
|
9
9
|
skipIntervalRangeConfig
|
|
10
|
-
} from "../../preferences/Types";
|
|
10
|
+
} from "../../preferences/Types.ts";
|
|
11
11
|
|
|
12
12
|
export interface IAudioDefaults {
|
|
13
13
|
volume?: number | null;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { ConfigurablePreferences } from "../../preferences/Configurable";
|
|
1
|
+
import { ConfigurablePreferences } from "../../preferences/Configurable.ts";
|
|
2
2
|
import {
|
|
3
3
|
ensureBoolean,
|
|
4
4
|
ensureValueInRange,
|
|
5
5
|
ensureNonNegative
|
|
6
|
-
} from "../../preferences/guards";
|
|
6
|
+
} from "../../preferences/guards.ts";
|
|
7
7
|
import {
|
|
8
8
|
volumeRangeConfig,
|
|
9
9
|
playbackRateRangeConfig,
|
|
10
10
|
skipIntervalRangeConfig
|
|
11
|
-
} from "../../preferences/Types";
|
|
11
|
+
} from "../../preferences/Types.ts";
|
|
12
12
|
|
|
13
13
|
export interface IAudioPreferences {
|
|
14
14
|
volume?: number | null;
|
|
@@ -22,14 +22,14 @@ export interface IAudioPreferences {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export class AudioPreferences implements ConfigurablePreferences<AudioPreferences> {
|
|
25
|
-
public
|
|
26
|
-
public
|
|
27
|
-
public
|
|
28
|
-
public
|
|
29
|
-
public
|
|
30
|
-
public
|
|
31
|
-
public
|
|
32
|
-
public
|
|
25
|
+
public volume: number | null | undefined;
|
|
26
|
+
public playbackRate: number | null | undefined;
|
|
27
|
+
public preservePitch: boolean | null | undefined;
|
|
28
|
+
public skipBackwardInterval: number | null | undefined;
|
|
29
|
+
public skipForwardInterval: number | null | undefined;
|
|
30
|
+
public pollInterval: number | null | undefined;
|
|
31
|
+
public autoPlay: boolean | null | undefined;
|
|
32
|
+
public enableMediaSession: boolean | null | undefined;
|
|
33
33
|
|
|
34
34
|
constructor(preferences: IAudioPreferences = {}) {
|
|
35
35
|
this.volume = ensureValueInRange(preferences.volume, volumeRangeConfig.range);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { IPreferencesEditor } from "../../preferences/PreferencesEditor";
|
|
2
|
-
import { AudioPreferences } from "./AudioPreferences";
|
|
3
|
-
import { AudioSettings } from "./AudioSettings";
|
|
4
|
-
import { Preference, BooleanPreference, RangePreference } from "../../preferences/Preference";
|
|
1
|
+
import { IPreferencesEditor } from "../../preferences/PreferencesEditor.ts";
|
|
2
|
+
import { AudioPreferences } from "./AudioPreferences.ts";
|
|
3
|
+
import { AudioSettings } from "./AudioSettings.ts";
|
|
4
|
+
import { Preference, BooleanPreference, RangePreference } from "../../preferences/Preference.ts";
|
|
5
5
|
import {
|
|
6
6
|
volumeRangeConfig,
|
|
7
7
|
playbackRateRangeConfig,
|
|
8
8
|
skipIntervalRangeConfig
|
|
9
|
-
} from "../../preferences/Types";
|
|
9
|
+
} from "../../preferences/Types.ts";
|
|
10
10
|
|
|
11
11
|
export class AudioPreferencesEditor implements IPreferencesEditor {
|
|
12
12
|
preferences: AudioPreferences;
|
|
@@ -31,7 +31,7 @@ export class AudioPreferencesEditor implements IPreferencesEditor {
|
|
|
31
31
|
effectiveValue: this.settings.volume,
|
|
32
32
|
isEffective: this.preferences.volume !== null,
|
|
33
33
|
onChange: (newValue: number | null | undefined) => {
|
|
34
|
-
this.updatePreference("volume", newValue ??
|
|
34
|
+
this.updatePreference("volume", newValue ?? null);
|
|
35
35
|
},
|
|
36
36
|
supportedRange: volumeRangeConfig.range,
|
|
37
37
|
step: volumeRangeConfig.step
|
|
@@ -44,7 +44,7 @@ export class AudioPreferencesEditor implements IPreferencesEditor {
|
|
|
44
44
|
effectiveValue: this.settings.playbackRate,
|
|
45
45
|
isEffective: this.preferences.playbackRate !== null,
|
|
46
46
|
onChange: (newValue: number | null | undefined) => {
|
|
47
|
-
this.updatePreference("playbackRate", newValue ??
|
|
47
|
+
this.updatePreference("playbackRate", newValue ?? null);
|
|
48
48
|
},
|
|
49
49
|
supportedRange: playbackRateRangeConfig.range,
|
|
50
50
|
step: playbackRateRangeConfig.step
|
|
@@ -57,7 +57,7 @@ export class AudioPreferencesEditor implements IPreferencesEditor {
|
|
|
57
57
|
effectiveValue: this.settings.preservePitch,
|
|
58
58
|
isEffective: this.preferences.preservePitch !== null,
|
|
59
59
|
onChange: (newValue: boolean | null | undefined) => {
|
|
60
|
-
this.updatePreference("preservePitch", newValue ??
|
|
60
|
+
this.updatePreference("preservePitch", newValue ?? null);
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
}
|
|
@@ -68,7 +68,7 @@ export class AudioPreferencesEditor implements IPreferencesEditor {
|
|
|
68
68
|
effectiveValue: this.settings.skipBackwardInterval,
|
|
69
69
|
isEffective: this.preferences.skipBackwardInterval !== null,
|
|
70
70
|
onChange: (newValue: number | null | undefined) => {
|
|
71
|
-
this.updatePreference("skipBackwardInterval", newValue ??
|
|
71
|
+
this.updatePreference("skipBackwardInterval", newValue ?? null);
|
|
72
72
|
},
|
|
73
73
|
supportedRange: skipIntervalRangeConfig.range,
|
|
74
74
|
step: skipIntervalRangeConfig.step
|
|
@@ -81,7 +81,7 @@ export class AudioPreferencesEditor implements IPreferencesEditor {
|
|
|
81
81
|
effectiveValue: this.settings.skipForwardInterval,
|
|
82
82
|
isEffective: this.preferences.skipForwardInterval !== null,
|
|
83
83
|
onChange: (newValue: number | null | undefined) => {
|
|
84
|
-
this.updatePreference("skipForwardInterval", newValue ??
|
|
84
|
+
this.updatePreference("skipForwardInterval", newValue ?? null);
|
|
85
85
|
},
|
|
86
86
|
supportedRange: skipIntervalRangeConfig.range,
|
|
87
87
|
step: skipIntervalRangeConfig.step
|
|
@@ -94,7 +94,7 @@ export class AudioPreferencesEditor implements IPreferencesEditor {
|
|
|
94
94
|
effectiveValue: this.settings.pollInterval,
|
|
95
95
|
isEffective: this.preferences.pollInterval !== null,
|
|
96
96
|
onChange: (newValue: number | null | undefined) => {
|
|
97
|
-
this.updatePreference("pollInterval", newValue ??
|
|
97
|
+
this.updatePreference("pollInterval", newValue ?? null);
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
100
|
}
|
|
@@ -105,7 +105,7 @@ export class AudioPreferencesEditor implements IPreferencesEditor {
|
|
|
105
105
|
effectiveValue: this.settings.autoPlay,
|
|
106
106
|
isEffective: this.preferences.autoPlay !== null,
|
|
107
107
|
onChange: (newValue: boolean | null | undefined) => {
|
|
108
|
-
this.updatePreference("autoPlay", newValue ??
|
|
108
|
+
this.updatePreference("autoPlay", newValue ?? null);
|
|
109
109
|
}
|
|
110
110
|
});
|
|
111
111
|
}
|
|
@@ -116,7 +116,7 @@ export class AudioPreferencesEditor implements IPreferencesEditor {
|
|
|
116
116
|
effectiveValue: this.settings.enableMediaSession,
|
|
117
117
|
isEffective: this.preferences.enableMediaSession !== null,
|
|
118
118
|
onChange: (newValue: boolean | null | undefined) => {
|
|
119
|
-
this.updatePreference("enableMediaSession", newValue ??
|
|
119
|
+
this.updatePreference("enableMediaSession", newValue ?? null);
|
|
120
120
|
}
|
|
121
121
|
});
|
|
122
122
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AudioPreferences } from "./AudioPreferences";
|
|
2
|
-
import { AudioDefaults } from "./AudioDefaults";
|
|
3
|
-
import { ConfigurableSettings } from "../../preferences/Configurable";
|
|
1
|
+
import { AudioPreferences } from "./AudioPreferences.ts";
|
|
2
|
+
import { AudioDefaults } from "./AudioDefaults.ts";
|
|
3
|
+
import { ConfigurableSettings } from "../../preferences/Configurable.ts";
|
|
4
4
|
|
|
5
5
|
export interface IAudioSettings extends ConfigurableSettings {
|
|
6
6
|
volume: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export * from './AudioPreferences';
|
|
2
|
-
export * from './AudioDefaults';
|
|
3
|
-
export * from './AudioSettings';
|
|
4
|
-
export * from './AudioPreferencesEditor';
|
|
1
|
+
export * from './AudioPreferences.ts';
|
|
2
|
+
export * from './AudioDefaults.ts';
|
|
3
|
+
export * from './AudioSettings.ts';
|
|
4
|
+
export * from './AudioPreferencesEditor.ts';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { NavigatorProtector } from "../../protection/NavigatorProtector";
|
|
2
|
-
import { DragAndDropProtector } from "../../protection/DragAndDropProtector";
|
|
3
|
-
import { CopyProtector } from "../../protection/CopyProtector";
|
|
4
|
-
import { IContentProtectionConfig } from "../../Navigator";
|
|
1
|
+
import { NavigatorProtector } from "../../protection/NavigatorProtector.ts";
|
|
2
|
+
import { DragAndDropProtector } from "../../protection/DragAndDropProtector.ts";
|
|
3
|
+
import { CopyProtector } from "../../protection/CopyProtector.ts";
|
|
4
|
+
import { IContentProtectionConfig } from "../../Navigator.ts";
|
|
5
5
|
|
|
6
6
|
export class AudioNavigatorProtector extends NavigatorProtector {
|
|
7
7
|
private dragAndDropProtector?: DragAndDropProtector;
|
package/src/css/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./Properties";
|
|
1
|
+
export * from "./Properties.ts";
|