@readium/navigator 2.7.0-alpha.3 → 2.7.0-alpha.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readium/navigator",
3
- "version": "2.7.0-alpha.3",
3
+ "version": "2.7.0-alpha.4",
4
4
  "type": "module",
5
5
  "description": "Next generation SDK for publications in Web Apps",
6
6
  "author": "readium",
@@ -1,7 +1,7 @@
1
1
  import { Link, Locator, LocatorLocations, Publication, Timeline, TimelineItem } from "@readium/shared";
2
2
  import { MediaNavigator, IContentProtectionConfig, IKeyboardPeripheralsConfig, KeyboardPeripheralEventData } from "../Navigator.ts";
3
3
  import { Configurable } from "../preferences/Configurable.ts";
4
- import { WebAudioEngine, PlaybackState } from "./engine/index.ts";
4
+ import { WebAudioEngine, PlaybackState, AudioMseLoaderFactory } from "./engine/index.ts";
5
5
  import {
6
6
  AudioPreferences,
7
7
  AudioDefaults,
@@ -69,6 +69,26 @@ export interface AudioNavigatorConfiguration {
69
69
  defaults: IAudioDefaults;
70
70
  contentProtection?: IAudioContentProtectionConfig;
71
71
  keyboardPeripherals?: IKeyboardPeripheralsConfig;
72
+
73
+ /**
74
+ * Called with the persistent playback element before
75
+ * the first src is assigned, so the host can prepare it. Use for MSE/EME
76
+ * setups. When it returns a promise, loading of
77
+ * the initial track (and prefetching of adjacent ones) is deferred until
78
+ * the promise settles; a rejection is forwarded to the error listener and
79
+ * loading proceeds anyway.
80
+ */
81
+ mediaElementSetup?: (element: HTMLMediaElement) => void | Promise<void>;
82
+
83
+ /**
84
+ * When provided, media bytes reach the playback element through Media
85
+ * Source Extensions instead of direct `src` assignment: the engine
86
+ * creates one loader per track via this factory and the loader owns all
87
+ * fetching. Supply one for e.g. EME encrypted audio, since browsers have
88
+ * poor support for encrypted audio directly through `src`. The container
89
+ * handling (WebM, fMP4, …) is the loader implementation's concern.
90
+ */
91
+ mseLoaderFactory?: AudioMseLoaderFactory;
72
92
  }
73
93
 
74
94
  export class AudioNavigator extends MediaNavigator implements Configurable<AudioSettings, AudioPreferences> {
@@ -94,6 +114,8 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
94
114
  /** True while a track transition is in progress; suppresses spurious mid-navigation events. */
95
115
  private _isNavigating: boolean = false;
96
116
  private _isStalled: boolean = false;
117
+ /** Set by destroy(); stops the deferred initial load from touching a dead instance. */
118
+ private _destroyed: boolean = false;
97
119
  private _stalledWatchdog: ReturnType<typeof setInterval> | null = null;
98
120
  private _stalledCheckTime: number = 0;
99
121
 
@@ -131,9 +153,19 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
131
153
  }
132
154
 
133
155
  const initialHref = this.currentLocation.href.split("#")[0];
134
- const trackIndex = this.hrefToTrackIndex(initialHref);
156
+ let trackIndex = this.hrefToTrackIndex(initialHref);
135
157
  if (trackIndex === -1) {
136
- throw new Error(`AudioNavigator: initial href "${ initialHref }" not found in reading order`);
158
+ // Progression-only locators (e.g. restored from an OPDS progression)
159
+ // Resolve totalProgression against the
160
+ // cumulative track durations instead of failing outright.
161
+ const totalProgression = this.currentLocation.locations?.totalProgression;
162
+ if (totalProgression !== undefined) {
163
+ const resolved = this.locatorFromTotalProgression(totalProgression);
164
+ this.currentLocation = resolved.locator;
165
+ trackIndex = resolved.trackIndex;
166
+ } else {
167
+ throw new Error(`AudioNavigator: initial href "${ initialHref }" not found in reading order`);
168
+ }
137
169
  }
138
170
  const initialTime = this.currentLocation.locations?.time() || 0;
139
171
 
@@ -145,7 +177,8 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
145
177
  } as PlaybackState,
