ngx-kit 7.3.3 → 7.4.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,671 @@
1
+ import * as ngx_kit_media_control from 'ngx-kit/media-control';
2
+ import * as _angular_core from '@angular/core';
3
+ import { Signal, ElementRef, InjectionToken, EnvironmentProviders, Type } from '@angular/core';
4
+
5
+ /**
6
+ * A single playable media item.
7
+ *
8
+ * `src` intentionally accepts the shapes a consumer already has lying around:
9
+ * a remote/relative URL string, a `Blob`/`File` (turned into an object URL
10
+ * internally and revoked on teardown), or a live `MediaStream`.
11
+ */
12
+ interface NgxMediaSource {
13
+ /** Playback source. */
14
+ src: string | Blob | File | MediaStream;
15
+ /** MIME type, e.g. `audio/mpeg`, `video/mp4`. Helps capability detection avoid guessing from the extension. */
16
+ type?: string;
17
+ /** Optional codec string per RFC 6381, e.g. `avc1.42E01E,mp4a.40.2`. */
18
+ codecs?: string;
19
+ /** Display metadata, also used for Media Session integration. */
20
+ title?: string;
21
+ artist?: string;
22
+ album?: string;
23
+ /** Poster image for video. */
24
+ poster?: string;
25
+ /**
26
+ * Separate download target. If omitted, falls back to `src` (only when
27
+ * `src` is a string — Blob/File/MediaStream have no meaningful download URL
28
+ * unless one is provided here).
29
+ */
30
+ downloadUrl?: string;
31
+ /** Suggested file name for the download action. */
32
+ downloadFileName?: string;
33
+ /** Headers to send when the engine has to fetch this source itself (e.g. duration probing, WASM decoding). */
34
+ requestHeaders?: HeadersInit;
35
+ }
36
+ /** A queue of sources plus the active index. Replaces the old bare `PlayList[]`. */
37
+ interface NgxMediaPlaylist {
38
+ items: NgxMediaSource[];
39
+ currentIndex: number;
40
+ }
41
+ /**
42
+ * @deprecated Use {@link NgxMediaSource} instead. Kept so existing `PlayList[]`
43
+ * consumers keep compiling; `fileAddress`/`title` map 1:1 onto `src`/`title`.
44
+ */
45
+ declare class PlayList {
46
+ title: string;
47
+ fileAddress: string;
48
+ }
49
+ declare function playListToMediaSource(item: PlayList): NgxMediaSource;
50
+ /**
51
+ * Content equality for `NgxMediaSource`/playlist arrays, not reference
52
+ * equality. `src` strings compare by value; `Blob`/`File`/`MediaStream`
53
+ * compare by identity (they can't be compared by content cheaply/safely).
54
+ *
55
+ * Every field that can change *downstream behavior* is compared —
56
+ * `artist`/`album`/`poster` feed Media Session metadata, `downloadUrl`/
57
+ * `downloadFileName` change what the download button actually does, and
58
+ * `requestHeaders` changes what bytes a server returns for the same URL.
59
+ * Fields that are purely cosmetic and never drive a decision (there
60
+ * currently are none — if you add one, document why it's excluded here).
61
+ *
62
+ * This exists specifically so a consumer that re-creates an equivalent
63
+ * array/object literal on every change-detection cycle (`[sources]="[{...}]"`,
64
+ * or a `.map()` derived from an otherwise-stable list) doesn't cause the
65
+ * player to treat it as "a new playlist" and reload media on every tick —
66
+ * see `computed(..., { equal: mediaSourcesEqual })` in `NgxMediaControl` and
67
+ * the same guard in `NgxMediaFacade.setPlaylist`.
68
+ */
69
+ declare function mediaSourcesEqual(a: readonly NgxMediaSource[], b: readonly NgxMediaSource[]): boolean;
70
+ /**
71
+ * Derives a display file name from a source, tolerating everything a real
72
+ * URL can throw at `decodeURIComponent` (malformed `%` sequences, stray
73
+ * `#`/`?`) instead of letting the player crash just because it tried to
74
+ * make a pretty label. Never throws.
75
+ */
76
+ declare function extractMediaFileName(source: Pick<NgxMediaSource, 'src' | 'title'>): string;
77
+
78
+ /**
79
+ * Exhaustive playback state machine. Only one state is active at a time;
80
+ * transient conditions (buffering while playing, seeking while paused) are
81
+ * modeled as their own states rather than boolean flags layered on top of
82
+ * `playing`/`paused`, so `playing + destroyed` etc. cannot occur.
83
+ */
84
+ type NgxMediaPlaybackState = 'idle' | 'loading' | 'ready' | 'playing' | 'paused' | 'buffering' | 'seeking' | 'ended' | 'error' | 'destroyed';
85
+ interface NgxTimeRange {
86
+ start: number;
87
+ end: number;
88
+ }
89
+ /** Read-only snapshot of engine state, exposed to the UI as a signal. */
90
+ interface NgxMediaState {
91
+ playback: NgxMediaPlaybackState;
92
+ currentTime: number;
93
+ duration: number;
94
+ buffered: readonly NgxTimeRange[];
95
+ volume: number;
96
+ muted: boolean;
97
+ playbackRate: number;
98
+ /** Only meaningful for video engines; undefined for audio-only. */
99
+ videoWidth?: number;
100
+ videoHeight?: number;
101
+ }
102
+ declare function initialMediaState(): NgxMediaState;
103
+ /** Guards against illegal transitions (e.g. anything -> playing after destroyed). */
104
+ declare function canTransition(from: NgxMediaPlaybackState, to: NgxMediaPlaybackState): boolean;
105
+
106
+ type NgxMediaKind = 'audio' | 'video';
107
+ /**
108
+ * Stable contract the UI/facade programs against. The UI never knows whether
109
+ * playback is happening through a native `<audio>`/`<video>` element, MSE, or
110
+ * a WASM decoder writing into an AudioBuffer — it only sees this interface.
111
+ */
112
+ interface NgxMediaEngine {
113
+ readonly id: string;
114
+ readonly kind: NgxMediaKind;
115
+ /** Reactive, read-only state snapshot. Engines update this via an internal writable signal. */
116
+ readonly state: Signal<NgxMediaState>;
117
+ /**
118
+ * The element the UI should attach to the DOM (for video: for sizing/fullscreen/PiP;
119
+ * for audio: usually detached, kept alive only to drive playback).
120
+ * Null for engines that don't render through a media element (rare; e.g. a pure
121
+ * Web-Audio-buffer engine).
122
+ */
123
+ readonly mediaElement: HTMLMediaElement | null;
124
+ load(source: NgxMediaSource, signal?: AbortSignal): Promise<void>;
125
+ play(): Promise<void>;
126
+ pause(): void;
127
+ seek(time: number): Promise<void> | void;
128
+ setVolume(volume: number): void;
129
+ setMuted(muted: boolean): void;
130
+ setPlaybackRate(rate: number): void;
131
+ destroy(): void;
132
+ }
133
+ /** Given a source, can this engine play it without further help? Cheap, synchronous-preferred check. */
134
+ interface NgxEngineCapability {
135
+ canPlay(source: NgxMediaSource): 'probably' | 'maybe' | '';
136
+ }
137
+
138
+ type NgxMediaErrorCategory = 'NETWORK' | 'UNSUPPORTED_FORMAT' | 'DECODER' | 'ABORTED' | 'AUTOPLAY_BLOCKED' | 'MEDIA' | 'PERMISSION' | 'BROWSER_UNSUPPORTED' | 'UNKNOWN';
139
+ declare class NgxMediaError extends Error {
140
+ readonly category: NgxMediaErrorCategory;
141
+ /** Original DOM/browser error, if any — kept for `console.error`/telemetry, not surfaced to templates by default. */
142
+ readonly cause?: unknown | undefined;
143
+ constructor(category: NgxMediaErrorCategory, message: string,
144
+ /** Original DOM/browser error, if any — kept for `console.error`/telemetry, not surfaced to templates by default. */
145
+ cause?: unknown | undefined);
146
+ static fromMediaElementError(el: HTMLMediaElement): NgxMediaError;
147
+ static autoplayBlocked(cause?: unknown): NgxMediaError;
148
+ static unsupported(message: string): NgxMediaError;
149
+ static noDecoder(source: string): NgxMediaError;
150
+ }
151
+
152
+ /**
153
+ * Optional processing graph: `<audio>/<video>` → MediaElementAudioSourceNode
154
+ * → gain → (optional analyser) → destination.
155
+ *
156
+ * Never constructed unless a consumer asks for gain/analyser/effects — plain
157
+ * playback never creates an `AudioContext`, respecting autoplay policy and
158
+ * avoiding the "one AudioContext per instance whether needed or not" issue
159
+ * in the original component.
160
+ *
161
+ * Autoplay note: this class never calls `resume()` on its own outside a
162
+ * user-triggered `play()`/`connect()` call — bypassing the suspended-state
163
+ * policy is explicitly out of scope.
164
+ */
165
+ declare class NgxWebAudioGraph {
166
+ private readonly element;
167
+ private ctx;
168
+ private sourceNode;
169
+ private gainNode;
170
+ private analyserNode;
171
+ constructor(element: HTMLMediaElement);
172
+ /** Idempotent — safe to call multiple times; only the first call does any work. */
173
+ ensureConnected(): {
174
+ context: AudioContext;
175
+ gain: GainNode;
176
+ };
177
+ setGain(value: number): void;
178
+ getAnalyser(fftSize?: number): AnalyserNode;
179
+ resumeOnUserGesture(): Promise<void>;
180
+ destroy(): void;
181
+ }
182
+
183
+ /**
184
+ * Everything the UI component needs, expressed as signals it can bind to
185
+ * directly, plus imperative commands. This is the only class
186
+ * `NgxMediaControl` talks to — it never touches an engine, a decoder, or the
187
+ * DOM media element itself. Swapping native/MSE/WASM playback never touches
188
+ * the component.
189
+ */
190
+ declare class NgxMediaFacade {
191
+ private readonly zone;
192
+ private readonly engineFactory;
193
+ private readonly decoderRegistry;
194
+ private readonly destroyRef;
195
+ private readonly mediaSession;
196
+ private webAudio;
197
+ private loadAbort;
198
+ private kind;
199
+ private preload;
200
+ /**
201
+ * Belt-and-suspenders alongside `AbortController`: not every operation a
202
+ * superseded load might be mid-way through is actually cancellable (a
203
+ * decoder's own internal work, a promise someone else is holding a
204
+ * reference to, ...). Every `loadCurrent()` call captures the generation
205
+ * counter at its start and checks it again before committing any result —
206
+ * a stale generation means "a newer load has already started", full stop,
207
+ * regardless of whether the stale one's `AbortSignal` was actually honored
208
+ * by everything downstream.
209
+ */
210
+ private loadGeneration;
211
+ private readonly _playlist;
212
+ private readonly _currentIndex;
213
+ /** Holds the live engine instance; swapped whenever a new source loads. */
214
+ private readonly _engine;
215
+ private get engine();
216
+ private readonly _error;
217
+ readonly playlist: _angular_core.Signal<NgxMediaSource[]>;
218
+ readonly currentIndex: _angular_core.Signal<number>;
219
+ /**
220
+ * Reads through to the current engine's own state signal, so every native
221
+ * media event (timeupdate, progress, ...) that the engine writes flows
222
+ * straight through here with no polling, no manual refresh, and no risk of
223
+ * going stale across an engine swap (native -> WASM fallback, etc.).
224
+ *
225
+ * When there is NO engine (playlist empty, or the engine was torn down
226
+ * after a load failure) but `error()` is set, this reports `playback:
227
+ * 'error'` rather than falling through to `idle` — otherwise `state()` and
228
+ * `error()` would be two sources of truth that can disagree, and anything
229
+ * built on `state().playback === 'error'` would silently miss real errors.
230
+ */
231
+ readonly state: _angular_core.Signal<NgxMediaState>;
232
+ readonly error: _angular_core.Signal<NgxMediaError | null>;
233
+ readonly currentSource: _angular_core.Signal<NgxMediaSource>;
234
+ readonly mediaElement: _angular_core.Signal<HTMLMediaElement | null>;
235
+ constructor();
236
+ /** Deterministic on a kind change: if a source is already active, immediately rebuilds the engine as the new kind (audio <-> video element) instead of leaving a stale-kind engine behind a UI that now expects the other one. */
237
+ configure(kind: NgxMediaKind): void;
238
+ /** Applies to loads started after this call — not retroactive to an element already created. */
239
+ setPreload(preload: 'none' | 'metadata' | 'auto'): void;
240
+ setPlaylist(sources: NgxMediaSource[], startIndex?: number, autoLoad?: boolean): void;
241
+ loadCurrent(playAfter?: boolean): Promise<void>;
242
+ /** Forces the current track to actually reload from scratch — e.g. a "retry after error" action — bypassing the engine's own idempotent-load skip (which is otherwise correct: selecting the same, already-loaded track should NOT re-fetch it). */
243
+ reloadCurrent(): Promise<void>;
244
+ /**
245
+ * Tries the native engine first (the normal fast path). Falls back to the
246
+ * decoder registry ONLY when the failure category indicates the format
247
+ * itself is the problem (`UNSUPPORTED_FORMAT`, `DECODER`) — never for
248
+ * `NETWORK` (404/500/CORS/interrupted), `ABORTED`, or `PERMISSION`, where
249
+ * retrying via a decoder would just repeat the same failing request
250
+ * (or a fresh one to the same broken URL) for no benefit. This matters
251
+ * because `canPlayType()`/extension/mime are only ever a hint and can be
252
+ * wrong (a mislabeled or fake extension), so the decision to fall back
253
+ * has to be made from the real failure, not guessed upfront.
254
+ */
255
+ private resolveEngine;
256
+ private shouldFallbackToDecoder;
257
+ play(): Promise<void>;
258
+ pause(): void;
259
+ togglePlayPause(): void;
260
+ seek(time: number): void;
261
+ skip(deltaSeconds: number): void;
262
+ setVolume(volume: number): void;
263
+ setMuted(muted: boolean): void;
264
+ setPlaybackRate(rate: number): void;
265
+ previous(): Promise<void>;
266
+ next(): Promise<void>;
267
+ selectIndex(index: number): Promise<void>;
268
+ /** Lazily creates the Web Audio graph — never touched unless a consumer asks for it. Always tied to the CURRENT engine's element; invalidated automatically on every engine swap (see `loadCurrent`). */
269
+ getWebAudioGraph(): NgxWebAudioGraph;
270
+ private bindMediaSession;
271
+ destroy(): void;
272
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgxMediaFacade, never>;
273
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<NgxMediaFacade>;
274
+ }
275
+
276
+ type NgxMediaType = 'audio' | 'video' | 'both';
277
+ declare class NgxMediaControl {
278
+ /**
279
+ * `'audio'` renders an audio-style control surface (no video frame).
280
+ * `'video'` renders a video frame with fullscreen/PiP controls.
281
+ * `'both'` renders the video frame but degrades gracefully to audio-only
282
+ * controls when the active source has no video track.
283
+ */
284
+ readonly type: _angular_core.InputSignal<NgxMediaType>;
285
+ /**
286
+ * Poster precedence: this input, when set, ALWAYS wins over a per-source
287
+ * `poster` (useful for a fixed placeholder while sources vary). Leave it
288
+ * unset to let each `NgxMediaSource.poster` take effect per track, and
289
+ * clearing either one correctly clears the poster actually applied (see
290
+ * the mount effect below) instead of leaving a stale image behind.
291
+ */
292
+ readonly poster: _angular_core.InputSignal<string | undefined>;
293
+ readonly showList: _angular_core.InputSignal<boolean>;
294
+ readonly download: _angular_core.InputSignal<boolean>;
295
+ readonly showFileName: _angular_core.InputSignal<boolean>;
296
+ readonly showSpeed: _angular_core.InputSignal<boolean>;
297
+ readonly showVolume: _angular_core.InputSignal<boolean>;
298
+ readonly linear: _angular_core.InputSignal<boolean>;
299
+ readonly preload: _angular_core.InputSignal<"none" | "metadata" | "auto">;
300
+ /**
301
+ * IMPORTANT, and different from earlier versions of this component: a
302
+ * native `<audio>`/`<video src>` fetch cannot carry arbitrary request
303
+ * headers — that's a browser platform limitation, not something this
304
+ * library can paper over without fetching the whole file into a `Blob`
305
+ * first (which defeats streaming/range-requests for anything but small
306
+ * clips, and was explicitly the wrong default per the library's
307
+ * performance goals). So `fetchHeaders` is applied as the DEFAULT
308
+ * `requestHeaders` for any resolved source that doesn't specify its own
309
+ * (see `resolvedSources` below) — it takes effect for sources that end
310
+ * up on a decoder/MSE backend (which do fetch through
311
+ * `NGX_MEDIA_REQUEST_HANDLER` and can carry headers), and has no effect
312
+ * on plain natively-playable sources, which never go through a fetch at
313
+ * all. Set headers per-source via `NgxMediaSource.requestHeaders`
314
+ * whenever different tracks need different auth.
315
+ */
316
+ readonly fetchHeaders: _angular_core.InputSignal<HeadersInit | undefined>;
317
+ /** Legacy simple form: bare URLs, one per playlist entry. Prefer `sources` for titles/artwork/per-track headers. */
318
+ readonly fileList: _angular_core.InputSignal<string[] | undefined>;
319
+ /** Preferred over `fileList`: full `NgxMediaSource` objects. */
320
+ readonly sources: _angular_core.InputSignal<NgxMediaSource[] | undefined>;
321
+ readonly trackChange: _angular_core.OutputEmitterRef<NgxMediaSource>;
322
+ readonly error: _angular_core.OutputEmitterRef<string>;
323
+ private readonly doc;
324
+ private readonly destroyRef;
325
+ protected readonly facade: NgxMediaFacade;
326
+ protected readonly containerRef: _angular_core.Signal<ElementRef<HTMLElement>>;
327
+ protected readonly videoHostRef: _angular_core.Signal<ElementRef<HTMLElement> | undefined>;
328
+ protected readonly downloading: _angular_core.WritableSignal<boolean>;
329
+ protected readonly togglePlayList: _angular_core.WritableSignal<boolean>;
330
+ protected readonly isFullscreen: _angular_core.WritableSignal<boolean>;
331
+ /** Local seek preview — see the seek-slider handlers below for why the slider never binds `state().currentTime` directly while the user is dragging it. */
332
+ protected readonly isSeeking: _angular_core.WritableSignal<boolean>;
333
+ protected readonly seekPreviewTime: _angular_core.WritableSignal<number>;
334
+ protected readonly state: _angular_core.Signal<ngx_kit_media_control.NgxMediaState>;
335
+ protected readonly playlist: _angular_core.Signal<NgxMediaSource[]>;
336
+ protected readonly currentIndex: _angular_core.Signal<number>;
337
+ protected readonly currentSource: _angular_core.Signal<NgxMediaSource>;
338
+ protected readonly playerError: _angular_core.Signal<ngx_kit_media_control.NgxMediaError | null>;
339
+ protected readonly effectiveKind: _angular_core.Signal<"audio" | "video">;
340
+ protected readonly displaySeekTime: _angular_core.Signal<number>;
341
+ protected readonly currentTimeDisplay: _angular_core.Signal<string>;
342
+ protected readonly totalTimeDisplay: _angular_core.Signal<string>;
343
+ protected readonly speedDisplay: _angular_core.Signal<string>;
344
+ protected readonly fileName: _angular_core.Signal<string>;
345
+ protected readonly isPlaying: _angular_core.Signal<boolean>;
346
+ protected readonly isBuffering: _angular_core.Signal<boolean>;
347
+ protected readonly hasLoadError: _angular_core.Signal<boolean>;
348
+ protected readonly errorMessage: _angular_core.Signal<string>;
349
+ protected readonly canFullscreen: _angular_core.Signal<boolean>;
350
+ protected readonly canPip: _angular_core.Signal<boolean>;
351
+ /**
352
+ * Derived from `sources()` OR `fileList()` (whichever the consumer used),
353
+ * memoized by CONTENT rather than reference (`equal: mediaSourcesEqual`).
354
+ * Angular's `input()` signal only compares by reference — a parent that
355
+ * binds an inline literal, e.g. `[sources]="[{...}]"`, or otherwise
356
+ * re-derives an equivalent array on every change-detection pass, produces
357
+ * a *new* array every tick. Without the custom `equal`, the effect below
358
+ * would see "a new value" on every tick and reload media in a loop. A
359
+ * content-equal `computed` keeps returning its previous, `===`-stable
360
+ * value whenever the new content is equivalent, so the effect below
361
+ * simply doesn't re-run.
362
+ */
363
+ protected readonly resolvedSources: _angular_core.Signal<NgxMediaSource[]>;
364
+ /** Tracks what's actually mounted in `videoHostRef`, so the mount effect only touches the DOM (and re-registers listeners) when the element itself changes, not on every unrelated signal read. */
365
+ private mountedElement;
366
+ private dblClickHandler;
367
+ private downloadResetTimeout;
368
+ constructor();
369
+ private detachMountedElement;
370
+ playPause(): void;
371
+ play(): void;
372
+ stop(): void;
373
+ muteUnmute(): void;
374
+ /**
375
+ * Seek strategy: dragging the slider only updates a local preview
376
+ * (`seekPreviewTime`) — no `facade.seek()` call, so no HTTP range
377
+ * request or media-pipeline seek happens per pixel of drag. The actual
378
+ * seek fires once, on `change` (`onSeekCommit`), which browsers fire
379
+ * both on pointer-release-after-drag AND after each discrete keyboard
380
+ * adjustment — so keyboard accessibility (arrow keys) still seeks
381
+ * immediately per keypress, it's only continuous dragging that's
382
+ * coalesced into a single request.
383
+ */
384
+ onSeekInput(ev: Event): void;
385
+ onSeekCommit(ev: Event): void;
386
+ skipTrack(seconds: number): void;
387
+ increaseSpeed(): void;
388
+ decreaseSpeed(): void;
389
+ changeVolume(ev: Event): void;
390
+ previous(): void;
391
+ next(): void;
392
+ onClickPlayList(index: number): void;
393
+ togglePlaylistPanel(): void;
394
+ /** Force-reload the current track from scratch (e.g. a "retry" affordance after an error) rather than relying on selecting the same playlist entry, which deliberately does NOT reload an already-loaded track. */
395
+ retryCurrent(): void;
396
+ downloadCurrentFile(): void;
397
+ toggleFullscreen(): Promise<void>;
398
+ togglePictureInPicture(): Promise<void>;
399
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgxMediaControl, never>;
400
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<NgxMediaControl, "ngx-media-control", never, { "type": { "alias": "type"; "required": false; "isSignal": true; }; "poster": { "alias": "poster"; "required": false; "isSignal": true; }; "showList": { "alias": "showList"; "required": false; "isSignal": true; }; "download": { "alias": "download"; "required": false; "isSignal": true; }; "showFileName": { "alias": "showFileName"; "required": false; "isSignal": true; }; "showSpeed": { "alias": "showSpeed"; "required": false; "isSignal": true; }; "showVolume": { "alias": "showVolume"; "required": false; "isSignal": true; }; "linear": { "alias": "linear"; "required": false; "isSignal": true; }; "preload": { "alias": "preload"; "required": false; "isSignal": true; }; "fetchHeaders": { "alias": "fetchHeaders"; "required": false; "isSignal": true; }; "fileList": { "alias": "fileList"; "required": false; "isSignal": true; }; "sources": { "alias": "sources"; "required": false; "isSignal": true; }; }, { "trackChange": "trackChange"; "error": "error"; }, never, never, true, never>;
401
+ }
402
+
403
+ /**
404
+ * A decoder plugin doesn't decode into a static buffer and hand it back —
405
+ * real media needs streaming, seeking and duration discovery, so a decoder's
406
+ * job is to produce a fully-formed {@link NgxMediaEngine} that the facade can
407
+ * drive identically to the native engine. This keeps the abstraction usable
408
+ * for both "decode once into an AudioBuffer" (short clips) and "feed a
409
+ * MediaSource/SourceBuffer progressively" (long/streamed media) strategies,
410
+ * which a flat `decode() -> ArrayBuffer` contract cannot support.
411
+ */
412
+ interface NgxMediaDecoder {
413
+ /** Unique id, used for logging/priority/debugging. */
414
+ readonly id: string;
415
+ /** Formats this decoder claims, informational (used by the registry for a fast pre-filter). */
416
+ readonly formats: readonly string[];
417
+ /**
418
+ * Higher runs first when multiple decoders claim the same source.
419
+ * Default treated as 0 by the registry.
420
+ */
421
+ readonly priority?: number;
422
+ /** Cheap capability check; may be async if it needs to sniff bytes. `signal` lets the registry cancel an in-flight probe (e.g. the user switched tracks before this decoder answered). */
423
+ canDecode(source: NgxMediaSource, signal?: AbortSignal): boolean | Promise<boolean>;
424
+ /**
425
+ * Build a ready-to-use engine for this source. `signal` allows the caller
426
+ * to cancel an in-flight WASM module load / stream open.
427
+ */
428
+ createEngine(source: NgxMediaSource, signal?: AbortSignal): Promise<NgxMediaEngine>;
429
+ /** Release any module-level resources (worker pools, cached WASM instances) held by this decoder itself. */
430
+ destroy(): void;
431
+ }
432
+
433
+ /**
434
+ * Base class for WASM-backed decoders. It does the two things every such
435
+ * decoder needs regardless of which codec module it wraps:
436
+ *
437
+ * 1. Lazily `import()`s the codec module on first real use, in a Web Worker
438
+ * when a worker factory is supplied, so the (typically large) WASM binary
439
+ * never enters the main bundle or the main thread's JS heap unless a
440
+ * source actually needs it.
441
+ * 2. Provides a cancellable, disposable lifecycle (`AbortSignal`-aware load,
442
+ * `destroy()` terminates the worker / frees the WASM instance) that is
443
+ * safe under overlap: a `destroy()` that lands while `loadModule()` is
444
+ * still pending must not let that pending load "resurrect" a module
445
+ * reference for a decoder instance that's already gone, and must not
446
+ * let a subsequent `createEngine()` call kick off a SECOND concurrent
447
+ * module load while the first is still in flight.
448
+ *
449
+ * This class intentionally does NOT ship a real codec — bundling e.g.
450
+ * `@ffmpeg/ffmpeg` here would violate "no WASM in the default bundle" for
451
+ * every consumer, including ones who never touch this file. Concrete
452
+ * decoders (FLAC, WMA, a consumer's own 3GP decoder, ...) extend this and
453
+ * supply `loadModule` + `buildEngine`.
454
+ */
455
+ declare abstract class NgxWasmDecoderBase implements NgxMediaDecoder {
456
+ abstract readonly id: string;
457
+ abstract readonly formats: readonly string[];
458
+ priority: number;
459
+ private worker;
460
+ private modulePromise;
461
+ /** Bumped by every `destroy()`; lets an in-flight `loadModule()`/`createEngine()` notice it's talking to a dead instance once it resolves. */
462
+ private generation;
463
+ /** Import path/factory for the codec's WASM glue module. Not evaluated until first `canDecode()`/`createEngine()` call. */
464
+ protected abstract loadModule(): Promise<unknown>;
465
+ /** Optional: return a `Worker` to run decoding off the main thread. Omit for short clips where worker overhead isn't worth it. */
466
+ protected createWorker?(): Worker;
467
+ abstract canDecode(source: NgxMediaSource, signal?: AbortSignal): boolean | Promise<boolean>;
468
+ /**
469
+ * Subclasses implement the actual decode-to-playable-engine step once the
470
+ * module (and optionally worker) are ready. Typical strategies:
471
+ * - decode fully into an `AudioBuffer`, play via Web Audio (`AudioBufferSourceNode`)
472
+ * - decode progressively, feed an MSE `SourceBuffer`
473
+ */
474
+ protected abstract buildEngine(source: NgxMediaSource, module: unknown, worker: Worker | null, signal?: AbortSignal): Promise<NgxMediaEngine>;
475
+ createEngine(source: NgxMediaSource, signal?: AbortSignal): Promise<NgxMediaEngine>;
476
+ private ensureModuleLoaded;
477
+ destroy(): void;
478
+ }
479
+ /**
480
+ * Minimal playable-engine wrapper around a fully-decoded `AudioBuffer`,
481
+ * useful for WASM decoders that decode short clips completely up front
482
+ * rather than streaming. Reuses the browser's own `AudioContext` playback
483
+ * graph instead of a native `<audio>` element, since there's no compressed
484
+ * file for the browser to play directly anymore.
485
+ *
486
+ * Playback-position bookkeeping: `offset` is the buffer-time position when
487
+ * the current `AudioBufferSourceNode` was (re)started, `startedAt` is the
488
+ * `AudioContext.currentTime` at that moment, and `rate` is the playback
489
+ * rate in effect since then — so at any instant, position =
490
+ * `offset + (ctx.currentTime - startedAt) * rate`. Every operation that
491
+ * stops or restarts the source (pause, seek, rate change while playing,
492
+ * replay after ended) re-baselines these three fields together so that
493
+ * formula stays correct instead of drifting.
494
+ */
495
+ declare class DecodedBufferEngine implements NgxMediaEngine {
496
+ private readonly buffer;
497
+ readonly id: string;
498
+ readonly kind: NgxMediaKind;
499
+ readonly mediaElement: null;
500
+ private readonly _state;
501
+ readonly state: Signal<NgxMediaState>;
502
+ private ctx;
503
+ private source;
504
+ private gain;
505
+ private startedAt;
506
+ private offset;
507
+ private rate;
508
+ private volume;
509
+ private muted;
510
+ private rafId;
511
+ /** Bumped every time a NEW `AudioBufferSourceNode` is created, so a stale `onended` from a node that was `.stop()`-ed (rather than actually finishing) can recognize it's obsolete and not overwrite newer state. */
512
+ private playToken;
513
+ constructor(decoderId: string, ctx: AudioContext, buffer: AudioBuffer);
514
+ play(): Promise<void>;
515
+ pause(): void;
516
+ seek(time: number): void;
517
+ setVolume(volume: number): void;
518
+ setMuted(muted: boolean): void;
519
+ setPlaybackRate(rate: number): void;
520
+ load(): Promise<void>;
521
+ destroy(): void;
522
+ private currentPosition;
523
+ /** Drives `currentTime` for the UI while actually playing — never runs during pause/ended/destroyed, so there's no animation loop burning frames when nothing is moving. */
524
+ private startTimeTracking;
525
+ private stopTimeTracking;
526
+ }
527
+
528
+ /**
529
+ * Default fast-path engine: plays through a real `<audio>`/`<video>` element.
530
+ * Zero WASM, zero MediaSource — the browser's own decode pipeline does the work.
531
+ *
532
+ * Load semantics (the part that matters most for network behavior):
533
+ * - `load()` is a real 3-state machine (idle/loading/ready) keyed on
534
+ * {@link SourceIdentity}, not just a `readyState` snapshot:
535
+ * same source + currently loading -> returns the SAME in-flight promise
536
+ * same source + already loaded -> resolves immediately, no `el.load()` call
537
+ * different source -> proceeds; the browser's own load
538
+ * algorithm aborts whatever the
539
+ * element was doing before
540
+ * - Setting `el.src` already queues the browser's media load algorithm;
541
+ * the immediate `el.load()` call right after does not cause a second
542
+ * network request — its purpose is to make the abort-then-restart
543
+ * synchronous (needed for older WebKit), not to "load twice". The state
544
+ * machine above is what actually prevents redundant fetches across
545
+ * repeated `load()` calls for the same source.
546
+ *
547
+ * Performance notes:
548
+ * - `timeupdate`/`progress`/etc. fire far more often than the UI needs to
549
+ * re-render; listeners run via `runOutsideAngular`, and `patch()` skips
550
+ * the signal write entirely when nothing actually changed (including a
551
+ * content-equal `buffered` array, which would otherwise allocate and
552
+ * write on every `progress` tick even when the ranges didn't move).
553
+ */
554
+ declare class NativeMediaEngine implements NgxMediaEngine {
555
+ /** Provided by the facade so listener callbacks never re-enter Angular's zone. */
556
+ private readonly runOutsideAngular;
557
+ readonly id = "native";
558
+ readonly kind: NgxMediaKind;
559
+ private readonly _state;
560
+ readonly state: Signal<NgxMediaState>;
561
+ private readonly el;
562
+ private objectUrl;
563
+ private destroyed;
564
+ private readonly cleanupFns;
565
+ private loadState;
566
+ private loadedIdentity;
567
+ private pendingLoad;
568
+ private activeLoadController;
569
+ constructor(kind: NgxMediaKind,
570
+ /** Provided by the facade so listener callbacks never re-enter Angular's zone. */
571
+ runOutsideAngular?: <T>(fn: () => T) => T, existingElement?: HTMLMediaElement, preload?: 'none' | 'metadata' | 'auto');
572
+ get mediaElement(): HTMLMediaElement;
573
+ load(source: NgxMediaSource, abort?: AbortSignal): Promise<void>;
574
+ private performLoad;
575
+ play(): Promise<void>;
576
+ pause(): void;
577
+ seek(time: number): void;
578
+ setVolume(volume: number): void;
579
+ setMuted(muted: boolean): void;
580
+ setPlaybackRate(rate: number): void;
581
+ destroy(): void;
582
+ private resolveSrc;
583
+ private releaseObjectUrl;
584
+ /** Skips the signal write entirely when every field in `partial` is already equal to current state. */
585
+ private patch;
586
+ private bufferedRanges;
587
+ private attachListeners;
588
+ /**
589
+ * Some servers/streams report `duration === Infinity` until more data
590
+ * arrives. This forces the browser to resolve it immediately by seeking
591
+ * near the end and back — which works, but each call costs 1-2 extra
592
+ * HTTP range requests. No longer called automatically; opt-in for the
593
+ * rare case where you genuinely need duration *immediately* rather than
594
+ * letting `durationchange` update it naturally during normal playback.
595
+ */
596
+ probeUnseekableDuration(): Promise<void>;
597
+ private assertNotDestroyed;
598
+ }
599
+
600
+ /**
601
+ * Multi-provider token: every decoder a consumer registers via
602
+ * `provideNgxMediaDecoder()` is picked up automatically by the registry.
603
+ *
604
+ * IMPORTANT: this token intentionally has NO default factory. Angular does
605
+ * not allow mixing a token's own default provider with externally
606
+ * registered `multi: true` providers for the SAME token — doing so throws
607
+ * at DI-construction time the moment a consumer actually registers a
608
+ * decoder (a real, if latent, bug this fixes). The correct pattern for an
609
+ * "optionally empty" multi token is `@Optional()` injection at the
610
+ * consumption site (see `NgxMediaDecoderRegistry`), which yields `null`
611
+ * when nothing has registered and an array when one or more have.
612
+ */
613
+ declare const NGX_MEDIA_DECODER: InjectionToken<NgxMediaDecoder>;
614
+ /** Pluggable network layer so auth/signed URLs don't require coupling the core to HttpClient. */
615
+ interface NgxMediaRequestHandler {
616
+ request(source: NgxMediaSource, signal?: AbortSignal): Promise<Response>;
617
+ }
618
+ declare const NGX_MEDIA_REQUEST_HANDLER: InjectionToken<NgxMediaRequestHandler>;
619
+ interface NgxMediaConfig {
620
+ /** Default preload hint for native engines. */
621
+ preload: 'none' | 'metadata' | 'auto';
622
+ /** Ceiling for the number of decoder `canDecode` probes run in parallel. */
623
+ maxConcurrentDecoderProbes: number;
624
+ }
625
+ declare const NGX_MEDIA_CONFIG_DEFAULTS: NgxMediaConfig;
626
+ declare const NGX_MEDIA_CONFIG: InjectionToken<NgxMediaConfig>;
627
+
628
+ /**
629
+ * Root-level setup: `bootstrapApplication(App, { providers: [provideNgxMedia({...})] })`.
630
+ * The supplied config is merged over the library's own defaults, not
631
+ * replacing them outright — `provideNgxMedia({ preload: 'auto' })` must not
632
+ * silently drop `maxConcurrentDecoderProbes` and leave it `undefined`.
633
+ */
634
+ declare function provideNgxMedia(config?: Partial<NgxMediaConfig>): EnvironmentProviders;
635
+ /**
636
+ * Registers a decoder plugin into the `NGX_MEDIA_DECODER` multi-provider
637
+ * token. Accepts a class (instantiated via DI, so it can itself inject
638
+ * services) or a ready-made instance for simple cases.
639
+ *
640
+ * bootstrapApplication(AppComponent, {
641
+ * providers: [provideNgxMediaDecoder(My3gpDecoder)],
642
+ * });
643
+ */
644
+ declare function provideNgxMediaDecoder(decoder: Type<NgxMediaDecoder> | NgxMediaDecoder): EnvironmentProviders;
645
+ /** Registers a custom network layer (auth headers, signed URLs, a non-fetch HTTP client, ...). */
646
+ declare function provideNgxMediaRequestHandler(handler: NgxMediaRequestHandler): EnvironmentProviders;
647
+
648
+ /**
649
+ * Format time in seconds as MM:SS
650
+ * @param seconds number
651
+ * @returns MM:SS
652
+ */
653
+ declare function formatTime(seconds: number): string;
654
+
655
+ /** True only in a real browser context — guards every browser API access for SSR. */
656
+ declare function isBrowser(): boolean;
657
+ declare function hasMediaSource(): boolean;
658
+ declare function hasAudioContext(): boolean;
659
+ declare function getAudioContextCtor(): typeof AudioContext | undefined;
660
+ declare function hasMediaSession(): boolean;
661
+ declare function hasPictureInPicture(): boolean;
662
+ declare function hasFullscreen(): boolean;
663
+ /**
664
+ * Best-effort container/codec detection using `canPlayType`. Never trusts the
665
+ * file extension alone: extension is only a last-resort hint when neither
666
+ * `type` nor `codecs` was supplied.
667
+ */
668
+ declare function canPlayNatively(el: HTMLMediaElement, source: NgxMediaSource): 'probably' | 'maybe' | '';
669
+
670
+ export { DecodedBufferEngine, NGX_MEDIA_CONFIG, NGX_MEDIA_CONFIG_DEFAULTS, NGX_MEDIA_DECODER, NGX_MEDIA_REQUEST_HANDLER, NativeMediaEngine, NgxMediaControl, NgxMediaError, NgxWasmDecoderBase, PlayList, canPlayNatively, canTransition, extractMediaFileName, formatTime, getAudioContextCtor, hasAudioContext, hasFullscreen, hasMediaSession, hasMediaSource, hasPictureInPicture, initialMediaState, isBrowser, mediaSourcesEqual, playListToMediaSource, provideNgxMedia, provideNgxMediaDecoder, provideNgxMediaRequestHandler };
671
+ export type { NgxEngineCapability, NgxMediaConfig, NgxMediaDecoder, NgxMediaEngine, NgxMediaErrorCategory, NgxMediaKind, NgxMediaPlaybackState, NgxMediaPlaylist, NgxMediaRequestHandler, NgxMediaSource, NgxMediaState, NgxMediaType, NgxTimeRange };