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.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AvbridgeError, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, normalizeSource, sniffNormalizedSource, ERR_PROBE_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_LIBAV_NOT_REACHABLE, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-E76AMWI4.js';
|
|
2
2
|
import { dbg, loadLibav } from './chunk-IAYKFGFG.js';
|
|
3
|
-
import './chunk-
|
|
3
|
+
import './chunk-3AI5WFFN.js';
|
|
4
4
|
import { SubtitleResourceBag, discoverSidecars, attachSubtitleTracks, SubtitleOverlay } from './chunk-EDDWAN2L.js';
|
|
5
5
|
import './chunk-LUFA47FP.js';
|
|
6
6
|
|
|
@@ -237,7 +237,7 @@ async function probe(source, transport) {
|
|
|
237
237
|
const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
|
|
238
238
|
if (hasUnknownCodec) {
|
|
239
239
|
try {
|
|
240
|
-
const { probeWithLibav } = await import('./avi-
|
|
240
|
+
const { probeWithLibav } = await import('./avi-GX2H34IQ.js');
|
|
241
241
|
return await probeWithLibav(normalized, sniffed);
|
|
242
242
|
} catch {
|
|
243
243
|
return result;
|
|
@@ -250,7 +250,7 @@ async function probe(source, transport) {
|
|
|
250
250
|
mediabunnyErr.message
|
|
251
251
|
);
|
|
252
252
|
try {
|
|
253
|
-
const { probeWithLibav } = await import('./avi-
|
|
253
|
+
const { probeWithLibav } = await import('./avi-GX2H34IQ.js');
|
|
254
254
|
return await probeWithLibav(normalized, sniffed);
|
|
255
255
|
} catch (libavErr) {
|
|
256
256
|
const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
|
|
@@ -264,7 +264,7 @@ async function probe(source, transport) {
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
try {
|
|
267
|
-
const { probeWithLibav } = await import('./avi-
|
|
267
|
+
const { probeWithLibav } = await import('./avi-GX2H34IQ.js');
|
|
268
268
|
return await probeWithLibav(normalized, sniffed);
|
|
269
269
|
} catch (err) {
|
|
270
270
|
const inner = err instanceof Error ? err.message : String(err);
|
|
@@ -1333,10 +1333,20 @@ var VideoRenderer = class {
|
|
|
1333
1333
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
1334
1334
|
firstFrameReady;
|
|
1335
1335
|
resolveFirstFrame;
|
|
1336
|
-
/**
|
|
1336
|
+
/**
|
|
1337
|
+
* True once at least one frame has been enqueued *since the last flush*.
|
|
1338
|
+
* Used by `readyState` — initial cold-start reports HAVE_NOTHING until
|
|
1339
|
+
* any frame has arrived, and after a seek we want the same semantics
|
|
1340
|
+
* (HAVE_NOTHING until post-seek frames arrive), so the cumulative
|
|
1341
|
+
* `framesPainted > 0` that used to live here was wrong: it kept the
|
|
1342
|
+
* state "true forever" after the first frame ever, so post-seek
|
|
1343
|
+
* `waitForBuffer()` would exit immediately with an empty queue and
|
|
1344
|
+
* leave video frozen while audio kept going.
|
|
1345
|
+
*/
|
|
1337
1346
|
hasFrames() {
|
|
1338
|
-
return this.queue.length > 0 || this.
|
|
1347
|
+
return this.queue.length > 0 || this.hasEverEnqueuedSinceFlush;
|
|
1339
1348
|
}
|
|
1349
|
+
hasEverEnqueuedSinceFlush = false;
|
|
1340
1350
|
/** Current depth of the frame queue. Used by the decoder for backpressure. */
|
|
1341
1351
|
queueDepth() {
|
|
1342
1352
|
return this.queue.length;
|
|
@@ -1355,6 +1365,7 @@ var VideoRenderer = class {
|
|
|
1355
1365
|
return;
|
|
1356
1366
|
}
|
|
1357
1367
|
this.queue.push(frame);
|
|
1368
|
+
this.hasEverEnqueuedSinceFlush = true;
|
|
1358
1369
|
if (this.queue.length === 1 && this.framesPainted === 0) {
|
|
1359
1370
|
this.resolveFirstFrame();
|
|
1360
1371
|
}
|
|
@@ -1488,7 +1499,8 @@ var VideoRenderer = class {
|
|
|
1488
1499
|
}
|
|
1489
1500
|
return;
|
|
1490
1501
|
}
|
|
1491
|
-
const
|
|
1502
|
+
const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
|
|
1503
|
+
const dropThresholdUs = _relaxDrop ? audioNowUs - 60 * 1e6 : audioNowUs - frameDurationUs * 2;
|
|
1492
1504
|
let dropped = 0;
|
|
1493
1505
|
while (bestIdx > 0) {
|
|
1494
1506
|
const ts = this.queue[0].timestamp ?? 0;
|
|
@@ -1549,16 +1561,28 @@ var VideoRenderer = class {
|
|
|
1549
1561
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
1550
1562
|
this.prerolled = false;
|
|
1551
1563
|
this.ptsCalibrated = false;
|
|
1564
|
+
this.hasEverEnqueuedSinceFlush = false;
|
|
1552
1565
|
if (isDebug() && count > 0) {
|
|
1553
1566
|
console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
|
|
1554
1567
|
}
|
|
1555
1568
|
}
|
|
1556
1569
|
stats() {
|
|
1570
|
+
let queueSpanMs = 0;
|
|
1571
|
+
let queueHeadMs = 0;
|
|
1572
|
+
let queueTailMs = 0;
|
|
1573
|
+
if (this.queue.length > 0) {
|
|
1574
|
+
queueHeadMs = Math.round((this.queue[0].timestamp ?? 0) / 1e3);
|
|
1575
|
+
queueTailMs = Math.round((this.queue[this.queue.length - 1].timestamp ?? 0) / 1e3);
|
|
1576
|
+
queueSpanMs = Math.max(0, queueTailMs - queueHeadMs);
|
|
1577
|
+
}
|
|
1557
1578
|
return {
|
|
1558
1579
|
framesPainted: this.framesPainted,
|
|
1559
1580
|
framesDroppedLate: this.framesDroppedLate,
|
|
1560
1581
|
framesDroppedOverflow: this.framesDroppedOverflow,
|
|
1561
|
-
queueDepth: this.queue.length
|
|
1582
|
+
queueDepth: this.queue.length,
|
|
1583
|
+
queueHeadMs,
|
|
1584
|
+
queueTailMs,
|
|
1585
|
+
queueSpanMs
|
|
1562
1586
|
};
|
|
1563
1587
|
}
|
|
1564
1588
|
destroy() {
|
|
@@ -2021,7 +2045,7 @@ async function startHybridDecoder(opts) {
|
|
|
2021
2045
|
const variant = pickLibavVariant(opts.context);
|
|
2022
2046
|
const libav = await loadLibav(variant);
|
|
2023
2047
|
const bridge = await loadBridge();
|
|
2024
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
2048
|
+
const { prepareLibavInput } = await import('./libav-http-reader-2S5HAHW4.js');
|
|
2025
2049
|
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
2026
2050
|
const readPkt = await libav.av_packet_alloc();
|
|
2027
2051
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
@@ -2096,6 +2120,7 @@ async function startHybridDecoder(opts) {
|
|
|
2096
2120
|
}
|
|
2097
2121
|
let bsfCtx = null;
|
|
2098
2122
|
let bsfPkt = null;
|
|
2123
|
+
let bsfRequiredButMissing = false;
|
|
2099
2124
|
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
2100
2125
|
try {
|
|
2101
2126
|
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
@@ -2106,13 +2131,19 @@ async function startHybridDecoder(opts) {
|
|
|
2106
2131
|
bsfPkt = await libav.av_packet_alloc();
|
|
2107
2132
|
dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
|
|
2108
2133
|
} else {
|
|
2109
|
-
|
|
2134
|
+
bsfRequiredButMissing = true;
|
|
2110
2135
|
bsfCtx = null;
|
|
2111
2136
|
}
|
|
2112
2137
|
} catch (err) {
|
|
2113
|
-
|
|
2138
|
+
bsfRequiredButMissing = true;
|
|
2114
2139
|
bsfCtx = null;
|
|
2115
2140
|
bsfPkt = null;
|
|
2141
|
+
dbg.warn("bsf", `hybrid: mpeg4_unpack_bframes BSF init failed: ${err.message}`);
|
|
2142
|
+
}
|
|
2143
|
+
if (bsfRequiredButMissing) {
|
|
2144
|
+
console.error(
|
|
2145
|
+
"[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."
|
|
2146
|
+
);
|
|
2116
2147
|
}
|
|
2117
2148
|
}
|
|
2118
2149
|
async function applyBSF(packets) {
|
|
@@ -2122,7 +2153,6 @@ async function startHybridDecoder(opts) {
|
|
|
2122
2153
|
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
2123
2154
|
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
2124
2155
|
if (sendErr < 0) {
|
|
2125
|
-
out.push(pkt);
|
|
2126
2156
|
continue;
|
|
2127
2157
|
}
|
|
2128
2158
|
while (true) {
|
|
@@ -2136,10 +2166,13 @@ async function startHybridDecoder(opts) {
|
|
|
2136
2166
|
async function flushBSF() {
|
|
2137
2167
|
if (!bsfCtx || !bsfPkt) return;
|
|
2138
2168
|
try {
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2169
|
+
if (libav.av_bsf_flush) {
|
|
2170
|
+
await libav.av_bsf_flush(bsfCtx);
|
|
2171
|
+
} else {
|
|
2172
|
+
while (true) {
|
|
2173
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2174
|
+
if (err < 0) break;
|
|
2175
|
+
}
|
|
2143
2176
|
}
|
|
2144
2177
|
} catch {
|
|
2145
2178
|
}
|
|
@@ -2451,6 +2484,7 @@ async function startHybridDecoder(opts) {
|
|
|
2451
2484
|
videoChunksFed,
|
|
2452
2485
|
audioFramesDecoded,
|
|
2453
2486
|
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
2487
|
+
bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
|
|
2454
2488
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
2455
2489
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
2456
2490
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -2691,7 +2725,7 @@ async function startDecoder(opts) {
|
|
|
2691
2725
|
const variant = "avbridge";
|
|
2692
2726
|
const libav = await loadLibav(variant);
|
|
2693
2727
|
const bridge = await loadBridge2();
|
|
2694
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
2728
|
+
const { prepareLibavInput } = await import('./libav-http-reader-2S5HAHW4.js');
|
|
2695
2729
|
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
2696
2730
|
const readPkt = await libav.av_packet_alloc();
|
|
2697
2731
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
@@ -2750,6 +2784,7 @@ async function startDecoder(opts) {
|
|
|
2750
2784
|
}
|
|
2751
2785
|
let bsfCtx = null;
|
|
2752
2786
|
let bsfPkt = null;
|
|
2787
|
+
let bsfRequiredButMissing = false;
|
|
2753
2788
|
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
2754
2789
|
try {
|
|
2755
2790
|
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
@@ -2760,13 +2795,19 @@ async function startDecoder(opts) {
|
|
|
2760
2795
|
bsfPkt = await libav.av_packet_alloc();
|
|
2761
2796
|
dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
|
|
2762
2797
|
} else {
|
|
2763
|
-
|
|
2798
|
+
bsfRequiredButMissing = true;
|
|
2764
2799
|
bsfCtx = null;
|
|
2765
2800
|
}
|
|
2766
2801
|
} catch (err) {
|
|
2767
|
-
|
|
2802
|
+
bsfRequiredButMissing = true;
|
|
2768
2803
|
bsfCtx = null;
|
|
2769
2804
|
bsfPkt = null;
|
|
2805
|
+
dbg.warn("bsf", `mpeg4_unpack_bframes BSF init failed: ${err.message}`);
|
|
2806
|
+
}
|
|
2807
|
+
if (bsfRequiredButMissing) {
|
|
2808
|
+
console.error(
|
|
2809
|
+
"[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."
|
|
2810
|
+
);
|
|
2770
2811
|
}
|
|
2771
2812
|
}
|
|
2772
2813
|
async function applyBSF(packets) {
|
|
@@ -2776,7 +2817,6 @@ async function startDecoder(opts) {
|
|
|
2776
2817
|
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
2777
2818
|
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
2778
2819
|
if (sendErr < 0) {
|
|
2779
|
-
out.push(pkt);
|
|
2780
2820
|
continue;
|
|
2781
2821
|
}
|
|
2782
2822
|
while (true) {
|
|
@@ -2790,10 +2830,13 @@ async function startDecoder(opts) {
|
|
|
2790
2830
|
async function flushBSF() {
|
|
2791
2831
|
if (!bsfCtx || !bsfPkt) return;
|
|
2792
2832
|
try {
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2833
|
+
if (libav.av_bsf_flush) {
|
|
2834
|
+
await libav.av_bsf_flush(bsfCtx);
|
|
2835
|
+
} else {
|
|
2836
|
+
while (true) {
|
|
2837
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2838
|
+
if (err < 0) break;
|
|
2839
|
+
}
|
|
2797
2840
|
}
|
|
2798
2841
|
} catch {
|
|
2799
2842
|
}
|
|
@@ -2811,6 +2854,19 @@ async function startDecoder(opts) {
|
|
|
2811
2854
|
let watchdogOverflowWarned = false;
|
|
2812
2855
|
let syntheticVideoUs = 0;
|
|
2813
2856
|
let syntheticAudioUs = 0;
|
|
2857
|
+
let videoDecodeMsTotal = 0;
|
|
2858
|
+
let audioDecodeMsTotal = 0;
|
|
2859
|
+
let videoDecodeBatches = 0;
|
|
2860
|
+
let audioDecodeBatches = 0;
|
|
2861
|
+
let readMsTotal = 0;
|
|
2862
|
+
let readBatches = 0;
|
|
2863
|
+
let pumpThrottleMsTotal = 0;
|
|
2864
|
+
let pumpThrottleEntries = 0;
|
|
2865
|
+
let slowestVideoBatchMs = 0;
|
|
2866
|
+
let newestVideoPtsUs = 0;
|
|
2867
|
+
let lastEmittedPtsUs = -1;
|
|
2868
|
+
let ptsRegressions = 0;
|
|
2869
|
+
let worstPtsRegressionMs = 0;
|
|
2814
2870
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
2815
2871
|
const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
|
|
2816
2872
|
const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
|
|
@@ -2819,9 +2875,12 @@ async function startDecoder(opts) {
|
|
|
2819
2875
|
let readErr;
|
|
2820
2876
|
let packets;
|
|
2821
2877
|
try {
|
|
2878
|
+
const _readStart = performance.now();
|
|
2822
2879
|
[readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
|
|
2823
2880
|
limit: 16 * 1024
|
|
2824
2881
|
});
|
|
2882
|
+
readMsTotal += performance.now() - _readStart;
|
|
2883
|
+
readBatches++;
|
|
2825
2884
|
} catch (err) {
|
|
2826
2885
|
console.error("[avbridge] ff_read_frame_multi failed:", err);
|
|
2827
2886
|
return;
|
|
@@ -2883,8 +2942,17 @@ async function startDecoder(opts) {
|
|
|
2883
2942
|
}
|
|
2884
2943
|
}
|
|
2885
2944
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2945
|
+
{
|
|
2946
|
+
const _throttleStart = performance.now();
|
|
2947
|
+
let _throttled = false;
|
|
2948
|
+
while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
2949
|
+
_throttled = true;
|
|
2950
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2951
|
+
}
|
|
2952
|
+
if (_throttled) {
|
|
2953
|
+
pumpThrottleMsTotal += performance.now() - _throttleStart;
|
|
2954
|
+
pumpThrottleEntries++;
|
|
2955
|
+
}
|
|
2888
2956
|
}
|
|
2889
2957
|
if (readErr === libav.AVERROR_EOF) {
|
|
2890
2958
|
if (videoDec) await decodeVideoBatch(
|
|
@@ -2910,6 +2978,7 @@ async function startDecoder(opts) {
|
|
|
2910
2978
|
async function decodeVideoBatch(pkts, myToken, flush = false) {
|
|
2911
2979
|
if (!videoDec || destroyed || myToken !== pumpToken) return;
|
|
2912
2980
|
let frames;
|
|
2981
|
+
const _t0 = performance.now();
|
|
2913
2982
|
try {
|
|
2914
2983
|
frames = await libav.ff_decode_multi(
|
|
2915
2984
|
videoDec.c,
|
|
@@ -2922,18 +2991,38 @@ async function startDecoder(opts) {
|
|
|
2922
2991
|
console.error("[avbridge] video decode batch failed:", err);
|
|
2923
2992
|
return;
|
|
2924
2993
|
}
|
|
2994
|
+
{
|
|
2995
|
+
const _dt = performance.now() - _t0;
|
|
2996
|
+
videoDecodeMsTotal += _dt;
|
|
2997
|
+
videoDecodeBatches++;
|
|
2998
|
+
if (_dt > slowestVideoBatchMs) slowestVideoBatchMs = _dt;
|
|
2999
|
+
}
|
|
2925
3000
|
if (myToken !== pumpToken || destroyed) return;
|
|
2926
3001
|
for (const f of frames) {
|
|
2927
3002
|
if (myToken !== pumpToken || destroyed) return;
|
|
2928
3003
|
sanitizeFrameTimestamp(
|
|
2929
3004
|
f,
|
|
2930
3005
|
() => {
|
|
2931
|
-
const
|
|
2932
|
-
syntheticVideoUs
|
|
2933
|
-
return
|
|
3006
|
+
const base = lastEmittedPtsUs >= 0 ? lastEmittedPtsUs + videoFrameStepUs : syntheticVideoUs;
|
|
3007
|
+
syntheticVideoUs = base + videoFrameStepUs;
|
|
3008
|
+
return base;
|
|
2934
3009
|
},
|
|
2935
3010
|
videoTimeBase
|
|
2936
3011
|
);
|
|
3012
|
+
const _fPts = (f.ptshi ?? 0) * 4294967296 + (f.pts ?? 0);
|
|
3013
|
+
if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
|
|
3014
|
+
if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
|
|
3015
|
+
ptsRegressions++;
|
|
3016
|
+
const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
|
|
3017
|
+
if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
|
|
3018
|
+
if (ptsRegressions <= 10) {
|
|
3019
|
+
console.warn(
|
|
3020
|
+
`[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.`
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
continue;
|
|
3024
|
+
}
|
|
3025
|
+
lastEmittedPtsUs = _fPts;
|
|
2937
3026
|
try {
|
|
2938
3027
|
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
2939
3028
|
opts.renderer.enqueue(vf);
|
|
@@ -2948,6 +3037,7 @@ async function startDecoder(opts) {
|
|
|
2948
3037
|
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
2949
3038
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
2950
3039
|
let frames;
|
|
3040
|
+
const _t0 = performance.now();
|
|
2951
3041
|
try {
|
|
2952
3042
|
frames = await libav.ff_decode_multi(
|
|
2953
3043
|
audioDec.c,
|
|
@@ -2960,6 +3050,8 @@ async function startDecoder(opts) {
|
|
|
2960
3050
|
console.error("[avbridge] audio decode batch failed:", err);
|
|
2961
3051
|
return;
|
|
2962
3052
|
}
|
|
3053
|
+
audioDecodeMsTotal += performance.now() - _t0;
|
|
3054
|
+
audioDecodeBatches++;
|
|
2963
3055
|
if (myToken !== pumpToken || destroyed) return;
|
|
2964
3056
|
for (const f of frames) {
|
|
2965
3057
|
if (myToken !== pumpToken || destroyed) return;
|
|
@@ -3081,6 +3173,7 @@ async function startDecoder(opts) {
|
|
|
3081
3173
|
await flushBSF();
|
|
3082
3174
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
3083
3175
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
3176
|
+
lastEmittedPtsUs = -1;
|
|
3084
3177
|
pumpRunning = pumpLoop(newToken).catch(
|
|
3085
3178
|
(err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
|
|
3086
3179
|
);
|
|
@@ -3118,6 +3211,7 @@ async function startDecoder(opts) {
|
|
|
3118
3211
|
await flushBSF();
|
|
3119
3212
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
3120
3213
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
3214
|
+
lastEmittedPtsUs = -1;
|
|
3121
3215
|
pumpRunning = pumpLoop(newToken).catch(
|
|
3122
3216
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
3123
3217
|
);
|
|
@@ -3131,7 +3225,24 @@ async function startDecoder(opts) {
|
|
|
3131
3225
|
packetsRead,
|
|
3132
3226
|
videoFramesDecoded,
|
|
3133
3227
|
audioFramesDecoded,
|
|
3228
|
+
// Throughput instrumentation — the stats panel turns these into
|
|
3229
|
+
// "decode fps actual / realtime target" and shows slowest batch
|
|
3230
|
+
// + producer throttle share.
|
|
3231
|
+
videoDecodeMsTotal,
|
|
3232
|
+
videoDecodeBatches,
|
|
3233
|
+
audioDecodeMsTotal,
|
|
3234
|
+
audioDecodeBatches,
|
|
3235
|
+
readMsTotal,
|
|
3236
|
+
readBatches,
|
|
3237
|
+
pumpThrottleMsTotal,
|
|
3238
|
+
pumpThrottleEntries,
|
|
3239
|
+
slowestVideoBatchMs,
|
|
3240
|
+
newestVideoPtsMs: Math.round(newestVideoPtsUs / 1e3),
|
|
3241
|
+
ptsRegressions,
|
|
3242
|
+
worstPtsRegressionMs,
|
|
3243
|
+
sourceFps: videoFps,
|
|
3134
3244
|
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
3245
|
+
bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
|
|
3135
3246
|
// Confirmed transport info: once prepareLibavInput returns
|
|
3136
3247
|
// successfully, we *know* whether the source is http-range (probe
|
|
3137
3248
|
// succeeded and returned 206) or in-memory blob. Diagnostics hoists
|
|
@@ -3420,9 +3531,9 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3420
3531
|
constructor(options, registry) {
|
|
3421
3532
|
this.options = options;
|
|
3422
3533
|
this.registry = registry;
|
|
3423
|
-
const { requestInit, fetchFn } = options;
|
|
3424
|
-
if (requestInit || fetchFn) {
|
|
3425
|
-
this.transport = { requestInit, fetchFn };
|
|
3534
|
+
const { requestInit, fetchFn, cacheBytes } = options;
|
|
3535
|
+
if (requestInit || fetchFn || cacheBytes !== void 0) {
|
|
3536
|
+
this.transport = { requestInit, fetchFn, cacheBytes };
|
|
3426
3537
|
}
|
|
3427
3538
|
}
|
|
3428
3539
|
options;
|
|
@@ -5033,6 +5144,12 @@ var PLAYER_STYLES = (
|
|
|
5033
5144
|
display: flex;
|
|
5034
5145
|
align-items: center;
|
|
5035
5146
|
cursor: pointer;
|
|
5147
|
+
/* Claim all touch gestures on the seek bar. Without this, Android
|
|
5148
|
+
* browsers (Chrome, Samsung Internet) treat horizontal drags as
|
|
5149
|
+
* scroll candidates and cancel pointermove once the gesture
|
|
5150
|
+
* resolves, breaking scrub. touch-action must be set in CSS \u2014
|
|
5151
|
+
* preventDefault() on pointerdown is too late. */
|
|
5152
|
+
touch-action: none;
|
|
5036
5153
|
}
|
|
5037
5154
|
|
|
5038
5155
|
.avp-seek-track {
|
|
@@ -5050,7 +5167,13 @@ var PLAYER_STYLES = (
|
|
|
5050
5167
|
|
|
5051
5168
|
.avp-seek-buffered {
|
|
5052
5169
|
position: absolute;
|
|
5053
|
-
|
|
5170
|
+
inset: 0;
|
|
5171
|
+
pointer-events: none;
|
|
5172
|
+
}
|
|
5173
|
+
|
|
5174
|
+
.avp-seek-buffered-range {
|
|
5175
|
+
position: absolute;
|
|
5176
|
+
top: 0;
|
|
5054
5177
|
height: 100%;
|
|
5055
5178
|
background: rgba(255, 255, 255, 0.35);
|
|
5056
5179
|
border-radius: inherit;
|
|
@@ -5610,6 +5733,7 @@ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
|
5610
5733
|
on(this._video, "ended", () => this._setState("ended"));
|
|
5611
5734
|
on(this._video, "error", () => this._setState("error"));
|
|
5612
5735
|
on(this._video, "timeupdate", () => this._updateTime());
|
|
5736
|
+
on(this._video, "progress", () => this._updateBuffered());
|
|
5613
5737
|
on(this._video, "volumechange", () => this._updateVolume());
|
|
5614
5738
|
on(this._video, "trackschange", () => this._buildSettingsMenu());
|
|
5615
5739
|
on(this._video, "durationchange", () => {
|
|
@@ -5826,13 +5950,45 @@ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
|
5826
5950
|
this._seekInput.value = String(t);
|
|
5827
5951
|
this._updateSeekVisuals(t);
|
|
5828
5952
|
this._timeDisplay.textContent = `${formatTime(t)} / ${formatTime(d)}`;
|
|
5953
|
+
this._updateBuffered();
|
|
5954
|
+
}
|
|
5955
|
+
/**
|
|
5956
|
+
* Render every buffered range as its own segment so gaps (common on MSE
|
|
5957
|
+
* after seeks) are visible. Not gated by `_userSeeking` — ranges should
|
|
5958
|
+
* keep updating while the user scrubs, and runs cheaply on `progress`.
|
|
5959
|
+
*/
|
|
5960
|
+
_updateBuffered() {
|
|
5961
|
+
const d = this._video.duration;
|
|
5962
|
+
if (!(d > 0)) return;
|
|
5963
|
+
let buf;
|
|
5829
5964
|
try {
|
|
5830
|
-
|
|
5831
|
-
if (buf && buf.length > 0 && d > 0) {
|
|
5832
|
-
const end = buf.end(buf.length - 1);
|
|
5833
|
-
this._seekBuffered.style.width = `${end / d * 100}%`;
|
|
5834
|
-
}
|
|
5965
|
+
buf = this._video.buffered;
|
|
5835
5966
|
} catch {
|
|
5967
|
+
return;
|
|
5968
|
+
}
|
|
5969
|
+
const count = buf ? buf.length : 0;
|
|
5970
|
+
const host = this._seekBuffered;
|
|
5971
|
+
while (host.childElementCount > count) host.lastElementChild.remove();
|
|
5972
|
+
while (host.childElementCount < count) {
|
|
5973
|
+
const seg = document.createElement("div");
|
|
5974
|
+
seg.className = "avp-seek-buffered-range";
|
|
5975
|
+
host.appendChild(seg);
|
|
5976
|
+
}
|
|
5977
|
+
for (let i = 0; i < count; i++) {
|
|
5978
|
+
let start;
|
|
5979
|
+
let end;
|
|
5980
|
+
try {
|
|
5981
|
+
start = buf.start(i);
|
|
5982
|
+
end = buf.end(i);
|
|
5983
|
+
} catch {
|
|
5984
|
+
continue;
|
|
5985
|
+
}
|
|
5986
|
+
const s = Math.max(0, start);
|
|
5987
|
+
const e = Math.min(d, end);
|
|
5988
|
+
if (e <= s) continue;
|
|
5989
|
+
const seg = host.children[i];
|
|
5990
|
+
seg.style.left = `${s / d * 100}%`;
|
|
5991
|
+
seg.style.width = `${(e - s) / d * 100}%`;
|
|
5836
5992
|
}
|
|
5837
5993
|
}
|
|
5838
5994
|
// ── Controls: volume ───────────────────────────────────────────────────
|
|
@@ -5975,10 +6131,12 @@ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
|
5975
6131
|
}
|
|
5976
6132
|
}
|
|
5977
6133
|
// ── Stats for nerds ────────────────────────────────────────────────────
|
|
6134
|
+
_statsPrev = null;
|
|
5978
6135
|
_toggleStats() {
|
|
5979
6136
|
this._statsOpen = !this._statsOpen;
|
|
5980
6137
|
this._statsEl.classList.toggle("open", this._statsOpen);
|
|
5981
6138
|
if (this._statsOpen) {
|
|
6139
|
+
this._statsPrev = null;
|
|
5982
6140
|
this._updateStats();
|
|
5983
6141
|
this._statsInterval = setInterval(() => this._updateStats(), 1e3);
|
|
5984
6142
|
} else {
|
|
@@ -5995,23 +6153,76 @@ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
|
5995
6153
|
return;
|
|
5996
6154
|
}
|
|
5997
6155
|
const rt = d.runtime ?? {};
|
|
5998
|
-
const
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6156
|
+
const now = performance.now();
|
|
6157
|
+
const prev = this._statsPrev;
|
|
6158
|
+
const dtSec = prev ? Math.max(1e-3, (now - prev.ts) / 1e3) : 0;
|
|
6159
|
+
const delta = (key) => {
|
|
6160
|
+
if (!prev) return null;
|
|
6161
|
+
const a = rt[key];
|
|
6162
|
+
const b = prev.rt[key];
|
|
6163
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
6164
|
+
return null;
|
|
6165
|
+
};
|
|
6166
|
+
const rate = (key) => {
|
|
6167
|
+
const d_ = delta(key);
|
|
6168
|
+
return d_ != null ? d_ / dtSec : null;
|
|
6169
|
+
};
|
|
6170
|
+
const fmt = (n, digits = 1) => n == null ? "?" : n.toFixed(digits);
|
|
6171
|
+
const sourceFps = typeof rt.sourceFps === "number" ? rt.sourceFps : d.fps;
|
|
6172
|
+
const lines = [];
|
|
6173
|
+
lines.push(`Container: ${d.container ?? "?"} Strategy: ${d.strategy ?? "?"} (${d.strategyClass ?? "?"})`);
|
|
6174
|
+
lines.push(`Video: ${d.videoCodec ?? "?"} ${d.width ?? "?"}\xD7${d.height ?? "?"}${sourceFps ? ` @ ${sourceFps.toFixed(3)} fps` : ""}`);
|
|
6175
|
+
lines.push(`Audio: ${d.audioCodec ?? "none"} Transport: ${d.transport ?? "?"}${d.rangeSupported === true ? "/range" : ""}`);
|
|
6176
|
+
lines.push(`Duration: ${typeof d.duration === "number" ? d.duration.toFixed(1) + "s" : "?"} Playback rate: ${this._video.playbackRate.toFixed(2)}x`);
|
|
6177
|
+
if (rt.videoFramesDecoded != null) {
|
|
6178
|
+
const decFps = rate("videoFramesDecoded");
|
|
6179
|
+
const paintFps = rate("framesPainted");
|
|
6180
|
+
const dropLateFps = rate("framesDroppedLate");
|
|
6181
|
+
const dropOverflowFps = rate("framesDroppedOverflow");
|
|
6182
|
+
const pct = sourceFps && decFps != null ? ` (${(decFps / sourceFps * 100).toFixed(0)}% of realtime)` : "";
|
|
6183
|
+
lines.push(`Decode fps: ${fmt(decFps)}${pct} Paint fps: ${fmt(paintFps)}`);
|
|
6184
|
+
lines.push(`Drops/sec: late=${fmt(dropLateFps)} overflow=${fmt(dropOverflowFps)} Totals: painted=${rt.framesPainted} dropped=${rt.framesDroppedLate}`);
|
|
6185
|
+
}
|
|
6186
|
+
if (typeof rt.videoDecodeMsTotal === "number") {
|
|
6187
|
+
const msDelta = delta("videoDecodeMsTotal");
|
|
6188
|
+
const batchesDelta = delta("videoDecodeBatches");
|
|
6189
|
+
const perBatch = msDelta != null && batchesDelta && batchesDelta > 0 ? msDelta / batchesDelta : null;
|
|
6190
|
+
const share = msDelta != null && dtSec > 0 ? msDelta / (dtSec * 1e3) * 100 : null;
|
|
6191
|
+
lines.push(
|
|
6192
|
+
`Video decode: ${fmt(perBatch)}ms/batch avg ${fmt(share)}% of wall slowest=${fmt(rt.slowestVideoBatchMs)}ms`
|
|
6193
|
+
);
|
|
6194
|
+
}
|
|
6195
|
+
if (typeof rt.audioDecodeMsTotal === "number") {
|
|
6196
|
+
const msDelta = delta("audioDecodeMsTotal");
|
|
6197
|
+
const share = msDelta != null && dtSec > 0 ? msDelta / (dtSec * 1e3) * 100 : null;
|
|
6198
|
+
lines.push(`Audio decode: ${fmt(share)}% of wall`);
|
|
6199
|
+
}
|
|
6200
|
+
if (typeof rt.pumpThrottleMsTotal === "number") {
|
|
6201
|
+
const msDelta = delta("pumpThrottleMsTotal");
|
|
6202
|
+
const share = msDelta != null && dtSec > 0 ? msDelta / (dtSec * 1e3) * 100 : null;
|
|
6203
|
+
lines.push(`Producer throttled: ${fmt(share)}% of wall`);
|
|
6204
|
+
}
|
|
6205
|
+
if (rt.queueDepth != null) {
|
|
6206
|
+
lines.push(
|
|
6207
|
+
`Queue: depth=${rt.queueDepth} span=${fmt(rt.queueSpanMs)}ms head=${fmt(rt.queueHeadMs)}ms tail=${fmt(rt.queueTailMs)}ms`
|
|
6208
|
+
);
|
|
6209
|
+
}
|
|
6210
|
+
if (typeof rt.newestVideoPtsMs === "number") {
|
|
6211
|
+
lines.push(`Newest decoded PTS: ${(rt.newestVideoPtsMs / 1e3).toFixed(2)}s`);
|
|
6212
|
+
}
|
|
6213
|
+
if (rt.audioState != null) {
|
|
6214
|
+
lines.push(`Audio state: ${rt.audioState} bufferAhead=${fmt(rt.bufferAhead, 2)}s clock=${rt.clockMode ?? "?"}`);
|
|
6215
|
+
}
|
|
6216
|
+
if (typeof rt.ptsRegressions === "number" && rt.ptsRegressions > 0) {
|
|
6217
|
+
lines.push(`PTS REGRESSIONS: ${rt.ptsRegressions} (worst=${fmt(rt.worstPtsRegressionMs)}ms) \u2014 decoder emitting out of order`);
|
|
6218
|
+
}
|
|
6219
|
+
if (rt.bsfApplied && rt.bsfApplied.length > 0) lines.push(`BSF active: ${rt.bsfApplied.join(", ")}`);
|
|
6220
|
+
if (rt.bsfMissing && rt.bsfMissing.length > 0) {
|
|
6221
|
+
lines.push(`BSF MISSING: ${rt.bsfMissing.join(", ")} (rebuild libav with avbsf)`);
|
|
6222
|
+
}
|
|
6013
6223
|
if (d.probedBy) lines.push(`Probed by: ${d.probedBy}`);
|
|
6014
6224
|
this._statsEl.textContent = lines.join("\n");
|
|
6225
|
+
this._statsPrev = { ts: now, rt: { ...rt } };
|
|
6015
6226
|
}
|
|
6016
6227
|
// ── Controls: fullscreen ───────────────────────────────────────────────
|
|
6017
6228
|
_toggleFullscreen() {
|