avbridge 2.1.0 → 2.1.2

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/dist/index.d.cts CHANGED
@@ -11,12 +11,16 @@ declare function classifyContext(ctx: MediaContext): Classification;
11
11
  *
12
12
  * Routing:
13
13
  * 1. Sniff the magic header. Cheap, deterministic, ignores file extensions.
14
- * 2. If the container is one mediabunny supports → mediabunny. If mediabunny
15
- * rejects, surface the real error rather than blindly falling through to
16
- * libav (which would mask the real failure with a confusing libav error).
17
- * 3. If sniffing identifies AVI/ASF/FLV (or `unknown`) libav.js, lazy-loaded.
18
- * `unknown` is included so genuinely unfamiliar files at least get a shot
19
- * at the broader libav demuxer set.
14
+ * 2. If the container is one mediabunny supports → try mediabunny first
15
+ * (fast path it's a single pass of WASM-free JS parsing). If mediabunny
16
+ * throws (e.g. an assertion on an unsupported sample entry like `mp4v`
17
+ * for MPEG-4 Part 2 in ISOBMFF, or an exotic MKV codec), fall through to
18
+ * libav.js which handles the long tail of codecs mediabunny doesn't.
19
+ * The combined-error case surfaces *both* failures so the user sees
20
+ * which path each step took.
21
+ * 3. If sniffing identifies AVI/ASF/FLV (or `unknown`) → libav.js directly.
22
+ * mediabunny can't read those containers at all, so there's no fast path
23
+ * to try.
20
24
  */
21
25
  declare function probe(source: MediaInput): Promise<MediaContext>;
22
26
 
package/dist/index.d.ts CHANGED
@@ -11,12 +11,16 @@ declare function classifyContext(ctx: MediaContext): Classification;
11
11
  *
12
12
  * Routing:
13
13
  * 1. Sniff the magic header. Cheap, deterministic, ignores file extensions.
14
- * 2. If the container is one mediabunny supports → mediabunny. If mediabunny
15
- * rejects, surface the real error rather than blindly falling through to
16
- * libav (which would mask the real failure with a confusing libav error).
17
- * 3. If sniffing identifies AVI/ASF/FLV (or `unknown`) libav.js, lazy-loaded.
18
- * `unknown` is included so genuinely unfamiliar files at least get a shot
19
- * at the broader libav demuxer set.
14
+ * 2. If the container is one mediabunny supports → try mediabunny first
15
+ * (fast path it's a single pass of WASM-free JS parsing). If mediabunny
16
+ * throws (e.g. an assertion on an unsupported sample entry like `mp4v`
17
+ * for MPEG-4 Part 2 in ISOBMFF, or an exotic MKV codec), fall through to
18
+ * libav.js which handles the long tail of codecs mediabunny doesn't.
19
+ * The combined-error case surfaces *both* failures so the user sees
20
+ * which path each step took.
21
+ * 3. If sniffing identifies AVI/ASF/FLV (or `unknown`) → libav.js directly.
22
+ * mediabunny can't read those containers at all, so there's no fast path
23
+ * to try.
20
24
  */
21
25
  declare function probe(source: MediaInput): Promise<MediaContext>;
22
26
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { probe, avbridgeVideoToMediabunny, avbridgeAudioToMediabunny, buildMediabunnySourceFromInput } from './chunk-CUQD23WO.js';
2
- export { UnifiedPlayer, classifyContext as classify, createPlayer, probe, srtToVtt } from './chunk-CUQD23WO.js';
1
+ import { probe, avbridgeVideoToMediabunny, avbridgeAudioToMediabunny, buildMediabunnySourceFromInput } from './chunk-3AUGRKPY.js';
2
+ export { UnifiedPlayer, classifyContext as classify, createPlayer, probe, srtToVtt } from './chunk-3AUGRKPY.js';
3
3
  import { normalizeSource } from './chunk-PQTZS7OA.js';
4
4
  import { prepareLibavInput } from './chunk-WD2ZNQA7.js';
5
5
  import './chunk-EJH67FXG.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "avbridge",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Play and convert arbitrary video files in the browser. Native, remux, hybrid, fallback, and transcode — one API.",
5
5
  "license": "MIT",
6
6
  "author": "Keishi Hattori",
@@ -170,11 +170,22 @@ export class AvbridgeVideoElement extends HTMLElementCtor {
170
170
  constructor() {
171
171
  super();
172
172
  const root = this.attachShadow({ mode: "open" });
173
+
174
+ // A positioned wrapper inside the shadow root. The fallback strategy
175
+ // overlays a canvas on top of the <video> via `target.parentNode` —
176
+ // that only works if the parent is a real Element with layout. Without
177
+ // this wrapper, `target.parentElement` would be null (ShadowRoot is
178
+ // not an Element) and the canvas would never attach to the DOM.
179
+ const stage = document.createElement("div");
180
+ stage.setAttribute("part", "stage");
181
+ stage.style.cssText = "position:relative;width:100%;height:100%;display:block;";
182
+ root.appendChild(stage);
183
+
173
184
  this._videoEl = document.createElement("video");
174
185
  this._videoEl.setAttribute("part", "video");
175
186
  this._videoEl.style.cssText = "width:100%;height:100%;display:block;background:#000;";
176
187
  this._videoEl.playsInline = true;
177
- root.appendChild(this._videoEl);
188
+ stage.appendChild(this._videoEl);
178
189
 
179
190
  // Forward the underlying <video>'s `progress` event so consumers can
180
191
  // observe buffered-range updates without reaching into the shadow DOM.
@@ -21,12 +21,16 @@ const MEDIABUNNY_CONTAINERS = new Set<ContainerKind>([
21
21
  *
22
22
  * Routing:
23
23
  * 1. Sniff the magic header. Cheap, deterministic, ignores file extensions.
24
- * 2. If the container is one mediabunny supports → mediabunny. If mediabunny
25
- * rejects, surface the real error rather than blindly falling through to
26
- * libav (which would mask the real failure with a confusing libav error).
27
- * 3. If sniffing identifies AVI/ASF/FLV (or `unknown`) libav.js, lazy-loaded.
28
- * `unknown` is included so genuinely unfamiliar files at least get a shot
29
- * at the broader libav demuxer set.
24
+ * 2. If the container is one mediabunny supports → try mediabunny first
25
+ * (fast path it's a single pass of WASM-free JS parsing). If mediabunny
26
+ * throws (e.g. an assertion on an unsupported sample entry like `mp4v`
27
+ * for MPEG-4 Part 2 in ISOBMFF, or an exotic MKV codec), fall through to
28
+ * libav.js which handles the long tail of codecs mediabunny doesn't.
29
+ * The combined-error case surfaces *both* failures so the user sees
30
+ * which path each step took.
31
+ * 3. If sniffing identifies AVI/ASF/FLV (or `unknown`) → libav.js directly.
32
+ * mediabunny can't read those containers at all, so there's no fast path
33
+ * to try.
30
34
  */
31
35
  export async function probe(source: MediaInput): Promise<MediaContext> {
32
36
  const normalized = await normalizeSource(source);
@@ -35,10 +39,27 @@ export async function probe(source: MediaInput): Promise<MediaContext> {
35
39
  if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
36
40
  try {
37
41
  return await probeWithMediabunny(normalized, sniffed);
38
- } catch (err) {
39
- throw new Error(
40
- `mediabunny failed to probe a ${sniffed} file: ${(err as Error).message}`,
42
+ } catch (mediabunnyErr) {
43
+ // mediabunny rejected the file. Before giving up, try libav — it can
44
+ // demux a much wider range of codec combinations in ISOBMFF/MKV/etc.
45
+ // than mediabunny's pure-JS parser (e.g. mp4v, wmv3-in-asf, flac in
46
+ // an MP4 container). This is "escalation", not "masking": if libav
47
+ // also fails we surface both errors below.
48
+ // eslint-disable-next-line no-console
49
+ console.warn(
50
+ `[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
51
+ (mediabunnyErr as Error).message,
41
52
  );
53
+ try {
54
+ const { probeWithLibav } = await import("./avi.js");
55
+ return await probeWithLibav(normalized, sniffed);
56
+ } catch (libavErr) {
57
+ const mbMsg = (mediabunnyErr as Error).message || String(mediabunnyErr);
58
+ const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
59
+ throw new Error(
60
+ `failed to probe ${sniffed} file. mediabunny: ${mbMsg}. libav fallback: ${lvMsg}.`,
61
+ );
62
+ }
42
63
  }
43
64
  }
44
65
 
@@ -53,11 +53,36 @@ export class VideoRenderer {
53
53
  this.canvas = document.createElement("canvas");
54
54
  this.canvas.style.cssText =
55
55
  "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
56
- const parent = target.parentElement;
57
- if (parent && getComputedStyle(parent).position === "static") {
58
- parent.style.position = "relative";
56
+
57
+ // Attach the canvas next to the video. When the video lives inside an
58
+ // `<avbridge-video>` shadow root, `target.parentElement` is the
59
+ // positioned `<div part="stage">` wrapper the element created
60
+ // precisely for this purpose. When the video is used standalone
61
+ // (legacy `createPlayer({ target: videoEl })` path), we fall back to
62
+ // `parentNode` — which handles plain Elements, and also ShadowRoots
63
+ // if someone inserts a bare <video> inside their own shadow DOM
64
+ // without a wrapper.
65
+ const parent: ParentNode | null =
66
+ (target.parentElement as ParentNode | null) ?? target.parentNode;
67
+ if (parent && parent instanceof HTMLElement) {
68
+ if (getComputedStyle(parent).position === "static") {
69
+ parent.style.position = "relative";
70
+ }
71
+ }
72
+ if (parent) {
73
+ parent.insertBefore(this.canvas, target);
74
+ } else {
75
+ // No parent at all — the target is detached. Fall back to appending
76
+ // the canvas to document.body so at least the frames are visible
77
+ // somewhere while the consumer fixes their DOM layout. This is a
78
+ // loud fallback: log a warning so the misuse is obvious.
79
+ // eslint-disable-next-line no-console
80
+ console.warn(
81
+ "[avbridge] fallback renderer: target <video> has no parent; " +
82
+ "appending canvas to document.body as a fallback.",
83
+ );
84
+ document.body.appendChild(this.canvas);
59
85
  }
60
- parent?.insertBefore(this.canvas, target);
61
86
  target.style.visibility = "hidden";
62
87
 
63
88
  const ctx = this.canvas.getContext("2d");
@@ -187,7 +187,16 @@ export async function createRemuxPipeline(
187
187
  const vTs = !vNext.done ? vNext.value.timestamp : Number.POSITIVE_INFINITY;
188
188
  const aTs = !aNext.done ? aNext.value.timestamp : Number.POSITIVE_INFINITY;
189
189
 
190
- if (!vNext.done && vTs <= aTs) {
190
+ // Mediabunny's muxer requires the first packet on a fresh Output to
191
+ // be a key packet. We fetched `startVideoPacket` via
192
+ // `videoSink.getKeyPacket(fromTime)` so the first video packet is
193
+ // guaranteed to be a keyframe — but a demuxer can hand us an audio
194
+ // packet with a lower timestamp, which mediabunny rejects with
195
+ // "First packet must be a key packet." Force the first video
196
+ // packet out before we let any audio through.
197
+ const forceVideoFirst = firstVideo && !vNext.done;
198
+
199
+ if (!vNext.done && (forceVideoFirst || vTs <= aTs)) {
191
200
  await videoSource.add(
192
201
  vNext.value,
193
202
  firstVideo && videoConfig ? { decoderConfig: videoConfig } : undefined,