146
178
  playWhenReady: false,
147
179
  index: trackIndex
148
- }
180
+ },
181
+ mseLoaderFactory: configuration.mseLoaderFactory,
149
182
  });
150
183
 
151
184
  this.pool = new AudioPoolManager(audioEngine, publication, configuration.contentProtection);
@@ -188,6 +221,28 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
188
221
  this.setupEventListeners();
189
222
 
190
223
  this._isNavigating = true;
224
+
225
+ const startInitialLoad = () => this.startInitialLoad(trackIndex, initialTime);
226
+ if (configuration.mediaElementSetup) {
227
+ // Defer the initial load until the host has prepared the element
228
+ // (e.g. attached MediaKeys for EME) so no media data is fetched or
229
+ // decoded before protection is in place.
230
+ Promise.resolve()
231
+ .then(() => configuration.mediaElementSetup!(this.pool.audioEngine.getMediaElement()))
232
+ .catch((error) => { this.listeners.error(error, this.currentLocator); })
233
+ .then(startInitialLoad);
234
+ } else {
235
+ startInitialLoad();
236
+ }
237
+ }
238
+
239
+ /** Sets the initial track on the primary element and seeks to the starting position. */
240
+ private startInitialLoad(trackIndex: number, initialTime: number): void {
241
+ // The navigator may have been destroyed while an async
242
+ // mediaElementSetup was pending (React StrictMode does this in dev) —
243
+ // don't start loading media on the orphaned element.
244
+ if (this._destroyed) return;
245
+
191
246
  this.pool.setCurrentAudio(trackIndex, "forward");
192
247
 
193
248
  // applyPreferences() must come after setCurrentAudio() so that the src
@@ -289,6 +344,47 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
289
344
  return this.hrefToTrackIndex(this.currentLocation.href);
290
345
  }
291
346
 
347
+ /**
348
+ * Builds a locator for an overall publication progression (0–1) by walking
349
+ * the cumulative track durations. Falls back to the start of the first
350
+ * track when the reading order carries no duration metadata.
351
+ */
352
+ private locatorFromTotalProgression(totalProgression: number): { locator: Locator, trackIndex: number } {
353
+ const items = this.pub.readingOrder.items;
354
+ const durations = items.map(link => link.duration ?? 0);
355
+ const totalDuration = durations.reduce((sum, d) => sum + d, 0);
356
+
357
+ let trackIndex = 0;
358
+ let time = 0;
359
+ if (totalDuration > 0) {
360
+ let remaining = Math.min(Math.max(totalProgression, 0), 1) * totalDuration;
361
+ for (let i = 0; i < durations.length; i++) {
362
+ trackIndex = i;
363
+ if (remaining <= durations[i] || i === durations.length - 1) {
364
+ time = remaining;
365
+ break;
366
+ }
367
+ remaining -= durations[i];
368
+ }
369
+ }
370
+
371
+ const link = items[trackIndex];
372
+ return {
373
+ trackIndex,
374
+ locator: new Locator({
375
+ href: link.href,
376
+ type: link.type || "", // Should have a mimetype
377
+ title: link.title,
378
+ locations: new LocatorLocations({
379
+ position: trackIndex + 1,
380
+ progression: durations[trackIndex] > 0 ? time / durations[trackIndex] : 0,
381
+ totalProgression: Math.min(Math.max(totalProgression, 0), 1),
382
+ fragments: [`t=${ time }`]
383
+ })
384
+ })
385
+ };
386
+ }
387
+
292
388
  get currentLocator(): Locator {
293
389
  return this.currentLocation;
294
390
  }
@@ -384,7 +480,11 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
384
480
  fragments: [`t=${this.duration}`]
385
481
  }));
