avbridge 2.1.2 → 2.2.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 (62) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/dist/{avi-GNTV5ZOH.cjs → avi-6SJLWIWW.cjs} +19 -4
  3. package/dist/avi-6SJLWIWW.cjs.map +1 -0
  4. package/dist/{avi-V6HYQVR2.js → avi-GCGM7OJI.js} +18 -3
  5. package/dist/avi-GCGM7OJI.js.map +1 -0
  6. package/dist/{chunk-EJH67FXG.js → chunk-5DMTJVIU.js} +99 -3
  7. package/dist/chunk-5DMTJVIU.js.map +1 -0
  8. package/dist/{chunk-3AUGRKPY.js → chunk-C5VA5U5O.js} +94 -16
  9. package/dist/chunk-C5VA5U5O.js.map +1 -0
  10. package/dist/{chunk-JQH6D4OE.cjs → chunk-G4APZMCP.cjs} +100 -3
  11. package/dist/chunk-G4APZMCP.cjs.map +1 -0
  12. package/dist/{chunk-Y5FYF5KG.cjs → chunk-HZLQNKFN.cjs} +5 -2
  13. package/dist/chunk-HZLQNKFN.cjs.map +1 -0
  14. package/dist/{chunk-PQTZS7OA.js → chunk-ILKDNBSE.js} +5 -2
  15. package/dist/chunk-ILKDNBSE.js.map +1 -0
  16. package/dist/{chunk-DPVIOYGC.cjs → chunk-OE66B34H.cjs} +98 -20
  17. package/dist/chunk-OE66B34H.cjs.map +1 -0
  18. package/dist/element-browser.js +210 -10
  19. package/dist/element-browser.js.map +1 -1
  20. package/dist/element.cjs +4 -4
  21. package/dist/element.d.cts +1 -1
  22. package/dist/element.d.ts +1 -1
  23. package/dist/element.js +3 -3
  24. package/dist/index.cjs +18 -18
  25. package/dist/index.d.cts +2 -2
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +5 -5
  28. package/dist/libav-loader-27RDIN2I.js +3 -0
  29. package/dist/{libav-loader-XKH2TKUW.js.map → libav-loader-27RDIN2I.js.map} +1 -1
  30. package/dist/libav-loader-IV4AJ2HW.cjs +12 -0
  31. package/dist/{libav-loader-6APXVNIV.cjs.map → libav-loader-IV4AJ2HW.cjs.map} +1 -1
  32. package/dist/{player-BdtUG4rh.d.cts → player-DUyvltvy.d.cts} +3 -3
  33. package/dist/{player-BdtUG4rh.d.ts → player-DUyvltvy.d.ts} +3 -3
  34. package/dist/source-CN43EI7Z.cjs +28 -0
  35. package/dist/{source-SC6ZEQYR.cjs.map → source-CN43EI7Z.cjs.map} +1 -1
  36. package/dist/source-FFZ7TW2B.js +3 -0
  37. package/dist/{source-ZFS4H7J3.js.map → source-FFZ7TW2B.js.map} +1 -1
  38. package/package.json +1 -1
  39. package/src/classify/rules.ts +9 -2
  40. package/src/player.ts +22 -1
  41. package/src/probe/avi.ts +8 -1
  42. package/src/strategies/fallback/audio-output.ts +25 -3
  43. package/src/strategies/fallback/decoder.ts +96 -8
  44. package/src/strategies/fallback/index.ts +90 -6
  45. package/src/strategies/fallback/libav-loader.ts +12 -0
  46. package/src/types.ts +10 -1
  47. package/src/util/debug.ts +131 -0
  48. package/src/util/source.ts +4 -0
  49. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
  50. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
  51. package/dist/avi-GNTV5ZOH.cjs.map +0 -1
  52. package/dist/avi-V6HYQVR2.js.map +0 -1
  53. package/dist/chunk-3AUGRKPY.js.map +0 -1
  54. package/dist/chunk-DPVIOYGC.cjs.map +0 -1
  55. package/dist/chunk-EJH67FXG.js.map +0 -1
  56. package/dist/chunk-JQH6D4OE.cjs.map +0 -1
  57. package/dist/chunk-PQTZS7OA.js.map +0 -1
  58. package/dist/chunk-Y5FYF5KG.cjs.map +0 -1
  59. package/dist/libav-loader-6APXVNIV.cjs +0 -12
  60. package/dist/libav-loader-XKH2TKUW.js +0 -3
  61. package/dist/source-SC6ZEQYR.cjs +0 -28
  62. package/dist/source-ZFS4H7J3.js +0 -3
