avbridge 2.2.0 → 2.3.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +125 -1
  2. package/NOTICE.md +2 -2
  3. package/README.md +100 -74
  4. package/THIRD_PARTY_LICENSES.md +2 -2
  5. package/dist/avi-2JPBSHGA.js +183 -0
  6. package/dist/avi-2JPBSHGA.js.map +1 -0
  7. package/dist/avi-F6WZJK5T.cjs +185 -0
  8. package/dist/avi-F6WZJK5T.cjs.map +1 -0
  9. package/dist/{avi-GCGM7OJI.js → avi-NJXAXUXK.js} +9 -3
  10. package/dist/avi-NJXAXUXK.js.map +1 -0
  11. package/dist/{avi-6SJLWIWW.cjs → avi-W6L3BTWU.cjs} +10 -4
  12. package/dist/avi-W6L3BTWU.cjs.map +1 -0
  13. package/dist/{chunk-ILKDNBSE.js → chunk-2PGRFCWB.js} +59 -10
  14. package/dist/chunk-2PGRFCWB.js.map +1 -0
  15. package/dist/chunk-5YAWWKA3.js +18 -0
  16. package/dist/chunk-5YAWWKA3.js.map +1 -0
  17. package/dist/chunk-6UUT4BEA.cjs +219 -0
  18. package/dist/chunk-6UUT4BEA.cjs.map +1 -0
  19. package/dist/{chunk-OE66B34H.cjs → chunk-7RGG6ME7.cjs} +562 -94
  20. package/dist/chunk-7RGG6ME7.cjs.map +1 -0
  21. package/dist/{chunk-WD2ZNQA7.js → chunk-DCSOQH2N.js} +7 -4
  22. package/dist/chunk-DCSOQH2N.js.map +1 -0
  23. package/dist/chunk-F3LQJKXK.cjs +20 -0
  24. package/dist/chunk-F3LQJKXK.cjs.map +1 -0
  25. package/dist/chunk-IAYKFGFG.js +200 -0
  26. package/dist/chunk-IAYKFGFG.js.map +1 -0
  27. package/dist/chunk-NNVOHKXJ.cjs +204 -0
  28. package/dist/chunk-NNVOHKXJ.cjs.map +1 -0
  29. package/dist/{chunk-C5VA5U5O.js → chunk-NV7ILLWH.js} +556 -92
  30. package/dist/chunk-NV7ILLWH.js.map +1 -0
  31. package/dist/{chunk-HZLQNKFN.cjs → chunk-QQXBPW72.js} +54 -15
  32. package/dist/chunk-QQXBPW72.js.map +1 -0
  33. package/dist/chunk-XKPSTC34.cjs +210 -0
  34. package/dist/chunk-XKPSTC34.cjs.map +1 -0
  35. package/dist/{chunk-L4NPOJ36.cjs → chunk-Z33SBWL5.cjs} +7 -4
  36. package/dist/chunk-Z33SBWL5.cjs.map +1 -0
  37. package/dist/element-browser.js +631 -103
  38. package/dist/element-browser.js.map +1 -1
  39. package/dist/element.cjs +4 -4
  40. package/dist/element.d.cts +1 -1
  41. package/dist/element.d.ts +1 -1
  42. package/dist/element.js +3 -3
  43. package/dist/index.cjs +174 -26
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +48 -4
  46. package/dist/index.d.ts +48 -4
  47. package/dist/index.js +93 -12
  48. package/dist/index.js.map +1 -1
  49. package/dist/libav-http-reader-AZLE7YFS.cjs +16 -0
  50. package/dist/{libav-http-reader-FPYDBMYK.cjs.map → libav-http-reader-AZLE7YFS.cjs.map} +1 -1
  51. package/dist/libav-http-reader-WXG3Z7AI.js +3 -0
  52. package/dist/{libav-http-reader-NQJVY273.js.map → libav-http-reader-WXG3Z7AI.js.map} +1 -1
  53. package/dist/{player-DUyvltvy.d.cts → player-B6WB74RD.d.cts} +63 -3
  54. package/dist/{player-DUyvltvy.d.ts → player-B6WB74RD.d.ts} +63 -3
  55. package/dist/player.cjs +5500 -0
  56. package/dist/player.cjs.map +1 -0
  57. package/dist/player.d.cts +649 -0
  58. package/dist/player.d.ts +649 -0
  59. package/dist/player.js +5498 -0
  60. package/dist/player.js.map +1 -0
  61. package/dist/source-73CAH6HW.cjs +28 -0
  62. package/dist/{source-CN43EI7Z.cjs.map → source-73CAH6HW.cjs.map} +1 -1
  63. package/dist/source-F656KYYV.js +3 -0
  64. package/dist/{source-FFZ7TW2B.js.map → source-F656KYYV.js.map} +1 -1
  65. package/dist/source-QJR3OHTW.js +3 -0
  66. package/dist/source-QJR3OHTW.js.map +1 -0
  67. package/dist/source-VB74JQ7Z.cjs +28 -0
  68. package/dist/source-VB74JQ7Z.cjs.map +1 -0
  69. package/dist/variant-routing-434STYAB.js +3 -0
  70. package/dist/{variant-routing-JOBWXYKD.js.map → variant-routing-434STYAB.js.map} +1 -1
  71. package/dist/variant-routing-HONNAA6R.cjs +12 -0
  72. package/dist/{variant-routing-GOHB2RZN.cjs.map → variant-routing-HONNAA6R.cjs.map} +1 -1
  73. package/package.json +9 -1
  74. package/src/classify/rules.ts +27 -5
  75. package/src/convert/remux.ts +8 -0
  76. package/src/convert/transcode.ts +41 -8
  77. package/src/element/avbridge-player.ts +845 -0
  78. package/src/element/player-icons.ts +25 -0
  79. package/src/element/player-styles.ts +472 -0
  80. package/src/errors.ts +47 -0
  81. package/src/index.ts +23 -0
  82. package/src/player-element.ts +18 -0
  83. package/src/player.ts +127 -27
  84. package/src/plugins/builtin.ts +2 -2
  85. package/src/probe/avi.ts +4 -0
  86. package/src/probe/index.ts +40 -10
  87. package/src/strategies/fallback/audio-output.ts +31 -0
  88. package/src/strategies/fallback/decoder.ts +83 -2
  89. package/src/strategies/fallback/index.ts +34 -1
  90. package/src/strategies/fallback/variant-routing.ts +7 -13
  91. package/src/strategies/fallback/video-renderer.ts +129 -33
  92. package/src/strategies/hybrid/decoder.ts +131 -20
  93. package/src/strategies/hybrid/index.ts +36 -2
  94. package/src/strategies/remux/index.ts +13 -1
  95. package/src/strategies/remux/mse.ts +12 -2
  96. package/src/strategies/remux/pipeline.ts +6 -0
  97. package/src/subtitles/index.ts +7 -3
  98. package/src/types.ts +53 -1
  99. package/src/util/libav-http-reader.ts +5 -1
  100. package/src/util/source.ts +28 -8
  101. package/src/util/transport.ts +26 -0
  102. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
  103. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
  104. package/dist/avi-6SJLWIWW.cjs.map +0 -1
  105. package/dist/avi-GCGM7OJI.js.map +0 -1
  106. package/dist/chunk-C5VA5U5O.js.map +0 -1
  107. package/dist/chunk-HZLQNKFN.cjs.map +0 -1
  108. package/dist/chunk-ILKDNBSE.js.map +0 -1
  109. package/dist/chunk-J5MCMN3S.js +0 -27
  110. package/dist/chunk-J5MCMN3S.js.map +0 -1
  111. package/dist/chunk-L4NPOJ36.cjs.map +0 -1
  112. package/dist/chunk-NZU7W256.cjs +0 -29
  113. package/dist/chunk-NZU7W256.cjs.map +0 -1
  114. package/dist/chunk-OE66B34H.cjs.map +0 -1
  115. package/dist/chunk-WD2ZNQA7.js.map +0 -1
  116. package/dist/libav-http-reader-FPYDBMYK.cjs +0 -16
  117. package/dist/libav-http-reader-NQJVY273.js +0 -3
  118. package/dist/source-CN43EI7Z.cjs +0 -28
  119. package/dist/source-FFZ7TW2B.js +0 -3
  120. package/dist/variant-routing-GOHB2RZN.cjs +0 -12
  121. package/dist/variant-routing-JOBWXYKD.js +0 -3
