avbridge 1.0.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 (103) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +21 -0
  3. package/README.md +415 -0
  4. package/dist/avi-M5B4SHRM.cjs +164 -0
  5. package/dist/avi-M5B4SHRM.cjs.map +1 -0
  6. package/dist/avi-POCGZ4JX.js +162 -0
  7. package/dist/avi-POCGZ4JX.js.map +1 -0
  8. package/dist/chunk-5ISVAODK.js +80 -0
  9. package/dist/chunk-5ISVAODK.js.map +1 -0
  10. package/dist/chunk-F7YS2XOA.cjs +2966 -0
  11. package/dist/chunk-F7YS2XOA.cjs.map +1 -0
  12. package/dist/chunk-FKM7QBZU.js +2957 -0
  13. package/dist/chunk-FKM7QBZU.js.map +1 -0
  14. package/dist/chunk-J5MCMN3S.js +27 -0
  15. package/dist/chunk-J5MCMN3S.js.map +1 -0
  16. package/dist/chunk-L4NPOJ36.cjs +180 -0
  17. package/dist/chunk-L4NPOJ36.cjs.map +1 -0
  18. package/dist/chunk-NZU7W256.cjs +29 -0
  19. package/dist/chunk-NZU7W256.cjs.map +1 -0
  20. package/dist/chunk-PQTZS7OA.js +147 -0
  21. package/dist/chunk-PQTZS7OA.js.map +1 -0
  22. package/dist/chunk-WD2ZNQA7.js +177 -0
  23. package/dist/chunk-WD2ZNQA7.js.map +1 -0
  24. package/dist/chunk-Y5FYF5KG.cjs +153 -0
  25. package/dist/chunk-Y5FYF5KG.cjs.map +1 -0
  26. package/dist/chunk-Z2FJ5TJC.cjs +82 -0
  27. package/dist/chunk-Z2FJ5TJC.cjs.map +1 -0
  28. package/dist/element.cjs +433 -0
  29. package/dist/element.cjs.map +1 -0
  30. package/dist/element.d.cts +158 -0
  31. package/dist/element.d.ts +158 -0
  32. package/dist/element.js +431 -0
  33. package/dist/element.js.map +1 -0
  34. package/dist/index.cjs +576 -0
  35. package/dist/index.cjs.map +1 -0
  36. package/dist/index.d.cts +80 -0
  37. package/dist/index.d.ts +80 -0
  38. package/dist/index.js +554 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/libav-http-reader-FPYDBMYK.cjs +16 -0
  41. package/dist/libav-http-reader-FPYDBMYK.cjs.map +1 -0
  42. package/dist/libav-http-reader-NQJVY273.js +3 -0
  43. package/dist/libav-http-reader-NQJVY273.js.map +1 -0
  44. package/dist/libav-import-2JURFHEW.js +8 -0
  45. package/dist/libav-import-2JURFHEW.js.map +1 -0
  46. package/dist/libav-import-GST2AMPL.cjs +30 -0
  47. package/dist/libav-import-GST2AMPL.cjs.map +1 -0
  48. package/dist/libav-loader-KA2MAWLM.js +3 -0
  49. package/dist/libav-loader-KA2MAWLM.js.map +1 -0
  50. package/dist/libav-loader-ZHOERPHW.cjs +12 -0
  51. package/dist/libav-loader-ZHOERPHW.cjs.map +1 -0
  52. package/dist/player-BBwbCkdL.d.cts +365 -0
  53. package/dist/player-BBwbCkdL.d.ts +365 -0
  54. package/dist/source-SC6ZEQYR.cjs +28 -0
  55. package/dist/source-SC6ZEQYR.cjs.map +1 -0
  56. package/dist/source-ZFS4H7J3.js +3 -0
  57. package/dist/source-ZFS4H7J3.js.map +1 -0
  58. package/dist/variant-routing-GOHB2RZN.cjs +12 -0
  59. package/dist/variant-routing-GOHB2RZN.cjs.map +1 -0
  60. package/dist/variant-routing-JOBWXYKD.js +3 -0
  61. package/dist/variant-routing-JOBWXYKD.js.map +1 -0
  62. package/package.json +95 -0
  63. package/src/classify/index.ts +1 -0
  64. package/src/classify/rules.ts +214 -0
  65. package/src/convert/index.ts +2 -0
  66. package/src/convert/remux.ts +522 -0
  67. package/src/convert/transcode.ts +329 -0
  68. package/src/diagnostics.ts +99 -0
  69. package/src/element/avbridge-player.ts +576 -0
  70. package/src/element.ts +19 -0
  71. package/src/events.ts +71 -0
  72. package/src/index.ts +42 -0
  73. package/src/libav-stubs.d.ts +24 -0
  74. package/src/player.ts +455 -0
  75. package/src/plugins/builtin.ts +37 -0
  76. package/src/plugins/registry.ts +32 -0
  77. package/src/probe/avi.ts +242 -0
  78. package/src/probe/index.ts +59 -0
  79. package/src/probe/mediabunny.ts +194 -0
  80. package/src/strategies/fallback/audio-output.ts +293 -0
  81. package/src/strategies/fallback/clock.ts +7 -0
  82. package/src/strategies/fallback/decoder.ts +660 -0
  83. package/src/strategies/fallback/index.ts +170 -0
  84. package/src/strategies/fallback/libav-import.ts +27 -0
  85. package/src/strategies/fallback/libav-loader.ts +190 -0
  86. package/src/strategies/fallback/variant-routing.ts +43 -0
  87. package/src/strategies/fallback/video-renderer.ts +216 -0
  88. package/src/strategies/hybrid/decoder.ts +641 -0
  89. package/src/strategies/hybrid/index.ts +139 -0
  90. package/src/strategies/native.ts +107 -0
  91. package/src/strategies/remux/annexb.ts +112 -0
  92. package/src/strategies/remux/index.ts +79 -0
  93. package/src/strategies/remux/mse.ts +234 -0
  94. package/src/strategies/remux/pipeline.ts +254 -0
  95. package/src/subtitles/index.ts +91 -0
  96. package/src/subtitles/render.ts +62 -0
  97. package/src/subtitles/srt.ts +62 -0
  98. package/src/subtitles/vtt.ts +5 -0
  99. package/src/types-shim.d.ts +3 -0
  100. package/src/types.ts +360 -0
  101. package/src/util/codec-strings.ts +86 -0
  102. package/src/util/libav-http-reader.ts +315 -0
  103. package/src/util/source.ts +274 -0
