avbridge 2.3.0 → 2.6.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 (111) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/dist/{chunk-6UUT4BEA.cjs → chunk-2IJ66NTD.cjs} +13 -20
  3. package/dist/chunk-2IJ66NTD.cjs.map +1 -0
  4. package/dist/{chunk-XKPSTC34.cjs → chunk-2XW2O3YI.cjs} +5 -20
  5. package/dist/chunk-2XW2O3YI.cjs.map +1 -0
  6. package/dist/chunk-5KVLE6YI.js +167 -0
  7. package/dist/chunk-5KVLE6YI.js.map +1 -0
  8. package/dist/{chunk-7RGG6ME7.cjs → chunk-6SOFJV44.cjs} +422 -688
  9. package/dist/chunk-6SOFJV44.cjs.map +1 -0
  10. package/dist/{chunk-2PGRFCWB.js → chunk-CPJLFFCC.js} +8 -18
  11. package/dist/chunk-CPJLFFCC.js.map +1 -0
  12. package/dist/chunk-CPZ7PXAM.cjs +240 -0
  13. package/dist/chunk-CPZ7PXAM.cjs.map +1 -0
  14. package/dist/{chunk-QQXBPW72.js → chunk-E76AMWI4.js} +4 -18
  15. package/dist/chunk-E76AMWI4.js.map +1 -0
  16. package/dist/chunk-LUFA47FP.js +19 -0
  17. package/dist/chunk-LUFA47FP.js.map +1 -0
  18. package/dist/{chunk-NV7ILLWH.js → chunk-OGYHFY6K.js} +404 -665
  19. package/dist/chunk-OGYHFY6K.js.map +1 -0
  20. package/dist/chunk-Q2VUO52Z.cjs +374 -0
  21. package/dist/chunk-Q2VUO52Z.cjs.map +1 -0
  22. package/dist/chunk-QDJLQR53.cjs +22 -0
  23. package/dist/chunk-QDJLQR53.cjs.map +1 -0
  24. package/dist/chunk-S4WAZC2T.cjs +173 -0
  25. package/dist/chunk-S4WAZC2T.cjs.map +1 -0
  26. package/dist/chunk-SMH6IOP2.js +368 -0
  27. package/dist/chunk-SMH6IOP2.js.map +1 -0
  28. package/dist/chunk-SR3MPV4D.js +237 -0
  29. package/dist/chunk-SR3MPV4D.js.map +1 -0
  30. package/dist/chunk-X2K3GIWE.js +235 -0
  31. package/dist/chunk-X2K3GIWE.js.map +1 -0
  32. package/dist/chunk-ZCUXHW55.cjs +242 -0
  33. package/dist/chunk-ZCUXHW55.cjs.map +1 -0
  34. package/dist/element-browser.js +883 -492
  35. package/dist/element-browser.js.map +1 -1
  36. package/dist/element.cjs +88 -6
  37. package/dist/element.cjs.map +1 -1
  38. package/dist/element.d.cts +51 -1
  39. package/dist/element.d.ts +51 -1
  40. package/dist/element.js +87 -5
  41. package/dist/element.js.map +1 -1
  42. package/dist/index.cjs +523 -393
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +2 -2
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.js +494 -366
  47. package/dist/index.js.map +1 -1
  48. package/dist/libav-demux-H2GS46GH.cjs +27 -0
  49. package/dist/libav-demux-H2GS46GH.cjs.map +1 -0
  50. package/dist/libav-demux-OWZ4T2YW.js +6 -0
  51. package/dist/libav-demux-OWZ4T2YW.js.map +1 -0
  52. package/dist/{libav-import-GST2AMPL.cjs → libav-import-2ZVKV2E7.cjs} +2 -2
  53. package/dist/{libav-import-GST2AMPL.cjs.map → libav-import-2ZVKV2E7.cjs.map} +1 -1
  54. package/dist/{libav-import-2JURFHEW.js → libav-import-6MGLCXVQ.js} +2 -2
  55. package/dist/{libav-import-2JURFHEW.js.map → libav-import-6MGLCXVQ.js.map} +1 -1
  56. package/dist/{player-B6WB74RD.d.ts → player-DGXeCNfD.d.cts} +41 -1
  57. package/dist/{player-B6WB74RD.d.cts → player-DGXeCNfD.d.ts} +41 -1
  58. package/dist/player.cjs +731 -472
  59. package/dist/player.cjs.map +1 -1
  60. package/dist/player.d.cts +229 -120
  61. package/dist/player.d.ts +229 -120
  62. package/dist/player.js +710 -451
  63. package/dist/player.js.map +1 -1
  64. package/dist/remux-OBSMIENG.cjs +35 -0
  65. package/dist/remux-OBSMIENG.cjs.map +1 -0
  66. package/dist/remux-WBYIZBBX.js +10 -0
  67. package/dist/remux-WBYIZBBX.js.map +1 -0
  68. package/dist/source-4TZ6KMNV.js +4 -0
  69. package/dist/{source-F656KYYV.js.map → source-4TZ6KMNV.js.map} +1 -1
  70. package/dist/source-7YLO6E7X.cjs +29 -0
  71. package/dist/{source-73CAH6HW.cjs.map → source-7YLO6E7X.cjs.map} +1 -1
  72. package/dist/source-MTX5ELUZ.js +4 -0
  73. package/dist/{source-QJR3OHTW.js.map → source-MTX5ELUZ.js.map} +1 -1
  74. package/dist/source-VFLXLOCN.cjs +29 -0
  75. package/dist/{source-VB74JQ7Z.cjs.map → source-VFLXLOCN.cjs.map} +1 -1
  76. package/dist/subtitles-4T74JRGT.js +4 -0
  77. package/dist/subtitles-4T74JRGT.js.map +1 -0
  78. package/dist/subtitles-QUH4LPI4.cjs +29 -0
  79. package/dist/subtitles-QUH4LPI4.cjs.map +1 -0
  80. package/package.json +1 -1
  81. package/src/convert/remux.ts +1 -35
  82. package/src/convert/transcode-libav.ts +691 -0
  83. package/src/convert/transcode.ts +12 -4
  84. package/src/element/avbridge-player.ts +100 -0
  85. package/src/element/avbridge-video.ts +140 -3
  86. package/src/element/player-styles.ts +12 -0
  87. package/src/errors.ts +6 -0
  88. package/src/player.ts +15 -16
  89. package/src/strategies/fallback/decoder.ts +96 -173
  90. package/src/strategies/fallback/index.ts +46 -2
  91. package/src/strategies/fallback/libav-import.ts +9 -1
  92. package/src/strategies/fallback/video-renderer.ts +107 -0
  93. package/src/strategies/hybrid/decoder.ts +88 -180
  94. package/src/strategies/hybrid/index.ts +35 -2
  95. package/src/strategies/native.ts +6 -3
  96. package/src/strategies/remux/index.ts +14 -2
  97. package/src/strategies/remux/pipeline.ts +72 -12
  98. package/src/subtitles/render.ts +8 -0
  99. package/src/types.ts +32 -0
  100. package/src/util/libav-demux.ts +405 -0
  101. package/src/util/time-ranges.ts +40 -0
  102. package/dist/chunk-2PGRFCWB.js.map +0 -1
  103. package/dist/chunk-6UUT4BEA.cjs.map +0 -1
  104. package/dist/chunk-7RGG6ME7.cjs.map +0 -1
  105. package/dist/chunk-NV7ILLWH.js.map +0 -1
  106. package/dist/chunk-QQXBPW72.js.map +0 -1
  107. package/dist/chunk-XKPSTC34.cjs.map +0 -1
  108. package/dist/source-73CAH6HW.cjs +0 -28
  109. package/dist/source-F656KYYV.js +0 -3
  110. package/dist/source-QJR3OHTW.js +0 -3
  111. package/dist/source-VB74JQ7Z.cjs +0 -28
