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