@@ -0,0 +1,649 @@
1
+ /**
2
+ * `<avbridge-player>` — YouTube-style controls element.
3
+ *
4
+ * Wraps `<avbridge-video>` with a full player UI: play/pause, seek bar,
5
+ * volume, settings menu (speed, subtitles, audio tracks), fullscreen,
6
+ * keyboard shortcuts, touch gestures, and auto-hiding controls.
7
+ *
8
+ * All properties, methods, and events from `<avbridge-video>` are proxied
9
+ * through. Consumers interact with `<avbridge-player>` exclusively.
10
+ */
11
+ declare class AvbridgePlayerElement extends HTMLElement {
12
+ static readonly observedAttributes: ("src" | "muted" | "autoplay" | "loop" | "preload" | "poster" | "playsinline" | "crossorigin" | "disableremoteplayback" | "preferstrategy")[];
13
+ private _video;
14
+ private _playBtn;
15
+ private _overlayBtn;
16
+ private _seekInput;
17
+ private _seekProgress;
18
+ private _seekBuffered;
19
+ private _seekThumb;
20
+ private _seekTooltip;
21
+ private _timeDisplay;
22
+ private _volumeBtn;
23
+ private _volumeInput;
24
+ private _settingsBtn;
25
+ private _settingsMenu;
26
+ private _fullscreenBtn;
27
+ private _speedIndicator;
28
+ private _rippleLeft;
29
+ private _rippleRight;
30
+ private _state;
31
+ private _controlsTimer;
32
+ private _settingsOpen;
33
+ private _userSeeking;
34
+ private _holdTimer;
35
+ private _holdSpeedActive;
36
+ private _savedPlaybackRate;
37
+ private _lastTapTime;
38
+ private _tapTimer;
39
+ private _statsOpen;
40
+ private _statsEl;
41
+ private _statsInterval;
42
+ private _eventCleanup;
43
+ constructor();
44
+ private _template;
45
+ private _bindEvents;
46
+ connectedCallback(): void;
47
+ disconnectedCallback(): void;
48
+ attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
49
+ private _setState;
50
+ private _togglePlay;
51
+ private _onSeekInput;
52
+ private _onSeekCommit;
53
+ /** Linear click-to-time mapping across the full track width (no edge clamping). */
54
+ private _timeFromSeekPointer;
55
+ private _onSeekPointerDown;
56
+ private _onSeekHover;
57
+ private _updateSeekVisuals;
58
+ private _updateTime;
59
+ private _toggleMute;
60
+ private _updateVolume;
61
+ private _toggleSettings;
62
+ private _closeSettings;
63
+ private _buildSettingsMenu;
64
+ private _toggleStats;
65
+ private _updateStats;
66
+ private _toggleFullscreen;
67
+ private _updateFullscreenIcon;
68
+ private _showControls;
69
+ private _scheduleHide;
70
+ /** Track whether the last interaction was touch so click handler can skip. */
71
+ private _lastPointerTypeWasTouch;
72
+ private _onContainerClick;
73
+ private _onContainerDblClick;
74
+ private _onPointerDown;
75
+ private _onPointerUp;
76
+ private _cancelHold;
77
+ private _doDoubleTap;
78
+ private _onKeydown;
79
+ private _clearTimers;
80
+ get src(): string;
81
+ set src(v: string);
82
+ get source(): unknown;
83
+ set source(v: unknown);
84
+ get currentTime(): number;
85
+ set currentTime(v: number);
86
+ get duration(): number;
87
+ get paused(): boolean;
88
+ get ended(): boolean;
89
+ get readyState(): number;
90
+ get volume(): number;
91
+ set volume(v: number);
92
+ get muted(): boolean;
93
+ set muted(v: boolean);
94
+ get playbackRate(): number;
95
+ set playbackRate(v: number);
96
+ get autoplay(): boolean;
97
+ set autoplay(v: boolean);
98
+ get loop(): boolean;
99
+ set loop(v: boolean);
100
+ get videoWidth(): number;
101
+ get videoHeight(): number;
102
+ get buffered(): TimeRanges;
103
+ get played(): TimeRanges;
104
+ get seekable(): TimeRanges;
105
+ get strategy(): string | undefined;
106
+ get strategyClass(): string | undefined;
107
+ get audioTracks(): unknown[];
108
+ get subtitleTracks(): unknown[];
109
+ get player(): unknown;
110
+ get videoElement(): HTMLVideoElement;
111
+ play(): Promise<void>;
112
+ pause(): void;
113
+ load(): Promise<void>;
114
+ destroy(): Promise<void>;
115
+ setAudioTrack(id: number): Promise<void>;
116
+ setSubtitleTrack(id: number | null): Promise<void>;
117
+ getDiagnostics(): unknown;
118
+ canPlayType(mime: string): string;
119
+ }
120
+
121
+ /**
122
+ * Core types shared across avbridge modules.
123
+ *
124
+ * The four main concepts:
125
+ * - {@link MediaInput} — what the user gives us (File / Blob / URL / bytes).
126
+ * - {@link MediaContext} — what we learned about it from probing.
127
+ * - {@link Classification} — which playback strategy we picked.
128
+ * - {@link PlaybackSession} — the running playback, returned by a strategy.
129
+ */
130
+ /**
131
+ * Anything we accept as a media source. We do not accept arbitrary
132
+ * `ReadableStream`s in v1 because we need random access for seeking.
133
+ */
134
+ type MediaInput = File | Blob | string | URL | ArrayBuffer | Uint8Array;
135
+ /** Container format families we know about. */
136
+ type ContainerKind = "mp4" | "mov" | "mkv" | "webm" | "avi" | "asf" | "flv" | "rm" | "ogg" | "wav" | "mp3" | "flac" | "adts" | "mpegts" | "unknown";
137
+ /** Video codec families. Strings, not enums, so plugins can extend. */
138
+ type VideoCodec = "h264" | "h265" | "vp8" | "vp9" | "av1" | "mpeg4" | "wmv3" | "vc1" | "rv10" | "rv20" | "rv30" | "rv40" | "mpeg2" | "mpeg1" | "theora" | (string & {});
139
+ /** Audio codec families. */
140
+ type AudioCodec = "aac" | "mp3" | "opus" | "vorbis" | "flac" | "pcm" | "ac3" | "eac3" | "wmav2" | "wmapro" | "alac" | "cook" | "ra_144" | "ra_288" | "sipr" | "atrac3" | "dts" | "truehd" | (string & {});
141
+ interface VideoTrackInfo {
142
+ id: number;
143
+ codec: VideoCodec;
144
+ /** Codec-private profile string when known (e.g. "High", "Main 10"). */
145
+ profile?: string;
146
+ level?: number;
147
+ width: number;
148
+ height: number;
149
+ /** Pixel format string in ffmpeg style (e.g. "yuv420p", "yuv420p10le"). */
150
+ pixelFormat?: string;
151
+ /** Frames per second, when known. */
152
+ fps?: number;
153
+ bitDepth?: number;
154
+ /** RFC 6381 codec string for `MediaSource.isTypeSupported`, when computable. */
155
+ codecString?: string;
156
+ }
157
+ interface AudioTrackInfo {
158
+ id: number;
159
+ codec: AudioCodec;
160
+ channels: number;
161
+ sampleRate: number;
162
+ language?: string;
163
+ codecString?: string;
164
+ }
165
+ interface SubtitleTrackInfo {
166
+ id: number;
167
+ /** "vtt" | "srt" | "ass" | "pgs" | "embedded" */
168
+ format: string;
169
+ language?: string;
170
+ /** Set if this is a sidecar file rather than embedded in the container. */
171
+ sidecarUrl?: string;
172
+ }
173
+ /**
174
+ * Everything the probe layer learned about a source.
175
+ * This is the input to the classification engine.
176
+ */
177
+ interface MediaContext {
178
+ source: MediaInput;
179
+ /** Stable display name for diagnostics, if we have one. */
180
+ name?: string;
181
+ byteLength?: number;
182
+ container: ContainerKind;
183
+ videoTracks: VideoTrackInfo[];
184
+ audioTracks: AudioTrackInfo[];
185
+ subtitleTracks: SubtitleTrackInfo[];
186
+ /** Which probe backend produced this context, for diagnostics. */
187
+ probedBy: "mediabunny" | "libav" | "sniff";
188
+ /** Total duration in seconds, if known. */
189
+ duration?: number;
190
+ }
191
+ /**
192
+ * The four playback strategies, ordered from lightest to heaviest:
193
+ * - `"native"` — direct `<video>` playback (zero overhead)
194
+ * - `"remux"` — repackage to fragmented MP4 via MSE (preserves hardware decode)
195
+ * - `"hybrid"` — libav.js demux + WebCodecs hardware decode (for AVI/ASF/FLV with modern codecs)
196
+ * - `"fallback"` — full WASM software decode via libav.js (universal, CPU-intensive)
197
+ */
198
+ type StrategyName = "native" | "remux" | "hybrid" | "fallback";
199
+ /**
200
+ * Classification outcome from the rules engine. Determines which strategy to use:
201
+ * - `NATIVE` — browser plays this directly
202
+ * - `REMUX_CANDIDATE` — codecs are native, container needs repackaging
203
+ * - `HYBRID_CANDIDATE` — container needs libav demux, codecs are hardware-decodable
204
+ * - `FALLBACK_REQUIRED` — codec has no browser decoder, WASM decode needed
205
+ * - `RISKY_NATIVE` — might work natively but may stall (e.g. Hi10P, 4K120)
206
+ */
207
+ type StrategyClass = "NATIVE" | "REMUX_CANDIDATE" | "HYBRID_CANDIDATE" | "FALLBACK_REQUIRED" | "RISKY_NATIVE";
208
+ /**
209
+ * A live playback session created by a strategy. The {@link UnifiedPlayer}
210
+ * delegates user-facing controls to whichever session is currently active.
211
+ */
212
+ interface PlaybackSession {
213
+ readonly strategy: StrategyName;
214
+ play(): Promise<void>;
215
+ pause(): void;
216
+ seek(time: number): Promise<void>;
217
+ setAudioTrack(id: number): Promise<void>;
218
+ setSubtitleTrack(id: number | null): Promise<void>;
219
+ /** Tear down everything: revoke object URLs, close decoders, etc. */
220
+ destroy(): Promise<void>;
221
+ /** Strategy-specific runtime stats merged into Diagnostics. */
222
+ getRuntimeStats(): Record<string, unknown>;
223
+ /** Current playback position in seconds. Used to capture position before strategy switch. */
224
+ getCurrentTime(): number;
225
+ /** Register a callback for unrecoverable errors that should trigger escalation. */
226
+ onFatalError?(handler: (reason: string) => void): void;
227
+ }
228
+ interface DiagnosticsSnapshot {
229
+ container: ContainerKind | "unknown";
230
+ videoCodec?: VideoCodec;
231
+ audioCodec?: AudioCodec;
232
+ width?: number;
233
+ height?: number;
234
+ fps?: number;
235
+ duration?: number;
236
+ strategy: StrategyName | "pending";
237
+ strategyClass: StrategyClass | "pending";
238
+ reason: string;
239
+ probedBy?: "mediabunny" | "libav" | "sniff";
240
+ /**
241
+ * Where the source is coming from. `"blob"` means File / Blob /
242
+ * ArrayBuffer / Uint8Array (in-memory). `"url"` means an HTTP/HTTPS URL
243
+ * being streamed via Range requests.
244
+ */
245
+ sourceType?: "blob" | "url";
246
+ /**
247
+ * Transport used to read the source. `"memory"` for in-memory blobs;
248
+ * `"http-range"` for URL sources streamed via HTTP Range requests.
249
+ */
250
+ transport?: "memory" | "http-range";
251
+ /**
252
+ * For URL sources, true if the server supports HTTP Range requests
253
+ * (the only mode we accept — see `attachLibavHttpReader`). Always true
254
+ * when `transport === "http-range"` because we fail fast otherwise.
255
+ */
256
+ rangeSupported?: boolean;
257
+ runtime?: Record<string, unknown>;
258
+ strategyHistory?: Array<{
259
+ strategy: StrategyName;
260
+ reason: string;
261
+ at: number;
262
+ }>;
263
+ }
264
+ /** §8.2 plugin interface, kept structurally identical to the design doc. */
265
+ interface Plugin {
266
+ name: string;
267
+ canHandle(context: MediaContext): boolean;
268
+ /** Returns a session if it claims the context, otherwise throws. */
269
+ execute(context: MediaContext, target: HTMLVideoElement, transport?: TransportConfig): Promise<PlaybackSession>;
270
+ }
271
+ /** Player creation options. */
272
+ interface CreatePlayerOptions {
273
+ source: MediaInput;
274
+ target: HTMLVideoElement;
275
+ /**
276
+ * Optional explicit subtitle list. The player otherwise tries to discover
277
+ * sidecar files via the FileSystemDirectoryHandle (when supplied), or pulls
278
+ * embedded subtitle tracks if the container exposes them.
279
+ */
280
+ subtitles?: Array<{
281
+ url: string;
282
+ language?: string;
283
+ format?: "vtt" | "srt";
284
+ }>;
285
+ /**
286
+ * Optional directory handle for sidecar discovery. When the source is a
287
+ * `File` selected from this directory, sibling `*.srt`/`*.vtt` files are
288
+ * picked up automatically.
289
+ */
290
+ directory?: FileSystemDirectoryHandle;
291
+ /**
292
+ * Skip classification and start with the given strategy. Useful for
293
+ * diagnostics, tests, and consumers that already know the right path.
294
+ *
295
+ * **Note:** this is the *initial* strategy, not a hard force — if the
296
+ * named strategy fails to start, the player still walks the fallback
297
+ * chain like a normal classification would. The strategy class shown in
298
+ * diagnostics matches whatever the picked strategy actually is, not
299
+ * "NATIVE" by default.
300
+ */
301
+ initialStrategy?: StrategyName;
302
+ /** Inject extra plugins; they take priority over built-ins. */
303
+ plugins?: Plugin[];
304
+ /**
305
+ * When true (default), the player automatically escalates to the next
306
+ * strategy in the fallback chain on failure or stall.
307
+ */
308
+ autoEscalate?: boolean;
309
+ /**
310
+ * Behavior when the browser tab becomes hidden.
311
+ * - `"pause"` (default): auto-pause on hide, auto-resume on visible
312
+ * if the user had been playing. Matches YouTube, Netflix, and
313
+ * native media players. Prevents degraded playback from Chrome's
314
+ * background throttling of requestAnimationFrame and setTimeout.
315
+ * - `"continue"`: keep playing. Playback will degrade anyway due to
316
+ * browser throttling, but useful for consumers who want full
317
+ * control of visibility handling themselves.
318
+ */
319
+ backgroundBehavior?: "pause" | "continue";
320
+ /**
321
+ * Extra {@link RequestInit} merged into every HTTP request the player
322
+ * makes (probe Range requests, subtitle fetches, libav HTTP reader).
323
+ * Headers are merged, not overwritten — so you can add `Authorization`
324
+ * without losing the player's `Range` header.
325
+ */
326
+ requestInit?: RequestInit;
327
+ /**
328
+ * Custom fetch implementation. Defaults to `globalThis.fetch`. Useful
329
+ * for interceptors, logging, or environments without a global fetch.
330
+ */
331
+ fetchFn?: FetchFn;
332
+ }
333
+ /** Signature-compatible with `globalThis.fetch`. */
334
+ type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
335
+ /** Internal transport config bundle. Not part of the public API. */
336
+ interface TransportConfig {
337
+ requestInit?: RequestInit;
338
+ fetchFn?: FetchFn;
339
+ }
340
+ /** Events emitted by {@link UnifiedPlayer}. Strongly typed. */
341
+ interface PlayerEventMap {
342
+ strategy: {
343
+ strategy: StrategyName;
344
+ reason: string;
345
+ };
346
+ strategychange: {
347
+ from: StrategyName;
348
+ to: StrategyName;
349
+ reason: string;
350
+ currentTime: number;
351
+ };
352
+ tracks: {
353
+ video: VideoTrackInfo[];
354
+ audio: AudioTrackInfo[];
355
+ subtitle: SubtitleTrackInfo[];
356
+ };
357
+ error: Error;
358
+ timeupdate: {
359
+ currentTime: number;
360
+ };
361
+ ended: void;
362
+ ready: void;
363
+ }
364
+ type PlayerEventName = keyof PlayerEventMap;
365
+ /** Generic listener type re-exported for player.on overloads. */
366
+ type Listener<T> = (payload: T) => void;
367
+
368
+ declare class UnifiedPlayer {
369
+ private readonly options;
370
+ private readonly registry;
371
+ private emitter;
372
+ private session;
373
+ private diag;
374
+ private timeupdateInterval;
375
+ private mediaContext;
376
+ private classification;
377
+ private stallTimer;
378
+ private lastProgressTime;
379
+ private lastProgressPosition;
380
+ private errorListener;
381
+ private endedListener;
382
+ private userIntent;
383
+ private autoPausedForVisibility;
384
+ private visibilityListener;
385
+ private switchingPromise;
386
+ private subtitleResources;
387
+ private readonly transport;
388
+ /**
389
+ * @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
390
+ */
391
+ private constructor();
392
+ static create(options: CreatePlayerOptions): Promise<UnifiedPlayer>;
393
+ private bootstrap;
394
+ /**
395
+ * Try to start a session with the given strategy. On failure, walk the
396
+ * fallback chain. Throws only if all strategies are exhausted.
397
+ */
398
+ private startSession;
399
+ private escalate;
400
+ private doEscalate;
401
+ private attachSupervisor;
402
+ private clearSupervisor;
403
+ /** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
404
+ setStrategy(strategy: StrategyName, reason?: string): Promise<void>;
405
+ private doSetStrategy;
406
+ private startTimeupdateLoop;
407
+ /** Subscribe to a player event. Returns an unsubscribe function. Sticky events (strategy, ready, tracks) replay for late subscribers. */
408
+ on<K extends PlayerEventName>(event: K, fn: Listener<PlayerEventMap[K]>): () => void;
409
+ /** Remove a previously registered event listener. */
410
+ off<K extends PlayerEventName>(event: K, fn: Listener<PlayerEventMap[K]>): void;
411
+ /** Begin or resume playback. Throws if the player is not ready. */
412
+ play(): Promise<void>;
413
+ /** Pause playback. No-op if the player is not ready or already paused. */
414
+ pause(): void;
415
+ /**
416
+ * Handle browser tab visibility changes. On hide: pause if the user
417
+ * had been playing. On show: resume if we were the one who paused.
418
+ * Skips when `backgroundBehavior: "continue"` is set (listener isn't
419
+ * installed in that case).
420
+ */
421
+ private onVisibilityChange;
422
+ /** Seek to the given time in seconds. Throws if the player is not ready. */
423
+ seek(time: number): Promise<void>;
424
+ /** Switch the active audio track by track ID. Throws if the player is not ready. */
425
+ setAudioTrack(id: number): Promise<void>;
426
+ /** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
427
+ setSubtitleTrack(id: number | null): Promise<void>;
428
+ /** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
429
+ getDiagnostics(): DiagnosticsSnapshot;
430
+ /** Return the total duration in seconds, or `NaN` if unknown. */
431
+ getDuration(): number;
432
+ /** Return the current playback position in seconds. */
433
+ getCurrentTime(): number;
434
+ /** Tear down the player: stop timers, destroy the active session, remove all event listeners. The player is unusable after this call. */
435
+ destroy(): Promise<void>;
436
+ }
437
+
438
+ /**
439
+ * `<avbridge-video>` — `HTMLMediaElement`-compatible primitive backed by the
440
+ * avbridge engine. Drop-in replacement for a `<video>` element with no
441
+ * built-in UI.
442
+ *
443
+ * Purpose:
444
+ *
445
+ * 1. Validate the public API by being a real consumer of `createPlayer()`.
446
+ * 2. Drive lifecycle correctness in the core via adversarial integration tests.
447
+ * 3. Give consumers a `<video>`-compatible primitive they can wrap with
448
+ * their own UI.
449
+ *
450
+ * **It is not a player UI framework.** The tag name `<avbridge-player>` is
451
+ * reserved for a future controls-bearing element. See
452
+ * `docs/dev/WEB_COMPONENT_SPEC.md` for the full spec, lifecycle invariants,
453
+ * and edge case list.
454
+ */
455
+
456
+ /** Strategy preference passed via the `preferstrategy` attribute. */
457
+ type PreferredStrategy = "auto" | StrategyName;
458
+ /**
459
+ * `HTMLElement` is a browser-only global. SSR frameworks (Next.js, Astro,
460
+ * Remix, etc.) commonly import library modules on the server to extract
461
+ * types or do tree-shaking, even if the user only ends up using them in
462
+ * the browser. If we extended `HTMLElement` directly, the `class extends`
463
+ * expression would be evaluated at module load time and crash in Node.
464
+ *
465
+ * The fix: in non-browser environments, fall back to an empty stub class.
466
+ * The element is never *constructed* server-side (the registration in
467
+ * `element.ts` is guarded by `typeof customElements !== "undefined"`), so
468
+ * the stub is never instantiated — it just lets the class declaration
469
+ * evaluate cleanly so the module can be imported anywhere.
470
+ */
471
+ declare const HTMLElementCtor: typeof HTMLElement;
472
+ /**
473
+ * Custom element. Lifecycle correctness is enforced via a monotonically
474
+ * increasing `_bootstrapId`: every async bootstrap captures the ID at start
475
+ * and discards itself if the ID has changed by the time it resolves. This
476
+ * single pattern handles disconnect-during-bootstrap, rapid src reassignment,
477
+ * bootstrap races, and destroy-during-bootstrap.
478
+ */
479
+ declare class AvbridgeVideoElement extends HTMLElementCtor {
480
+ static readonly observedAttributes: string[];
481
+ /** The shadow DOM `<video>` element that strategies render into. */
482
+ private _videoEl;
483
+ /** Active player session, if any. Cleared on teardown. */
484
+ private _player;
485
+ /**
486
+ * Monotonic counter incremented on every (re)bootstrap. Async bootstrap
487
+ * work captures the current ID; if it doesn't match by the time the work
488
+ * resolves, the work is discarded.
489
+ */
490
+ private _bootstrapId;
491
+ /** True after destroy() — element is permanently unusable. */
492
+ private _destroyed;
493
+ /** Internal source state. Either string-form (src) OR rich (source). */
494
+ private _src;
495
+ private _source;
496
+ /**
497
+ * Set when the `source` property setter is in the middle of clearing the
498
+ * `src` attribute as part of mutual exclusion. The attributeChangedCallback
499
+ * checks this flag and skips its normal "clear source" side effect, which
500
+ * would otherwise wipe the value we just set.
501
+ */
502
+ private _suppressSrcAttrCallback;
503
+ /** Last-known runtime state surfaced via getters. */
504
+ private _strategy;
505
+ private _strategyClass;
506
+ private _audioTracks;
507
+ private _subtitleTracks;
508
+ /**
509
+ * Initial strategy preference. `"auto"` means "let the classifier decide";
510
+ * any other value is passed to `createPlayer({ initialStrategy })` and
511
+ * skips classification on the next bootstrap. Note that this only affects
512
+ * the *initial* pick — runtime fallback escalation still applies, so a
513
+ * preference of `"native"` may still escalate to remux/hybrid/fallback if
514
+ * native fails.
515
+ */
516
+ private _preferredStrategy;
517
+ /** Set if currentTime was assigned before the player was ready. */
518
+ private _pendingSeek;
519
+ /** Set if play() was called before the player was ready. */
520
+ private _pendingPlay;
521
+ /** MutationObserver tracking light-DOM `<track>` children. */
522
+ private _trackObserver;
523
+ constructor();
524
+ connectedCallback(): void;
525
+ disconnectedCallback(): void;
526
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void;
527
+ /** Returns the currently-active source (src or source), whichever is set. */
528
+ private _activeSource;
529
+ /**
530
+ * Mirror light-DOM `<track>` children into the shadow `<video>` so that
531
+ * the browser's native text-track machinery picks them up. Called on
532
+ * connect, on every mutation of light-DOM children, and once after each
533
+ * source change so newly-set tracks survive a fresh `<video>`.
534
+ *
535
+ * Strategy: clone the children. We don't move them because the user's
536
+ * code may still hold references to the originals (e.g. to set `default`).
537
+ * The shadow copies are throwaway — we wipe them on every sync.
538
+ */
539
+ private _syncTextTracks;
540
+ /** Internal src setter — separate from the property setter so the
541
+ * attributeChangedCallback can use it without re-entering reflection. */
542
+ private _setSrcInternal;
543
+ /** Called whenever the active source changes (src or source). */
544
+ private _onSourceChanged;
545
+ private _bootstrap;
546
+ /**
547
+ * Tear down the active player and reset runtime state. Idempotent.
548
+ * If `currentBootstrapId` is provided, the bootstrap counter is NOT
549
+ * incremented (used by `_bootstrap()` to avoid invalidating itself).
550
+ */
551
+ private _teardown;
552
+ get src(): string | null;
553
+ set src(value: string | null);
554
+ get source(): MediaInput | null;
555
+ set source(value: MediaInput | null);
556
+ get autoplay(): boolean;
557
+ set autoplay(value: boolean);
558
+ get muted(): boolean;
559
+ set muted(value: boolean);
560
+ get loop(): boolean;
561
+ set loop(value: boolean);
562
+ get preload(): "none" | "metadata" | "auto";
563
+ set preload(value: "none" | "metadata" | "auto");
564
+ get diagnostics(): boolean;
565
+ set diagnostics(value: boolean);
566
+ get preferredStrategy(): PreferredStrategy;
567
+ set preferredStrategy(value: PreferredStrategy);
568
+ get currentTime(): number;
569
+ set currentTime(value: number);
570
+ get duration(): number;
571
+ get paused(): boolean;
572
+ get ended(): boolean;
573
+ get readyState(): number;
574
+ /**
575
+ * Buffered time ranges for the active source. Mirrors the standard
576
+ * `<video>.buffered` `TimeRanges` API. For the native and remux strategies
577
+ * this reflects the underlying SourceBuffer / progressive download state.
578
+ * For the hybrid and fallback (canvas-rendered) strategies it currently
579
+ * returns an empty TimeRanges; a future release will synthesize a coarse
580
+ * range from the decoder's read position.
581
+ */
582
+ get buffered(): TimeRanges;
583
+ get poster(): string;
584
+ set poster(value: string);
585
+ get volume(): number;
586
+ set volume(value: number);
587
+ get playbackRate(): number;
588
+ set playbackRate(value: number);
589
+ get videoWidth(): number;
590
+ get videoHeight(): number;
591
+ get played(): TimeRanges;
592
+ get seekable(): TimeRanges;
593
+ get crossOrigin(): string | null;
594
+ set crossOrigin(value: string | null);
595
+ get disableRemotePlayback(): boolean;
596
+ set disableRemotePlayback(value: boolean);
597
+ /**
598
+ * Native `HTMLMediaElement.canPlayType()` passthrough. Note that this
599
+ * answers about the *browser's* native support, not avbridge's full
600
+ * capabilities — avbridge can play many formats this method returns ""
601
+ * for, by routing them to the remux/hybrid/fallback strategies.
602
+ */
603
+ canPlayType(mimeType: string): CanPlayTypeResult;
604
+ /**
605
+ * **Escape hatch.** The underlying shadow-DOM `<video>` element.
606
+ *
607
+ * Use for native browser APIs the wrapper doesn't expose:
608
+ * - `el.videoElement.requestPictureInPicture()`
609
+ * - `el.videoElement.audioTracks` (browser native, not avbridge's track list)
610
+ * - direct integration with libraries that need a real HTMLVideoElement
611
+ *
612
+ * **Caveat:** When the active strategy is `"fallback"` or `"hybrid"`,
613
+ * frames are rendered to a canvas overlay, not into this `<video>`.
614
+ * APIs that depend on the actual pixels (Picture-in-Picture, captureStream)
615
+ * will not show the playing content in those modes. Check `el.strategy`
616
+ * before using such APIs.
617
+ */
618
+ get videoElement(): HTMLVideoElement;
619
+ get strategy(): StrategyName | null;
620
+ get strategyClass(): StrategyClass | null;
621
+ get player(): UnifiedPlayer | null;
622
+ get audioTracks(): AudioTrackInfo[];
623
+ get subtitleTracks(): SubtitleTrackInfo[];
624
+ /** Force a (re-)bootstrap if a source is currently set. */
625
+ load(): Promise<void>;
626
+ /**
627
+ * Begin or resume playback. If the player isn't ready yet, the call is
628
+ * queued and applied once `ready` fires.
629
+ */
630
+ play(): Promise<void>;
631
+ pause(): void;
632
+ /**
633
+ * Tear down the element permanently. After destroy(), the element ignores
634
+ * all method calls and attribute changes.
635
+ */
636
+ destroy(): Promise<void>;
637
+ setAudioTrack(id: number): Promise<void>;
638
+ setSubtitleTrack(id: number | null): Promise<void>;
639
+ getDiagnostics(): DiagnosticsSnapshot | null;
640
+ private _dispatch;
641
+ private _dispatchError;
642
+ }
643
+ declare global {
644
+ interface HTMLElementTagNameMap {
645
+ "avbridge-video": AvbridgeVideoElement;
646
+ }
647
+ }
648
+
649
+ export { AvbridgePlayerElement, AvbridgeVideoElement };