@@ -2,6 +2,7 @@ import type { MediaContext, PlaybackSession } from "../../types.js";
2
2
  import { VideoRenderer } from "./video-renderer.js";
3
3
  import { AudioOutput } from "./audio-output.js";
4
4
  import { startDecoder, type DecoderHandles } from "./decoder.js";
5
+ import { dbg } from "../../util/debug.js";
5
6
 
6
7
  /**
7
8
  * Fallback strategy session.
@@ -30,8 +31,27 @@ import { startDecoder, type DecoderHandles } from "./decoder.js";
30
31
  * seek(t)` — none of the buffering choreography leaks out.
31
32
  */
32
33
 
33
- const READY_AUDIO_BUFFER_SECONDS = 0.3;
34
- const READY_TIMEOUT_SECONDS = 10;
34
+ // Gate for cold-start playback. We want to start playing as soon as
35
+ // there's any decoded output — the decoder will keep pumping during
36
+ // playback, so more-is-better buffering only helps for fast decoders.
37
+ //
38
+ // For software-decode-bound content (rv40 / wmv3 / mpeg4 @ 720p+ on
39
+ // single-threaded WASM), the decoder may run *slower* than realtime.
40
+ // Waiting for a large audio-buffer threshold is actively wrong in that
41
+ // case: it will never be reached, so the old gate would sit out its
42
+ // full 10-second timeout before playing anything. An aggressive gate
43
+ // ships the first frame to the screen fast, at the cost of the audio
44
+ // clock racing a little ahead of video in the first few seconds —
45
+ // which is the same situation we'd have been in after the timeout
46
+ // anyway.
47
+ //
48
+ // READY_AUDIO_BUFFER_SECONDS: minimum audio queued before start. Set
49
+ // low enough that a slow decoder still reaches it before the user
50
+ // loses patience; 40 ms ≈ 2 cook packets or ~2 AAC packets.
51
+ // READY_TIMEOUT_SECONDS: hard safety. If even 40 ms of audio can't be
52
+ // produced in 3 s, give up and play whatever we have.
53
+ const READY_AUDIO_BUFFER_SECONDS = 0.04;
54
+ const READY_TIMEOUT_SECONDS = 3;
35
55
 
