avbridge 2.1.1 → 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.
- package/CHANGELOG.md +136 -0
- package/dist/{avi-GNTV5ZOH.cjs → avi-6SJLWIWW.cjs} +19 -4
- package/dist/avi-6SJLWIWW.cjs.map +1 -0
- package/dist/{avi-V6HYQVR2.js → avi-GCGM7OJI.js} +18 -3
- package/dist/avi-GCGM7OJI.js.map +1 -0
- package/dist/{chunk-EJH67FXG.js → chunk-5DMTJVIU.js} +99 -3
- package/dist/chunk-5DMTJVIU.js.map +1 -0
- package/dist/{chunk-CUQD23WO.js → chunk-C5VA5U5O.js} +122 -23
- package/dist/chunk-C5VA5U5O.js.map +1 -0
- package/dist/{chunk-JQH6D4OE.cjs → chunk-G4APZMCP.cjs} +100 -3
- package/dist/chunk-G4APZMCP.cjs.map +1 -0
- package/dist/{chunk-Y5FYF5KG.cjs → chunk-HZLQNKFN.cjs} +5 -2
- package/dist/chunk-HZLQNKFN.cjs.map +1 -0
- package/dist/{chunk-PQTZS7OA.js → chunk-ILKDNBSE.js} +5 -2
- package/dist/chunk-ILKDNBSE.js.map +1 -0
- package/dist/{chunk-O34444ID.cjs → chunk-OE66B34H.cjs} +126 -27
- package/dist/chunk-OE66B34H.cjs.map +1 -0
- package/dist/element-browser.js +244 -19
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +9 -5
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +8 -4
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +18 -18
- package/dist/index.d.cts +12 -8
- package/dist/index.d.ts +12 -8
- package/dist/index.js +5 -5
- package/dist/libav-loader-27RDIN2I.js +3 -0
- package/dist/{libav-loader-XKH2TKUW.js.map → libav-loader-27RDIN2I.js.map} +1 -1
- package/dist/libav-loader-IV4AJ2HW.cjs +12 -0
- package/dist/{libav-loader-6APXVNIV.cjs.map → libav-loader-IV4AJ2HW.cjs.map} +1 -1
- package/dist/{player-BdtUG4rh.d.cts → player-DUyvltvy.d.cts} +3 -3
- package/dist/{player-BdtUG4rh.d.ts → player-DUyvltvy.d.ts} +3 -3
- package/dist/source-CN43EI7Z.cjs +28 -0
- package/dist/{source-SC6ZEQYR.cjs.map → source-CN43EI7Z.cjs.map} +1 -1
- package/dist/source-FFZ7TW2B.js +3 -0
- package/dist/{source-ZFS4H7J3.js.map → source-FFZ7TW2B.js.map} +1 -1
- package/package.json +1 -1
- package/src/classify/rules.ts +9 -2
- package/src/element/avbridge-video.ts +12 -1
- package/src/player.ts +22 -1
- package/src/probe/avi.ts +8 -1
- package/src/probe/index.ts +30 -9
- package/src/strategies/fallback/audio-output.ts +25 -3
- package/src/strategies/fallback/decoder.ts +96 -8
- package/src/strategies/fallback/index.ts +90 -6
- package/src/strategies/fallback/libav-loader.ts +12 -0
- package/src/strategies/fallback/video-renderer.ts +29 -4
- package/src/strategies/remux/pipeline.ts +10 -1
- package/src/types.ts +10 -1
- package/src/util/debug.ts +131 -0
- package/src/util/source.ts +4 -0
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
- package/dist/avi-GNTV5ZOH.cjs.map +0 -1
- package/dist/avi-V6HYQVR2.js.map +0 -1
- package/dist/chunk-CUQD23WO.js.map +0 -1
- package/dist/chunk-EJH67FXG.js.map +0 -1
- package/dist/chunk-JQH6D4OE.cjs.map +0 -1
- package/dist/chunk-O34444ID.cjs.map +0 -1
- package/dist/chunk-PQTZS7OA.js.map +0 -1
- package/dist/chunk-Y5FYF5KG.cjs.map +0 -1
- package/dist/libav-loader-6APXVNIV.cjs +0 -12
- package/dist/libav-loader-XKH2TKUW.js +0 -3
- package/dist/source-SC6ZEQYR.cjs +0 -28
- package/dist/source-ZFS4H7J3.js +0 -3
|
@@ -135,6 +135,20 @@ export async function startDecoder(opts: StartDecoderOptions): Promise<DecoderHa
|
|
|
135
135
|
let videoFramesDecoded = 0;
|
|
136
136
|
let audioFramesDecoded = 0;
|
|
137
137
|
|
|
138
|
+
// Decode-rate watchdog. Samples framesDecoded every second and
|
|
139
|
+
// compares against realtime expected frames for the source fps. If
|
|
140
|
+
// the decoder sustains less than 60% of realtime for more than
|
|
141
|
+
// 5 seconds (counting only time since the first frame emerged),
|
|
142
|
+
// emits a one-shot diagnostic so users know why playback is
|
|
143
|
+
// stuttering instead of guessing. A second one-shot fires if the
|
|
144
|
+
// renderer's overflow-drop rate exceeds 10% of decoded frames —
|
|
145
|
+
// that symptom means the decoder is BURSTING faster than the
|
|
146
|
+
// renderer can drain, which is a different bug from "decoder slow".
|
|
147
|
+
let watchdogFirstFrameMs = 0;
|
|
148
|
+
let watchdogSlowSinceMs = 0;
|
|
149
|
+
let watchdogSlowWarned = false;
|
|
150
|
+
let watchdogOverflowWarned = false;
|
|
151
|
+
|
|
138
152
|
// Synthetic timestamp counters. Reset on seek.
|
|
139
153
|
let syntheticVideoUs = 0;
|
|
140
154
|
let syntheticAudioUs = 0;
|
|
@@ -150,10 +164,18 @@ export async function startDecoder(opts: StartDecoderOptions): Promise<DecoderHa
|
|
|
150
164
|
let readErr: number;
|
|
151
165
|
let packets: Record<number, LibavPacket[]>;
|
|
152
166
|
try {
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
167
|
+
// Batch size tunes the tradeoff between JS↔WASM call overhead
|
|
168
|
+
// (small = more crossings per second) and queue burstiness
|
|
169
|
+
// (large = decoder hands the renderer big bursts at once that
|
|
170
|
+
// can blow past the renderer's 64-frame hard cap before the
|
|
171
|
+
// per-batch `queueHighWater` throttle runs).
|
|
172
|
+
//
|
|
173
|
+
// We tried 64 KB and saw ~30% overflow drops on RMVB:rv40 at
|
|
174
|
+
// 1024x768 because one decode batch regularly produced >30
|
|
175
|
+
// frames. 16 KB keeps each batch ≈ 4-6 video packets at
|
|
176
|
+
// typical bitrates, so the worst-case queue spike stays under
|
|
177
|
+
// `queueHighWater` and the throttle has a chance to apply
|
|
178
|
+
// backpressure *between* batches rather than within one.
|
|
157
179
|
[readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
|
|
158
180
|
limit: 16 * 1024,
|
|
159
181
|
});
|
|
@@ -167,16 +189,82 @@ export async function startDecoder(opts: StartDecoderOptions): Promise<DecoderHa
|
|
|
167
189
|
const videoPackets = videoStream ? packets[videoStream.index] : undefined;
|
|
168
190
|
const audioPackets = audioStream ? packets[audioStream.index] : undefined;
|
|
169
191
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
192
|
+
// Decode audio BEFORE video. On software-decode-bound content
|
|
193
|
+
// (rv40/mpeg4/wmv3 @ 720p+) a single video batch can take
|
|
194
|
+
// 200-400 ms of wall time; if the scheduler hasn't been fed
|
|
195
|
+
// during that window, audio output runs dry and the user hears
|
|
196
|
+
// clicks/gaps. Audio is time-critical; video can drop a frame
|
|
197
|
+
// and nobody notices. Audio decode is also typically <1 ms per
|
|
198
|
+
// packet for cook/mp3/aac, so doing it first barely delays
|
|
199
|
+
// video decoding at all.
|
|
174
200
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
175
201
|
await decodeAudioBatch(audioPackets, myToken);
|
|
176
202
|
}
|
|
203
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
204
|
+
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
205
|
+
await decodeVideoBatch(videoPackets, myToken);
|
|
206
|
+
}
|
|
177
207
|
|
|
178
208
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
179
209
|
|
|
210
|
+
// ── Decode-rate watchdog ──────────────────────────────────────
|
|
211
|
+
if (videoFramesDecoded > 0) {
|
|
212
|
+
if (watchdogFirstFrameMs === 0) {
|
|
213
|
+
watchdogFirstFrameMs = performance.now();
|
|
214
|
+
}
|
|
215
|
+
const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1000;
|
|
216
|
+
|
|
217
|
+
// 1. Slow-decode detection (sustained <60% of realtime fps).
|
|
218
|
+
if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
|
|
219
|
+
const expectedFrames = elapsedSinceFirst * videoFps;
|
|
220
|
+
const ratio = videoFramesDecoded / expectedFrames;
|
|
221
|
+
if (ratio < 0.6) {
|
|
222
|
+
if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
|
|
223
|
+
if ((performance.now() - watchdogSlowSinceMs) / 1000 > 5) {
|
|
224
|
+
watchdogSlowWarned = true;
|
|
225
|
+
console.warn(
|
|
226
|
+
"[avbridge:decode-rate]",
|
|
227
|
+
`decoder is running slower than realtime: ` +
|
|
228
|
+
`${videoFramesDecoded} frames in ${elapsedSinceFirst.toFixed(1)}s ` +
|
|
229
|
+
`(${(videoFramesDecoded / elapsedSinceFirst).toFixed(1)} fps vs ${videoFps} fps source — ` +
|
|
230
|
+
`${(ratio * 100).toFixed(0)}% of realtime). ` +
|
|
231
|
+
`Playback will stutter. Typical causes: software decode of a codec with no WebCodecs support ` +
|
|
232
|
+
`(rv40, mpeg4 @ 720p+, wmv3), or a resolution too large for single-threaded WASM to keep up with.`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
watchdogSlowSinceMs = 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 2. Overflow-drop detection (>10% of decoded frames dropped
|
|
241
|
+
// by the renderer's hard cap). This means the decoder
|
|
242
|
+
// produces BURSTS — it's fast enough on average but one
|
|
243
|
+
// batch delivers >30 frames at a time, overflowing before
|
|
244
|
+
// the queueHighWater throttle can apply backpressure.
|
|
245
|
+
// Symptom is different from "decoder slow": here the fps
|
|
246
|
+
// ratio looks fine but the user sees choppy playback.
|
|
247
|
+
if (
|
|
248
|
+
!watchdogOverflowWarned &&
|
|
249
|
+
videoFramesDecoded > 100 // wait for a meaningful sample
|
|
250
|
+
) {
|
|
251
|
+
const rendererStats = opts.renderer.stats() as { framesDroppedOverflow?: number };
|
|
252
|
+
const overflow = rendererStats.framesDroppedOverflow ?? 0;
|
|
253
|
+
if (overflow / videoFramesDecoded > 0.1) {
|
|
254
|
+
watchdogOverflowWarned = true;
|
|
255
|
+
console.warn(
|
|
256
|
+
"[avbridge:overflow-drop]",
|
|
257
|
+
`renderer is dropping ${overflow}/${videoFramesDecoded} frames ` +
|
|
258
|
+
`(${((overflow / videoFramesDecoded) * 100).toFixed(0)}%) because the decoder ` +
|
|
259
|
+
`is producing bursts faster than the canvas can drain. Symptom: choppy ` +
|
|
260
|
+
`playback despite decoder keeping up on average. Fix would be smaller ` +
|
|
261
|
+
`read batches in the pump loop or a lower queueHighWater cap — see ` +
|
|
262
|
+
`src/strategies/fallback/decoder.ts.`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
180
268
|
// Throttle: don't run too far ahead of playback. Two backpressure
|
|
181
269
|
// signals:
|
|
182
270
|
// - Audio buffer (mediaTimeOfNext - now()) > 2 sec — we have
|
|
@@ -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
|
-
|
|
34
|
-
|
|
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
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
@@ -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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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,
|
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
|
-
| "
|
|
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
|
+
}
|
package/src/util/source.ts
CHANGED
|
@@ -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
|