@prose-reader/enhancer-audio 1.285.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/audioEnhancer.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +908 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.cjs +917 -0
- package/dist/index.umd.cjs.map +1 -0
- package/dist/playback/AudioController.d.ts +35 -0
- package/dist/playback/AudioController.test.d.ts +1 -0
- package/dist/playback/AudioElementAdapter.d.ts +20 -0
- package/dist/playback/ResourcesResolver.d.ts +10 -0
- package/dist/playback/trackStreams.d.ts +26 -0
- package/dist/playback/trackStreams.test.d.ts +1 -0
- package/dist/playback/types.d.ts +17 -0
- package/dist/renderer/AudioRenderer.d.ts +13 -0
- package/dist/types.d.ts +39 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/visualizer/AudioVisualizer.d.ts +16 -0
- package/dist/visualizer/AudioVisualizer.test.d.ts +1 -0
- package/dist/visualizer/VisualizerAudioGraph.d.ts +12 -0
- package/dist/visualizer/index.d.ts +2 -0
- package/dist/visualizer/levels.d.ts +2 -0
- package/package.json +28 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
import { ReactiveEntity, mapKeysTo, isDefined, DocumentRenderer } from '@prose-reader/core';
|
|
2
|
+
import { BehaviorSubject, Subscription, distinctUntilChanged, switchMap, of, concat, map, defaultIfEmpty, defer, from, EMPTY, animationFrames, fromEvent, share, merge, retry, take, shareReplay, combineLatest, Subject, withLatestFrom, filter, tap, catchError, takeUntil } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
const AUDIO_VISUALIZER_LEVEL_COUNT = 80;
|
|
5
|
+
const AUDIO_VISUALIZER_NOISE_FLOOR = 0.035;
|
|
6
|
+
const getIdleVisualizerLevels = () => Array.from({ length: AUDIO_VISUALIZER_LEVEL_COUNT }, () => 0);
|
|
7
|
+
const getVisualizerLevels = (data) => {
|
|
8
|
+
if (data.length === 0) return getIdleVisualizerLevels();
|
|
9
|
+
const bucketSize = Math.max(
|
|
10
|
+
1,
|
|
11
|
+
Math.floor(data.length / AUDIO_VISUALIZER_LEVEL_COUNT)
|
|
12
|
+
);
|
|
13
|
+
return Array.from({ length: AUDIO_VISUALIZER_LEVEL_COUNT }, (_, index) => {
|
|
14
|
+
const start = index * bucketSize;
|
|
15
|
+
const end = Math.min(data.length, start + bucketSize);
|
|
16
|
+
if (start >= data.length || start === end) return 0;
|
|
17
|
+
let total = 0;
|
|
18
|
+
for (let cursor = start; cursor < end; cursor += 1) {
|
|
19
|
+
total += data[cursor] ?? 0;
|
|
20
|
+
}
|
|
21
|
+
const average = total / (end - start);
|
|
22
|
+
const normalizedAverage = average / 255;
|
|
23
|
+
const gatedAverage = (normalizedAverage - AUDIO_VISUALIZER_NOISE_FLOOR) / (1 - AUDIO_VISUALIZER_NOISE_FLOOR);
|
|
24
|
+
return Math.max(0, Math.min(1, gatedAverage));
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const AUDIO_VISUALIZER_FFT_SIZE = 256;
|
|
29
|
+
class VisualizerAudioGraph {
|
|
30
|
+
constructor(audioElement) {
|
|
31
|
+
this.audioElement = audioElement;
|
|
32
|
+
}
|
|
33
|
+
audioContext;
|
|
34
|
+
audioSourceNode;
|
|
35
|
+
analyserNode;
|
|
36
|
+
frequencyData;
|
|
37
|
+
async resumeIfNeeded() {
|
|
38
|
+
if (!this.ensure()) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const audioContext = this.audioContext;
|
|
42
|
+
if (!audioContext) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (audioContext.state !== `suspended`) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
await audioContext.resume();
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
readLevels() {
|
|
56
|
+
if (!this.analyserNode || !this.frequencyData) {
|
|
57
|
+
return getIdleVisualizerLevels();
|
|
58
|
+
}
|
|
59
|
+
this.analyserNode.getByteFrequencyData(this.frequencyData);
|
|
60
|
+
return getVisualizerLevels(this.frequencyData);
|
|
61
|
+
}
|
|
62
|
+
destroy() {
|
|
63
|
+
this.audioSourceNode?.disconnect();
|
|
64
|
+
this.analyserNode?.disconnect();
|
|
65
|
+
this.audioContext?.close().catch(() => void 0);
|
|
66
|
+
this.audioContext = void 0;
|
|
67
|
+
this.audioSourceNode = void 0;
|
|
68
|
+
this.analyserNode = void 0;
|
|
69
|
+
this.frequencyData = void 0;
|
|
70
|
+
}
|
|
71
|
+
ensure() {
|
|
72
|
+
if (this.audioContext && this.analyserNode && this.frequencyData) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (typeof window === `undefined` || !window.AudioContext) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const audioContext = new window.AudioContext();
|
|
79
|
+
const audioSourceNode = audioContext.createMediaElementSource(
|
|
80
|
+
this.audioElement
|
|
81
|
+
);
|
|
82
|
+
const analyserNode = audioContext.createAnalyser();
|
|
83
|
+
analyserNode.fftSize = AUDIO_VISUALIZER_FFT_SIZE;
|
|
84
|
+
analyserNode.smoothingTimeConstant = 0.8;
|
|
85
|
+
audioSourceNode.connect(analyserNode);
|
|
86
|
+
analyserNode.connect(audioContext.destination);
|
|
87
|
+
this.audioContext = audioContext;
|
|
88
|
+
this.audioSourceNode = audioSourceNode;
|
|
89
|
+
this.analyserNode = analyserNode;
|
|
90
|
+
this.frequencyData = new Uint8Array(analyserNode.frequencyBinCount);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class AudioVisualizer extends ReactiveEntity {
|
|
96
|
+
audioGraph;
|
|
97
|
+
playbackState$ = new BehaviorSubject({
|
|
98
|
+
trackId: void 0,
|
|
99
|
+
isRunning: false,
|
|
100
|
+
resetLevels: false
|
|
101
|
+
});
|
|
102
|
+
subscriptions = new Subscription();
|
|
103
|
+
constructor(audioElement) {
|
|
104
|
+
super({
|
|
105
|
+
levels: getIdleVisualizerLevels(),
|
|
106
|
+
isActive: false,
|
|
107
|
+
trackId: void 0
|
|
108
|
+
});
|
|
109
|
+
this.audioGraph = new VisualizerAudioGraph(audioElement);
|
|
110
|
+
this.subscriptions.add(
|
|
111
|
+
this.playbackState$.pipe(
|
|
112
|
+
distinctUntilChanged(
|
|
113
|
+
(previous, next) => previous.trackId === next.trackId && previous.isRunning === next.isRunning && previous.resetLevels === next.resetLevels
|
|
114
|
+
),
|
|
115
|
+
switchMap(({ trackId, isRunning, resetLevels }) => {
|
|
116
|
+
if (!trackId || !isRunning) {
|
|
117
|
+
return of({
|
|
118
|
+
trackId,
|
|
119
|
+
isActive: false,
|
|
120
|
+
...resetLevels ? { levels: getIdleVisualizerLevels() } : void 0
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const shouldResetLevels = this.value.trackId !== trackId;
|
|
124
|
+
return concat(
|
|
125
|
+
of({
|
|
126
|
+
trackId,
|
|
127
|
+
isActive: true,
|
|
128
|
+
...shouldResetLevels ? { levels: getIdleVisualizerLevels() } : void 0
|
|
129
|
+
}),
|
|
130
|
+
this.createLevels$().pipe(
|
|
131
|
+
map((levels) => ({
|
|
132
|
+
trackId,
|
|
133
|
+
levels,
|
|
134
|
+
isActive: true
|
|
135
|
+
})),
|
|
136
|
+
defaultIfEmpty({
|
|
137
|
+
trackId,
|
|
138
|
+
isActive: false,
|
|
139
|
+
levels: getIdleVisualizerLevels()
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
})
|
|
144
|
+
).subscribe((value) => {
|
|
145
|
+
this.mergeCompare(value);
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
start(currentTrack) {
|
|
150
|
+
if (!currentTrack) return;
|
|
151
|
+
this.playbackState$.next({
|
|
152
|
+
trackId: currentTrack.id,
|
|
153
|
+
isRunning: true,
|
|
154
|
+
resetLevels: false
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
setTrack(trackId) {
|
|
158
|
+
this.playbackState$.next({
|
|
159
|
+
trackId,
|
|
160
|
+
isRunning: false,
|
|
161
|
+
resetLevels: false
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
reset(trackId) {
|
|
165
|
+
this.playbackState$.next({
|
|
166
|
+
trackId,
|
|
167
|
+
isRunning: false,
|
|
168
|
+
resetLevels: true
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
stop({ resetLevels = false } = {}) {
|
|
172
|
+
this.playbackState$.next({
|
|
173
|
+
trackId: this.playbackState$.value.trackId,
|
|
174
|
+
isRunning: false,
|
|
175
|
+
resetLevels
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
destroy() {
|
|
179
|
+
this.stop({
|
|
180
|
+
resetLevels: true
|
|
181
|
+
});
|
|
182
|
+
this.subscriptions.unsubscribe();
|
|
183
|
+
this.playbackState$.complete();
|
|
184
|
+
this.audioGraph.destroy();
|
|
185
|
+
super.destroy();
|
|
186
|
+
}
|
|
187
|
+
createLevels$() {
|
|
188
|
+
return defer(() => {
|
|
189
|
+
return from(this.audioGraph.resumeIfNeeded()).pipe(
|
|
190
|
+
switchMap(
|
|
191
|
+
(isReady) => isReady ? animationFrames().pipe(map(() => this.audioGraph.readLevels())) : EMPTY
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
class AudioElementAdapter {
|
|
199
|
+
element;
|
|
200
|
+
canPlay$;
|
|
201
|
+
ended$;
|
|
202
|
+
isPlaying$;
|
|
203
|
+
metrics$;
|
|
204
|
+
constructor() {
|
|
205
|
+
this.element = document.createElement(`audio`);
|
|
206
|
+
this.element.preload = `metadata`;
|
|
207
|
+
this.canPlay$ = fromEvent(this.element, `canplay`).pipe(share());
|
|
208
|
+
this.ended$ = fromEvent(this.element, `ended`).pipe(share());
|
|
209
|
+
this.isPlaying$ = merge(
|
|
210
|
+
fromEvent(this.element, `play`).pipe(map(() => true)),
|
|
211
|
+
fromEvent(this.element, `pause`).pipe(map(() => false))
|
|
212
|
+
).pipe(share());
|
|
213
|
+
this.metrics$ = merge(
|
|
214
|
+
fromEvent(this.element, `timeupdate`),
|
|
215
|
+
fromEvent(this.element, `seeking`),
|
|
216
|
+
fromEvent(this.element, `seeked`),
|
|
217
|
+
fromEvent(this.element, `loadedmetadata`),
|
|
218
|
+
fromEvent(this.element, `durationchange`),
|
|
219
|
+
this.canPlay$
|
|
220
|
+
).pipe(
|
|
221
|
+
map(() => ({
|
|
222
|
+
currentTime: this.element.currentTime,
|
|
223
|
+
duration: Number.isFinite(this.element.duration) ? this.element.duration : void 0
|
|
224
|
+
})),
|
|
225
|
+
share()
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
get paused() {
|
|
229
|
+
return this.element.paused;
|
|
230
|
+
}
|
|
231
|
+
get hasSource() {
|
|
232
|
+
return this.element.hasAttribute(`src`);
|
|
233
|
+
}
|
|
234
|
+
play$() {
|
|
235
|
+
return defer(() => from(this.element.play())).pipe(
|
|
236
|
+
retry({
|
|
237
|
+
count: 1,
|
|
238
|
+
delay: () => this.canPlay$.pipe(take(1))
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
pause() {
|
|
243
|
+
this.element.pause();
|
|
244
|
+
}
|
|
245
|
+
loadSource(src) {
|
|
246
|
+
this.element.src = src;
|
|
247
|
+
this.element.load();
|
|
248
|
+
}
|
|
249
|
+
unloadSource() {
|
|
250
|
+
this.element.removeAttribute(`src`);
|
|
251
|
+
this.element.load();
|
|
252
|
+
}
|
|
253
|
+
setCurrentTime(value) {
|
|
254
|
+
this.element.currentTime = value;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
class ResourcesResolver {
|
|
259
|
+
cachedSourceByTrackId = /* @__PURE__ */ new Map();
|
|
260
|
+
hasCachedSource(trackId) {
|
|
261
|
+
return this.cachedSourceByTrackId.has(trackId);
|
|
262
|
+
}
|
|
263
|
+
getTrackResourceUrl$ = (track, resourcesHandler) => {
|
|
264
|
+
const cachedSource = this.cachedSourceByTrackId.get(track.id);
|
|
265
|
+
if (cachedSource) {
|
|
266
|
+
return of(cachedSource.url);
|
|
267
|
+
}
|
|
268
|
+
const resource$ = from(resourcesHandler.getResource());
|
|
269
|
+
return resource$.pipe(
|
|
270
|
+
switchMap((resource) => {
|
|
271
|
+
if (resource instanceof URL) {
|
|
272
|
+
this.cachedSourceByTrackId.set(track.id, {
|
|
273
|
+
url: resource.href
|
|
274
|
+
});
|
|
275
|
+
return of(resource.href);
|
|
276
|
+
}
|
|
277
|
+
if (resource instanceof Response) {
|
|
278
|
+
return from(resource.blob()).pipe(
|
|
279
|
+
map((blob) => {
|
|
280
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
281
|
+
this.cachedSourceByTrackId.set(track.id, {
|
|
282
|
+
url: objectUrl,
|
|
283
|
+
release: () => {
|
|
284
|
+
URL.revokeObjectURL(objectUrl);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
return objectUrl;
|
|
288
|
+
})
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
this.cachedSourceByTrackId.set(track.id, {
|
|
292
|
+
url: track.href
|
|
293
|
+
});
|
|
294
|
+
return of(track.href);
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
releaseTrackSource(trackId) {
|
|
299
|
+
const cachedSource = this.cachedSourceByTrackId.get(trackId);
|
|
300
|
+
if (!cachedSource) return;
|
|
301
|
+
this.cachedSourceByTrackId.delete(trackId);
|
|
302
|
+
cachedSource.release?.();
|
|
303
|
+
}
|
|
304
|
+
releaseAll() {
|
|
305
|
+
for (const trackId of this.cachedSourceByTrackId.keys()) {
|
|
306
|
+
this.releaseTrackSource(trackId);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
destroy() {
|
|
310
|
+
this.releaseAll();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const $ = (e, n) => e === n ? true : e.length !== n.length ? false : e.every((t, r) => t === n[r]);
|
|
315
|
+
const d = Object.prototype.hasOwnProperty, h = (e, n) => e === n ? e !== 0 || n !== 0 || 1 / e === 1 / n : false, _ = (e, n, t) => {
|
|
316
|
+
if (e === n)
|
|
317
|
+
return true;
|
|
318
|
+
if (typeof e != "object" || e === null || typeof n != "object" || n === null)
|
|
319
|
+
return false;
|
|
320
|
+
const r = Object.keys(e), l = Object.keys(n);
|
|
321
|
+
if (r.length !== l.length)
|
|
322
|
+
return false;
|
|
323
|
+
const o = t && typeof t.customEqual == "function" ? t.customEqual : h;
|
|
324
|
+
for (let c = 0; c < r.length; c++) {
|
|
325
|
+
const u = r[c] || "";
|
|
326
|
+
if (!d.call(n, u) || !o(e[u], n[u]))
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
};
|
|
331
|
+
const E = () => {
|
|
332
|
+
if (!(typeof window > "u"))
|
|
333
|
+
return window;
|
|
334
|
+
};
|
|
335
|
+
function w() {
|
|
336
|
+
const e = E()?.__PROSE_READER_DEBUG;
|
|
337
|
+
return e === true || e === "true";
|
|
338
|
+
}
|
|
339
|
+
const s = (e, n = w(), t) => {
|
|
340
|
+
let r = n;
|
|
341
|
+
const l = t?.color ? `color: ${t.color}` : void 0, o = {
|
|
342
|
+
enable: (i) => {
|
|
343
|
+
u(i);
|
|
344
|
+
},
|
|
345
|
+
namespace: (i, a) => s(`${e} [${i}]`, a, t),
|
|
346
|
+
isEnabled: () => r,
|
|
347
|
+
debug: () => {
|
|
348
|
+
},
|
|
349
|
+
info: () => {
|
|
350
|
+
},
|
|
351
|
+
log: () => {
|
|
352
|
+
},
|
|
353
|
+
groupCollapsed: () => {
|
|
354
|
+
},
|
|
355
|
+
groupEnd: () => {
|
|
356
|
+
},
|
|
357
|
+
getGroupArgs: (i) => l ? [`%c${e ? `${e} ${i}` : i}`, l] : [e ? `${e} ${i}` : i],
|
|
358
|
+
warn: () => {
|
|
359
|
+
},
|
|
360
|
+
error: () => {
|
|
361
|
+
}
|
|
362
|
+
}, c = (i) => {
|
|
363
|
+
if (!i) {
|
|
364
|
+
o.debug = () => {
|
|
365
|
+
}, o.info = () => {
|
|
366
|
+
}, o.log = () => {
|
|
367
|
+
}, o.groupCollapsed = () => {
|
|
368
|
+
}, o.groupEnd = () => {
|
|
369
|
+
}, o.warn = () => {
|
|
370
|
+
}, o.error = () => {
|
|
371
|
+
};
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
o.debug = e ? Function.prototype.bind.call(
|
|
375
|
+
console.debug,
|
|
376
|
+
console,
|
|
377
|
+
e,
|
|
378
|
+
`%c${e}`,
|
|
379
|
+
l
|
|
380
|
+
) : Function.prototype.bind.call(
|
|
381
|
+
console.debug,
|
|
382
|
+
console,
|
|
383
|
+
`%c${e}`,
|
|
384
|
+
l
|
|
385
|
+
), o.info = Function.prototype.bind.call(
|
|
386
|
+
console.info,
|
|
387
|
+
console,
|
|
388
|
+
`%c${e}`,
|
|
389
|
+
l
|
|
390
|
+
), o.log = e ? Function.prototype.bind.call(
|
|
391
|
+
console.log,
|
|
392
|
+
console,
|
|
393
|
+
`%c${e}`,
|
|
394
|
+
l
|
|
395
|
+
) : Function.prototype.bind.call(console.log, console), o.groupCollapsed = Function.prototype.bind.call(
|
|
396
|
+
console.groupCollapsed,
|
|
397
|
+
console
|
|
398
|
+
), o.groupEnd = Function.prototype.bind.call(console.groupEnd, console), o.warn = Function.prototype.bind.call(
|
|
399
|
+
console.warn,
|
|
400
|
+
console,
|
|
401
|
+
`%c${e}`,
|
|
402
|
+
l
|
|
403
|
+
), o.error = Function.prototype.bind.call(
|
|
404
|
+
console.error,
|
|
405
|
+
console,
|
|
406
|
+
`%c${e}`,
|
|
407
|
+
l
|
|
408
|
+
);
|
|
409
|
+
}, u = (i) => {
|
|
410
|
+
r !== i && (r = i, c(r));
|
|
411
|
+
};
|
|
412
|
+
return c(r), o;
|
|
413
|
+
}; ({
|
|
414
|
+
...s()});
|
|
415
|
+
|
|
416
|
+
const AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
417
|
+
`mp3`,
|
|
418
|
+
`m4a`,
|
|
419
|
+
`m4b`,
|
|
420
|
+
`aac`,
|
|
421
|
+
`ogg`,
|
|
422
|
+
`oga`,
|
|
423
|
+
`wav`,
|
|
424
|
+
`flac`,
|
|
425
|
+
`opus`
|
|
426
|
+
]);
|
|
427
|
+
const getExtensionFromHref = (href) => {
|
|
428
|
+
const [pathname = ``] = href.split(/[?#]/);
|
|
429
|
+
const segments = pathname.split(`/`);
|
|
430
|
+
const basename = segments.at(-1) ?? ``;
|
|
431
|
+
const extension = basename.split(`.`).at(-1);
|
|
432
|
+
return extension?.toLowerCase();
|
|
433
|
+
};
|
|
434
|
+
const isAudioSpineItem = (item) => {
|
|
435
|
+
const mediaType = item.mediaType?.split(`;`).at(0)?.trim().toLowerCase();
|
|
436
|
+
if (mediaType?.startsWith(`audio/`)) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
const extension = getExtensionFromHref(item.href);
|
|
440
|
+
return extension ? AUDIO_EXTENSIONS.has(extension) : false;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const getTrackAtSpineItemIndex = (tracks, index) => {
|
|
444
|
+
if (index === void 0) return void 0;
|
|
445
|
+
return tracks.find((track) => track.index === index);
|
|
446
|
+
};
|
|
447
|
+
const getVisibleTracks = (tracks, pagination) => {
|
|
448
|
+
const beginTrack = getTrackAtSpineItemIndex(
|
|
449
|
+
tracks,
|
|
450
|
+
pagination.beginSpineItemIndex
|
|
451
|
+
);
|
|
452
|
+
const endTrack = getTrackAtSpineItemIndex(
|
|
453
|
+
tracks,
|
|
454
|
+
pagination.endSpineItemIndex
|
|
455
|
+
);
|
|
456
|
+
return [beginTrack, endTrack].filter(
|
|
457
|
+
(track, i, arr) => track !== void 0 && arr.indexOf(track) === i
|
|
458
|
+
);
|
|
459
|
+
};
|
|
460
|
+
function createTrackStreams(reader, state$) {
|
|
461
|
+
const tracks$ = reader.context.manifest$.pipe(
|
|
462
|
+
map(
|
|
463
|
+
(manifest) => manifest.spineItems.filter(isAudioSpineItem).map((item) => ({
|
|
464
|
+
id: item.id,
|
|
465
|
+
href: item.href,
|
|
466
|
+
index: item.index,
|
|
467
|
+
mediaType: item.mediaType
|
|
468
|
+
}))
|
|
469
|
+
),
|
|
470
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
471
|
+
);
|
|
472
|
+
const pagination$ = reader.pagination.state$.pipe(
|
|
473
|
+
mapKeysTo([`beginSpineItemIndex`, `endSpineItemIndex`]),
|
|
474
|
+
distinctUntilChanged(_),
|
|
475
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
476
|
+
);
|
|
477
|
+
const visibleTrackIds$ = combineLatest([tracks$, pagination$]).pipe(
|
|
478
|
+
map(
|
|
479
|
+
([tracks, pagination]) => getVisibleTracks(tracks, pagination).map(({ id }) => id)
|
|
480
|
+
),
|
|
481
|
+
distinctUntilChanged($),
|
|
482
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
483
|
+
);
|
|
484
|
+
const currentTrack$ = state$.pipe(
|
|
485
|
+
map((state) => state.currentTrack),
|
|
486
|
+
distinctUntilChanged()
|
|
487
|
+
);
|
|
488
|
+
const nextTrack$ = combineLatest([tracks$, pagination$, currentTrack$]).pipe(
|
|
489
|
+
map(([tracks, { endSpineItemIndex }, currentTrack]) => {
|
|
490
|
+
const nextTrackInPaginationWindow = currentTrack && endSpineItemIndex !== void 0 ? tracks.find(
|
|
491
|
+
({ index }) => index > currentTrack.index && index <= endSpineItemIndex
|
|
492
|
+
) : void 0;
|
|
493
|
+
const nextTrackAfterCurrentTrack = currentTrack ? tracks.find(({ index }) => index > currentTrack.index) : void 0;
|
|
494
|
+
return { nextTrackInPaginationWindow, nextTrackAfterCurrentTrack };
|
|
495
|
+
}),
|
|
496
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
497
|
+
);
|
|
498
|
+
return {
|
|
499
|
+
tracks$,
|
|
500
|
+
visibleTrackIds$,
|
|
501
|
+
nextTrack$
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const initialDesiredPlayback = {
|
|
506
|
+
shouldPlay: false,
|
|
507
|
+
trackId: void 0
|
|
508
|
+
};
|
|
509
|
+
const initialState = {
|
|
510
|
+
tracks: [],
|
|
511
|
+
currentTrack: void 0,
|
|
512
|
+
isPlaying: false,
|
|
513
|
+
isLoading: false,
|
|
514
|
+
hasError: false,
|
|
515
|
+
currentTime: 0,
|
|
516
|
+
duration: void 0
|
|
517
|
+
};
|
|
518
|
+
class AudioController extends ReactiveEntity {
|
|
519
|
+
reader;
|
|
520
|
+
audio;
|
|
521
|
+
visualizer$;
|
|
522
|
+
resourcesResolver = new ResourcesResolver();
|
|
523
|
+
visibleTrackIds$;
|
|
524
|
+
playCommandSubject = new Subject();
|
|
525
|
+
pauseCommandSubject = new Subject();
|
|
526
|
+
selectCommandSubject = new Subject();
|
|
527
|
+
desiredPlayback$ = new BehaviorSubject(
|
|
528
|
+
initialDesiredPlayback
|
|
529
|
+
);
|
|
530
|
+
subscriptions = new Subscription();
|
|
531
|
+
constructor(reader, audio = new AudioElementAdapter()) {
|
|
532
|
+
super(initialState);
|
|
533
|
+
this.reader = reader;
|
|
534
|
+
this.audio = audio;
|
|
535
|
+
this.visualizer$ = new AudioVisualizer(this.audio.element);
|
|
536
|
+
const { tracks$, visibleTrackIds$, nextTrack$ } = createTrackStreams(
|
|
537
|
+
this.reader,
|
|
538
|
+
this.state$
|
|
539
|
+
);
|
|
540
|
+
this.visibleTrackIds$ = visibleTrackIds$;
|
|
541
|
+
const firstVisibleTrackId$ = this.visibleTrackIds$.pipe(
|
|
542
|
+
map((trackIds) => trackIds[0]),
|
|
543
|
+
distinctUntilChanged(),
|
|
544
|
+
share()
|
|
545
|
+
);
|
|
546
|
+
const visibleTrackReset$ = firstVisibleTrackId$.pipe(
|
|
547
|
+
withLatestFrom(this.state$),
|
|
548
|
+
filter(
|
|
549
|
+
([trackId, state]) => trackId === void 0 && (state.currentTrack?.id !== void 0 || state.isLoading)
|
|
550
|
+
),
|
|
551
|
+
map(([, state]) => state.tracks)
|
|
552
|
+
);
|
|
553
|
+
const visibleTrackSelectionIntent$ = firstVisibleTrackId$.pipe(
|
|
554
|
+
filter((trackId) => trackId !== void 0),
|
|
555
|
+
withLatestFrom(this.desiredPlayback$),
|
|
556
|
+
map(([trackId, { shouldPlay }]) => ({
|
|
557
|
+
trackId,
|
|
558
|
+
options: {
|
|
559
|
+
navigate: false,
|
|
560
|
+
play: shouldPlay ? true : void 0
|
|
561
|
+
}
|
|
562
|
+
}))
|
|
563
|
+
);
|
|
564
|
+
const tracksChanged$ = tracks$.pipe(
|
|
565
|
+
tap(() => this.resourcesResolver.releaseAll())
|
|
566
|
+
);
|
|
567
|
+
const playbackReset$ = merge(visibleTrackReset$, tracksChanged$).pipe(
|
|
568
|
+
tap((tracks) => {
|
|
569
|
+
this.emitDesiredPlayback({ shouldPlay: false, trackId: void 0 });
|
|
570
|
+
this.visualizer$.stop({ resetLevels: true });
|
|
571
|
+
this.unmountCurrentSource();
|
|
572
|
+
this.mergeCompare({
|
|
573
|
+
tracks,
|
|
574
|
+
currentTrack: void 0,
|
|
575
|
+
isLoading: false,
|
|
576
|
+
isPlaying: false,
|
|
577
|
+
hasError: false,
|
|
578
|
+
currentTime: 0,
|
|
579
|
+
duration: void 0
|
|
580
|
+
});
|
|
581
|
+
this.visualizer$.reset(void 0);
|
|
582
|
+
}),
|
|
583
|
+
share()
|
|
584
|
+
);
|
|
585
|
+
const playSelectionIntent$ = this.playCommandSubject.pipe(
|
|
586
|
+
filter(() => !this.audio.hasSource),
|
|
587
|
+
map(() => this.state.currentTrack ?? this.state.tracks[0]),
|
|
588
|
+
filter(isDefined),
|
|
589
|
+
map((track) => ({
|
|
590
|
+
trackId: track.id,
|
|
591
|
+
options: { navigate: false, play: true }
|
|
592
|
+
}))
|
|
593
|
+
);
|
|
594
|
+
const resumePlayback$ = this.playCommandSubject.pipe(
|
|
595
|
+
filter(() => this.audio.hasSource),
|
|
596
|
+
tap(() => {
|
|
597
|
+
this.emitDesiredPlayback({
|
|
598
|
+
shouldPlay: true,
|
|
599
|
+
trackId: this.state.currentTrack?.id
|
|
600
|
+
});
|
|
601
|
+
})
|
|
602
|
+
);
|
|
603
|
+
const endedSelectionIntent$ = this.audio.ended$.pipe(
|
|
604
|
+
withLatestFrom(nextTrack$),
|
|
605
|
+
switchMap(
|
|
606
|
+
([, { nextTrackAfterCurrentTrack, nextTrackInPaginationWindow }]) => {
|
|
607
|
+
this.visualizer$.stop({ resetLevels: true });
|
|
608
|
+
if (nextTrackInPaginationWindow) {
|
|
609
|
+
return of({
|
|
610
|
+
trackId: nextTrackInPaginationWindow.id,
|
|
611
|
+
options: { navigate: false, play: true }
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
if (!nextTrackAfterCurrentTrack) {
|
|
615
|
+
this.emitDesiredPlayback({ shouldPlay: false, trackId: void 0 });
|
|
616
|
+
}
|
|
617
|
+
this.reader.navigation.goToRightOrBottomSpineItem();
|
|
618
|
+
return EMPTY;
|
|
619
|
+
}
|
|
620
|
+
)
|
|
621
|
+
);
|
|
622
|
+
const selection$ = merge(
|
|
623
|
+
this.selectCommandSubject,
|
|
624
|
+
visibleTrackSelectionIntent$,
|
|
625
|
+
endedSelectionIntent$,
|
|
626
|
+
playSelectionIntent$
|
|
627
|
+
).pipe(
|
|
628
|
+
withLatestFrom(this.state$),
|
|
629
|
+
filter(
|
|
630
|
+
([selectionIntent, state]) => state.tracks.some(({ id }) => id === selectionIntent.trackId)
|
|
631
|
+
),
|
|
632
|
+
map(([selectionIntent, state]) => ({
|
|
633
|
+
selectionIntent,
|
|
634
|
+
state,
|
|
635
|
+
isReselectionWhileLoading: state.currentTrack?.id === selectionIntent.trackId && state.isLoading
|
|
636
|
+
})),
|
|
637
|
+
tap(({ selectionIntent, state, isReselectionWhileLoading }) => {
|
|
638
|
+
if (selectionIntent.options.navigate !== false) {
|
|
639
|
+
this.reader.navigation.navigate({
|
|
640
|
+
spineItem: selectionIntent.trackId,
|
|
641
|
+
animation: `turn`
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (isReselectionWhileLoading && selectionIntent.options.play !== void 0) {
|
|
645
|
+
this.emitDesiredPlayback({
|
|
646
|
+
shouldPlay: selectionIntent.options.play,
|
|
647
|
+
trackId: state.currentTrack?.id
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}),
|
|
651
|
+
filter(({ isReselectionWhileLoading }) => !isReselectionWhileLoading),
|
|
652
|
+
switchMap(
|
|
653
|
+
({ selectionIntent: { trackId, options } }) => this.selectTrack$({ trackId, options, playbackReset$ })
|
|
654
|
+
)
|
|
655
|
+
);
|
|
656
|
+
const playback$ = merge(resumePlayback$, selection$).pipe(
|
|
657
|
+
withLatestFrom(this.desiredPlayback$),
|
|
658
|
+
switchMap(([, { shouldPlay, trackId }]) => {
|
|
659
|
+
if (!shouldPlay || !this.audio.hasSource || this.state.currentTrack?.id !== trackId) {
|
|
660
|
+
return EMPTY;
|
|
661
|
+
}
|
|
662
|
+
this.mergeCompare({ hasError: false });
|
|
663
|
+
return this.audio.play$().pipe(
|
|
664
|
+
catchError(() => {
|
|
665
|
+
this.mergeCompare({ hasError: true });
|
|
666
|
+
return EMPTY;
|
|
667
|
+
})
|
|
668
|
+
);
|
|
669
|
+
})
|
|
670
|
+
);
|
|
671
|
+
this.subscriptions.add(playbackReset$.subscribe());
|
|
672
|
+
this.subscriptions.add(playback$.subscribe());
|
|
673
|
+
this.subscriptions.add(
|
|
674
|
+
this.pauseCommandSubject.subscribe(() => {
|
|
675
|
+
this.emitDesiredPlayback({ shouldPlay: false, trackId: void 0 });
|
|
676
|
+
})
|
|
677
|
+
);
|
|
678
|
+
this.subscriptions.add(
|
|
679
|
+
this.audio.isPlaying$.subscribe((isPlaying) => {
|
|
680
|
+
this.mergeCompare({ isPlaying });
|
|
681
|
+
if (!isPlaying) {
|
|
682
|
+
this.visualizer$.stop();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (!this.state.currentTrack) return;
|
|
686
|
+
this.visualizer$.start(this.state.currentTrack);
|
|
687
|
+
})
|
|
688
|
+
);
|
|
689
|
+
this.subscriptions.add(
|
|
690
|
+
this.audio.metrics$.subscribe(({ currentTime, duration }) => {
|
|
691
|
+
this.mergeCompare({ currentTime, duration });
|
|
692
|
+
})
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
get state() {
|
|
696
|
+
return this.value;
|
|
697
|
+
}
|
|
698
|
+
get visualizer() {
|
|
699
|
+
return this.visualizer$.value;
|
|
700
|
+
}
|
|
701
|
+
resetTrackSelection({
|
|
702
|
+
currentTrack,
|
|
703
|
+
isLoading
|
|
704
|
+
}) {
|
|
705
|
+
this.mergeCompare({
|
|
706
|
+
currentTrack,
|
|
707
|
+
isLoading,
|
|
708
|
+
isPlaying: false,
|
|
709
|
+
hasError: false,
|
|
710
|
+
currentTime: 0,
|
|
711
|
+
duration: void 0
|
|
712
|
+
});
|
|
713
|
+
this.visualizer$.reset(currentTrack?.id);
|
|
714
|
+
}
|
|
715
|
+
emitDesiredPlayback({ shouldPlay, trackId }) {
|
|
716
|
+
this.desiredPlayback$.next({
|
|
717
|
+
shouldPlay,
|
|
718
|
+
trackId: shouldPlay ? trackId : void 0
|
|
719
|
+
});
|
|
720
|
+
if (!shouldPlay) {
|
|
721
|
+
this.audio.pause();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
unmountCurrentSource() {
|
|
725
|
+
if (!this.audio.hasSource) return;
|
|
726
|
+
const trackId = this.state.currentTrack?.id;
|
|
727
|
+
if (!this.audio.paused) {
|
|
728
|
+
this.audio.pause();
|
|
729
|
+
}
|
|
730
|
+
this.audio.unloadSource();
|
|
731
|
+
if (trackId) {
|
|
732
|
+
this.resourcesResolver.releaseTrackSource(trackId);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
mountSource(source) {
|
|
736
|
+
this.unmountCurrentSource();
|
|
737
|
+
this.audio.loadSource(source);
|
|
738
|
+
}
|
|
739
|
+
resolveTrackSource(track) {
|
|
740
|
+
const spineItem = this.reader.spineItemsManager.get(track.index);
|
|
741
|
+
if (!spineItem) return EMPTY;
|
|
742
|
+
return this.resourcesResolver.getTrackResourceUrl$(
|
|
743
|
+
track,
|
|
744
|
+
spineItem.resourcesHandler
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
selectTrack$({
|
|
748
|
+
trackId,
|
|
749
|
+
options,
|
|
750
|
+
playbackReset$
|
|
751
|
+
}) {
|
|
752
|
+
const track = this.state.tracks.find(({ id }) => id === trackId);
|
|
753
|
+
if (!track) return EMPTY;
|
|
754
|
+
const currentTrack = this.state.currentTrack;
|
|
755
|
+
const shouldPlay = options.play ?? (!this.audio.paused && currentTrack !== void 0);
|
|
756
|
+
this.emitDesiredPlayback({ shouldPlay, trackId: track.id });
|
|
757
|
+
if (currentTrack?.id === track.id && this.audio.hasSource) {
|
|
758
|
+
this.visualizer$.setTrack(track.id);
|
|
759
|
+
return shouldPlay ? of(void 0) : EMPTY;
|
|
760
|
+
}
|
|
761
|
+
this.unmountCurrentSource();
|
|
762
|
+
this.resetTrackSelection({
|
|
763
|
+
currentTrack: track,
|
|
764
|
+
isLoading: true
|
|
765
|
+
});
|
|
766
|
+
return this.resolveTrackSource(track).pipe(
|
|
767
|
+
map((source) => ({ source })),
|
|
768
|
+
defaultIfEmpty({ source: void 0 }),
|
|
769
|
+
catchError(() => of({ source: void 0 })),
|
|
770
|
+
takeUntil(playbackReset$),
|
|
771
|
+
tap(({ source }) => {
|
|
772
|
+
if (!source) {
|
|
773
|
+
this.emitDesiredPlayback({ shouldPlay: false, trackId: void 0 });
|
|
774
|
+
} else {
|
|
775
|
+
this.mountSource(source);
|
|
776
|
+
}
|
|
777
|
+
this.mergeCompare({ isLoading: false });
|
|
778
|
+
}),
|
|
779
|
+
filter(({ source }) => source !== void 0),
|
|
780
|
+
map(() => void 0)
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
select(trackId, options = {}) {
|
|
784
|
+
this.selectCommandSubject.next({ trackId, options });
|
|
785
|
+
}
|
|
786
|
+
play() {
|
|
787
|
+
this.playCommandSubject.next();
|
|
788
|
+
}
|
|
789
|
+
pause() {
|
|
790
|
+
this.pauseCommandSubject.next();
|
|
791
|
+
}
|
|
792
|
+
toggle() {
|
|
793
|
+
if (this.audio.paused) {
|
|
794
|
+
this.play();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
this.pause();
|
|
798
|
+
}
|
|
799
|
+
setCurrentTime(value) {
|
|
800
|
+
this.mergeCompare({ currentTime: value });
|
|
801
|
+
this.audio.setCurrentTime(value);
|
|
802
|
+
}
|
|
803
|
+
destroy() {
|
|
804
|
+
this.subscriptions.unsubscribe();
|
|
805
|
+
this.playCommandSubject.complete();
|
|
806
|
+
this.pauseCommandSubject.complete();
|
|
807
|
+
this.selectCommandSubject.complete();
|
|
808
|
+
this.desiredPlayback$.complete();
|
|
809
|
+
this.visualizer$.destroy();
|
|
810
|
+
this.unmountCurrentSource();
|
|
811
|
+
this.resourcesResolver.destroy();
|
|
812
|
+
super.destroy();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
class AudioRenderer extends DocumentRenderer {
|
|
817
|
+
onCreateDocument() {
|
|
818
|
+
const ownerDocument = this.containerElement.ownerDocument;
|
|
819
|
+
const rootElement = ownerDocument.createElement(`div`);
|
|
820
|
+
rootElement.style.cssText = `
|
|
821
|
+
box-sizing: border-box;
|
|
822
|
+
width: 100%;
|
|
823
|
+
height: 100%;
|
|
824
|
+
display: block;
|
|
825
|
+
`;
|
|
826
|
+
rootElement.setAttribute(`data-prose-reader-audio-page`, this.item.id);
|
|
827
|
+
this.setDocumentContainer(rootElement);
|
|
828
|
+
return of(rootElement);
|
|
829
|
+
}
|
|
830
|
+
onLoadDocument() {
|
|
831
|
+
this.attach();
|
|
832
|
+
return EMPTY;
|
|
833
|
+
}
|
|
834
|
+
onUnload() {
|
|
835
|
+
this.detach();
|
|
836
|
+
return EMPTY;
|
|
837
|
+
}
|
|
838
|
+
onLayout() {
|
|
839
|
+
const { width, height } = this.viewport.pageSize;
|
|
840
|
+
const rootElement = this.documentContainer;
|
|
841
|
+
if (rootElement) {
|
|
842
|
+
rootElement.style.width = `${width}px`;
|
|
843
|
+
rootElement.style.height = `${height}px`;
|
|
844
|
+
}
|
|
845
|
+
return of({ width, height });
|
|
846
|
+
}
|
|
847
|
+
onRenderHeadless() {
|
|
848
|
+
return EMPTY;
|
|
849
|
+
}
|
|
850
|
+
getDocumentFrame() {
|
|
851
|
+
return void 0;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Audio spine items are always pre-paginated (one track per page).
|
|
855
|
+
* Mixed audio/text books with reflowable audio chapters may need
|
|
856
|
+
* to revisit this if that use case arises.
|
|
857
|
+
*/
|
|
858
|
+
get renditionLayout() {
|
|
859
|
+
return `pre-paginated`;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const audioEnhancer = (next) => (options) => {
|
|
864
|
+
const readerOptions = { ...options };
|
|
865
|
+
const reader = next({
|
|
866
|
+
...readerOptions,
|
|
867
|
+
getRenderer(item) {
|
|
868
|
+
const maybeFactory = options.getRenderer?.(item);
|
|
869
|
+
if (maybeFactory) {
|
|
870
|
+
return maybeFactory;
|
|
871
|
+
}
|
|
872
|
+
if (isAudioSpineItem(item)) {
|
|
873
|
+
return (params) => new AudioRenderer(params);
|
|
874
|
+
}
|
|
875
|
+
return void 0;
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
const controller = new AudioController(reader);
|
|
879
|
+
const destroy = () => {
|
|
880
|
+
controller.destroy();
|
|
881
|
+
reader.destroy();
|
|
882
|
+
};
|
|
883
|
+
return {
|
|
884
|
+
...reader,
|
|
885
|
+
__PROSE_READER_ENHANCER_AUDIO: true,
|
|
886
|
+
destroy,
|
|
887
|
+
audio: {
|
|
888
|
+
state$: controller.state$,
|
|
889
|
+
visualizer$: controller.visualizer$,
|
|
890
|
+
visibleTrackIds$: controller.visibleTrackIds$,
|
|
891
|
+
get state() {
|
|
892
|
+
return controller.state;
|
|
893
|
+
},
|
|
894
|
+
get visualizer() {
|
|
895
|
+
return controller.visualizer;
|
|
896
|
+
},
|
|
897
|
+
isAudioRenderer: (renderer) => renderer instanceof AudioRenderer,
|
|
898
|
+
play: () => controller.play(),
|
|
899
|
+
pause: () => controller.pause(),
|
|
900
|
+
toggle: () => controller.toggle(),
|
|
901
|
+
setCurrentTime: (value) => controller.setCurrentTime(value),
|
|
902
|
+
select: (trackId, options2) => controller.select(trackId, options2)
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
export { AudioRenderer, audioEnhancer, isAudioSpineItem };
|
|
908
|
+
//# sourceMappingURL=index.js.map
|