avbridge 2.11.0 → 2.12.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 +111 -0
- package/dist/{avi-B5CQYB7L.cjs → avi-32UABODO.cjs} +14 -6
- package/dist/avi-32UABODO.cjs.map +1 -0
- package/dist/{avi-2ILLBNPQ.cjs → avi-5BPR6QUX.cjs} +14 -6
- package/dist/avi-5BPR6QUX.cjs.map +1 -0
- package/dist/{avi-RWWPN2PR.js → avi-BLIH7KKV.js} +13 -5
- package/dist/avi-BLIH7KKV.js.map +1 -0
- package/dist/{avi-JXU4GQL2.js → avi-GX2H34IQ.js} +13 -5
- package/dist/avi-GX2H34IQ.js.map +1 -0
- package/dist/{chunk-DCSOQH2N.js → chunk-3AI5WFFN.js} +40 -16
- package/dist/chunk-3AI5WFFN.js.map +1 -0
- package/dist/{chunk-2NSOOMXW.js → chunk-3YKWU4FM.js} +3 -3
- package/dist/{chunk-2NSOOMXW.js.map → chunk-3YKWU4FM.js.map} +1 -1
- package/dist/{chunk-GYIJU44C.js → chunk-5CX7BVVV.js} +5 -5
- package/dist/{chunk-GYIJU44C.js.map → chunk-5CX7BVVV.js.map} +1 -1
- package/dist/{chunk-CL6UEUQF.js → chunk-B76QWPFM.js} +5 -5
- package/dist/{chunk-CL6UEUQF.js.map → chunk-B76QWPFM.js.map} +1 -1
- package/dist/{chunk-IHNHHEA2.js → chunk-BN7BRTLY.js} +143 -32
- package/dist/chunk-BN7BRTLY.js.map +1 -0
- package/dist/{chunk-OTFS7DC4.cjs → chunk-E5MAM2P4.cjs} +14 -14
- package/dist/{chunk-OTFS7DC4.cjs.map → chunk-E5MAM2P4.cjs.map} +1 -1
- package/dist/{chunk-L7A3ECI2.cjs → chunk-HZUVMXBN.cjs} +4 -4
- package/dist/{chunk-L7A3ECI2.cjs.map → chunk-HZUVMXBN.cjs.map} +1 -1
- package/dist/{chunk-37UOSAVI.cjs → chunk-UM6WCSGL.cjs} +157 -46
- package/dist/chunk-UM6WCSGL.cjs.map +1 -0
- package/dist/{chunk-BYGZN4Z5.cjs → chunk-VLI3Y6IJ.cjs} +5 -5
- package/dist/{chunk-BYGZN4Z5.cjs.map → chunk-VLI3Y6IJ.cjs.map} +1 -1
- package/dist/{chunk-Z33SBWL5.cjs → chunk-YPZFGJV3.cjs} +40 -16
- package/dist/chunk-YPZFGJV3.cjs.map +1 -0
- package/dist/element-browser.js +186 -43
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +5 -5
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +4 -4
- package/dist/index.cjs +21 -21
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -9
- package/dist/{libav-demux-3N5Y3VQA.cjs → libav-demux-575OYCT2.cjs} +9 -9
- package/dist/{libav-demux-3N5Y3VQA.cjs.map → libav-demux-575OYCT2.cjs.map} +1 -1
- package/dist/{libav-demux-JXD4OTLM.js → libav-demux-SXZDLC7W.js} +4 -4
- package/dist/{libav-demux-JXD4OTLM.js.map → libav-demux-SXZDLC7W.js.map} +1 -1
- package/dist/libav-http-reader-2S5HAHW4.js +3 -0
- package/dist/{libav-http-reader-WXG3Z7AI.js.map → libav-http-reader-2S5HAHW4.js.map} +1 -1
- package/dist/libav-http-reader-Q356EO2K.cjs +16 -0
- package/dist/{libav-http-reader-AZLE7YFS.cjs.map → libav-http-reader-Q356EO2K.cjs.map} +1 -1
- package/dist/{player-DDdNVFDv.d.cts → player-bQ6n4hVp.d.cts} +15 -0
- package/dist/{player-DDdNVFDv.d.ts → player-bQ6n4hVp.d.ts} +15 -0
- package/dist/player.cjs +264 -53
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +22 -0
- package/dist/player.d.ts +22 -0
- package/dist/player.js +264 -53
- package/dist/player.js.map +1 -1
- package/dist/remux-NSBJFMLG.cjs +35 -0
- package/dist/{remux-KUS5GIL6.cjs.map → remux-NSBJFMLG.cjs.map} +1 -1
- package/dist/remux-PHUHO3VV.js +10 -0
- package/dist/{remux-56V7LDAD.js.map → remux-PHUHO3VV.js.map} +1 -1
- package/package.json +1 -1
- package/src/element/avbridge-player.ts +123 -23
- package/src/element/player-styles.ts +13 -1
- package/src/player.ts +3 -3
- package/src/probe/avi.ts +34 -2
- package/src/strategies/fallback/decoder.ts +148 -19
- package/src/strategies/fallback/video-renderer.ts +41 -3
- package/src/strategies/hybrid/decoder.ts +34 -9
- package/src/types.ts +15 -0
- package/src/util/libav-http-reader.ts +58 -19
- 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/vendor/libav/avbridge/libav-avbridge.mjs +1 -1
- package/dist/avi-2ILLBNPQ.cjs.map +0 -1
- package/dist/avi-B5CQYB7L.cjs.map +0 -1
- package/dist/avi-JXU4GQL2.js.map +0 -1
- package/dist/avi-RWWPN2PR.js.map +0 -1
- package/dist/chunk-37UOSAVI.cjs.map +0 -1
- package/dist/chunk-DCSOQH2N.js.map +0 -1
- package/dist/chunk-IHNHHEA2.js.map +0 -1
- package/dist/chunk-Z33SBWL5.cjs.map +0 -1
- package/dist/libav-http-reader-AZLE7YFS.cjs +0 -16
- package/dist/libav-http-reader-WXG3Z7AI.js +0 -3
- package/dist/remux-56V7LDAD.js +0 -10
- package/dist/remux-KUS5GIL6.cjs +0 -35
package/dist/player.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var chunk2XW2O3YI_cjs = require('./chunk-2XW2O3YI.cjs');
|
|
4
4
|
var chunkNNVOHKXJ_cjs = require('./chunk-NNVOHKXJ.cjs');
|
|
5
|
-
require('./chunk-
|
|
5
|
+
require('./chunk-YPZFGJV3.cjs');
|
|
6
6
|
var chunkWRKO6Q42_cjs = require('./chunk-WRKO6Q42.cjs');
|
|
7
7
|
require('./chunk-QDJLQR53.cjs');
|
|
8
8
|
|
|
@@ -239,7 +239,7 @@ async function probe(source, transport) {
|
|
|
239
239
|
const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
|
|
240
240
|
if (hasUnknownCodec) {
|
|
241
241
|
try {
|
|
242
|
-
const { probeWithLibav } = await import('./avi-
|
|
242
|
+
const { probeWithLibav } = await import('./avi-5BPR6QUX.cjs');
|
|
243
243
|
return await probeWithLibav(normalized, sniffed);
|
|
244
244
|
} catch {
|
|
245
245
|
return result;
|
|
@@ -252,7 +252,7 @@ async function probe(source, transport) {
|
|
|
252
252
|
mediabunnyErr.message
|
|
253
253
|
);
|
|
254
254
|
try {
|
|
255
|
-
const { probeWithLibav } = await import('./avi-
|
|
255
|
+
const { probeWithLibav } = await import('./avi-5BPR6QUX.cjs');
|
|
256
256
|
return await probeWithLibav(normalized, sniffed);
|
|
257
257
|
} catch (libavErr) {
|
|
258
258
|
const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
|
|
@@ -266,7 +266,7 @@ async function probe(source, transport) {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
try {
|
|
269
|
-
const { probeWithLibav } = await import('./avi-
|
|
269
|
+
const { probeWithLibav } = await import('./avi-5BPR6QUX.cjs');
|
|
270
270
|
return await probeWithLibav(normalized, sniffed);
|
|
271
271
|
} catch (err) {
|
|
272
272
|
const inner = err instanceof Error ? err.message : String(err);
|
|
@@ -1335,10 +1335,20 @@ var VideoRenderer = class {
|
|
|
1335
1335
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
1336
1336
|
firstFrameReady;
|
|
1337
1337
|
resolveFirstFrame;
|
|
1338
|
-
/**
|
|
1338
|
+
/**
|
|
1339
|
+
* True once at least one frame has been enqueued *since the last flush*.
|
|
1340
|
+
* Used by `readyState` — initial cold-start reports HAVE_NOTHING until
|
|
1341
|
+
* any frame has arrived, and after a seek we want the same semantics
|
|
1342
|
+
* (HAVE_NOTHING until post-seek frames arrive), so the cumulative
|
|
1343
|
+
* `framesPainted > 0` that used to live here was wrong: it kept the
|
|
1344
|
+
* state "true forever" after the first frame ever, so post-seek
|
|
1345
|
+
* `waitForBuffer()` would exit immediately with an empty queue and
|
|
1346
|
+
* leave video frozen while audio kept going.
|
|
1347
|
+
*/
|
|
1339
1348
|
hasFrames() {
|
|
1340
|
-
return this.queue.length > 0 || this.
|
|
1349
|
+
return this.queue.length > 0 || this.hasEverEnqueuedSinceFlush;
|
|
1341
1350
|
}
|
|
1351
|
+
hasEverEnqueuedSinceFlush = false;
|
|
1342
1352
|
/** Current depth of the frame queue. Used by the decoder for backpressure. */
|
|
1343
1353
|
queueDepth() {
|
|
1344
1354
|
return this.queue.length;
|
|
@@ -1357,6 +1367,7 @@ var VideoRenderer = class {
|
|
|
1357
1367
|
return;
|
|
1358
1368
|
}
|
|
1359
1369
|
this.queue.push(frame);
|
|
1370
|
+
this.hasEverEnqueuedSinceFlush = true;
|
|
1360
1371
|
if (this.queue.length === 1 && this.framesPainted === 0) {
|
|
1361
1372
|
this.resolveFirstFrame();
|
|
1362
1373
|
}
|
|
@@ -1490,7 +1501,8 @@ var VideoRenderer = class {
|
|
|
1490
1501
|
}
|
|
1491
1502
|
return;
|
|
1492
1503
|
}
|
|
1493
|
-
const
|
|
1504
|
+
const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
|
|
1505
|
+
const dropThresholdUs = _relaxDrop ? audioNowUs - 60 * 1e6 : audioNowUs - frameDurationUs * 2;
|
|
1494
1506
|
let dropped = 0;
|
|
1495
1507
|
while (bestIdx > 0) {
|
|
1496
1508
|
const ts = this.queue[0].timestamp ?? 0;
|
|
@@ -1551,16 +1563,28 @@ var VideoRenderer = class {
|
|
|
1551
1563
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
1552
1564
|
this.prerolled = false;
|
|
1553
1565
|
this.ptsCalibrated = false;
|
|
1566
|
+
this.hasEverEnqueuedSinceFlush = false;
|
|
1554
1567
|
if (isDebug() && count > 0) {
|
|
1555
1568
|
console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
|
|
1556
1569
|
}
|
|
1557
1570
|
}
|
|
1558
1571
|
stats() {
|
|
1572
|
+
let queueSpanMs = 0;
|
|
1573
|
+
let queueHeadMs = 0;
|
|
1574
|
+
let queueTailMs = 0;
|
|
1575
|
+
if (this.queue.length > 0) {
|
|
1576
|
+
queueHeadMs = Math.round((this.queue[0].timestamp ?? 0) / 1e3);
|
|
1577
|
+
queueTailMs = Math.round((this.queue[this.queue.length - 1].timestamp ?? 0) / 1e3);
|
|
1578
|
+
queueSpanMs = Math.max(0, queueTailMs - queueHeadMs);
|
|
1579
|
+
}
|
|
1559
1580
|
return {
|
|
1560
1581
|
framesPainted: this.framesPainted,
|
|
1561
1582
|
framesDroppedLate: this.framesDroppedLate,
|
|
1562
1583
|
framesDroppedOverflow: this.framesDroppedOverflow,
|
|
1563
|
-
queueDepth: this.queue.length
|
|
1584
|
+
queueDepth: this.queue.length,
|
|
1585
|
+
queueHeadMs,
|
|
1586
|
+
queueTailMs,
|
|
1587
|
+
queueSpanMs
|
|
1564
1588
|
};
|
|
1565
1589
|
}
|
|
1566
1590
|
destroy() {
|
|
@@ -2023,7 +2047,7 @@ async function startHybridDecoder(opts) {
|
|
|
2023
2047
|
const variant = pickLibavVariant(opts.context);
|
|
2024
2048
|
const libav = await chunkNNVOHKXJ_cjs.loadLibav(variant);
|
|
2025
2049
|
const bridge = await loadBridge();
|
|
2026
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
2050
|
+
const { prepareLibavInput } = await import('./libav-http-reader-Q356EO2K.cjs');
|
|
2027
2051
|
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
2028
2052
|
const readPkt = await libav.av_packet_alloc();
|
|
2029
2053
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
@@ -2098,6 +2122,7 @@ async function startHybridDecoder(opts) {
|
|
|
2098
2122
|
}
|
|
2099
2123
|
let bsfCtx = null;
|
|
2100
2124
|
let bsfPkt = null;
|
|
2125
|
+
let bsfRequiredButMissing = false;
|
|
2101
2126
|
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
2102
2127
|
try {
|
|
2103
2128
|
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
@@ -2108,13 +2133,19 @@ async function startHybridDecoder(opts) {
|
|
|
2108
2133
|
bsfPkt = await libav.av_packet_alloc();
|
|
2109
2134
|
chunkNNVOHKXJ_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
|
|
2110
2135
|
} else {
|
|
2111
|
-
|
|
2136
|
+
bsfRequiredButMissing = true;
|
|
2112
2137
|
bsfCtx = null;
|
|
2113
2138
|
}
|
|
2114
2139
|
} catch (err) {
|
|
2115
|
-
|
|
2140
|
+
bsfRequiredButMissing = true;
|
|
2116
2141
|
bsfCtx = null;
|
|
2117
2142
|
bsfPkt = null;
|
|
2143
|
+
chunkNNVOHKXJ_cjs.dbg.warn("bsf", `hybrid: mpeg4_unpack_bframes BSF init failed: ${err.message}`);
|
|
2144
|
+
}
|
|
2145
|
+
if (bsfRequiredButMissing) {
|
|
2146
|
+
console.error(
|
|
2147
|
+
"[avbridge] MPEG-4 Part 2 (DivX/Xvid) detected but mpeg4_unpack_bframes BSF is unavailable in this libav variant. Files with packed B-frames will play with incorrect frame ordering. Rebuild the libav variant with the `avbsf` fragment included."
|
|
2148
|
+
);
|
|
2118
2149
|
}
|
|
2119
2150
|
}
|
|
2120
2151
|
async function applyBSF(packets) {
|
|
@@ -2124,7 +2155,6 @@ async function startHybridDecoder(opts) {
|
|
|
2124
2155
|
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
2125
2156
|
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
2126
2157
|
if (sendErr < 0) {
|
|
2127
|
-
out.push(pkt);
|
|
2128
2158
|
continue;
|
|
2129
2159
|
}
|
|
2130
2160
|
while (true) {
|
|
@@ -2138,10 +2168,13 @@ async function startHybridDecoder(opts) {
|
|
|
2138
2168
|
async function flushBSF() {
|
|
2139
2169
|
if (!bsfCtx || !bsfPkt) return;
|
|
2140
2170
|
try {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2171
|
+
if (libav.av_bsf_flush) {
|
|
2172
|
+
await libav.av_bsf_flush(bsfCtx);
|
|
2173
|
+
} else {
|
|
2174
|
+
while (true) {
|
|
2175
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2176
|
+
if (err < 0) break;
|
|
2177
|
+
}
|
|
2145
2178
|
}
|
|
2146
2179
|
} catch {
|
|
2147
2180
|
}
|
|
@@ -2453,6 +2486,7 @@ async function startHybridDecoder(opts) {
|
|
|
2453
2486
|
videoChunksFed,
|
|
2454
2487
|
audioFramesDecoded,
|
|
2455
2488
|
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
2489
|
+
bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
|
|
2456
2490
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
2457
2491
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
2458
2492
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -2693,7 +2727,7 @@ async function startDecoder(opts) {
|
|
|
2693
2727
|
const variant = "avbridge";
|
|
2694
2728
|
const libav = await chunkNNVOHKXJ_cjs.loadLibav(variant);
|
|
2695
2729
|
const bridge = await loadBridge2();
|
|
2696
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
2730
|
+
const { prepareLibavInput } = await import('./libav-http-reader-Q356EO2K.cjs');
|
|
2697
2731
|
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
2698
2732
|
const readPkt = await libav.av_packet_alloc();
|
|
2699
2733
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
@@ -2752,6 +2786,7 @@ async function startDecoder(opts) {
|
|
|
2752
2786
|
}
|
|
2753
2787
|
let bsfCtx = null;
|
|
2754
2788
|
let bsfPkt = null;
|
|
2789
|
+
let bsfRequiredButMissing = false;
|
|
2755
2790
|
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
2756
2791
|
try {
|
|
2757
2792
|
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
@@ -2762,13 +2797,19 @@ async function startDecoder(opts) {
|
|
|
2762
2797
|
bsfPkt = await libav.av_packet_alloc();
|
|
2763
2798
|
chunkNNVOHKXJ_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
|
|
2764
2799
|
} else {
|
|
2765
|
-
|
|
2800
|
+
bsfRequiredButMissing = true;
|
|
2766
2801
|
bsfCtx = null;
|
|
2767
2802
|
}
|
|
2768
2803
|
} catch (err) {
|
|
2769
|
-
|
|
2804
|
+
bsfRequiredButMissing = true;
|
|
2770
2805
|
bsfCtx = null;
|
|
2771
2806
|
bsfPkt = null;
|
|
2807
|
+
chunkNNVOHKXJ_cjs.dbg.warn("bsf", `mpeg4_unpack_bframes BSF init failed: ${err.message}`);
|
|
2808
|
+
}
|
|
2809
|
+
if (bsfRequiredButMissing) {
|
|
2810
|
+
console.error(
|
|
2811
|
+
"[avbridge] MPEG-4 Part 2 (DivX/Xvid) detected but mpeg4_unpack_bframes BSF is unavailable in this libav variant. Files with packed B-frames will play with incorrect frame ordering (backwards PTS jumps, heavy late-drop stuttering). Rebuild the libav variant with the `avbsf` fragment included. See docs/dev/POSTMORTEMS.md for details."
|
|
2812
|
+
);
|
|
2772
2813
|
}
|
|
2773
2814
|
}
|
|
2774
2815
|
async function applyBSF(packets) {
|
|
@@ -2778,7 +2819,6 @@ async function startDecoder(opts) {
|
|
|
2778
2819
|
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
2779
2820
|
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
2780
2821
|
if (sendErr < 0) {
|
|
2781
|
-
out.push(pkt);
|
|
2782
2822
|
continue;
|
|
2783
2823
|
}
|
|
2784
2824
|
while (true) {
|
|
@@ -2792,10 +2832,13 @@ async function startDecoder(opts) {
|
|
|
2792
2832
|
async function flushBSF() {
|
|
2793
2833
|
if (!bsfCtx || !bsfPkt) return;
|
|
2794
2834
|
try {
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2835
|
+
if (libav.av_bsf_flush) {
|
|
2836
|
+
await libav.av_bsf_flush(bsfCtx);
|
|
2837
|
+
} else {
|
|
2838
|
+
while (true) {
|
|
2839
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2840
|
+
if (err < 0) break;
|
|
2841
|
+
}
|
|
2799
2842
|
}
|
|
2800
2843
|
} catch {
|
|
2801
2844
|
}
|
|
@@ -2813,6 +2856,19 @@ async function startDecoder(opts) {
|
|
|
2813
2856
|
let watchdogOverflowWarned = false;
|
|
2814
2857
|
let syntheticVideoUs = 0;
|
|
2815
2858
|
let syntheticAudioUs = 0;
|
|
2859
|
+
let videoDecodeMsTotal = 0;
|
|
2860
|
+
let audioDecodeMsTotal = 0;
|
|
2861
|
+
let videoDecodeBatches = 0;
|
|
2862
|
+
let audioDecodeBatches = 0;
|
|
2863
|
+
let readMsTotal = 0;
|
|
2864
|
+
let readBatches = 0;
|
|
2865
|
+
let pumpThrottleMsTotal = 0;
|
|
2866
|
+
let pumpThrottleEntries = 0;
|
|
2867
|
+
let slowestVideoBatchMs = 0;
|
|
2868
|
+
let newestVideoPtsUs = 0;
|
|
2869
|
+
let lastEmittedPtsUs = -1;
|
|
2870
|
+
let ptsRegressions = 0;
|
|
2871
|
+
let worstPtsRegressionMs = 0;
|
|
2816
2872
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
2817
2873
|
const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
|
|
2818
2874
|
const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
|
|
@@ -2821,9 +2877,12 @@ async function startDecoder(opts) {
|
|
|
2821
2877
|
let readErr;
|
|
2822
2878
|
let packets;
|
|
2823
2879
|
try {
|
|
2880
|
+
const _readStart = performance.now();
|
|
2824
2881
|
[readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
|
|
2825
2882
|
limit: 16 * 1024
|
|
2826
2883
|
});
|
|
2884
|
+
readMsTotal += performance.now() - _readStart;
|
|
2885
|
+
readBatches++;
|
|
2827
2886
|
} catch (err) {
|
|
2828
2887
|
console.error("[avbridge] ff_read_frame_multi failed:", err);
|
|
2829
2888
|
return;
|
|
@@ -2885,8 +2944,17 @@ async function startDecoder(opts) {
|
|
|
2885
2944
|
}
|
|
2886
2945
|
}
|
|
2887
2946
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2947
|
+
{
|
|
2948
|
+
const _throttleStart = performance.now();
|
|
2949
|
+
let _throttled = false;
|
|
2950
|
+
while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
2951
|
+
_throttled = true;
|
|
2952
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2953
|
+
}
|
|
2954
|
+
if (_throttled) {
|
|
2955
|
+
pumpThrottleMsTotal += performance.now() - _throttleStart;
|
|
2956
|
+
pumpThrottleEntries++;
|
|
2957
|
+
}
|
|
2890
2958
|
}
|
|
2891
2959
|
if (readErr === libav.AVERROR_EOF) {
|
|
2892
2960
|
if (videoDec) await decodeVideoBatch(
|
|
@@ -2912,6 +2980,7 @@ async function startDecoder(opts) {
|
|
|
2912
2980
|
async function decodeVideoBatch(pkts, myToken, flush = false) {
|
|
2913
2981
|
if (!videoDec || destroyed || myToken !== pumpToken) return;
|
|
2914
2982
|
let frames;
|
|
2983
|
+
const _t0 = performance.now();
|
|
2915
2984
|
try {
|
|
2916
2985
|
frames = await libav.ff_decode_multi(
|
|
2917
2986
|
videoDec.c,
|
|
@@ -2924,18 +2993,38 @@ async function startDecoder(opts) {
|
|
|
2924
2993
|
console.error("[avbridge] video decode batch failed:", err);
|
|
2925
2994
|
return;
|
|
2926
2995
|
}
|
|
2996
|
+
{
|
|
2997
|
+
const _dt = performance.now() - _t0;
|
|
2998
|
+
videoDecodeMsTotal += _dt;
|
|
2999
|
+
videoDecodeBatches++;
|
|
3000
|
+
if (_dt > slowestVideoBatchMs) slowestVideoBatchMs = _dt;
|
|
3001
|
+
}
|
|
2927
3002
|
if (myToken !== pumpToken || destroyed) return;
|
|
2928
3003
|
for (const f of frames) {
|
|
2929
3004
|
if (myToken !== pumpToken || destroyed) return;
|
|
2930
3005
|
sanitizeFrameTimestamp(
|
|
2931
3006
|
f,
|
|
2932
3007
|
() => {
|
|
2933
|
-
const
|
|
2934
|
-
syntheticVideoUs
|
|
2935
|
-
return
|
|
3008
|
+
const base = lastEmittedPtsUs >= 0 ? lastEmittedPtsUs + videoFrameStepUs : syntheticVideoUs;
|
|
3009
|
+
syntheticVideoUs = base + videoFrameStepUs;
|
|
3010
|
+
return base;
|
|
2936
3011
|
},
|
|
2937
3012
|
videoTimeBase
|
|
2938
3013
|
);
|
|
3014
|
+
const _fPts = (f.ptshi ?? 0) * 4294967296 + (f.pts ?? 0);
|
|
3015
|
+
if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
|
|
3016
|
+
if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
|
|
3017
|
+
ptsRegressions++;
|
|
3018
|
+
const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
|
|
3019
|
+
if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
|
|
3020
|
+
if (ptsRegressions <= 10) {
|
|
3021
|
+
console.warn(
|
|
3022
|
+
`[avbridge:decoder] dropped out-of-order frame #${ptsRegressions}: pts=${(_fPts / 1e3).toFixed(1)}ms < previous=${(lastEmittedPtsUs / 1e3).toFixed(1)}ms (regression=${regressMs.toFixed(1)}ms). Typically a post-seek B-frame reorder tail.`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
lastEmittedPtsUs = _fPts;
|
|
2939
3028
|
try {
|
|
2940
3029
|
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
2941
3030
|
opts.renderer.enqueue(vf);
|
|
@@ -2950,6 +3039,7 @@ async function startDecoder(opts) {
|
|
|
2950
3039
|
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
2951
3040
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
2952
3041
|
let frames;
|
|
3042
|
+
const _t0 = performance.now();
|
|
2953
3043
|
try {
|
|
2954
3044
|
frames = await libav.ff_decode_multi(
|
|
2955
3045
|
audioDec.c,
|
|
@@ -2962,6 +3052,8 @@ async function startDecoder(opts) {
|
|
|
2962
3052
|
console.error("[avbridge] audio decode batch failed:", err);
|
|
2963
3053
|
return;
|
|
2964
3054
|
}
|
|
3055
|
+
audioDecodeMsTotal += performance.now() - _t0;
|
|
3056
|
+
audioDecodeBatches++;
|
|
2965
3057
|
if (myToken !== pumpToken || destroyed) return;
|
|
2966
3058
|
for (const f of frames) {
|
|
2967
3059
|
if (myToken !== pumpToken || destroyed) return;
|
|
@@ -3083,6 +3175,7 @@ async function startDecoder(opts) {
|
|
|
3083
3175
|
await flushBSF();
|
|
3084
3176
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
3085
3177
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
3178
|
+
lastEmittedPtsUs = -1;
|
|
3086
3179
|
pumpRunning = pumpLoop(newToken).catch(
|
|
3087
3180
|
(err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
|
|
3088
3181
|
);
|
|
@@ -3120,6 +3213,7 @@ async function startDecoder(opts) {
|
|
|
3120
3213
|
await flushBSF();
|
|
3121
3214
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
3122
3215
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
3216
|
+
lastEmittedPtsUs = -1;
|
|
3123
3217
|
pumpRunning = pumpLoop(newToken).catch(
|
|
3124
3218
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
3125
3219
|
);
|
|
@@ -3133,7 +3227,24 @@ async function startDecoder(opts) {
|
|
|
3133
3227
|
packetsRead,
|
|
3134
3228
|
videoFramesDecoded,
|
|
3135
3229
|
audioFramesDecoded,
|
|
3230
|
+
// Throughput instrumentation — the stats panel turns these into
|
|
3231
|
+
// "decode fps actual / realtime target" and shows slowest batch
|
|
3232
|
+
// + producer throttle share.
|
|
3233
|
+
videoDecodeMsTotal,
|
|
3234
|
+
videoDecodeBatches,
|
|
3235
|
+
audioDecodeMsTotal,
|
|
3236
|
+
audioDecodeBatches,
|
|
3237
|
+
readMsTotal,
|
|
3238
|
+
readBatches,
|
|
3239
|
+
pumpThrottleMsTotal,
|
|
3240
|
+
pumpThrottleEntries,
|
|
3241
|
+
slowestVideoBatchMs,
|
|
3242
|
+
newestVideoPtsMs: Math.round(newestVideoPtsUs / 1e3),
|
|
3243
|
+
ptsRegressions,
|
|
3244
|
+
worstPtsRegressionMs,
|
|
3245
|
+
sourceFps: videoFps,
|
|
3136
3246
|
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
3247
|
+
bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
|
|
3137
3248
|
// Confirmed transport info: once prepareLibavInput returns
|
|
3138
3249
|
// successfully, we *know* whether the source is http-range (probe
|
|
3139
3250
|
// succeeded and returned 206) or in-memory blob. Diagnostics hoists
|
|
@@ -3422,9 +3533,9 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3422
3533
|
constructor(options, registry) {
|
|
3423
3534
|
this.options = options;
|
|
3424
3535
|
this.registry = registry;
|
|
3425
|
-
const { requestInit, fetchFn } = options;
|
|
3426
|
-
if (requestInit || fetchFn) {
|
|
3427
|
-
this.transport = { requestInit, fetchFn };
|
|
3536
|
+
const { requestInit, fetchFn, cacheBytes } = options;
|
|
3537
|
+
if (requestInit || fetchFn || cacheBytes !== void 0) {
|
|
3538
|
+
this.transport = { requestInit, fetchFn, cacheBytes };
|
|
3428
3539
|
}
|
|
3429
3540
|
}
|
|
3430
3541
|
options;
|
|
@@ -5035,6 +5146,12 @@ var PLAYER_STYLES = (
|
|
|
5035
5146
|
display: flex;
|
|
5036
5147
|
align-items: center;
|
|
5037
5148
|
cursor: pointer;
|
|
5149
|
+
/* Claim all touch gestures on the seek bar. Without this, Android
|
|
5150
|
+
* browsers (Chrome, Samsung Internet) treat horizontal drags as
|
|
5151
|
+
* scroll candidates and cancel pointermove once the gesture
|
|
5152
|
+
* resolves, breaking scrub. touch-action must be set in CSS \u2014
|
|
5153
|
+
* preventDefault() on pointerdown is too late. */
|
|
5154
|
+
touch-action: none;
|
|
5038
5155
|
}
|
|
5039
5156
|
|
|
5040
5157
|
.avp-seek-track {
|
|
@@ -5052,7 +5169,13 @@ var PLAYER_STYLES = (
|
|
|
5052
5169
|
|
|
5053
5170
|
.avp-seek-buffered {
|
|
5054
5171
|
position: absolute;
|
|
5055
|
-
|
|
5172
|
+
inset: 0;
|
|
5173
|
+
pointer-events: none;
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
.avp-seek-buffered-range {
|
|
5177
|
+
position: absolute;
|
|
5178
|
+
top: 0;
|
|
5056
5179
|
height: 100%;
|
|
5057
5180
|
background: rgba(255, 255, 255, 0.35);
|
|
5058
5181
|
border-radius: inherit;
|
|
@@ -5612,6 +5735,7 @@ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
|
5612
5735
|
on(this._video, "ended", () => this._setState("ended"));
|
|
5613
5736
|
on(this._video, "error", () => this._setState("error"));
|
|
5614
5737
|
on(this._video, "timeupdate", () => this._updateTime());
|
|
5738
|
+
on(this._video, "progress", () => this._updateBuffered());
|
|
5615
5739
|
on(this._video, "volumechange", () => this._updateVolume());
|
|
5616
5740
|
on(this._video, "trackschange", () => this._buildSettingsMenu());
|
|
5617
5741
|
on(this._video, "durationchange", () => {
|
|
@@ -5828,13 +5952,45 @@ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
|
5828
5952
|
this._seekInput.value = String(t);
|
|
5829
5953
|
this._updateSeekVisuals(t);
|
|
5830
5954
|
this._timeDisplay.textContent = `${formatTime(t)} / ${formatTime(d)}`;
|
|
5955
|
+
this._updateBuffered();
|
|
5956
|
+
}
|
|
5957
|
+
/**
|
|
5958
|
+
* Render every buffered range as its own segment so gaps (common on MSE
|
|
5959
|
+
* after seeks) are visible. Not gated by `_userSeeking` — ranges should
|
|
5960
|
+
* keep updating while the user scrubs, and runs cheaply on `progress`.
|
|
5961
|
+
*/
|
|
5962
|
+
_updateBuffered() {
|
|
5963
|
+
const d = this._video.duration;
|
|
5964
|
+
if (!(d > 0)) return;
|
|
5965
|
+
let buf;
|
|
5831
5966
|
try {
|
|
5832
|
-
|
|
5833
|
-
if (buf && buf.length > 0 && d > 0) {
|
|
5834
|
-
const end = buf.end(buf.length - 1);
|
|
5835
|
-
this._seekBuffered.style.width = `${end / d * 100}%`;
|
|
5836
|
-
}
|
|
5967
|
+
buf = this._video.buffered;
|
|
5837
5968
|
} catch {
|
|
5969
|
+
return;
|
|
5970
|
+
}
|
|
5971
|
+
const count = buf ? buf.length : 0;
|
|
5972
|
+
const host = this._seekBuffered;
|
|
5973
|
+
while (host.childElementCount > count) host.lastElementChild.remove();
|
|
5974
|
+
while (host.childElementCount < count) {
|
|
5975
|
+
const seg = document.createElement("div");
|
|
5976
|
+
seg.className = "avp-seek-buffered-range";
|
|
5977
|
+
host.appendChild(seg);
|
|
5978
|
+
}
|
|
5979
|
+
for (let i = 0; i < count; i++) {
|
|
5980
|
+
let start;
|
|
5981
|
+
let end;
|
|
5982
|
+
try {
|
|
5983
|
+
start = buf.start(i);
|
|
5984
|
+
end = buf.end(i);
|
|
5985
|
+
} catch {
|
|
5986
|
+
continue;
|
|
5987
|
+
}
|
|
5988
|
+
const s = Math.max(0, start);
|
|
5989
|
+
const e = Math.min(d, end);
|
|
5990
|
+
if (e <= s) continue;
|
|
5991
|
+
const seg = host.children[i];
|
|
5992
|
+
seg.style.left = `${s / d * 100}%`;
|
|
5993
|
+
seg.style.width = `${(e - s) / d * 100}%`;
|
|
5838
5994
|
}
|
|
5839
5995
|
}
|
|
5840
5996
|
// ── Controls: volume ───────────────────────────────────────────────────
|
|
@@ -5977,10 +6133,12 @@ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
|
5977
6133
|
}
|
|
5978
6134
|
}
|
|
5979
6135
|
// ── Stats for nerds ────────────────────────────────────────────────────
|
|
6136
|
+
_statsPrev = null;
|
|
5980
6137
|
_toggleStats() {
|
|
5981
6138
|
this._statsOpen = !this._statsOpen;
|
|
5982
6139
|
this._statsEl.classList.toggle("open", this._statsOpen);
|
|
5983
6140
|
if (this._statsOpen) {
|
|
6141
|
+
this._statsPrev = null;
|
|
5984
6142
|
this._updateStats();
|
|
5985
6143
|
this._statsInterval = setInterval(() => this._updateStats(), 1e3);
|
|
5986
6144
|
} else {
|
|
@@ -5997,23 +6155,76 @@ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
|
5997
6155
|
return;
|
|
5998
6156
|
}
|
|
5999
6157
|
const rt = d.runtime ?? {};
|
|
6000
|
-
const
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6158
|
+
const now = performance.now();
|
|
6159
|
+
const prev = this._statsPrev;
|
|
6160
|
+
const dtSec = prev ? Math.max(1e-3, (now - prev.ts) / 1e3) : 0;
|
|
6161
|
+
const delta = (key) => {
|
|
6162
|
+
if (!prev) return null;
|
|
6163
|
+
const a = rt[key];
|
|
6164
|
+
const b = prev.rt[key];
|
|
6165
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
6166
|
+
return null;
|
|
6167
|
+
};
|
|
6168
|
+
const rate = (key) => {
|
|
6169
|
+
const d_ = delta(key);
|
|
6170
|
+
return d_ != null ? d_ / dtSec : null;
|
|
6171
|
+
};
|
|
6172
|
+
const fmt = (n, digits = 1) => n == null ? "?" : n.toFixed(digits);
|
|
6173
|
+
const sourceFps = typeof rt.sourceFps === "number" ? rt.sourceFps : d.fps;
|
|
6174
|
+
const lines = [];
|
|
6175
|
+
lines.push(`Container: ${d.container ?? "?"} Strategy: ${d.strategy ?? "?"} (${d.strategyClass ?? "?"})`);
|
|
6176
|
+
lines.push(`Video: ${d.videoCodec ?? "?"} ${d.width ?? "?"}\xD7${d.height ?? "?"}${sourceFps ? ` @ ${sourceFps.toFixed(3)} fps` : ""}`);
|
|
6177
|
+
lines.push(`Audio: ${d.audioCodec ?? "none"} Transport: ${d.transport ?? "?"}${d.rangeSupported === true ? "/range" : ""}`);
|
|
6178
|
+
lines.push(`Duration: ${typeof d.duration === "number" ? d.duration.toFixed(1) + "s" : "?"} Playback rate: ${this._video.playbackRate.toFixed(2)}x`);
|
|
6179
|
+
if (rt.videoFramesDecoded != null) {
|
|
6180
|
+
const decFps = rate("videoFramesDecoded");
|
|
6181
|
+
const paintFps = rate("framesPainted");
|
|
6182
|
+
const dropLateFps = rate("framesDroppedLate");
|
|
6183
|
+
const dropOverflowFps = rate("framesDroppedOverflow");
|
|
6184
|
+
const pct = sourceFps && decFps != null ? ` (${(decFps / sourceFps * 100).toFixed(0)}% of realtime)` : "";
|
|
6185
|
+
lines.push(`Decode fps: ${fmt(decFps)}${pct} Paint fps: ${fmt(paintFps)}`);
|
|
6186
|
+
lines.push(`Drops/sec: late=${fmt(dropLateFps)} overflow=${fmt(dropOverflowFps)} Totals: painted=${rt.framesPainted} dropped=${rt.framesDroppedLate}`);
|
|
6187
|
+
}
|
|
6188
|
+
if (typeof rt.videoDecodeMsTotal === "number") {
|
|
6189
|
+
const msDelta = delta("videoDecodeMsTotal");
|
|
6190
|
+
const batchesDelta = delta("videoDecodeBatches");
|
|
6191
|
+
const perBatch = msDelta != null && batchesDelta && batchesDelta > 0 ? msDelta / batchesDelta : null;
|
|
6192
|
+
const share = msDelta != null && dtSec > 0 ? msDelta / (dtSec * 1e3) * 100 : null;
|
|
6193
|
+
lines.push(
|
|
6194
|
+
`Video decode: ${fmt(perBatch)}ms/batch avg ${fmt(share)}% of wall slowest=${fmt(rt.slowestVideoBatchMs)}ms`
|
|
6195
|
+
);
|
|
6196
|
+
}
|
|
6197
|
+
if (typeof rt.audioDecodeMsTotal === "number") {
|
|
6198
|
+
const msDelta = delta("audioDecodeMsTotal");
|
|
6199
|
+
const share = msDelta != null && dtSec > 0 ? msDelta / (dtSec * 1e3) * 100 : null;
|
|
6200
|
+
lines.push(`Audio decode: ${fmt(share)}% of wall`);
|
|
6201
|
+
}
|
|
6202
|
+
if (typeof rt.pumpThrottleMsTotal === "number") {
|
|
6203
|
+
const msDelta = delta("pumpThrottleMsTotal");
|
|
6204
|
+
const share = msDelta != null && dtSec > 0 ? msDelta / (dtSec * 1e3) * 100 : null;
|
|
6205
|
+
lines.push(`Producer throttled: ${fmt(share)}% of wall`);
|
|
6206
|
+
}
|
|
6207
|
+
if (rt.queueDepth != null) {
|
|
6208
|
+
lines.push(
|
|
6209
|
+
`Queue: depth=${rt.queueDepth} span=${fmt(rt.queueSpanMs)}ms head=${fmt(rt.queueHeadMs)}ms tail=${fmt(rt.queueTailMs)}ms`
|
|
6210
|
+
);
|
|
6211
|
+
}
|
|
6212
|
+
if (typeof rt.newestVideoPtsMs === "number") {
|
|
6213
|
+
lines.push(`Newest decoded PTS: ${(rt.newestVideoPtsMs / 1e3).toFixed(2)}s`);
|
|
6214
|
+
}
|
|
6215
|
+
if (rt.audioState != null) {
|
|
6216
|
+
lines.push(`Audio state: ${rt.audioState} bufferAhead=${fmt(rt.bufferAhead, 2)}s clock=${rt.clockMode ?? "?"}`);
|
|
6217
|
+
}
|
|
6218
|
+
if (typeof rt.ptsRegressions === "number" && rt.ptsRegressions > 0) {
|
|
6219
|
+
lines.push(`PTS REGRESSIONS: ${rt.ptsRegressions} (worst=${fmt(rt.worstPtsRegressionMs)}ms) \u2014 decoder emitting out of order`);
|
|
6220
|
+
}
|
|
6221
|
+
if (rt.bsfApplied && rt.bsfApplied.length > 0) lines.push(`BSF active: ${rt.bsfApplied.join(", ")}`);
|
|
6222
|
+
if (rt.bsfMissing && rt.bsfMissing.length > 0) {
|
|
6223
|
+
lines.push(`BSF MISSING: ${rt.bsfMissing.join(", ")} (rebuild libav with avbsf)`);
|
|
6224
|
+
}
|
|
6015
6225
|
if (d.probedBy) lines.push(`Probed by: ${d.probedBy}`);
|
|
6016
6226
|
this._statsEl.textContent = lines.join("\n");
|
|
6227
|
+
this._statsPrev = { ts: now, rt: { ...rt } };
|
|
6017
6228
|
}
|
|
6018
6229
|
// ── Controls: fullscreen ───────────────────────────────────────────────
|
|
6019
6230
|
_toggleFullscreen() {
|