36
56
  export async function createFallbackSession(
37
57
  ctx: MediaContext,
@@ -86,16 +106,80 @@ export async function createFallbackSession(
86
106
  * playback smoothly. Returns early on timeout so we don't hang forever
87
107
  * if the decoder is producing nothing (e.g. immediately past EOF after
88
108
  * a seek to the end).
109
+ *
110
+ * The gate has three exit paths in order of preference:
111
+ *
112
+ * 1. **Fully ready** — audio buffer ≥ target AND ≥1 video frame.
113
+ * The happy path for fast decoders (native + remux never reach
114
+ * this function; this is fallback only).
115
+ *
116
+ * 2. **Video-ready, audio grace period elapsed** — we have video
117
+ * frames but the audio scheduler is still empty. RM/AVI
118
+ * containers commonly deliver a video GOP before their first
119
+ * audio packet, so "no audio yet" ≠ "no audio coming". We give
120
+ * the demuxer a 500 ms grace window from first-frame, then
121
+ * start regardless. Audio will be scheduled at its correct
122
+ * media time once its packets arrive.
123
+ *
124
+ * 3. **Hard timeout** — after {@link READY_TIMEOUT_SECONDS} seconds
125
+ * with neither condition met, start anyway and emit an
126
+ * unconditional diagnostic so the specific underflow is visible.
127
+ *
128
+ * Path #2 is what fixed the "RMVB sits on the play button for 10 s
129
+ * with audio=0ms, frames=N" case — the gate was waiting on audio
130
+ * packets that were several seconds behind in the file stream, and
131
+ * the timeout was the only way out.
89
132
  */
90
133
  async function waitForBuffer(): Promise<void> {
91
134
  const start = performance.now();
135
+ let firstFrameAtMs = 0;
136
+ dbg.info("cold-start",
137
+ `gate entry: want audio ≥ ${READY_AUDIO_BUFFER_SECONDS * 1000}ms + 1 frame`,
138
+ );
92
139
  while (true) {
93
- const audioReady = audio.isNoAudio() || audio.bufferAhead() >= READY_AUDIO_BUFFER_SECONDS;
94
- if (audioReady && renderer.hasFrames()) {
140
+ const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
141
+ const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS;
142
+ const hasFrames = renderer.hasFrames();
143
+ const nowMs = performance.now();
144
+
145
+ if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
146
+
147
+ // Happy path: both ready.
148
+ if (audioReady && hasFrames) {
149
+ dbg.info("cold-start",
150
+ `gate satisfied in ${(nowMs - start).toFixed(0)}ms ` +
151
+ `(audio=${(audioAhead * 1000).toFixed(0)}ms, frames=${renderer.queueDepth()})`,
152
+ );
95
153
  return;
96
154
  }
97
- if ((performance.now() - start) / 1000 > READY_TIMEOUT_SECONDS) {
98
- // Give up waiting; play whatever we have.
155
+
156
+ // Grace path: have video, still waiting for audio that's
157
+ // on its way (first 500 ms after first-frame).
158
+ if (
159
+ hasFrames &&
160
+ firstFrameAtMs > 0 &&
161
+ nowMs - firstFrameAtMs >= 500
162
+ ) {
163
+ dbg.info("cold-start",
164
+ `gate released on video-only grace at ${(nowMs - start).toFixed(0)}ms ` +
165
+ `(frames=${renderer.queueDepth()}, audio=${(audioAhead * 1000).toFixed(0)}ms — ` +
166
+ `demuxer hasn't delivered audio packets yet, starting anyway and letting ` +
167
+ `the audio scheduler catch up at its media-time anchor)`,
168
+ );
169
+ return;
170
+ }
171
+
172
+ // Hard timeout.
173
+ if ((nowMs - start) / 1000 > READY_TIMEOUT_SECONDS) {
174
+ dbg.diag("cold-start",
175
+ `gate TIMEOUT after ${READY_TIMEOUT_SECONDS}s — ` +
176
+ `audio=${(audioAhead * 1000).toFixed(0)}ms ` +
177
+ `(needed ${READY_AUDIO_BUFFER_SECONDS * 1000}ms), ` +
178
+ `frames=${renderer.queueDepth()} (needed ≥1). ` +
179
+ `Decoder produced nothing in ${READY_TIMEOUT_SECONDS}s — either a corrupt source, ` +
180
+ `a missing codec, or WASM is catastrophically slow on this file. ` +
181
+ `Check getDiagnostics().runtime for decode counters.`,
182
+ );
99
183
  return;
100
184
  }
101
185
  await new Promise((r) => setTimeout(r, 50));
@@ -22,6 +22,8 @@
22
22
  * breaks libav's sibling-binary loading.
23
23
  */
24
24
 
25
+ import { dbg } from "../../util/debug.js";
26
+
25
27
  export type LibavVariant = "webcodecs" | "default" | "avbridge";
26
28
 
27
29
  export interface LoadLibavOptions {
@@ -85,12 +87,22 @@ export function loadLibav(
85
87
  async function loadVariant(
86
88
  variant: LibavVariant,
87
89
  wantThreads: boolean,
90
+ ): Promise<LibavInstance> {
91
+ return dbg.timed("libav-load", `load "${variant}" (threads=${wantThreads})`, 5000, () =>
92
+ loadVariantInner(variant, wantThreads),
93
+ );
94
+ }
95
+
96
+ async function loadVariantInner(
97
+ variant: LibavVariant,
98
+ wantThreads: boolean,
88
99
  ): Promise<LibavInstance> {
89
100
  const key = cacheKey(variant, wantThreads);
90
101
  const base = `${libavBaseUrl()}/${variant}`;
91
102
  // The custom variant is named `libav-avbridge.mjs`; the npm variants follow
92
103
  // the same convention (`libav-webcodecs.mjs`, `libav-default.mjs`).
93
104
  const variantUrl = `${base}/libav-${variant}.mjs`;
105
+ dbg.info("libav-load", `fetching ${variantUrl}`);
94
106
 
95
107
  // Preflight HEAD-ish check: issue a bytes=0-0 range request so a missing
96
108
  // file fails fast with a clear error instead of hanging deep inside the
package/src/types.ts CHANGED
@@ -23,6 +23,7 @@ export type ContainerKind =
23
23
  | "avi"
24
24
  | "asf"
25
25
  | "flv"
26
+ | "rm" // RealMedia (.rm / .rmvb)
26
27
  | "ogg"
27
28
  | "wav"
28
29
  | "mp3"
@@ -41,7 +42,10 @@ export type VideoCodec =
41
42
  | "mpeg4" // MPEG-4 Part 2 (DivX/Xvid)
42
43
  | "wmv3"
43
44
  | "vc1"
44
- | "rv40"
45
+ | "rv10" // RealVideo 1.0 (H.263-like)
46
+ | "rv20" // RealVideo G2
47
+ | "rv30" // RealVideo 8
48
+ | "rv40" // RealVideo 9/10
45
49
  | "mpeg2"
46
50
  | "mpeg1"
47
51
  | "theora"
@@ -60,6 +64,11 @@ export type AudioCodec =
60
64
  | "wmav2"
61
65
  | "wmapro"
62
66
  | "alac"
67
+ | "cook" // RealAudio Cooker (G2/RealAudio 8)
68
+ | "ra_144" // RealAudio 1.0 (14.4 kbps)
69
+ | "ra_288" // RealAudio 2.0 (28.8 kbps)
70
+ | "sipr" // RealAudio Sipr (voice codec)
71
+ | "atrac3" // Sony ATRAC3 (sometimes seen in .rm)
63
72
  | (string & {});
64
73
 
65
74
  export interface VideoTrackInfo {
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Debug + self-diagnosis helper.
3
+ *
4
+ * avbridge has a lot of async stages (probe → classify → libav load →
5
+ * strategy.execute → decoder pump → cold-start gate → first paint) and
6
+ * when something's slow or wrong the symptom — "it hangs", "it stutters",
7
+ * "it plays audio without video" — is usually nowhere near the actual
8
+ * cause. This module gives us two things:
9
+ *
10
+ * 1. **Gated verbose logging.** `dbg.info(tag, ...)` etc. are no-ops
11
+ * unless the consumer sets `globalThis.AVBRIDGE_DEBUG = true` (or
12
+ * uses the matching `?avbridge_debug` URL search param at dev time).
13
+ * When enabled, every log is prefixed with `[avbridge:<tag>]` so the
14
+ * console is filterable.
15
+ *
16
+ * 2. **Unconditional self-diagnosis.** `dbg.warnIf(cond, tag, ...)`
17
+ * always fires when a suspicious condition is detected, even with
18
+ * debug off. These are the things we *know* mean something is
19
+ * broken or degraded and the user would want to know about — e.g.
20
+ * the cold-start gate timing out, the decoder running slower than
21
+ * realtime, a libav variant taking longer to load than any network
22
+ * should take, >20% of packets getting rejected.
23
+ *
24
+ * The guiding principle: **if a symptom caused more than 10 minutes of
25
+ * human debugging once, add a targeted warning so the next instance
26
+ * self-identifies in the console.** This module is where those
27
+ * warnings live.
28
+ */
29
+
30
+ /** Read the debug flag fresh on every call so it's runtime-toggleable. */
31
+ function isDebugEnabled(): boolean {
32
+ if (typeof globalThis === "undefined") return false;
33
+ const g = globalThis as { AVBRIDGE_DEBUG?: unknown };
34
+ if (g.AVBRIDGE_DEBUG === true) return true;
35
+ // Convenience: if running in a browser with a `?avbridge_debug` search
36
+ // param, flip the flag on automatically. Useful for demos and quick
37
+ // user reproduction without editing code.
38
+ if (typeof location !== "undefined" && typeof URLSearchParams !== "undefined") {
39
+ try {
40
+ const p = new URLSearchParams(location.search);
41
+ if (p.has("avbridge_debug")) {
42
+ g.AVBRIDGE_DEBUG = true;
43
+ return true;
44
+ }
45
+ } catch { /* ignore */ }
46
+ }
47
+ return false;
48
+ }
49
+
50
+ function fmt(tag: string): string {
51
+ return `[avbridge:${tag}]`;
52
+ }
53
+
54
+ /* eslint-disable no-console */
55
+
56
+ export const dbg = {
57
+ /** Verbose — only when debug is enabled. The hot-path normal case. */
58
+ info(tag: string, ...args: unknown[]): void {
59
+ if (isDebugEnabled()) console.info(fmt(tag), ...args);
60
+ },
61
+
62
+ /** Warning — only when debug is enabled. Non-fatal oddities. */
63
+ warn(tag: string, ...args: unknown[]): void {
64
+ if (isDebugEnabled()) console.warn(fmt(tag), ...args);
65
+ },
66
+
67
+ /**
68
+ * Self-diagnosis warning. **Always** emits regardless of debug flag.
69
+ * Use this only for conditions that mean something is actually wrong
70
+ * or degraded — not for routine chatter.
71
+ */
72
+ diag(tag: string, ...args: unknown[]): void {
73
+ console.warn(fmt(tag), ...args);
74
+ },
75
+
76
+ /**
77
+ * Timing helper: wraps an async call and logs its elapsed time when
78
+ * debug is on. The callback runs whether debug is on or off — this is
79
+ * just for the `dbg.info` at the end.
80
+ *
81
+ * Also unconditionally fires `dbg.diag` if the elapsed time exceeds
82
+ * `slowMs`, so "the bootstrap took 8 seconds" shows up even without
83
+ * debug mode enabled.
84
+ */
85
+ async timed<T>(
86
+ tag: string,
87
+ label: string,
88
+ slowMs: number,
89
+ fn: () => Promise<T>,
90
+ ): Promise<T> {
91
+ const start = performance.now();
92
+ try {
93
+ const result = await fn();
94
+ const elapsed = performance.now() - start;
95
+ if (isDebugEnabled()) {
96
+ console.info(fmt(tag), `${label} ${elapsed.toFixed(0)}ms`);
97
+ }
98
+ if (elapsed > slowMs) {
99
+ console.warn(
100
+ fmt(tag),
101
+ `${label} took ${elapsed.toFixed(0)}ms (>${slowMs}ms expected) — ` +
102
+ `this is unusually slow; possible causes: ${hintForTag(tag)}`,
103
+ );
104
+ }
105
+ return result;
106
+ } catch (err) {
107
+ const elapsed = performance.now() - start;
108
+ console.warn(
109
+ fmt(tag),
110
+ `${label} FAILED after ${elapsed.toFixed(0)}ms:`,
111
+ err,
112
+ );
113
+ throw err;
114
+ }
115
+ },
116
+ };
117
+
118
+ function hintForTag(tag: string): string {
119
+ switch (tag) {
120
+ case "probe":
121
+ return "slow network (range request), large sniff window, or libav cold-start";
122
+ case "libav-load":
123
+ return "large .wasm download, misconfigured AVBRIDGE_LIBAV_BASE, or server-side MIME type";
124
+ case "bootstrap":
125
+ return "probe+classify+strategy-init chain; enable AVBRIDGE_DEBUG for a phase breakdown";
126
+ case "cold-start":
127
+ return "decoder is producing output slower than realtime — check framesDecoded in getDiagnostics()";
128
+ default:
129
+ return "unknown stage — enable globalThis.AVBRIDGE_DEBUG for more detail";
130
+ }
131
+ }
@@ -215,6 +215,10 @@ export function sniffContainerFromBytes(head: Uint8Array): ContainerKind {
215
215
  ) return "asf";
216
216
  // FLV: 46 4C 56
217
217
  if (head[0] === 0x46 && head[1] === 0x4c && head[2] === 0x56) return "flv";
218
+ // RealMedia (.rm / .rmvb): ".RMF" — 2E 52 4D 46
219
+ if (head[0] === 0x2e && head[1] === 0x52 && head[2] === 0x4d && head[3] === 0x46) {
220
+ return "rm";
221
+ }
218
222
  // OggS: 4F 67 67 53
219
223
  if (head[0] === 0x4f && head[1] === 0x67 && head[2] === 0x67 && head[3] === 0x53) return "ogg";
220
224
  // FLAC: 66 4C 61 43