@@ -16,6 +16,8 @@
16
16
  import { probe } from "../probe/index.js";
17
17
  import { buildMediabunnySourceFromInput } from "../probe/mediabunny.js";
18
18
  import { createOutputFormat, mimeForFormat, generateFilename } from "./remux.js";
19
+ import { isLibavTranscodeContainer, transcodeViaLibav } from "./transcode-libav.js";
20
+ import { AvbridgeError, ERR_CONTAINER_NOT_SUPPORTED } from "../errors.js";
19
21
  import type {
20
22
  MediaInput,
21
23
  MediaContext,
@@ -54,11 +56,17 @@ export async function transcode(
54
56
  const ctx = await probe(source);
55
57
  options.signal?.throwIfAborted();
56
58
 
59
+ // AVI/ASF/FLV → the libav-demux-backed pipeline (Phase 1: MP4 output only).
60
+ if (isLibavTranscodeContainer(ctx.container)) {
61
+ return transcodeViaLibav(ctx, options);
62
+ }
63
+
57
64
  if (!MEDIABUNNY_CONTAINERS.has(ctx.container)) {
58
- throw new Error(
59
- `Cannot transcode "${ctx.container}" sources in v1. ` +
60
- `transcode() only supports inputs that mediabunny can read (MP4, MKV, WebM, OGG, MP3, FLAC, WAV, MOV). ` +
61
- `For AVI/ASF/FLV sources, use the player's playback strategies instead.`,
65
+ throw new AvbridgeError(
66
+ ERR_CONTAINER_NOT_SUPPORTED,
67
+ `Cannot transcode "${ctx.container}" sources. ` +
68
+ `transcode() supports mediabunny-readable containers (MP4, MKV, WebM, OGG, MP3, FLAC, WAV, MOV) and legacy containers via the libav path (AVI, ASF, FLV).`,
69
+ `If this is a legacy container we don't yet support, use createPlayer() to play it. Transcode support for more containers is on the roadmap.`,
62
70
  );
63
71
  }
64
72
 
@@ -23,6 +23,7 @@ import {
23
23
  ICON_FULLSCREEN, ICON_FULLSCREEN_EXIT,
24
24
  ICON_REPLAY_10, ICON_FORWARD_10,
25
25
  } from "./player-icons.js";
26
+ import type { AvbridgeVideoElementEventMap } from "../types.js";
26
27
 
27
28
  // ── Helpers ──────────────────────────────────────────────────────────────
28
29
 
@@ -291,6 +292,38 @@ export class AvbridgePlayerElement extends HTMLElement {
291
292
  on(container, "pointerup", (e) => this._onPointerUp(e as PointerEvent));
292
293
  on(container, "pointercancel", () => this._cancelHold());
293
294
 
295
+ // Drag-and-drop file input. Drop a video file onto the player area
296
+ // and it loads + plays. Files of non-video types are rejected silently
297
+ // (no MIME sniffing — we let probe() decide). The dragover listener
298
+ // calls preventDefault so the drop event actually fires.
299
+ on(container, "dragenter", (e) => {
300
+ e.preventDefault();
301
+ const dt = (e as DragEvent).dataTransfer;
302
+ if (!dt || !Array.from(dt.types).includes("Files")) return;
303
+ (container as HTMLElement).classList.add("avp-dragover");
304
+ });
305
+ on(container, "dragover", (e) => {
306
+ e.preventDefault();
307
+ const dt = (e as DragEvent).dataTransfer;
308
+ if (dt) dt.dropEffect = "copy";
309
+ });
310
+ on(container, "dragleave", (e) => {
311
+ // dragleave fires on every child — only clear when we leave the container.
312
+ if ((e as DragEvent).target === container) {
313
+ (container as HTMLElement).classList.remove("avp-dragover");
314
+ }
315
+ });
316
+ on(container, "drop", (e) => {
317
+ e.preventDefault();
318
+ (container as HTMLElement).classList.remove("avp-dragover");
319
+ const file = (e as DragEvent).dataTransfer?.files?.[0];
320
+ if (!file) return;
321
+ // Reuse the existing source-assignment path. play() errors are
322
+ // reported via the normal error event; don't swallow here.
323
+ (this._video as unknown as { source: unknown }).source = file;
324
+ void this._video.play().catch(() => { /* error event already fired */ });
325
+ });
326
+
294
327
  // Keyboard
295
328
  on(this, "keydown", (e) => this._onKeydown(e as KeyboardEvent));
296
329
 
@@ -824,6 +857,22 @@ export class AvbridgePlayerElement extends HTMLElement {
824
857
  get strategyClass(): string | undefined { return this._video.strategyClass ?? undefined; }
825
858
  get audioTracks(): unknown[] { return this._video.audioTracks ?? []; }
826
859
  get subtitleTracks(): unknown[] { return this._video.subtitleTracks ?? []; }
860
+
861
+ /**
862
+ * External subtitle files to attach when the source loads. Forwarded
863
+ * to the inner <avbridge-video>. Takes effect on next bootstrap.
864
+ */
865
+ get subtitles(): unknown {
866
+ return (this._video as unknown as { subtitles: unknown }).subtitles;
867
+ }
868
+ set subtitles(value: unknown) {
869
+ (this._video as unknown as { subtitles: unknown }).subtitles = value;
870
+ }
871
+
872
+ /** Attach a subtitle track to the current playback without a reload. */
873
+ async addSubtitle(subtitle: { url: string; language?: string; format?: "vtt" | "srt" }): Promise<void> {
874
+ return (this._video as unknown as { addSubtitle: (s: unknown) => Promise<void> }).addSubtitle(subtitle);
875
+ }
827
876
  get player(): unknown { return this._video.player; }
828
877
  get videoElement(): HTMLVideoElement { return this._video.videoElement; }
829
878
 
@@ -842,4 +891,55 @@ export class AvbridgePlayerElement extends HTMLElement {
842
891
  async setSubtitleTrack(id: number | null): Promise<void> { return this._video.setSubtitleTrack(id); }
843
892
  getDiagnostics(): unknown { return this._video.getDiagnostics(); }
844
893
  canPlayType(mime: string): string { return this._video.canPlayType(mime); }
894
+
895
+ // ── Typed addEventListener / removeEventListener overloads ────────────
896
+ // Forwarded events from the inner <avbridge-video> preserve their
897
+ // typed CustomEvent detail. Standard HTMLMediaElement events retain
898
+ // their native typing via HTMLElementEventMap.
899
+
900
+ override addEventListener<K extends keyof AvbridgeVideoElementEventMap>(
901
+ type: K,
902
+ listener: (this: AvbridgePlayerElement, ev: AvbridgeVideoElementEventMap[K]) => unknown,
903
+ options?: boolean | AddEventListenerOptions,
904
+ ): void;
905
+ override addEventListener<K extends keyof HTMLElementEventMap>(
906
+ type: K,
907
+ listener: (this: AvbridgePlayerElement, ev: HTMLElementEventMap[K]) => unknown,
908
+ options?: boolean | AddEventListenerOptions,
909
+ ): void;
910
+ override addEventListener(
911
+ type: string,
912
+ listener: EventListenerOrEventListenerObject,
913
+ options?: boolean | AddEventListenerOptions,
914
+ ): void;
915
+ override addEventListener(
916
+ type: string,
917
+ listener: EventListenerOrEventListenerObject,
918
+ options?: boolean | AddEventListenerOptions,
919
+ ): void {
920
+ super.addEventListener(type, listener, options);
921
+ }
922
+
923
+ override removeEventListener<K extends keyof AvbridgeVideoElementEventMap>(
924
+ type: K,
925
+ listener: (this: AvbridgePlayerElement, ev: AvbridgeVideoElementEventMap[K]) => unknown,
926
+ options?: boolean | EventListenerOptions,
927
+ ): void;
928
+ override removeEventListener<K extends keyof HTMLElementEventMap>(
929
+ type: K,
930
+ listener: (this: AvbridgePlayerElement, ev: HTMLElementEventMap[K]) => unknown,
931
+ options?: boolean | EventListenerOptions,
932
+ ): void;
933
+ override removeEventListener(
934
+ type: string,
935
+ listener: EventListenerOrEventListenerObject,
936
+ options?: boolean | EventListenerOptions,
937
+ ): void;
938
+ override removeEventListener(
939
+ type: string,
940
+ listener: EventListenerOrEventListenerObject,
941
+ options?: boolean | EventListenerOptions,
942
+ ): void {
943
+ super.removeEventListener(type, listener, options);
944
+ }
845
945
  }
@@ -24,6 +24,7 @@ import type {
24
24
  AudioTrackInfo,
25
25
  SubtitleTrackInfo,
26
26
  DiagnosticsSnapshot,
27
+ AvbridgeVideoElementEventMap,
27
28
  } from "../types.js";
28
29
 
29
30
  /** Strategy preference passed via the `preferstrategy` attribute. */
@@ -145,7 +146,21 @@ export class AvbridgeVideoElement extends HTMLElementCtor {
145
146
  private _strategy: StrategyName | null = null;
146
147
  private _strategyClass: StrategyClass | null = null;
147
148
  private _audioTracks: AudioTrackInfo[] = [];
149
+ /** Subtitle tracks reported by the active UnifiedPlayer (options.subtitles
150
+ * + embedded container tracks + programmatic addSubtitle calls). */
148
151
  private _subtitleTracks: SubtitleTrackInfo[] = [];
152
+ /** Subtitle tracks derived from light-DOM `<track>` children. Maintained
153
+ * by _syncTextTracks on every mutation. Merged into the public
154
+ * `subtitleTracks` getter so the player's settings menu sees them. */
155
+ private _htmlTrackInfo: SubtitleTrackInfo[] = [];
156
+
157
+ /**
158
+ * External subtitle list forwarded to `createPlayer()` on the next
159
+ * bootstrap. Setting this after bootstrap queues it for the next
160
+ * source change; consumers that need to swap subtitles mid-playback
161
+ * should set `source` to reload.
162
+ */
163
+ private _subtitles: Array<{ url: string; language?: string; format?: "vtt" | "srt" }> | null = null;
149
164
 
150
165
  /**
151
166
  * Initial strategy preference. `"auto"` means "let the classifier decide";
@@ -298,13 +313,33 @@ export class AvbridgeVideoElement extends HTMLElementCtor {
298
313
  // Remove existing shadow tracks.
299
314
  const existing = this._videoEl.querySelectorAll("track");
300
315
  for (const t of Array.from(existing)) t.remove();
301
- // Clone every <track> light-DOM child into the shadow video.
316
+ // Clone every <track> light-DOM child into the shadow video, and
317
+ // rebuild the HTML-derived subtitle info list so the `<avbridge-player>`
318
+ // settings menu can render them alongside options-sourced tracks.
319
+ // HTML tracks are assigned high, stable IDs (10000+index) to avoid
320
+ // colliding with container-embedded ids (typically < 32).
321
+ this._htmlTrackInfo = [];
322
+ let htmlIdx = 0;
302
323
  for (const child of Array.from(this.children)) {
303
324
  if (child.tagName === "TRACK") {
304
- const clone = child.cloneNode(true) as HTMLTrackElement;
325
+ const track = child as HTMLTrackElement;
326
+ const clone = track.cloneNode(true) as HTMLTrackElement;
305
327
  this._videoEl.appendChild(clone);
328
+ const src = track.getAttribute("src") ?? undefined;
329
+ const format = src?.toLowerCase().endsWith(".srt") ? "srt" : "vtt";
330
+ this._htmlTrackInfo.push({
331
+ id: 10000 + htmlIdx,
332
+ format,
333
+ language: track.srclang || track.getAttribute("label") || undefined,
334
+ sidecarUrl: src,
335
+ });
336
+ htmlIdx++;
306
337
  }
307
338
  }
339
+ this._dispatch("trackschange", {
340
+ audioTracks: this._audioTracks,
341
+ subtitleTracks: this.subtitleTracks,
342
+ });
308
343
  }
309
344
 
310
345
  /** Internal src setter — separate from the property setter so the
@@ -358,6 +393,7 @@ export class AvbridgeVideoElement extends HTMLElementCtor {
358
393
  ...(this._preferredStrategy !== "auto"
359
394
  ? { initialStrategy: this._preferredStrategy }
360
395
  : {}),
396
+ ...(this._subtitles ? { subtitles: this._subtitles } : {}),
361
397
  });
362
398
  } catch (err) {
363
399
  // Stale or destroyed — silently abandon.
@@ -706,7 +742,58 @@ export class AvbridgeVideoElement extends HTMLElementCtor {
706
742
  }
707
743
 
708
744
  get subtitleTracks(): SubtitleTrackInfo[] {
709
- return this._subtitleTracks;
745
+ // Merge player-sourced tracks with light-DOM `<track>` children.
746
+ // Both sources coexist: options.subtitles + embedded-in-container
747
+ // tracks contribute to _subtitleTracks; HTML `<track>` children
748
+ // contribute _htmlTrackInfo with ids in the 10000+ range.
749
+ return this._htmlTrackInfo.length === 0
750
+ ? this._subtitleTracks
751
+ : [...this._subtitleTracks, ...this._htmlTrackInfo];
752
+ }
753
+
754
+ /**
755
+ * External subtitle files to attach when the source loads. Takes effect
756
+ * on the next bootstrap — set before assigning `source`, or reload via
757
+ * `load()` after changing. For dynamic post-bootstrap addition, use
758
+ * `addSubtitle()` instead.
759
+ *
760
+ * @example
761
+ * el.subtitles = [{ url: "/en.srt", format: "srt", language: "en" }];
762
+ * el.src = "/movie.mp4";
763
+ */
764
+ get subtitles(): Array<{ url: string; language?: string; format?: "vtt" | "srt" }> | null {
765
+ return this._subtitles;
766
+ }
767
+
768
+ set subtitles(value: Array<{ url: string; language?: string; format?: "vtt" | "srt" }> | null) {
769
+ this._subtitles = value;
770
+ }
771
+
772
+ /**
773
+ * Attach a subtitle track to the current playback without rebuilding
774
+ * the player. Works while the element is playing — converts SRT to
775
+ * VTT if needed, adds a `<track>` to the inner `<video>`. Canvas
776
+ * strategies pick up the new track via their textTracks watcher.
777
+ */
778
+ async addSubtitle(subtitle: { url: string; language?: string; format?: "vtt" | "srt" }): Promise<void> {
779
+ const { attachSubtitleTracks } = await import("../subtitles/index.js");
780
+ const format = subtitle.format ?? (subtitle.url.endsWith(".srt") ? "srt" : "vtt");
781
+ const track = {
782
+ id: this._subtitleTracks.length,
783
+ format,
784
+ language: subtitle.language,
785
+ sidecarUrl: subtitle.url,
786
+ };
787
+ this._subtitleTracks.push(track);
788
+ await attachSubtitleTracks(
789
+ this._videoEl,
790
+ this._subtitleTracks,
791
+ undefined,
792
+ (err, t) => {
793
+ // eslint-disable-next-line no-console
794
+ console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
795
+ },
796
+ );
710
797
  }
711
798
 
712
799
  // ── Public methods ─────────────────────────────────────────────────────
@@ -763,6 +850,56 @@ export class AvbridgeVideoElement extends HTMLElementCtor {
763
850
  return this._player?.getDiagnostics() ?? null;
764
851
  }
765
852
 
853
+ // ── Typed addEventListener / removeEventListener overloads ────────────
854
+ // Consumers using avbridge-specific events get a typed CustomEvent
855
+ // payload; standard HTMLMediaElement events retain their native types.
856
+
857
+ override addEventListener<K extends keyof AvbridgeVideoElementEventMap>(
858
+ type: K,
859
+ listener: (this: AvbridgeVideoElement, ev: AvbridgeVideoElementEventMap[K]) => unknown,
860
+ options?: boolean | AddEventListenerOptions,
861
+ ): void;
862
+ override addEventListener<K extends keyof HTMLElementEventMap>(
863
+ type: K,
864
+ listener: (this: AvbridgeVideoElement, ev: HTMLElementEventMap[K]) => unknown,
865
+ options?: boolean | AddEventListenerOptions,
866
+ ): void;
867
+ override addEventListener(
868
+ type: string,
869
+ listener: EventListenerOrEventListenerObject,
870
+ options?: boolean | AddEventListenerOptions,
871
+ ): void;
872
+ override addEventListener(
873
+ type: string,
874
+ listener: EventListenerOrEventListenerObject,
875
+ options?: boolean | AddEventListenerOptions,
876
+ ): void {
877
+ super.addEventListener(type, listener, options);
878
+ }
879
+
880
+ override removeEventListener<K extends keyof AvbridgeVideoElementEventMap>(
881
+ type: K,
882
+ listener: (this: AvbridgeVideoElement, ev: AvbridgeVideoElementEventMap[K]) => unknown,
883
+ options?: boolean | EventListenerOptions,
884
+ ): void;
885
+ override removeEventListener<K extends keyof HTMLElementEventMap>(
886
+ type: K,
887
+ listener: (this: AvbridgeVideoElement, ev: HTMLElementEventMap[K]) => unknown,
888
+ options?: boolean | EventListenerOptions,
889
+ ): void;
890
+ override removeEventListener(
891
+ type: string,
892
+ listener: EventListenerOrEventListenerObject,
893
+ options?: boolean | EventListenerOptions,
894
+ ): void;
895
+ override removeEventListener(
896
+ type: string,
897
+ listener: EventListenerOrEventListenerObject,
898
+ options?: boolean | EventListenerOptions,
899
+ ): void {
900
+ super.removeEventListener(type, listener, options);
901
+ }
902
+
766
903
  // ── Event helpers ──────────────────────────────────────────────────────
767
904
 
768
905
  private _dispatch<T>(name: string, detail: T): void {
@@ -38,6 +38,18 @@ export const PLAYER_STYLES = /* css */ `
38
38
  height: 100%;
39
39
  }
40
40
 
41
+ /* Drag-and-drop file target highlight. */
42
+ .avp.avp-dragover::after {
43
+ content: "";
44
+ position: absolute;
45
+ inset: 8px;
46
+ border: 2px dashed rgba(255, 255, 255, 0.75);
47
+ border-radius: 4px;
48
+ background: rgba(0, 0, 0, 0.25);
49
+ pointer-events: none;
50
+ z-index: 10;
51
+ }
52
+
41
53
  /* ── Center overlay ───────────────────────────────────────────────────── */
42
54
 
43
55
  .avp-overlay {
package/src/errors.ts CHANGED
@@ -45,3 +45,9 @@ export const ERR_LIBAV_NOT_REACHABLE = "ERR_AVBRIDGE_LIBAV_NOT_REACHABLE";
45
45
  // MSE
46
46
  export const ERR_MSE_NOT_SUPPORTED = "ERR_AVBRIDGE_MSE_NOT_SUPPORTED";
47
47
  export const ERR_MSE_CODEC_NOT_SUPPORTED = "ERR_AVBRIDGE_MSE_CODEC_NOT_SUPPORTED";
48
+
49
+ // Transcode
50
+ export const ERR_TRANSCODE_ABORTED = "ERR_AVBRIDGE_TRANSCODE_ABORTED";
51
+ export const ERR_TRANSCODE_UNSUPPORTED_COMBO = "ERR_AVBRIDGE_TRANSCODE_UNSUPPORTED_COMBO";
52
+ export const ERR_TRANSCODE_DECODE = "ERR_AVBRIDGE_TRANSCODE_DECODE";
53
+ export const ERR_CONTAINER_NOT_SUPPORTED = "ERR_AVBRIDGE_CONTAINER_NOT_SUPPORTED";
package/src/player.ts CHANGED
@@ -147,22 +147,21 @@ export class UnifiedPlayer {
147
147
  // Try the primary strategy, falling through the chain on failure
148
148
  await this.startSession(decision.strategy, decision.reason);
149
149
 
150
- // Apply subtitles for non-canvas strategies. Per-track failures are
151
- // caught inside attachSubtitleTracks and logged via console.warn
152
- // subtitles are not load-bearing, so a bad sidecar must not break
153
- // bootstrap.
154
- if (this.session!.strategy !== "fallback" && this.session!.strategy !== "hybrid") {
155
- await attachSubtitleTracks(
156
- this.options.target,
157
- ctx.subtitleTracks,
158
- this.subtitleResources,
159
- (err, track) => {
160
- // eslint-disable-next-line no-console
161
- console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
162
- },
163
- this.transport,
164
- );
165
- }
150
+ // Apply subtitles for all strategies. Native/remux render them via
151
+ // the inner <video>'s native text-track engine. Hybrid/fallback
152
+ // hide the <video> and render cues into the canvas overlay — see
153
+ // each session's SubtitleOverlay wiring. The <track> elements are
154
+ // attached in both cases so cues are parsed by the browser.
155
+ await attachSubtitleTracks(
156
+ this.options.target,
157
+ ctx.subtitleTracks,
158
+ this.subtitleResources,
159
+ (err, track) => {
160
+ // eslint-disable-next-line no-console
161
+ console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
162
+ },
163
+ this.transport,
164
+ );
166
165
 
167
166
  this.emitter.emitSticky("tracks", {
168
167
  video: ctx.videoTracks,