386
482
  this.listeners.trackEnded(this.currentLocator);
387
- if (!this.canGoForward) return;
483
+ if (!this.canGoForward) {
484
+ // Set final progress in audiobook
485
+ this.listeners.positionChanged(this.currentLocator);
486
+ return;
487
+ }
388
488
  await this.nextTrack();
389
489
  if (this._settings.autoPlay) this.play();
390
490
  });
@@ -698,6 +798,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
698
798
  }
699
799
 
700
800
  destroy(): void {
801
+ this._destroyed = true;
701
802
  this.stopPositionPolling();
702
803
  this._stopStalledWatchdog();
703
804
  this.destroyMediaSession();
@@ -38,19 +38,20 @@ export class AudioPoolManager {
38
38
  return supported;
39
39
  }
40
40
 
41
- private pickPlayableHref(link: Link): string {
41
+ private pickPlayableHref(link: Link): { href: string, type?: string } {
42
42
  const base = this._publication.baseURL;
43
43
  const candidates = [link, ...(link.alternates?.items ?? [])];
44
- let best: { href: string; confidence: "probably" | "maybe" } | undefined;
44
+ let best: { href: string; type?: string; confidence: "probably" | "maybe" } | undefined;
45
45
  for (const candidate of candidates) {
46
46
  if (!candidate.type) continue;
47
47
  const confidence = this._supportedAudioTypes.get(candidate.type);
48
48
  if (!confidence) continue;
49
49
  const href = candidate.toURL(base) ?? candidate.href;
50
- if (confidence === "probably") return href;
51
- if (!best) best = { href, confidence };
50
+ if (confidence === "probably") return { href, type: candidate.type };
51
+ if (!best) best = { href, type: candidate.type, confidence };
52
52
  }
53
- return best?.href ?? (link.toURL(base) ?? link.href);
53
+ if (best) return { href: best.href, type: best.type };
54
+ return { href: link.toURL(base) ?? link.href, type: link.type };
54
55
  }
55
56
 
56
57
  get audioEngine(): WebAudioEngine {
@@ -84,12 +85,16 @@ export class AudioPoolManager {
84
85
  * The current track is excluded — the primary engine element represents it.
85
86
  */
86
87
  private update(currentIndex: number): void {
88
+ // Progressive preload elements would feed encrypted media through the
89
+ // broken src= demuxer path — the MSE loader owns fetching instead
90
+ if (this._audioEngine.usesMse) return;
91
+
87
92
  const items = this._publication.readingOrder.items;
88
93
  const keep = new Set<string>();
89
94
 
90
95
  for (let j = 0; j < items.length; j++) {
91
96
  if (j === currentIndex) continue; // primary element handles the current track
92
- const href = this.pickPlayableHref(items[j]);
97
+ const { href } = this.pickPlayableHref(items[j]);
93
98
  if (j >= currentIndex - LOWER_BOUNDARY && j <= currentIndex + LOWER_BOUNDARY) {
94
99
  this.ensure(href);
95
100
  keep.add(href);
@@ -117,8 +122,8 @@ export class AudioPoolManager {
117
122
  * session and any Web Audio graph connections across track changes.
118
123
  */
119
124
  setCurrentAudio(currentIndex: number, _direction: 'forward' | 'backward'): void {
120
- const href = this.pickPlayableHref(this._publication.readingOrder.items[currentIndex]);
121
- this.audioEngine.changeSrc(href);
125
+ const { href, type } = this.pickPlayableHref(this._publication.readingOrder.items[currentIndex]);
126
+ this.audioEngine.changeSrc(href, type);
122
127
 
123
128
  // Discard any pool entry for this href — the primary element owns it now
124
129
  if (this.pool.has(href)) {
@@ -134,6 +139,7 @@ export class AudioPoolManager {
134
139
 
135
140
  destroy(): void {
136
141
  this.audioEngine.stop();
142
+ this.audioEngine.destroy();
137
143
  for (const [, element] of this.pool) {
138
144
  element.removeAttribute("src");
139
145
  element.load();
@@ -30,6 +30,37 @@ export interface Playback {
30
30
  buffered?: number;
31
31
  }
32
32
 
33
+ /**
34
+ * A consumer-supplied loader that feeds one resource to the playback element
35
+ * through Media Source Extensions. The engine constructs one per track via
36
+ * the [AudioMseLoaderFactory] and calls [start] immediately; the loader owns
37
+ * the element's `src` (a MediaSource object URL), all fetching, buffering and
38
+ * seek servicing until [destroy] is called. Container specifics (WebM, fMP4,
39
+ * byte ranges vs. segments…) are entirely up to the implementation.
40
+ */
41
+ export interface AudioMseLoader {
42
+ /** Attaches to the element and begins streaming. Called exactly once. */
43
+ start(): void;
44
+
45
+ /**
46
+ * Cancels in-flight fetches, detaches element listeners and releases the
47
+ * MediaSource object URL. The element itself must be left intact — the
48
+ * engine reuses it for the next track.
49
+ */
50
+ destroy(): void;
51
+ }
52
+
53
+ /**
54
+ * Creates the [AudioMseLoader] for a resource. [mimeType] is the resource's
55
+ * media type from the manifest, when known — needed to configure the
56
+ * SourceBuffer.
57
+ */
58
+ export type AudioMseLoaderFactory = (
59
+ element: HTMLMediaElement,
60
+ href: string,
61
+ mimeType?: string,
62
+ ) => AudioMseLoader;
63
+
33
64
  /**
34
65
  * An audio engine that plays audio resources from a publication.
35
66
  * @playback - The current [Playback] state.
@@ -58,8 +89,10 @@ export interface AudioEngine {
58
89
  * Changes the src of the primary media element without swapping it,
59
90
  * preserving the RemotePlayback session and all attached event listeners.
60
91
  * @param href The URL of the new audio resource.
92
+ * @param mimeType The media type of the resource, when known. Required for
93
+ * MSE-based loading to configure the SourceBuffer.
61
94
  */
62
- changeSrc(href: string): void;
95
+ changeSrc(href: string, mimeType?: string): void;
63
96
 
64
97
  /**
65
98
  * Plays the current audio resource.
@@ -2,6 +2,8 @@
2
2
 
3
3
  import {
4
4
  AudioEngine,
5
+ AudioMseLoader,
6
+ AudioMseLoaderFactory,
5
7
  Playback,
6
8
  } from "./AudioEngine.ts";
7
9
  import { PreservePitchWorklet } from "./PreservePitchWorklet.ts";
@@ -25,6 +27,10 @@ export class WebAudioEngine implements AudioEngine {
25
27
  private isStoppedValue: boolean = false;
26
28
  private worklet: PreservePitchWorklet | null = null;
27
29
  private webAudioActive: boolean = false;
30
+ private readonly mseLoaderFactory: AudioMseLoaderFactory | null;
31
+ private mseLoader: AudioMseLoader | null = null;
32
+ /** The resource href currently loaded (element src is a blob: URL in MSE mode). */
33
+ private currentHref: string = "";
28
34
 
29
35
  private readonly boundOnCanPlayThrough = this.onCanPlayThrough.bind(this);
30
36
  private readonly boundOnTimeUpdate = this.onTimeUpdate.bind(this);
@@ -42,8 +48,9 @@ export class WebAudioEngine implements AudioEngine {
42
48
  private readonly boundOnPause = this.onPause.bind(this);
43
49
  private readonly boundOnProgress = this.onProgress.bind(this);
44
50
 
45
- constructor(values: { playback: Playback }) {
51
+ constructor(values: { playback: Playback, mseLoaderFactory?: AudioMseLoaderFactory }) {
46
52
  this.playback = values.playback;
53
+ this.mseLoaderFactory = values.mseLoaderFactory ?? null;
47
54
 
48
55
  // crossOrigin is set lazily in activateWebAudio() only when the worklet is needed
49
56
  this.mediaElement = document.createElement("audio");
@@ -386,6 +393,19 @@ export class WebAudioEngine implements AudioEngine {
386
393
  private async activateWebAudio(): Promise<void> {
387
394
  if (this.webAudioActive) return;
388
395
 
396
+ if (this.usesMse) {
397
+ // blob: MediaSource URLs are same-origin — no CORS reload required
398
+ this.sourceNode = new MediaElementAudioSourceNode(this.getOrCreateAudioContext(), { mediaElement: this.mediaElement });
399
+ const audioContext = this.getOrCreateAudioContext();
400
+ this.gainNode = audioContext.createGain();
401
+ this.gainNode.gain.value = this.mediaElement.volume;
402
+ this.mediaElement.volume = 1;
403
+ this.sourceNode.connect(this.gainNode);
404
+ this.gainNode.connect(audioContext.destination);
405
+ this.webAudioActive = true;
406
+ return;
407
+ }
408
+
389
409
  const src = this.mediaElement.src;
390
410
  if (!src) return;
391
411
 
@@ -469,6 +489,11 @@ export class WebAudioEngine implements AudioEngine {
469
489
  return this.webAudioActive;
470
490
  }
471
491
 
492
+ /** True when this engine streams media through Media Source Extensions. */
493
+ public get usesMse(): boolean {
494
+ return this.mseLoaderFactory !== null;
495
+ }
496
+
472
497
  /**
473
498
  * Tears down the Web Audio graph and restores the media element to standalone
474
499
  * playback. Safe to call even if Web Audio was never activated.
@@ -499,10 +524,11 @@ export class WebAudioEngine implements AudioEngine {
499
524
  * the required headers), the graph is torn down and the src is reloaded
500
525
  * without CORS so playback continues — just without pitch correction.
501
526
  */
502
- public changeSrc(href: string): void {
503
- if (this.mediaElement.src === href) {
527
+ public changeSrc(href: string, mimeType?: string): void {
528
+ if (this.currentHref === href || this.mediaElement.src === href) {
504
529
  return;
505
530
  }
531
+ this.currentHref = href;
506
532
  this.mediaElement.pause();
507
533
  this.isPlayingValue = false;
508
534
  this.isPausedValue = false;
@@ -510,6 +536,16 @@ export class WebAudioEngine implements AudioEngine {
510
536
  this.isLoadingValue = true;
511
537
  this.isEndedValue = false;
512
538
 
539
+ if (this.mseLoaderFactory) {
540
+ // MSE path: swap loaders on the persistent element. The blob: object
541
+ // URL is same-origin, so none of the crossOrigin reload handling below
542
+ // applies here — the Web Audio graph (if active) keeps working.
543
+ this.mseLoader?.destroy();
544
+ this.mseLoader = this.mseLoaderFactory(this.mediaElement, href, mimeType);
545
+ this.mseLoader.start();
546
+ return;
547
+ }
548
+
513
549
  if (this.webAudioActive) {
514
550
  this.mediaElement.crossOrigin = "anonymous";
515
551
  this.mediaElement.src = href;
@@ -542,4 +578,13 @@ export class WebAudioEngine implements AudioEngine {
542
578
  public getMediaElement(): HTMLMediaElement {
543
579
  return this.mediaElement;
544
580
  }
581
+
582
+ /**
583
+ * Releases loader resources (in-flight fetches, MediaSource object URL).
584
+ * The media element itself is left intact.
585
+ */
586
+ public destroy(): void {
587
+ this.mseLoader?.destroy();
588
+ this.mseLoader = null;
589
+ }
545
590
  }
@@ -1,6 +1,7 @@
1
1
  import { Link, Locator, Publication, Timeline, TimelineItem } from "@readium/shared";
2
2
  import { MediaNavigator, IContentProtectionConfig, IKeyboardPeripheralsConfig, KeyboardPeripheralEventData } from "../Navigator.ts";
3
3
  import { Configurable } from "../preferences/Configurable.ts";
4
+ import { AudioMseLoaderFactory } from "./engine/index.ts";
4
5
  import { AudioPreferences, AudioSettings, AudioPreferencesEditor, IAudioPreferences, IAudioDefaults } from "./preferences/index.ts";
5
6
  import { ContextMenuEvent, SuspiciousActivityEvent } from "@readium/navigator-html-injectables";
6
7
  export interface AudioMetadata {
@@ -35,6 +36,24 @@ export interface AudioNavigatorConfiguration {
35
36
  defaults: IAudioDefaults;
36
37
  contentProtection?: IAudioContentProtectionConfig;
37
38
  keyboardPeripherals?: IKeyboardPeripheralsConfig;
39
+ /**
40
+ * Called with the persistent playback element before
41
+ * the first src is assigned, so the host can prepare it. Use for MSE/EME
42
+ * setups. When it returns a promise, loading of
43
+ * the initial track (and prefetching of adjacent ones) is deferred until
44
+ * the promise settles; a rejection is forwarded to the error listener and
45
+ * loading proceeds anyway.
46
+ */
47
+ mediaElementSetup?: (element: HTMLMediaElement) => void | Promise<void>;
48
+ /**
49
+ * When provided, media bytes reach the playback element through Media
50
+ * Source Extensions instead of direct `src` assignment: the engine
51
+ * creates one loader per track via this factory and the loader owns all
52
+ * fetching. Supply one for e.g. EME encrypted audio, since browsers have
53
+ * poor support for encrypted audio directly through `src`. The container
54
+ * handling (WebM, fMP4, …) is the loader implementation's concern.
55
+ */
56
+ mseLoaderFactory?: AudioMseLoaderFactory;
38
57
  }
39
58
  export declare class AudioNavigator extends MediaNavigator implements Configurable<AudioSettings, AudioPreferences> {
40
59
  private readonly pub;
@@ -58,9 +77,13 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
58
77
  /** True while a track transition is in progress; suppresses spurious mid-navigation events. */
59
78
  private _isNavigating;
60
79
  private _isStalled;
80
+ /** Set by destroy(); stops the deferred initial load from touching a dead instance. */
81
+ private _destroyed;
61
82
  private _stalledWatchdog;
62
83
  private _stalledCheckTime;
63
84
  constructor(publication: Publication, listeners: AudioNavigatorListeners, initialPosition?: Locator, configuration?: AudioNavigatorConfiguration);
85
+ /** Sets the initial track on the primary element and seeks to the starting position. */
86
+ private startInitialLoad;
64
87
  get settings(): AudioSettings;
65
88
  get preferencesEditor(): AudioPreferencesEditor;
66
89
  submitPreferences(preferences: AudioPreferences): Promise<void>;
@@ -73,6 +96,12 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
73
96
  private hrefToTrackIndex;
74
97
  /** Current track index derived from the current location's href. */
75
98
  private currentTrackIndex;
99
+ /**
100
+ * Builds a locator for an overall publication progression (0–1) by walking
101
+ * the cumulative track durations. Falls back to the start of the first
102
+ * track when the reading order carries no duration metadata.
103
+ */
104
+ private locatorFromTotalProgression;
76
105
  get currentLocator(): Locator;
77
106
  get isPlaying(): boolean;
78
107
  get isPaused(): boolean;
@@ -27,6 +27,30 @@ export interface Playback {
27
27
  offset?: number;
28
28
  buffered?: number;
29
29
  }
30
+ /**
31
+ * A consumer-supplied loader that feeds one resource to the playback element
32
+ * through Media Source Extensions. The engine constructs one per track via
33
+ * the [AudioMseLoaderFactory] and calls [start] immediately; the loader owns
34
+ * the element's `src` (a MediaSource object URL), all fetching, buffering and
35
+ * seek servicing until [destroy] is called. Container specifics (WebM, fMP4,
36
+ * byte ranges vs. segments…) are entirely up to the implementation.
37
+ */
38
+ export interface AudioMseLoader {
39
+ /** Attaches to the element and begins streaming. Called exactly once. */
40
+ start(): void;
41
+ /**
42
+ * Cancels in-flight fetches, detaches element listeners and releases the
43
+ * MediaSource object URL. The element itself must be left intact — the
44
+ * engine reuses it for the next track.
45
+ */
46
+ destroy(): void;
47
+ }
48
+ /**
49
+ * Creates the [AudioMseLoader] for a resource. [mimeType] is the resource's
50
+ * media type from the manifest, when known — needed to configure the
51
+ * SourceBuffer.
52
+ */
53
+ export type AudioMseLoaderFactory = (element: HTMLMediaElement, href: string, mimeType?: string) => AudioMseLoader;
30
54
  /**
31
55
  * An audio engine that plays audio resources from a publication.
32
56
  * @playback - The current [Playback] state.
@@ -52,8 +76,10 @@ export interface AudioEngine {
52
76
  * Changes the src of the primary media element without swapping it,
53
77
  * preserving the RemotePlayback session and all attached event listeners.
54
78
  * @param href The URL of the new audio resource.
79
+ * @param mimeType The media type of the resource, when known. Required for
80
+ * MSE-based loading to configure the SourceBuffer.
55
81
  */
56
- changeSrc(href: string): void;
82
+ changeSrc(href: string, mimeType?: string): void;
57
83
  /**
58
84
  * Plays the current audio resource.
59
85
  */
@@ -1,4 +1,4 @@
1
- import { AudioEngine, Playback } from "./AudioEngine.ts";
1
+ import { AudioEngine, AudioMseLoaderFactory, Playback } from "./AudioEngine.ts";
2
2
  type EventCallback = (data: any) => void;
3
3
  export declare class WebAudioEngine implements AudioEngine {
4
4
  readonly playback: Playback;
@@ -16,6 +16,10 @@ export declare class WebAudioEngine implements AudioEngine {
16
16
  private isStoppedValue;
17
17
  private worklet;
18
18
  private webAudioActive;
19
+ private readonly mseLoaderFactory;
20
+ private mseLoader;
21
+ /** The resource href currently loaded (element src is a blob: URL in MSE mode). */
22
+ private currentHref;
19
23
  private readonly boundOnCanPlayThrough;
20
24
  private readonly boundOnTimeUpdate;
21
25
  private readonly boundOnError;
@@ -33,6 +37,7 @@ export declare class WebAudioEngine implements AudioEngine {
33
37
  private readonly boundOnProgress;
34
38
  constructor(values: {
35
39
  playback: Playback;
40
+ mseLoaderFactory?: AudioMseLoaderFactory;
36
41
  });
37
42
  /**
38
43
  * Adds an event listener to the audio engine.
@@ -132,6 +137,8 @@ export declare class WebAudioEngine implements AudioEngine {
132
137
  */
133
138
  private activateWebAudio;
134
139
  get isWebAudioActive(): boolean;
140
+ /** True when this engine streams media through Media Source Extensions. */
141
+ get usesMse(): boolean;
135
142
  /**
136
143
  * Tears down the Web Audio graph and restores the media element to standalone
137
144
  * playback. Safe to call even if Web Audio was never activated.
@@ -145,10 +152,15 @@ export declare class WebAudioEngine implements AudioEngine {
145
152
  * the required headers), the graph is torn down and the src is reloaded
146
153
  * without CORS so playback continues — just without pitch correction.
147
154
  */
148
- changeSrc(href: string): void;
155
+ changeSrc(href: string, mimeType?: string): void;
149
156
  /**
150
157
  * Returns the HTML media element used for playback.
151
158
  */
152
159
  getMediaElement(): HTMLMediaElement;
160
+ /**
161
+ * Releases loader resources (in-flight fetches, MediaSource object URL).
162
+ * The media element itself is left intact.
163
+ */
164
+ destroy(): void;
153
165
  }
154
166
  export {};