@@ -0,0 +1,576 @@
1
+ /**
2
+ * `<avbridge-player>` — reference web component for the avbridge engine.
3
+ *
4
+ * This is a *thin* wrapper around `createPlayer()`. Its purpose is to:
5
+ *
6
+ * 1. Validate the public API by being a real consumer of it.
7
+ * 2. Drive lifecycle correctness in the core via adversarial integration tests.
8
+ * 3. Provide a drop-in player for users who don't want to wire `createPlayer()`
9
+ * themselves.
10
+ *
11
+ * It is **not** a player UI framework. See `docs/dev/WEB_COMPONENT_SPEC.md`
12
+ * for the full spec, lifecycle invariants, and edge case list.
13
+ *
14
+ * Phase A scope (this file): lifecycle scaffold only — `src` / `source` /
15
+ * `currentTime` / `play` / `pause` / `load` / `destroy` / events. No built-in
16
+ * controls. Shadow DOM contains a single `<video part="video">`.
17
+ */
18
+
19
+ import { createPlayer, type UnifiedPlayer } from "../player.js";
20
+ import type {
21
+ MediaInput,
22
+ StrategyName,
23
+ StrategyClass,
24
+ AudioTrackInfo,
25
+ SubtitleTrackInfo,
26
+ DiagnosticsSnapshot,
27
+ } from "../types.js";
28
+
29
+ /** Strategy preference passed via the `preferstrategy` attribute. */
30
+ type PreferredStrategy = "auto" | StrategyName;
31
+
32
+ const PREFERRED_STRATEGY_VALUES = new Set<PreferredStrategy>([
33
+ "auto",
34
+ "native",
35
+ "remux",
36
+ "hybrid",
37
+ "fallback",
38
+ ]);
39
+
40
+ /**
41
+ * `HTMLElement` is a browser-only global. SSR frameworks (Next.js, Astro,
42
+ * Remix, etc.) commonly import library modules on the server to extract
43
+ * types or do tree-shaking, even if the user only ends up using them in
44
+ * the browser. If we extended `HTMLElement` directly, the `class extends`
45
+ * expression would be evaluated at module load time and crash in Node.
46
+ *
47
+ * The fix: in non-browser environments, fall back to an empty stub class.
48
+ * The element is never *constructed* server-side (the registration in
49
+ * `element.ts` is guarded by `typeof customElements !== "undefined"`), so
50
+ * the stub is never instantiated — it just lets the class declaration
51
+ * evaluate cleanly so the module can be imported anywhere.
52
+ */
53
+ const HTMLElementCtor: typeof HTMLElement =
54
+ typeof HTMLElement !== "undefined"
55
+ ? HTMLElement
56
+ : (class {} as unknown as typeof HTMLElement);
57
+
58
+ /**
59
+ * Custom element. Lifecycle correctness is enforced via a monotonically
60
+ * increasing `_bootstrapId`: every async bootstrap captures the ID at start
61
+ * and discards itself if the ID has changed by the time it resolves. This
62
+ * single pattern handles disconnect-during-bootstrap, rapid src reassignment,
63
+ * bootstrap races, and destroy-during-bootstrap.
64
+ */
65
+ export class AvbridgePlayerElement extends HTMLElementCtor {
66
+ static readonly observedAttributes = [
67
+ "src",
68
+ "autoplay",
69
+ "muted",
70
+ "loop",
71
+ "preload",
72
+ "diagnostics",
73
+ "preferstrategy",
74
+ ];
75
+
76
+ // ── Internal state ─────────────────────────────────────────────────────
77
+
78
+ /** The shadow DOM `<video>` element that strategies render into. */
79
+ private _videoEl!: HTMLVideoElement;
80
+
81
+ /** Active player session, if any. Cleared on teardown. */
82
+ private _player: UnifiedPlayer | null = null;
83
+
84
+ /**
85
+ * Monotonic counter incremented on every (re)bootstrap. Async bootstrap
86
+ * work captures the current ID; if it doesn't match by the time the work
87
+ * resolves, the work is discarded.
88
+ */
89
+ private _bootstrapId = 0;
90
+
91
+ /** True after destroy() — element is permanently unusable. */
92
+ private _destroyed = false;
93
+
94
+ /** Internal source state. Either string-form (src) OR rich (source). */
95
+ private _src: string | null = null;
96
+ private _source: MediaInput | null = null;
97
+
98
+ /**
99
+ * Set when the `source` property setter is in the middle of clearing the
100
+ * `src` attribute as part of mutual exclusion. The attributeChangedCallback
101
+ * checks this flag and skips its normal "clear source" side effect, which
102
+ * would otherwise wipe the value we just set.
103
+ */
104
+ private _suppressSrcAttrCallback = false;
105
+
106
+ /** Last-known runtime state surfaced via getters. */
107
+ private _strategy: StrategyName | null = null;
108
+ private _strategyClass: StrategyClass | null = null;
109
+ private _audioTracks: AudioTrackInfo[] = [];
110
+ private _subtitleTracks: SubtitleTrackInfo[] = [];
111
+
112
+ /** Strategy preference (does not currently affect routing — reserved). */
113
+ private _preferredStrategy: PreferredStrategy = "auto";
114
+
115
+ /** Set if currentTime was assigned before the player was ready. */
116
+ private _pendingSeek: number | null = null;
117
+ /** Set if play() was called before the player was ready. */
118
+ private _pendingPlay = false;
119
+
120
+ // ── Construction & lifecycle ───────────────────────────────────────────
121
+
122
+ constructor() {
123
+ super();
124
+ const root = this.attachShadow({ mode: "open" });
125
+ this._videoEl = document.createElement("video");
126
+ this._videoEl.setAttribute("part", "video");
127
+ this._videoEl.style.cssText = "width:100%;height:100%;display:block;background:#000;";
128
+ this._videoEl.playsInline = true;
129
+ root.appendChild(this._videoEl);
130
+
131
+ // Forward the underlying <video>'s `progress` event so consumers can
132
+ // observe buffered-range updates without reaching into the shadow DOM.
133
+ // This works for native + remux (real video element with buffered
134
+ // ranges) and is a no-op for hybrid/fallback (canvas-rendered, no
135
+ // buffered ranges yet).
136
+ this._videoEl.addEventListener("progress", () => {
137
+ if (this._destroyed) return;
138
+ this._dispatch("progress", { buffered: this._videoEl.buffered });
139
+ });
140
+ }
141
+
142
+ connectedCallback(): void {
143
+ if (this._destroyed) return;
144
+ // Connection is the trigger for bootstrap. If we have a pending source
145
+ // (set before connect), kick off bootstrap now.
146
+ const source = this._activeSource();
147
+ if (source != null) {
148
+ void this._bootstrap(source);
149
+ }
150
+ }
151
+
152
+ disconnectedCallback(): void {
153
+ if (this._destroyed) return;
154
+ // Bump the bootstrap token so any in-flight async work is invalidated
155
+ // before we tear down. _teardown() also bumps but we want the bump to
156
+ // happen synchronously here so any awaited promise that resolves
157
+ // between `disconnect` and `_teardown` sees the new ID.
158
+ this._bootstrapId++;
159
+ void this._teardown();
160
+ }
161
+
162
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void {
163
+ if (this._destroyed) return;
164
+ switch (name) {
165
+ case "src":
166
+ if (this._suppressSrcAttrCallback) break;
167
+ this._setSrcInternal(newValue);
168
+ break;
169
+ case "autoplay":
170
+ case "muted":
171
+ case "loop":
172
+ // Reflect onto the underlying <video> element.
173
+ if (newValue == null) this._videoEl.removeAttribute(name);
174
+ else this._videoEl.setAttribute(name, newValue);
175
+ break;
176
+ case "preload":
177
+ if (newValue == null) this._videoEl.removeAttribute("preload");
178
+ else this._videoEl.setAttribute("preload", newValue);
179
+ break;
180
+ case "diagnostics":
181
+ // Phase A: no UI. Property is observable for users via getDiagnostics().
182
+ break;
183
+ case "preferstrategy":
184
+ if (newValue && PREFERRED_STRATEGY_VALUES.has(newValue as PreferredStrategy)) {
185
+ this._preferredStrategy = newValue as PreferredStrategy;
186
+ } else {
187
+ this._preferredStrategy = "auto";
188
+ }
189
+ break;
190
+ }
191
+ }
192
+
193
+ // ── Source handling ────────────────────────────────────────────────────
194
+
195
+ /** Returns the currently-active source (src or source), whichever is set. */
196
+ private _activeSource(): MediaInput | null {
197
+ if (this._source != null) return this._source;
198
+ if (this._src != null) return this._src;
199
+ return null;
200
+ }
201
+
202
+ /** Internal src setter — separate from the property setter so the
203
+ * attributeChangedCallback can use it without re-entering reflection. */
204
+ private _setSrcInternal(value: string | null): void {
205
+ // Same-value reassignment: no-op (#11 in the lifecycle list).
206
+ if (value === this._src && this._source == null) return;
207
+ this._src = value;
208
+ this._source = null;
209
+ this._onSourceChanged();
210
+ }
211
+
212
+ /** Called whenever the active source changes (src or source). */
213
+ private _onSourceChanged(): void {
214
+ if (this._destroyed) return;
215
+ const source = this._activeSource();
216
+ if (source == null) {
217
+ // Null transition: tear down and stay idle.
218
+ this._bootstrapId++;
219
+ void this._teardown();
220
+ return;
221
+ }
222
+ // Only bootstrap if we're connected to the DOM.
223
+ if (this.isConnected) {
224
+ void this._bootstrap(source);
225
+ }
226
+ }
227
+
228
+ // ── Bootstrap (the only place a UnifiedPlayer is created) ──────────────
229
+
230
+ private async _bootstrap(source: MediaInput): Promise<void> {
231
+ if (this._destroyed) return;
232
+ const id = ++this._bootstrapId;
233
+
234
+ // Tear down any existing player before starting a new one. Pass the
235
+ // bootstrap id we just claimed so teardown doesn't bump it again
236
+ // (which would invalidate ourselves).
237
+ await this._teardown(id);
238
+ if (id !== this._bootstrapId || this._destroyed) return;
239
+
240
+ this._dispatch("loadstart", {});
241
+
242
+ let player: UnifiedPlayer;
243
+ try {
244
+ player = await createPlayer({
245
+ source,
246
+ target: this._videoEl,
247
+ });
248
+ } catch (err) {
249
+ // Stale or destroyed — silently abandon.
250
+ if (id !== this._bootstrapId || this._destroyed) return;
251
+ this._dispatchError(err);
252
+ return;
253
+ }
254
+
255
+ // Race check: if anything happened during the await above, bail.
256
+ if (id !== this._bootstrapId || this._destroyed || !this.isConnected) {
257
+ try { await player.destroy(); } catch { /* ignore */ }
258
+ return;
259
+ }
260
+
261
+ this._player = player;
262
+
263
+ // Wire events. The unsubscribe handles are not stored individually
264
+ // because destroy() will tear down the whole session anyway.
265
+ player.on("strategy", ({ strategy, reason }) => {
266
+ // strategy event fires on initial classification AND any escalation.
267
+ const cls = player.getDiagnostics().strategyClass;
268
+ this._strategy = strategy;
269
+ this._strategyClass = cls === "pending" ? null : cls;
270
+ this._dispatch("strategychange", {
271
+ strategy,
272
+ strategyClass: this._strategyClass,
273
+ reason,
274
+ diagnostics: player.getDiagnostics(),
275
+ });
276
+ });
277
+
278
+ player.on("strategychange", ({ from, to, reason, currentTime }) => {
279
+ this._dispatch("strategychange", {
280
+ from,
281
+ strategy: to,
282
+ strategyClass: player.getDiagnostics().strategyClass === "pending" ? null : player.getDiagnostics().strategyClass,
283
+ reason,
284
+ currentTime,
285
+ diagnostics: player.getDiagnostics(),
286
+ });
287
+ });
288
+
289
+ player.on("tracks", ({ video: _v, audio, subtitle }) => {
290
+ this._audioTracks = audio;
291
+ this._subtitleTracks = subtitle;
292
+ this._dispatch("trackschange", {
293
+ audioTracks: audio,
294
+ subtitleTracks: subtitle,
295
+ });
296
+ });
297
+
298
+ player.on("error", (err: Error) => {
299
+ this._dispatchError(err);
300
+ });
301
+
302
+ player.on("timeupdate", ({ currentTime }) => {
303
+ this._dispatch("timeupdate", { currentTime });
304
+ });
305
+
306
+ player.on("ended", () => {
307
+ this._dispatch("ended", {});
308
+ });
309
+
310
+ player.on("ready", () => {
311
+ this._dispatch("ready", { diagnostics: player.getDiagnostics() });
312
+ // Apply any pending seek that was set before the player existed.
313
+ if (this._pendingSeek != null) {
314
+ const t = this._pendingSeek;
315
+ this._pendingSeek = null;
316
+ void player.seek(t).catch(() => { /* ignore */ });
317
+ }
318
+ // Honor any pending play() that was queued before bootstrap finished.
319
+ if (this._pendingPlay) {
320
+ this._pendingPlay = false;
321
+ void player.play().catch(() => { /* ignore — autoplay may be blocked */ });
322
+ } else if (this.autoplay) {
323
+ void player.play().catch(() => { /* ignore */ });
324
+ }
325
+ });
326
+ }
327
+
328
+ /**
329
+ * Tear down the active player and reset runtime state. Idempotent.
330
+ * If `currentBootstrapId` is provided, the bootstrap counter is NOT
331
+ * incremented (used by `_bootstrap()` to avoid invalidating itself).
332
+ */
333
+ private async _teardown(currentBootstrapId?: number): Promise<void> {
334
+ if (currentBootstrapId == null) {
335
+ // External callers (disconnect, destroy, source change) should bump
336
+ // the counter so any in-flight bootstrap is invalidated. The internal
337
+ // _bootstrap() call passes its own ID and we skip the bump.
338
+ this._bootstrapId++;
339
+ }
340
+ const player = this._player;
341
+ this._player = null;
342
+ this._strategy = null;
343
+ this._strategyClass = null;
344
+ this._audioTracks = [];
345
+ this._subtitleTracks = [];
346
+ if (player) {
347
+ try { await player.destroy(); } catch { /* ignore */ }
348
+ }
349
+ }
350
+
351
+ // ── Public properties ──────────────────────────────────────────────────
352
+
353
+ get src(): string | null {
354
+ return this._src;
355
+ }
356
+
357
+ set src(value: string | null) {
358
+ if (value == null) {
359
+ this.removeAttribute("src");
360
+ } else {
361
+ this.setAttribute("src", value);
362
+ }
363
+ // attributeChangedCallback handles the rest.
364
+ }
365
+
366
+ get source(): MediaInput | null {
367
+ return this._source;
368
+ }
369
+
370
+ set source(value: MediaInput | null) {
371
+ // Same-value reassignment for rich values is identity-based.
372
+ if (value === this._source && this._src == null) return;
373
+ this._source = value;
374
+ if (value != null) {
375
+ // Setting source clears src. Suppress the attribute callback so
376
+ // removing the src attribute doesn't wipe the source we just set.
377
+ this._src = null;
378
+ if (this.hasAttribute("src")) {
379
+ this._suppressSrcAttrCallback = true;
380
+ try {
381
+ this.removeAttribute("src");
382
+ } finally {
383
+ this._suppressSrcAttrCallback = false;
384
+ }
385
+ }
386
+ }
387
+ this._onSourceChanged();
388
+ }
389
+
390
+ get autoplay(): boolean {
391
+ return this.hasAttribute("autoplay");
392
+ }
393
+
394
+ set autoplay(value: boolean) {
395
+ if (value) this.setAttribute("autoplay", "");
396
+ else this.removeAttribute("autoplay");
397
+ }
398
+
399
+ get muted(): boolean {
400
+ return this.hasAttribute("muted");
401
+ }
402
+
403
+ set muted(value: boolean) {
404
+ if (value) this.setAttribute("muted", "");
405
+ else this.removeAttribute("muted");
406
+ }
407
+
408
+ get loop(): boolean {
409
+ return this.hasAttribute("loop");
410
+ }
411
+
412
+ set loop(value: boolean) {
413
+ if (value) this.setAttribute("loop", "");
414
+ else this.removeAttribute("loop");
415
+ }
416
+
417
+ get preload(): "none" | "metadata" | "auto" {
418
+ const v = this.getAttribute("preload");
419
+ return v === "none" || v === "metadata" || v === "auto" ? v : "auto";
420
+ }
421
+
422
+ set preload(value: "none" | "metadata" | "auto") {
423
+ this.setAttribute("preload", value);
424
+ }
425
+
426
+ get diagnostics(): boolean {
427
+ return this.hasAttribute("diagnostics");
428
+ }
429
+
430
+ set diagnostics(value: boolean) {
431
+ if (value) this.setAttribute("diagnostics", "");
432
+ else this.removeAttribute("diagnostics");
433
+ }
434
+
435
+ get preferredStrategy(): PreferredStrategy {
436
+ return this._preferredStrategy;
437
+ }
438
+
439
+ set preferredStrategy(value: PreferredStrategy) {
440
+ if (PREFERRED_STRATEGY_VALUES.has(value)) {
441
+ this.setAttribute("preferstrategy", value);
442
+ }
443
+ }
444
+
445
+ get currentTime(): number {
446
+ return this._player?.getCurrentTime() ?? 0;
447
+ }
448
+
449
+ set currentTime(value: number) {
450
+ if (this._player) {
451
+ void this._player.seek(value).catch(() => { /* ignore */ });
452
+ } else {
453
+ // Defer to the next bootstrap. The `ready` handler applies it.
454
+ this._pendingSeek = value;
455
+ }
456
+ }
457
+
458
+ get duration(): number {
459
+ return this._player?.getDuration() ?? NaN;
460
+ }
461
+
462
+ get paused(): boolean {
463
+ return this._videoEl.paused;
464
+ }
465
+
466
+ get ended(): boolean {
467
+ return this._videoEl.ended;
468
+ }
469
+
470
+ get readyState(): number {
471
+ return this._videoEl.readyState;
472
+ }
473
+
474
+ /**
475
+ * Buffered time ranges for the active source. Mirrors the standard
476
+ * `<video>.buffered` `TimeRanges` API. For the native and remux strategies
477
+ * this reflects the underlying SourceBuffer / progressive download state.
478
+ * For the hybrid and fallback (canvas-rendered) strategies it currently
479
+ * returns an empty TimeRanges; v1.1 will synthesize a coarse range from
480
+ * the decoder's read position.
481
+ */
482
+ get buffered(): TimeRanges {
483
+ return this._videoEl.buffered;
484
+ }
485
+
486
+ get strategy(): StrategyName | null {
487
+ return this._strategy;
488
+ }
489
+
490
+ get strategyClass(): StrategyClass | null {
491
+ return this._strategyClass;
492
+ }
493
+
494
+ get player(): UnifiedPlayer | null {
495
+ return this._player;
496
+ }
497
+
498
+ get audioTracks(): AudioTrackInfo[] {
499
+ return this._audioTracks;
500
+ }
501
+
502
+ get subtitleTracks(): SubtitleTrackInfo[] {
503
+ return this._subtitleTracks;
504
+ }
505
+
506
+ // ── Public methods ─────────────────────────────────────────────────────
507
+
508
+ /** Force a (re-)bootstrap if a source is currently set. */
509
+ async load(): Promise<void> {
510
+ if (this._destroyed) return;
511
+ const source = this._activeSource();
512
+ if (source == null) return;
513
+ await this._bootstrap(source);
514
+ }
515
+
516
+ /**
517
+ * Begin or resume playback. If the player isn't ready yet, the call is
518
+ * queued and applied once `ready` fires.
519
+ */
520
+ async play(): Promise<void> {
521
+ if (this._destroyed) return;
522
+ if (this._player) {
523
+ await this._player.play();
524
+ } else {
525
+ this._pendingPlay = true;
526
+ }
527
+ }
528
+
529
+ pause(): void {
530
+ if (this._destroyed) return;
531
+ this._pendingPlay = false;
532
+ this._player?.pause();
533
+ }
534
+
535
+ /**
536
+ * Tear down the element permanently. After destroy(), the element ignores
537
+ * all method calls and attribute changes.
538
+ */
539
+ async destroy(): Promise<void> {
540
+ if (this._destroyed) return;
541
+ this._destroyed = true;
542
+ await this._teardown();
543
+ this._dispatch("destroy", {});
544
+ }
545
+
546
+ async setAudioTrack(id: number): Promise<void> {
547
+ if (this._destroyed || !this._player) return;
548
+ await this._player.setAudioTrack(id);
549
+ }
550
+
551
+ async setSubtitleTrack(id: number | null): Promise<void> {
552
+ if (this._destroyed || !this._player) return;
553
+ await this._player.setSubtitleTrack(id);
554
+ }
555
+
556
+ getDiagnostics(): DiagnosticsSnapshot | null {
557
+ return this._player?.getDiagnostics() ?? null;
558
+ }
559
+
560
+ // ── Event helpers ──────────────────────────────────────────────────────
561
+
562
+ private _dispatch<T>(name: string, detail: T): void {
563
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: false }));
564
+ }
565
+
566
+ private _dispatchError(err: unknown): void {
567
+ const error = err instanceof Error ? err : new Error(String(err));
568
+ this._dispatch("error", { error, diagnostics: this._player?.getDiagnostics() ?? null });
569
+ }
570
+ }
571
+
572
+ declare global {
573
+ interface HTMLElementTagNameMap {
574
+ "avbridge-player": AvbridgePlayerElement;
575
+ }
576
+ }
package/src/element.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Subpath entry: `import "avbridge/element"` registers the
3
+ * `<avbridge-player>` custom element.
4
+ *
5
+ * This is a separate entry point from the core (`avbridge`) so that consumers
6
+ * who only want the engine don't pay for the element code, and consumers who
7
+ * want both pay for the element code exactly once.
8
+ *
9
+ * The registration is guarded so re-importing this module (e.g. via HMR or
10
+ * multiple bundles) does not throw a "name already defined" error.
11
+ */
12
+
13
+ import { AvbridgePlayerElement } from "./element/avbridge-player.js";
14
+
15
+ export { AvbridgePlayerElement };
16
+
17
+ if (typeof customElements !== "undefined" && !customElements.get("avbridge-player")) {
18
+ customElements.define("avbridge-player", AvbridgePlayerElement);
19
+ }
package/src/events.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Tiny strongly-typed event emitter. We avoid pulling in eventemitter3 / mitt
3
+ * because we only need a handful of methods and want zero deps.
4
+ *
5
+ * Supports "sticky" events via {@link TypedEmitter.emitSticky}: the last value
6
+ * for that event is remembered, and any future `on()` subscriber receives it
7
+ * immediately. This is the right pattern for one-shot state-snapshot events
8
+ * like "strategy chosen" or "player ready" — callers that subscribe after the
9
+ * event has already fired still need to react to it.
10
+ */
11
+
12
+ export type Listener<T> = (payload: T) => void;
13
+
14
+ export class TypedEmitter<EventMap> {
15
+ private listeners: { [K in keyof EventMap]?: Set<Listener<EventMap[K]>> } = {};
16
+ private sticky: { [K in keyof EventMap]?: EventMap[K] } = {};
17
+
18
+ on<K extends keyof EventMap>(event: K, fn: Listener<EventMap[K]>): () => void {
19
+ let set = this.listeners[event];
20
+ if (!set) {
21
+ set = new Set();
22
+ this.listeners[event] = set;
23
+ }
24
+ set.add(fn);
25
+
26
+ // Replay any sticky value that's already been emitted for this event.
27
+ if (Object.prototype.hasOwnProperty.call(this.sticky, event)) {
28
+ try {
29
+ fn(this.sticky[event] as EventMap[K]);
30
+ } catch (err) {
31
+ // eslint-disable-next-line no-console
32
+ console.error("[avbridge] listener threw replaying sticky value:", err);
33
+ }
34
+ }
35
+
36
+ return () => this.off(event, fn);
37
+ }
38
+
39
+ off<K extends keyof EventMap>(event: K, fn: Listener<EventMap[K]>): void {
40
+ this.listeners[event]?.delete(fn);
41
+ }
42
+
43
+ emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
44
+ const set = this.listeners[event];
45
+ if (!set) return;
46
+ // Snapshot so listeners can unsubscribe themselves.
47
+ for (const fn of [...set]) {
48
+ try {
49
+ fn(payload);
50
+ } catch (err) {
51
+ // Don't let one bad listener break the others.
52
+ // eslint-disable-next-line no-console
53
+ console.error("[avbridge] listener threw:", err);
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Like {@link emit} but also remembers the value so future subscribers
60
+ * receive it on `on()`. Use for one-shot state-snapshot events.
61
+ */
62
+ emitSticky<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
63
+ this.sticky[event] = payload;
64
+ this.emit(event, payload);
65
+ }
66
+
67
+ removeAll(): void {
68
+ this.listeners = {};
69
+ this.sticky = {};
70
+ }
71
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * avbridge — Universal Browser Media Player.
3
+ *
4
+ * Public entry point. Consumers should only import from `"avbridge"`; everything
5
+ * else (probe, classify, strategies) is internal and subject to change.
6
+ */
7
+
8
+ export { createPlayer, UnifiedPlayer } from "./player.js";
9
+ export type {
10
+ CreatePlayerOptions,
11
+ MediaContext,
12
+ MediaInput,
13
+ MediaInput as MediaSource,
14
+ Classification,
15
+ StrategyName,
16
+ StrategyClass,
17
+ PlaybackSession,
18
+ Plugin,
19
+ DiagnosticsSnapshot,
20
+ PlayerEventMap,
21
+ PlayerEventName,
22
+ VideoTrackInfo,
23
+ AudioTrackInfo,
24
+ SubtitleTrackInfo,
25
+ ContainerKind,
26
+ VideoCodec,
27
+ AudioCodec,
28
+ OutputFormat,
29
+ ConvertOptions,
30
+ ConvertResult,
31
+ ProgressInfo,
32
+ TranscodeOptions,
33
+ TranscodeQuality,
34
+ OutputVideoCodec,
35
+ OutputAudioCodec,
36
+ HardwareAccelerationHint,
37
+ } from "./types.js";
38
+
39
+ export { classify } from "./classify/index.js";
40
+ export { probe } from "./probe/index.js";
41
+ export { remux, transcode } from "./convert/index.js";
42
+ export { srtToVtt } from "./subtitles/srt.js";