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.
- package/CHANGELOG.md +114 -0
- package/dist/{chunk-6UUT4BEA.cjs → chunk-2IJ66NTD.cjs} +13 -20
- package/dist/chunk-2IJ66NTD.cjs.map +1 -0
- package/dist/{chunk-XKPSTC34.cjs → chunk-2XW2O3YI.cjs} +5 -20
- package/dist/chunk-2XW2O3YI.cjs.map +1 -0
- package/dist/chunk-5KVLE6YI.js +167 -0
- package/dist/chunk-5KVLE6YI.js.map +1 -0
- package/dist/{chunk-7RGG6ME7.cjs → chunk-6SOFJV44.cjs} +422 -688
- package/dist/chunk-6SOFJV44.cjs.map +1 -0
- package/dist/{chunk-2PGRFCWB.js → chunk-CPJLFFCC.js} +8 -18
- package/dist/chunk-CPJLFFCC.js.map +1 -0
- package/dist/chunk-CPZ7PXAM.cjs +240 -0
- package/dist/chunk-CPZ7PXAM.cjs.map +1 -0
- package/dist/{chunk-QQXBPW72.js → chunk-E76AMWI4.js} +4 -18
- package/dist/chunk-E76AMWI4.js.map +1 -0
- package/dist/chunk-LUFA47FP.js +19 -0
- package/dist/chunk-LUFA47FP.js.map +1 -0
- package/dist/{chunk-NV7ILLWH.js → chunk-OGYHFY6K.js} +404 -665
- package/dist/chunk-OGYHFY6K.js.map +1 -0
- package/dist/chunk-Q2VUO52Z.cjs +374 -0
- package/dist/chunk-Q2VUO52Z.cjs.map +1 -0
- package/dist/chunk-QDJLQR53.cjs +22 -0
- package/dist/chunk-QDJLQR53.cjs.map +1 -0
- package/dist/chunk-S4WAZC2T.cjs +173 -0
- package/dist/chunk-S4WAZC2T.cjs.map +1 -0
- package/dist/chunk-SMH6IOP2.js +368 -0
- package/dist/chunk-SMH6IOP2.js.map +1 -0
- package/dist/chunk-SR3MPV4D.js +237 -0
- package/dist/chunk-SR3MPV4D.js.map +1 -0
- package/dist/chunk-X2K3GIWE.js +235 -0
- package/dist/chunk-X2K3GIWE.js.map +1 -0
- package/dist/chunk-ZCUXHW55.cjs +242 -0
- package/dist/chunk-ZCUXHW55.cjs.map +1 -0
- package/dist/element-browser.js +883 -492
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +88 -6
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +51 -1
- package/dist/element.d.ts +51 -1
- package/dist/element.js +87 -5
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +523 -393
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +494 -366
- package/dist/index.js.map +1 -1
- package/dist/libav-demux-H2GS46GH.cjs +27 -0
- package/dist/libav-demux-H2GS46GH.cjs.map +1 -0
- package/dist/libav-demux-OWZ4T2YW.js +6 -0
- package/dist/libav-demux-OWZ4T2YW.js.map +1 -0
- package/dist/{libav-import-GST2AMPL.cjs → libav-import-2ZVKV2E7.cjs} +2 -2
- package/dist/{libav-import-GST2AMPL.cjs.map → libav-import-2ZVKV2E7.cjs.map} +1 -1
- package/dist/{libav-import-2JURFHEW.js → libav-import-6MGLCXVQ.js} +2 -2
- package/dist/{libav-import-2JURFHEW.js.map → libav-import-6MGLCXVQ.js.map} +1 -1
- package/dist/{player-B6WB74RD.d.ts → player-DGXeCNfD.d.cts} +41 -1
- package/dist/{player-B6WB74RD.d.cts → player-DGXeCNfD.d.ts} +41 -1
- package/dist/player.cjs +731 -472
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +229 -120
- package/dist/player.d.ts +229 -120
- package/dist/player.js +710 -451
- package/dist/player.js.map +1 -1
- package/dist/remux-OBSMIENG.cjs +35 -0
- package/dist/remux-OBSMIENG.cjs.map +1 -0
- package/dist/remux-WBYIZBBX.js +10 -0
- package/dist/remux-WBYIZBBX.js.map +1 -0
- package/dist/source-4TZ6KMNV.js +4 -0
- package/dist/{source-F656KYYV.js.map → source-4TZ6KMNV.js.map} +1 -1
- package/dist/source-7YLO6E7X.cjs +29 -0
- package/dist/{source-73CAH6HW.cjs.map → source-7YLO6E7X.cjs.map} +1 -1
- package/dist/source-MTX5ELUZ.js +4 -0
- package/dist/{source-QJR3OHTW.js.map → source-MTX5ELUZ.js.map} +1 -1
- package/dist/source-VFLXLOCN.cjs +29 -0
- package/dist/{source-VB74JQ7Z.cjs.map → source-VFLXLOCN.cjs.map} +1 -1
- package/dist/subtitles-4T74JRGT.js +4 -0
- package/dist/subtitles-4T74JRGT.js.map +1 -0
- package/dist/subtitles-QUH4LPI4.cjs +29 -0
- package/dist/subtitles-QUH4LPI4.cjs.map +1 -0
- package/package.json +1 -1
- package/src/convert/remux.ts +1 -35
- package/src/convert/transcode-libav.ts +691 -0
- package/src/convert/transcode.ts +12 -4
- package/src/element/avbridge-player.ts +100 -0
- package/src/element/avbridge-video.ts +140 -3
- package/src/element/player-styles.ts +12 -0
- package/src/errors.ts +6 -0
- package/src/player.ts +15 -16
- package/src/strategies/fallback/decoder.ts +96 -173
- package/src/strategies/fallback/index.ts +46 -2
- package/src/strategies/fallback/libav-import.ts +9 -1
- package/src/strategies/fallback/video-renderer.ts +107 -0
- package/src/strategies/hybrid/decoder.ts +88 -180
- package/src/strategies/hybrid/index.ts +35 -2
- package/src/strategies/native.ts +6 -3
- package/src/strategies/remux/index.ts +14 -2
- package/src/strategies/remux/pipeline.ts +72 -12
- package/src/subtitles/render.ts +8 -0
- package/src/types.ts +32 -0
- package/src/util/libav-demux.ts +405 -0
- package/src/util/time-ranges.ts +40 -0
- package/dist/chunk-2PGRFCWB.js.map +0 -1
- package/dist/chunk-6UUT4BEA.cjs.map +0 -1
- package/dist/chunk-7RGG6ME7.cjs.map +0 -1
- package/dist/chunk-NV7ILLWH.js.map +0 -1
- package/dist/chunk-QQXBPW72.js.map +0 -1
- package/dist/chunk-XKPSTC34.cjs.map +0 -1
- package/dist/source-73CAH6HW.cjs +0 -28
- package/dist/source-F656KYYV.js +0 -3
- package/dist/source-QJR3OHTW.js +0 -3
- package/dist/source-VB74JQ7Z.cjs +0 -28
package/src/convert/transcode.ts
CHANGED
|
@@ -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
|
|
59
|
-
|
|
60
|
-
`transcode
|
|
61
|
-
`
|
|
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
|
|
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
|
-
|
|
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
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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,
|