avbridge 2.12.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 +76 -0
- package/dist/{avi-EQE6AR75.cjs → avi-32UABODO.cjs} +12 -4
- package/dist/avi-32UABODO.cjs.map +1 -0
- package/dist/{avi-Y3N325WZ.cjs → avi-5BPR6QUX.cjs} +12 -4
- package/dist/avi-5BPR6QUX.cjs.map +1 -0
- package/dist/{avi-NNHH4AAA.js → avi-BLIH7KKV.js} +12 -4
- package/dist/avi-BLIH7KKV.js.map +1 -0
- package/dist/{avi-S7EY54YA.js → avi-GX2H34IQ.js} +12 -4
- package/dist/avi-GX2H34IQ.js.map +1 -0
- package/dist/{chunk-2LNXMGT6.js → chunk-5CX7BVVV.js} +5 -5
- package/dist/{chunk-2LNXMGT6.js.map → chunk-5CX7BVVV.js.map} +1 -1
- package/dist/{chunk-5Y5BTB5D.js → chunk-B76QWPFM.js} +3 -3
- package/dist/{chunk-5Y5BTB5D.js.map → chunk-B76QWPFM.js.map} +1 -1
- package/dist/{chunk-Z26PXRUY.js → chunk-BN7BRTLY.js} +137 -26
- package/dist/chunk-BN7BRTLY.js.map +1 -0
- package/dist/{chunk-GJBNLPGI.cjs → chunk-E5MAM2P4.cjs} +9 -9
- package/dist/{chunk-GJBNLPGI.cjs.map → chunk-E5MAM2P4.cjs.map} +1 -1
- package/dist/{chunk-7EF4VTUS.cjs → chunk-UM6WCSGL.cjs} +141 -30
- package/dist/chunk-UM6WCSGL.cjs.map +1 -0
- package/dist/{chunk-HBHSUGNI.cjs → chunk-VLI3Y6IJ.cjs} +5 -5
- package/dist/{chunk-HBHSUGNI.cjs.map → chunk-VLI3Y6IJ.cjs.map} +1 -1
- package/dist/element-browser.js +144 -25
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +3 -3
- package/dist/element.js +2 -2
- package/dist/index.cjs +18 -18
- package/dist/index.js +6 -6
- package/dist/player.cjs +207 -41
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +1 -0
- package/dist/player.d.ts +1 -0
- package/dist/player.js +207 -41
- package/dist/player.js.map +1 -1
- package/dist/{remux-VPKCLHHM.cjs → remux-NSBJFMLG.cjs} +9 -9
- package/dist/{remux-VPKCLHHM.cjs.map → remux-NSBJFMLG.cjs.map} +1 -1
- package/dist/{remux-7TA4FKTY.js → remux-PHUHO3VV.js} +4 -4
- package/dist/{remux-7TA4FKTY.js.map → remux-PHUHO3VV.js.map} +1 -1
- package/package.json +1 -1
- package/src/element/avbridge-player.ts +87 -15
- 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/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-EQE6AR75.cjs.map +0 -1
- package/dist/avi-NNHH4AAA.js.map +0 -1
- package/dist/avi-S7EY54YA.js.map +0 -1
- package/dist/avi-Y3N325WZ.cjs.map +0 -1
- package/dist/chunk-7EF4VTUS.cjs.map +0 -1
- package/dist/chunk-Z26PXRUY.js.map +0 -1
package/dist/element.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkUM6WCSGL_cjs = require('./chunk-UM6WCSGL.cjs');
|
|
4
4
|
require('./chunk-WRKO6Q42.cjs');
|
|
5
|
-
require('./chunk-
|
|
5
|
+
require('./chunk-VLI3Y6IJ.cjs');
|
|
6
6
|
require('./chunk-2IJ66NTD.cjs');
|
|
7
7
|
require('./chunk-QDJLQR53.cjs');
|
|
8
8
|
require('./chunk-HZUVMXBN.cjs');
|
|
@@ -296,7 +296,7 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
296
296
|
this._dispatch("loadstart", {});
|
|
297
297
|
let player;
|
|
298
298
|
try {
|
|
299
|
-
player = await
|
|
299
|
+
player = await chunkUM6WCSGL_cjs.createPlayer({
|
|
300
300
|
source,
|
|
301
301
|
target: this._videoEl,
|
|
302
302
|
// Honor the consumer's preferred initial strategy. "auto" means
|
package/dist/element.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createPlayer } from './chunk-
|
|
1
|
+
import { createPlayer } from './chunk-BN7BRTLY.js';
|
|
2
2
|
import './chunk-EDDWAN2L.js';
|
|
3
|
-
import './chunk-
|
|
3
|
+
import './chunk-5CX7BVVV.js';
|
|
4
4
|
import './chunk-CPJLFFCC.js';
|
|
5
5
|
import './chunk-LUFA47FP.js';
|
|
6
6
|
import './chunk-3YKWU4FM.js';
|
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var chunkE5MAM2P4_cjs = require('./chunk-E5MAM2P4.cjs');
|
|
4
|
+
var chunkUM6WCSGL_cjs = require('./chunk-UM6WCSGL.cjs');
|
|
5
5
|
var chunkWRKO6Q42_cjs = require('./chunk-WRKO6Q42.cjs');
|
|
6
|
-
var
|
|
6
|
+
var chunkVLI3Y6IJ_cjs = require('./chunk-VLI3Y6IJ.cjs');
|
|
7
7
|
var chunk2IJ66NTD_cjs = require('./chunk-2IJ66NTD.cjs');
|
|
8
8
|
require('./chunk-QDJLQR53.cjs');
|
|
9
9
|
require('./chunk-HZUVMXBN.cjs');
|
|
@@ -37,7 +37,7 @@ async function transcodeViaLibav(ctx, options) {
|
|
|
37
37
|
import('mediabunny'),
|
|
38
38
|
import('./libav-demux-575OYCT2.cjs'),
|
|
39
39
|
import('./source-VFLXLOCN.cjs'),
|
|
40
|
-
import('./remux-
|
|
40
|
+
import('./remux-NSBJFMLG.cjs')
|
|
41
41
|
]);
|
|
42
42
|
const normalized = await normalizeSource(ctx.source);
|
|
43
43
|
const demux = await openLibavDemux({
|
|
@@ -535,7 +535,7 @@ async function transcode(source, options = {}) {
|
|
|
535
535
|
const quality = options.quality ?? "medium";
|
|
536
536
|
validateCodecCompatibility(outputFormat, videoCodec, audioCodec);
|
|
537
537
|
options.signal?.throwIfAborted();
|
|
538
|
-
const ctx = await
|
|
538
|
+
const ctx = await chunkVLI3Y6IJ_cjs.probe(source);
|
|
539
539
|
options.signal?.throwIfAborted();
|
|
540
540
|
if (isLibavTranscodeContainer(ctx.container)) {
|
|
541
541
|
return transcodeViaLibav(ctx, options);
|
|
@@ -552,7 +552,7 @@ async function transcode(source, options = {}) {
|
|
|
552
552
|
async function attemptTranscode(ctx, outputFormat, videoCodec, audioCodec, quality, options) {
|
|
553
553
|
const mb = await import('mediabunny');
|
|
554
554
|
const input = new mb.Input({
|
|
555
|
-
source: await
|
|
555
|
+
source: await chunkVLI3Y6IJ_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
|
|
556
556
|
formats: mb.ALL_FORMATS
|
|
557
557
|
});
|
|
558
558
|
let bytesWritten = 0;
|
|
@@ -566,7 +566,7 @@ async function attemptTranscode(ctx, outputFormat, videoCodec, audioCodec, quali
|
|
|
566
566
|
}
|
|
567
567
|
})) : null;
|
|
568
568
|
const output = new mb.Output({
|
|
569
|
-
format:
|
|
569
|
+
format: chunkE5MAM2P4_cjs.createOutputFormat(mb, outputFormat),
|
|
570
570
|
target: streamTarget ?? bufferTarget
|
|
571
571
|
});
|
|
572
572
|
const videoOptions = options.dropVideo ? { discard: true } : {
|
|
@@ -657,8 +657,8 @@ async function doTranscode(ctx, outputFormat, videoCodec, audioCodec, quality, o
|
|
|
657
657
|
await new Promise((r) => setTimeout(r, 50 * (attempt + 1)));
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
|
-
const mimeType =
|
|
661
|
-
const filename =
|
|
660
|
+
const mimeType = chunkE5MAM2P4_cjs.mimeForFormat(outputFormat);
|
|
661
|
+
const filename = chunkE5MAM2P4_cjs.generateFilename(ctx.name, outputFormat);
|
|
662
662
|
if (options.outputStream) {
|
|
663
663
|
options.onProgress?.({ percent: 100, bytesWritten: 0 });
|
|
664
664
|
return {
|
|
@@ -759,35 +759,35 @@ function qualityToMediabunny2(mb, quality) {
|
|
|
759
759
|
|
|
760
760
|
Object.defineProperty(exports, "remux", {
|
|
761
761
|
enumerable: true,
|
|
762
|
-
get: function () { return
|
|
762
|
+
get: function () { return chunkE5MAM2P4_cjs.remux; }
|
|
763
763
|
});
|
|
764
764
|
Object.defineProperty(exports, "FALLBACK_AUDIO_CODECS", {
|
|
765
765
|
enumerable: true,
|
|
766
|
-
get: function () { return
|
|
766
|
+
get: function () { return chunkUM6WCSGL_cjs.FALLBACK_AUDIO_CODECS; }
|
|
767
767
|
});
|
|
768
768
|
Object.defineProperty(exports, "FALLBACK_VIDEO_CODECS", {
|
|
769
769
|
enumerable: true,
|
|
770
|
-
get: function () { return
|
|
770
|
+
get: function () { return chunkUM6WCSGL_cjs.FALLBACK_VIDEO_CODECS; }
|
|
771
771
|
});
|
|
772
772
|
Object.defineProperty(exports, "NATIVE_AUDIO_CODECS", {
|
|
773
773
|
enumerable: true,
|
|
774
|
-
get: function () { return
|
|
774
|
+
get: function () { return chunkUM6WCSGL_cjs.NATIVE_AUDIO_CODECS; }
|
|
775
775
|
});
|
|
776
776
|
Object.defineProperty(exports, "NATIVE_VIDEO_CODECS", {
|
|
777
777
|
enumerable: true,
|
|
778
|
-
get: function () { return
|
|
778
|
+
get: function () { return chunkUM6WCSGL_cjs.NATIVE_VIDEO_CODECS; }
|
|
779
779
|
});
|
|
780
780
|
Object.defineProperty(exports, "UnifiedPlayer", {
|
|
781
781
|
enumerable: true,
|
|
782
|
-
get: function () { return
|
|
782
|
+
get: function () { return chunkUM6WCSGL_cjs.UnifiedPlayer; }
|
|
783
783
|
});
|
|
784
784
|
Object.defineProperty(exports, "classify", {
|
|
785
785
|
enumerable: true,
|
|
786
|
-
get: function () { return
|
|
786
|
+
get: function () { return chunkUM6WCSGL_cjs.classifyContext; }
|
|
787
787
|
});
|
|
788
788
|
Object.defineProperty(exports, "createPlayer", {
|
|
789
789
|
enumerable: true,
|
|
790
|
-
get: function () { return
|
|
790
|
+
get: function () { return chunkUM6WCSGL_cjs.createPlayer; }
|
|
791
791
|
});
|
|
792
792
|
Object.defineProperty(exports, "srtToVtt", {
|
|
793
793
|
enumerable: true,
|
|
@@ -795,7 +795,7 @@ Object.defineProperty(exports, "srtToVtt", {
|
|
|
795
795
|
});
|
|
796
796
|
Object.defineProperty(exports, "probe", {
|
|
797
797
|
enumerable: true,
|
|
798
|
-
get: function () { return
|
|
798
|
+
get: function () { return chunkVLI3Y6IJ_cjs.probe; }
|
|
799
799
|
});
|
|
800
800
|
Object.defineProperty(exports, "AvbridgeError", {
|
|
801
801
|
enumerable: true,
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { mimeForFormat, generateFilename, createOutputFormat } from './chunk-
|
|
2
|
-
export { remux } from './chunk-
|
|
3
|
-
export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext as classify, createPlayer } from './chunk-
|
|
1
|
+
import { mimeForFormat, generateFilename, createOutputFormat } from './chunk-B76QWPFM.js';
|
|
2
|
+
export { remux } from './chunk-B76QWPFM.js';
|
|
3
|
+
export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext as classify, createPlayer } from './chunk-BN7BRTLY.js';
|
|
4
4
|
export { srtToVtt } from './chunk-EDDWAN2L.js';
|
|
5
|
-
import { probe, buildMediabunnySourceFromInput } from './chunk-
|
|
6
|
-
export { probe } from './chunk-
|
|
5
|
+
import { probe, buildMediabunnySourceFromInput } from './chunk-5CX7BVVV.js';
|
|
6
|
+
export { probe } from './chunk-5CX7BVVV.js';
|
|
7
7
|
import { AvbridgeError, ERR_CONTAINER_NOT_SUPPORTED, ERR_TRANSCODE_UNSUPPORTED_COMBO, ERR_TRANSCODE_ABORTED, ERR_TRANSCODE_DECODE, ERR_CODEC_NOT_SUPPORTED } from './chunk-CPJLFFCC.js';
|
|
8
8
|
export { AvbridgeError, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_CODEC_NOT_SUPPORTED, ERR_FETCH_FAILED, ERR_LIBAV_NOT_REACHABLE, ERR_MSE_CODEC_NOT_SUPPORTED, ERR_MSE_NOT_SUPPORTED, ERR_PLAYER_NOT_READY, ERR_PROBE_FAILED, ERR_PROBE_FETCH_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_RANGE_NOT_SUPPORTED, ERR_STRATEGY_FAILED } from './chunk-CPJLFFCC.js';
|
|
9
9
|
import './chunk-LUFA47FP.js';
|
|
@@ -38,7 +38,7 @@ async function transcodeViaLibav(ctx, options) {
|
|
|
38
38
|
import('mediabunny'),
|
|
39
39
|
import('./libav-demux-SXZDLC7W.js'),
|
|
40
40
|
import('./source-4TZ6KMNV.js'),
|
|
41
|
-
import('./remux-
|
|
41
|
+
import('./remux-PHUHO3VV.js')
|
|
42
42
|
]);
|
|
43
43
|
const normalized = await normalizeSource(ctx.source);
|
|
44
44
|
const demux = await openLibavDemux({
|
package/dist/player.cjs
CHANGED
|
@@ -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() {
|
|
@@ -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",
|
|
@@ -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
|
|
@@ -6022,10 +6133,12 @@ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
|
6022
6133
|
}
|
|
6023
6134
|
}
|
|
6024
6135
|
// ── Stats for nerds ────────────────────────────────────────────────────
|
|
6136
|
+
_statsPrev = null;
|
|
6025
6137
|
_toggleStats() {
|
|
6026
6138
|
this._statsOpen = !this._statsOpen;
|
|
6027
6139
|
this._statsEl.classList.toggle("open", this._statsOpen);
|
|
6028
6140
|
if (this._statsOpen) {
|
|
6141
|
+
this._statsPrev = null;
|
|
6029
6142
|
this._updateStats();
|
|
6030
6143
|
this._statsInterval = setInterval(() => this._updateStats(), 1e3);
|
|
6031
6144
|
} else {
|
|
@@ -6042,23 +6155,76 @@ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
|
6042
6155
|
return;
|
|
6043
6156
|
}
|
|
6044
6157
|
const rt = d.runtime ?? {};
|
|
6045
|
-
const
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
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
|
+
}
|
|
6060
6225
|
if (d.probedBy) lines.push(`Probed by: ${d.probedBy}`);
|
|
6061
6226
|
this._statsEl.textContent = lines.join("\n");
|
|
6227
|
+
this._statsPrev = { ts: now, rt: { ...rt } };
|
|
6062
6228
|
}
|
|
6063
6229
|
// ── Controls: fullscreen ───────────────────────────────────────────────
|
|
6064
6230
|
_toggleFullscreen() {
|