avbridge 2.1.2 → 2.2.1
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 +138 -0
- package/README.md +98 -71
- 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-3AUGRKPY.js → chunk-DMWARSEF.js} +160 -27
- package/dist/chunk-DMWARSEF.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-DPVIOYGC.cjs → chunk-UF2N5L63.cjs} +164 -31
- package/dist/chunk-UF2N5L63.cjs.map +1 -0
- package/dist/element-browser.js +276 -21
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +4 -4
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +3 -3
- package/dist/index.cjs +18 -18
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- 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-U2NPmFvA.d.cts} +4 -3
- package/dist/{player-BdtUG4rh.d.ts → player-U2NPmFvA.d.ts} +4 -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/player.ts +46 -17
- package/src/probe/avi.ts +8 -1
- package/src/strategies/fallback/audio-output.ts +25 -3
- package/src/strategies/fallback/decoder.ts +96 -8
- package/src/strategies/fallback/index.ts +98 -6
- package/src/strategies/fallback/libav-loader.ts +12 -0
- package/src/strategies/fallback/video-renderer.ts +5 -1
- package/src/strategies/hybrid/index.ts +9 -1
- package/src/strategies/remux/index.ts +13 -1
- package/src/strategies/remux/pipeline.ts +6 -0
- 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-3AUGRKPY.js.map +0 -1
- package/dist/chunk-DPVIOYGC.cjs.map +0 -1
- package/dist/chunk-EJH67FXG.js.map +0 -1
- package/dist/chunk-JQH6D4OE.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
package/dist/element-browser.js
CHANGED
|
@@ -145,6 +145,9 @@ function sniffContainerFromBytes(head) {
|
|
|
145
145
|
}
|
|
146
146
|
if (head[0] === 48 && head[1] === 38 && head[2] === 178 && head[3] === 117 && head[4] === 142 && head[5] === 102 && head[6] === 207 && head[7] === 17) return "asf";
|
|
147
147
|
if (head[0] === 70 && head[1] === 76 && head[2] === 86) return "flv";
|
|
148
|
+
if (head[0] === 46 && head[1] === 82 && head[2] === 77 && head[3] === 70) {
|
|
149
|
+
return "rm";
|
|
150
|
+
}
|
|
148
151
|
if (head[0] === 79 && head[1] === 103 && head[2] === 103 && head[3] === 83) return "ogg";
|
|
149
152
|
if (head[0] === 102 && head[1] === 76 && head[2] === 97 && head[3] === 67) return "flac";
|
|
150
153
|
if (head[0] === 73 && head[1] === 68 && head[2] === 51) return "mp3";
|
|
@@ -29628,6 +29631,98 @@ var init_libav_http_reader = __esm({
|
|
|
29628
29631
|
}
|
|
29629
29632
|
});
|
|
29630
29633
|
|
|
29634
|
+
// src/util/debug.ts
|
|
29635
|
+
function isDebugEnabled() {
|
|
29636
|
+
if (typeof globalThis === "undefined") return false;
|
|
29637
|
+
const g = globalThis;
|
|
29638
|
+
if (g.AVBRIDGE_DEBUG === true) return true;
|
|
29639
|
+
if (typeof location !== "undefined" && typeof URLSearchParams !== "undefined") {
|
|
29640
|
+
try {
|
|
29641
|
+
const p = new URLSearchParams(location.search);
|
|
29642
|
+
if (p.has("avbridge_debug")) {
|
|
29643
|
+
g.AVBRIDGE_DEBUG = true;
|
|
29644
|
+
return true;
|
|
29645
|
+
}
|
|
29646
|
+
} catch {
|
|
29647
|
+
}
|
|
29648
|
+
}
|
|
29649
|
+
return false;
|
|
29650
|
+
}
|
|
29651
|
+
function fmt(tag) {
|
|
29652
|
+
return `[avbridge:${tag}]`;
|
|
29653
|
+
}
|
|
29654
|
+
function hintForTag(tag) {
|
|
29655
|
+
switch (tag) {
|
|
29656
|
+
case "probe":
|
|
29657
|
+
return "slow network (range request), large sniff window, or libav cold-start";
|
|
29658
|
+
case "libav-load":
|
|
29659
|
+
return "large .wasm download, misconfigured AVBRIDGE_LIBAV_BASE, or server-side MIME type";
|
|
29660
|
+
case "bootstrap":
|
|
29661
|
+
return "probe+classify+strategy-init chain; enable AVBRIDGE_DEBUG for a phase breakdown";
|
|
29662
|
+
case "cold-start":
|
|
29663
|
+
return "decoder is producing output slower than realtime \u2014 check framesDecoded in getDiagnostics()";
|
|
29664
|
+
default:
|
|
29665
|
+
return "unknown stage \u2014 enable globalThis.AVBRIDGE_DEBUG for more detail";
|
|
29666
|
+
}
|
|
29667
|
+
}
|
|
29668
|
+
var dbg;
|
|
29669
|
+
var init_debug = __esm({
|
|
29670
|
+
"src/util/debug.ts"() {
|
|
29671
|
+
dbg = {
|
|
29672
|
+
/** Verbose — only when debug is enabled. The hot-path normal case. */
|
|
29673
|
+
info(tag, ...args) {
|
|
29674
|
+
if (isDebugEnabled()) console.info(fmt(tag), ...args);
|
|
29675
|
+
},
|
|
29676
|
+
/** Warning — only when debug is enabled. Non-fatal oddities. */
|
|
29677
|
+
warn(tag, ...args) {
|
|
29678
|
+
if (isDebugEnabled()) console.warn(fmt(tag), ...args);
|
|
29679
|
+
},
|
|
29680
|
+
/**
|
|
29681
|
+
* Self-diagnosis warning. **Always** emits regardless of debug flag.
|
|
29682
|
+
* Use this only for conditions that mean something is actually wrong
|
|
29683
|
+
* or degraded — not for routine chatter.
|
|
29684
|
+
*/
|
|
29685
|
+
diag(tag, ...args) {
|
|
29686
|
+
console.warn(fmt(tag), ...args);
|
|
29687
|
+
},
|
|
29688
|
+
/**
|
|
29689
|
+
* Timing helper: wraps an async call and logs its elapsed time when
|
|
29690
|
+
* debug is on. The callback runs whether debug is on or off — this is
|
|
29691
|
+
* just for the `dbg.info` at the end.
|
|
29692
|
+
*
|
|
29693
|
+
* Also unconditionally fires `dbg.diag` if the elapsed time exceeds
|
|
29694
|
+
* `slowMs`, so "the bootstrap took 8 seconds" shows up even without
|
|
29695
|
+
* debug mode enabled.
|
|
29696
|
+
*/
|
|
29697
|
+
async timed(tag, label, slowMs, fn) {
|
|
29698
|
+
const start = performance.now();
|
|
29699
|
+
try {
|
|
29700
|
+
const result = await fn();
|
|
29701
|
+
const elapsed = performance.now() - start;
|
|
29702
|
+
if (isDebugEnabled()) {
|
|
29703
|
+
console.info(fmt(tag), `${label} ${elapsed.toFixed(0)}ms`);
|
|
29704
|
+
}
|
|
29705
|
+
if (elapsed > slowMs) {
|
|
29706
|
+
console.warn(
|
|
29707
|
+
fmt(tag),
|
|
29708
|
+
`${label} took ${elapsed.toFixed(0)}ms (>${slowMs}ms expected) \u2014 this is unusually slow; possible causes: ${hintForTag(tag)}`
|
|
29709
|
+
);
|
|
29710
|
+
}
|
|
29711
|
+
return result;
|
|
29712
|
+
} catch (err) {
|
|
29713
|
+
const elapsed = performance.now() - start;
|
|
29714
|
+
console.warn(
|
|
29715
|
+
fmt(tag),
|
|
29716
|
+
`${label} FAILED after ${elapsed.toFixed(0)}ms:`,
|
|
29717
|
+
err
|
|
29718
|
+
);
|
|
29719
|
+
throw err;
|
|
29720
|
+
}
|
|
29721
|
+
}
|
|
29722
|
+
};
|
|
29723
|
+
}
|
|
29724
|
+
});
|
|
29725
|
+
|
|
29631
29726
|
// src/strategies/fallback/libav-loader.ts
|
|
29632
29727
|
function cacheKey(variant, threads) {
|
|
29633
29728
|
return `${variant}:${threads ? "thr" : "wasm"}`;
|
|
@@ -29644,9 +29739,18 @@ function loadLibav(variant = "webcodecs", opts = {}) {
|
|
|
29644
29739
|
return entry;
|
|
29645
29740
|
}
|
|
29646
29741
|
async function loadVariant(variant, wantThreads) {
|
|
29742
|
+
return dbg.timed(
|
|
29743
|
+
"libav-load",
|
|
29744
|
+
`load "${variant}" (threads=${wantThreads})`,
|
|
29745
|
+
5e3,
|
|
29746
|
+
() => loadVariantInner(variant, wantThreads)
|
|
29747
|
+
);
|
|
29748
|
+
}
|
|
29749
|
+
async function loadVariantInner(variant, wantThreads) {
|
|
29647
29750
|
const key = cacheKey(variant, wantThreads);
|
|
29648
29751
|
const base = `${libavBaseUrl()}/${variant}`;
|
|
29649
29752
|
const variantUrl = `${base}/libav-${variant}.mjs`;
|
|
29753
|
+
dbg.info("libav-load", `fetching ${variantUrl}`);
|
|
29650
29754
|
if (typeof fetch === "function") {
|
|
29651
29755
|
try {
|
|
29652
29756
|
const head = await fetch(variantUrl, { method: "GET", headers: { Range: "bytes=0-0" } });
|
|
@@ -29730,6 +29834,7 @@ function chain(message, err) {
|
|
|
29730
29834
|
var cache;
|
|
29731
29835
|
var init_libav_loader = __esm({
|
|
29732
29836
|
"src/strategies/fallback/libav-loader.ts"() {
|
|
29837
|
+
init_debug();
|
|
29733
29838
|
cache = /* @__PURE__ */ new Map();
|
|
29734
29839
|
}
|
|
29735
29840
|
});
|
|
@@ -29858,7 +29963,12 @@ function ffmpegToAvbridgeVideo(name) {
|
|
|
29858
29963
|
return "mpeg1";
|
|
29859
29964
|
case "theora":
|
|
29860
29965
|
return "theora";
|
|
29966
|
+
case "rv10":
|
|
29967
|
+
return "rv10";
|
|
29968
|
+
case "rv20":
|
|
29969
|
+
return "rv20";
|
|
29861
29970
|
case "rv30":
|
|
29971
|
+
return "rv30";
|
|
29862
29972
|
case "rv40":
|
|
29863
29973
|
return "rv40";
|
|
29864
29974
|
default:
|
|
@@ -29889,6 +29999,16 @@ function ffmpegToAvbridgeAudio(name) {
|
|
|
29889
29999
|
return "wmapro";
|
|
29890
30000
|
case "alac":
|
|
29891
30001
|
return "alac";
|
|
30002
|
+
case "cook":
|
|
30003
|
+
return "cook";
|
|
30004
|
+
case "ra_144":
|
|
30005
|
+
return "ra_144";
|
|
30006
|
+
case "ra_288":
|
|
30007
|
+
return "ra_288";
|
|
30008
|
+
case "sipr":
|
|
30009
|
+
return "sipr";
|
|
30010
|
+
case "atrac3":
|
|
30011
|
+
return "atrac3";
|
|
29892
30012
|
default:
|
|
29893
30013
|
return name;
|
|
29894
30014
|
}
|
|
@@ -31154,8 +31274,29 @@ var NATIVE_AUDIO_CODECS = /* @__PURE__ */ new Set([
|
|
|
31154
31274
|
"vorbis",
|
|
31155
31275
|
"flac"
|
|
31156
31276
|
]);
|
|
31157
|
-
var FALLBACK_VIDEO_CODECS = /* @__PURE__ */ new Set([
|
|
31158
|
-
|
|
31277
|
+
var FALLBACK_VIDEO_CODECS = /* @__PURE__ */ new Set([
|
|
31278
|
+
"wmv3",
|
|
31279
|
+
"vc1",
|
|
31280
|
+
"mpeg4",
|
|
31281
|
+
"rv10",
|
|
31282
|
+
"rv20",
|
|
31283
|
+
"rv30",
|
|
31284
|
+
"rv40",
|
|
31285
|
+
"mpeg2",
|
|
31286
|
+
"mpeg1",
|
|
31287
|
+
"theora"
|
|
31288
|
+
]);
|
|
31289
|
+
var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
|
|
31290
|
+
"wmav2",
|
|
31291
|
+
"wmapro",
|
|
31292
|
+
"ac3",
|
|
31293
|
+
"eac3",
|
|
31294
|
+
"cook",
|
|
31295
|
+
"ra_144",
|
|
31296
|
+
"ra_288",
|
|
31297
|
+
"sipr",
|
|
31298
|
+
"atrac3"
|
|
31299
|
+
]);
|
|
31159
31300
|
var NATIVE_CONTAINERS = /* @__PURE__ */ new Set([
|
|
31160
31301
|
"mp4",
|
|
31161
31302
|
"mov",
|
|
@@ -31837,6 +31978,10 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
31837
31978
|
console.error("[avbridge] remux pipeline reseek failed:", err);
|
|
31838
31979
|
});
|
|
31839
31980
|
},
|
|
31981
|
+
setAutoPlay(autoPlay) {
|
|
31982
|
+
pendingAutoPlay = autoPlay;
|
|
31983
|
+
if (sink) sink.setPlayOnSeek(autoPlay);
|
|
31984
|
+
},
|
|
31840
31985
|
async destroy() {
|
|
31841
31986
|
destroyed = true;
|
|
31842
31987
|
pumpToken++;
|
|
@@ -31877,7 +32022,11 @@ async function createRemuxSession(context, video) {
|
|
|
31877
32022
|
await pipeline.start(video.currentTime || 0, true);
|
|
31878
32023
|
return;
|
|
31879
32024
|
}
|
|
31880
|
-
|
|
32025
|
+
pipeline.setAutoPlay(true);
|
|
32026
|
+
try {
|
|
32027
|
+
await video.play();
|
|
32028
|
+
} catch {
|
|
32029
|
+
}
|
|
31881
32030
|
},
|
|
31882
32031
|
pause() {
|
|
31883
32032
|
wantPlay = false;
|
|
@@ -31925,7 +32074,7 @@ var VideoRenderer = class {
|
|
|
31925
32074
|
this.resolveFirstFrame = resolve;
|
|
31926
32075
|
});
|
|
31927
32076
|
this.canvas = document.createElement("canvas");
|
|
31928
|
-
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
|
|
32077
|
+
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
|
|
31929
32078
|
const parent = target.parentElement ?? target.parentNode;
|
|
31930
32079
|
if (parent && parent instanceof HTMLElement) {
|
|
31931
32080
|
if (getComputedStyle(parent).position === "static") {
|
|
@@ -32167,9 +32316,13 @@ var AudioOutput = class {
|
|
|
32167
32316
|
const node3 = this.ctx.createBufferSource();
|
|
32168
32317
|
node3.buffer = buffer;
|
|
32169
32318
|
node3.connect(this.gain);
|
|
32170
|
-
|
|
32171
|
-
|
|
32172
|
-
|
|
32319
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
|
|
32320
|
+
if (ctxStart < this.ctx.currentTime) {
|
|
32321
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
32322
|
+
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
32323
|
+
ctxStart = this.ctx.currentTime;
|
|
32324
|
+
}
|
|
32325
|
+
node3.start(ctxStart);
|
|
32173
32326
|
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
32174
32327
|
this.framesScheduled++;
|
|
32175
32328
|
}
|
|
@@ -32764,6 +32917,10 @@ async function createHybridSession(ctx, target) {
|
|
|
32764
32917
|
void doSeek(v);
|
|
32765
32918
|
}
|
|
32766
32919
|
});
|
|
32920
|
+
Object.defineProperty(target, "paused", {
|
|
32921
|
+
configurable: true,
|
|
32922
|
+
get: () => !audio.isPlaying()
|
|
32923
|
+
});
|
|
32767
32924
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
32768
32925
|
Object.defineProperty(target, "duration", {
|
|
32769
32926
|
configurable: true,
|
|
@@ -32828,6 +32985,7 @@ async function createHybridSession(ctx, target) {
|
|
|
32828
32985
|
try {
|
|
32829
32986
|
delete target.currentTime;
|
|
32830
32987
|
delete target.duration;
|
|
32988
|
+
delete target.paused;
|
|
32831
32989
|
} catch {
|
|
32832
32990
|
}
|
|
32833
32991
|
},
|
|
@@ -32906,6 +33064,10 @@ async function startDecoder(opts) {
|
|
|
32906
33064
|
let packetsRead = 0;
|
|
32907
33065
|
let videoFramesDecoded = 0;
|
|
32908
33066
|
let audioFramesDecoded = 0;
|
|
33067
|
+
let watchdogFirstFrameMs = 0;
|
|
33068
|
+
let watchdogSlowSinceMs = 0;
|
|
33069
|
+
let watchdogSlowWarned = false;
|
|
33070
|
+
let watchdogOverflowWarned = false;
|
|
32909
33071
|
let syntheticVideoUs = 0;
|
|
32910
33072
|
let syntheticAudioUs = 0;
|
|
32911
33073
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
@@ -32926,14 +33088,47 @@ async function startDecoder(opts) {
|
|
|
32926
33088
|
if (myToken !== pumpToken || destroyed) return;
|
|
32927
33089
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
32928
33090
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
32929
|
-
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
32930
|
-
await decodeVideoBatch(videoPackets, myToken);
|
|
32931
|
-
}
|
|
32932
|
-
if (myToken !== pumpToken || destroyed) return;
|
|
32933
33091
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
32934
33092
|
await decodeAudioBatch(audioPackets, myToken);
|
|
32935
33093
|
}
|
|
33094
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
33095
|
+
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
33096
|
+
await decodeVideoBatch(videoPackets, myToken);
|
|
33097
|
+
}
|
|
32936
33098
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
33099
|
+
if (videoFramesDecoded > 0) {
|
|
33100
|
+
if (watchdogFirstFrameMs === 0) {
|
|
33101
|
+
watchdogFirstFrameMs = performance.now();
|
|
33102
|
+
}
|
|
33103
|
+
const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
|
|
33104
|
+
if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
|
|
33105
|
+
const expectedFrames = elapsedSinceFirst * videoFps;
|
|
33106
|
+
const ratio = videoFramesDecoded / expectedFrames;
|
|
33107
|
+
if (ratio < 0.6) {
|
|
33108
|
+
if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
|
|
33109
|
+
if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
|
|
33110
|
+
watchdogSlowWarned = true;
|
|
33111
|
+
console.warn(
|
|
33112
|
+
"[avbridge:decode-rate]",
|
|
33113
|
+
`decoder is running slower than realtime: ${videoFramesDecoded} frames in ${elapsedSinceFirst.toFixed(1)}s (${(videoFramesDecoded / elapsedSinceFirst).toFixed(1)} fps vs ${videoFps} fps source \u2014 ${(ratio * 100).toFixed(0)}% of realtime). Playback will stutter. Typical causes: software decode of a codec with no WebCodecs support (rv40, mpeg4 @ 720p+, wmv3), or a resolution too large for single-threaded WASM to keep up with.`
|
|
33114
|
+
);
|
|
33115
|
+
}
|
|
33116
|
+
} else {
|
|
33117
|
+
watchdogSlowSinceMs = 0;
|
|
33118
|
+
}
|
|
33119
|
+
}
|
|
33120
|
+
if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
|
|
33121
|
+
const rendererStats = opts.renderer.stats();
|
|
33122
|
+
const overflow = rendererStats.framesDroppedOverflow ?? 0;
|
|
33123
|
+
if (overflow / videoFramesDecoded > 0.1) {
|
|
33124
|
+
watchdogOverflowWarned = true;
|
|
33125
|
+
console.warn(
|
|
33126
|
+
"[avbridge:overflow-drop]",
|
|
33127
|
+
`renderer is dropping ${overflow}/${videoFramesDecoded} frames (${(overflow / videoFramesDecoded * 100).toFixed(0)}%) because the decoder is producing bursts faster than the canvas can drain. Symptom: choppy playback despite decoder keeping up on average. Fix would be smaller read batches in the pump loop or a lower queueHighWater cap \u2014 see src/strategies/fallback/decoder.ts.`
|
|
33128
|
+
);
|
|
33129
|
+
}
|
|
33130
|
+
}
|
|
33131
|
+
}
|
|
32937
33132
|
while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
32938
33133
|
await new Promise((r) => setTimeout(r, 50));
|
|
32939
33134
|
}
|
|
@@ -33260,8 +33455,9 @@ async function loadBridge2() {
|
|
|
33260
33455
|
}
|
|
33261
33456
|
|
|
33262
33457
|
// src/strategies/fallback/index.ts
|
|
33263
|
-
|
|
33264
|
-
var
|
|
33458
|
+
init_debug();
|
|
33459
|
+
var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
|
|
33460
|
+
var READY_TIMEOUT_SECONDS2 = 3;
|
|
33265
33461
|
async function createFallbackSession(ctx, target) {
|
|
33266
33462
|
const { normalizeSource: normalizeSource2 } = await Promise.resolve().then(() => (init_source(), source_exports));
|
|
33267
33463
|
const source = await normalizeSource2(ctx.source);
|
|
@@ -33289,6 +33485,10 @@ async function createFallbackSession(ctx, target) {
|
|
|
33289
33485
|
void doSeek(v);
|
|
33290
33486
|
}
|
|
33291
33487
|
});
|
|
33488
|
+
Object.defineProperty(target, "paused", {
|
|
33489
|
+
configurable: true,
|
|
33490
|
+
get: () => !audio.isPlaying()
|
|
33491
|
+
});
|
|
33292
33492
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
33293
33493
|
Object.defineProperty(target, "duration", {
|
|
33294
33494
|
configurable: true,
|
|
@@ -33297,12 +33497,36 @@ async function createFallbackSession(ctx, target) {
|
|
|
33297
33497
|
}
|
|
33298
33498
|
async function waitForBuffer() {
|
|
33299
33499
|
const start = performance.now();
|
|
33500
|
+
let firstFrameAtMs = 0;
|
|
33501
|
+
dbg.info(
|
|
33502
|
+
"cold-start",
|
|
33503
|
+
`gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
|
|
33504
|
+
);
|
|
33300
33505
|
while (true) {
|
|
33301
|
-
const
|
|
33302
|
-
|
|
33506
|
+
const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
|
|
33507
|
+
const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
|
|
33508
|
+
const hasFrames = renderer.hasFrames();
|
|
33509
|
+
const nowMs = performance.now();
|
|
33510
|
+
if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
|
|
33511
|
+
if (audioReady && hasFrames) {
|
|
33512
|
+
dbg.info(
|
|
33513
|
+
"cold-start",
|
|
33514
|
+
`gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
|
|
33515
|
+
);
|
|
33303
33516
|
return;
|
|
33304
33517
|
}
|
|
33305
|
-
if (
|
|
33518
|
+
if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
|
|
33519
|
+
dbg.info(
|
|
33520
|
+
"cold-start",
|
|
33521
|
+
`gate released on video-only grace at ${(nowMs - start).toFixed(0)}ms (frames=${renderer.queueDepth()}, audio=${(audioAhead * 1e3).toFixed(0)}ms \u2014 demuxer hasn't delivered audio packets yet, starting anyway and letting the audio scheduler catch up at its media-time anchor)`
|
|
33522
|
+
);
|
|
33523
|
+
return;
|
|
33524
|
+
}
|
|
33525
|
+
if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
|
|
33526
|
+
dbg.diag(
|
|
33527
|
+
"cold-start",
|
|
33528
|
+
`gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651). Decoder produced nothing in ${READY_TIMEOUT_SECONDS2}s \u2014 either a corrupt source, a missing codec, or WASM is catastrophically slow on this file. Check getDiagnostics().runtime for decode counters.`
|
|
33529
|
+
);
|
|
33306
33530
|
return;
|
|
33307
33531
|
}
|
|
33308
33532
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -33350,6 +33574,7 @@ async function createFallbackSession(ctx, target) {
|
|
|
33350
33574
|
try {
|
|
33351
33575
|
delete target.currentTime;
|
|
33352
33576
|
delete target.duration;
|
|
33577
|
+
delete target.paused;
|
|
33353
33578
|
} catch {
|
|
33354
33579
|
}
|
|
33355
33580
|
},
|
|
@@ -33413,8 +33638,8 @@ function convertTiming(line) {
|
|
|
33413
33638
|
line.trim()
|
|
33414
33639
|
);
|
|
33415
33640
|
if (!m) return null;
|
|
33416
|
-
const
|
|
33417
|
-
return `${
|
|
33641
|
+
const fmt2 = (h, mm, s, ms) => `${h.padStart(2, "0")}:${mm}:${s}.${ms.padEnd(3, "0").slice(0, 3)}`;
|
|
33642
|
+
return `${fmt2(m[1], m[2], m[3], m[4])} --> ${fmt2(m[5], m[6], m[7], m[8])}${m[9] ?? ""}`;
|
|
33418
33643
|
}
|
|
33419
33644
|
|
|
33420
33645
|
// src/subtitles/vtt.ts
|
|
@@ -33500,6 +33725,7 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
|
33500
33725
|
}
|
|
33501
33726
|
|
|
33502
33727
|
// src/player.ts
|
|
33728
|
+
init_debug();
|
|
33503
33729
|
var UnifiedPlayer = class _UnifiedPlayer {
|
|
33504
33730
|
/**
|
|
33505
33731
|
* @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
|
|
@@ -33522,6 +33748,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33522
33748
|
lastProgressTime = 0;
|
|
33523
33749
|
lastProgressPosition = -1;
|
|
33524
33750
|
errorListener = null;
|
|
33751
|
+
// Bound so we can removeEventListener in destroy(); without this the
|
|
33752
|
+
// listener outlives the player and accumulates on elements that swap
|
|
33753
|
+
// source (e.g. <avbridge-video>).
|
|
33754
|
+
endedListener = null;
|
|
33525
33755
|
// Serializes escalation / setStrategy calls
|
|
33526
33756
|
switchingPromise = Promise.resolve();
|
|
33527
33757
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
@@ -33547,8 +33777,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33547
33777
|
return player;
|
|
33548
33778
|
}
|
|
33549
33779
|
async bootstrap() {
|
|
33780
|
+
const bootstrapStart = performance.now();
|
|
33550
33781
|
try {
|
|
33551
|
-
|
|
33782
|
+
dbg.info("bootstrap", "start");
|
|
33783
|
+
const ctx = await dbg.timed("probe", "probe", 3e3, () => probe(this.options.source));
|
|
33784
|
+
dbg.info(
|
|
33785
|
+
"probe",
|
|
33786
|
+
`container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
|
|
33787
|
+
);
|
|
33552
33788
|
this.diag.recordProbe(ctx);
|
|
33553
33789
|
this.mediaContext = ctx;
|
|
33554
33790
|
if (this.options.subtitles) {
|
|
@@ -33574,6 +33810,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33574
33810
|
}
|
|
33575
33811
|
}
|
|
33576
33812
|
const decision = this.options.initialStrategy ? buildInitialDecision(this.options.initialStrategy, ctx) : classifyContext(ctx);
|
|
33813
|
+
dbg.info(
|
|
33814
|
+
"classify",
|
|
33815
|
+
`strategy=${decision.strategy} class=${decision.class} reason="${decision.reason}"` + (decision.fallbackChain ? ` fallback=${decision.fallbackChain.join("\u2192")}` : "")
|
|
33816
|
+
);
|
|
33577
33817
|
this.classification = decision;
|
|
33578
33818
|
this.diag.recordClassification(decision);
|
|
33579
33819
|
this.emitter.emitSticky("strategy", {
|
|
@@ -33597,8 +33837,17 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33597
33837
|
subtitle: ctx.subtitleTracks
|
|
33598
33838
|
});
|
|
33599
33839
|
this.startTimeupdateLoop();
|
|
33600
|
-
this.
|
|
33840
|
+
this.endedListener = () => this.emitter.emit("ended", void 0);
|
|
33841
|
+
this.options.target.addEventListener("ended", this.endedListener);
|
|
33601
33842
|
this.emitter.emitSticky("ready", void 0);
|
|
33843
|
+
const bootstrapElapsed = performance.now() - bootstrapStart;
|
|
33844
|
+
dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
|
|
33845
|
+
if (bootstrapElapsed > 5e3) {
|
|
33846
|
+
console.warn(
|
|
33847
|
+
"[avbridge:bootstrap]",
|
|
33848
|
+
`total bootstrap time ${bootstrapElapsed.toFixed(0)}ms \u2014 unusually slow. Enable globalThis.AVBRIDGE_DEBUG for a per-phase breakdown.`
|
|
33849
|
+
);
|
|
33850
|
+
}
|
|
33602
33851
|
} catch (err) {
|
|
33603
33852
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
33604
33853
|
this.diag.recordError(e);
|
|
@@ -33875,6 +34124,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33875
34124
|
this.timeupdateInterval = null;
|
|
33876
34125
|
}
|
|
33877
34126
|
this.clearSupervisor();
|
|
34127
|
+
if (this.endedListener) {
|
|
34128
|
+
this.options.target.removeEventListener("ended", this.endedListener);
|
|
34129
|
+
this.endedListener = null;
|
|
34130
|
+
}
|
|
33878
34131
|
if (this.session) {
|
|
33879
34132
|
await this.session.destroy();
|
|
33880
34133
|
this.session = null;
|
|
@@ -33889,11 +34142,13 @@ async function createPlayer(options) {
|
|
|
33889
34142
|
function buildInitialDecision(initial, ctx) {
|
|
33890
34143
|
const natural = classifyContext(ctx);
|
|
33891
34144
|
const cls = strategyToClass(initial, natural);
|
|
34145
|
+
const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
|
|
34146
|
+
const fallbackChain = inherited.filter((s) => s !== initial);
|
|
33892
34147
|
return {
|
|
33893
34148
|
class: cls,
|
|
33894
34149
|
strategy: initial,
|
|
33895
34150
|
reason: `initial strategy "${initial}" requested via options.initialStrategy`,
|
|
33896
|
-
fallbackChain
|
|
34151
|
+
fallbackChain
|
|
33897
34152
|
};
|
|
33898
34153
|
}
|
|
33899
34154
|
function strategyToClass(strategy, natural) {
|