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.
Files changed (52) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/dist/{avi-EQE6AR75.cjs → avi-32UABODO.cjs} +12 -4
  3. package/dist/avi-32UABODO.cjs.map +1 -0
  4. package/dist/{avi-Y3N325WZ.cjs → avi-5BPR6QUX.cjs} +12 -4
  5. package/dist/avi-5BPR6QUX.cjs.map +1 -0
  6. package/dist/{avi-NNHH4AAA.js → avi-BLIH7KKV.js} +12 -4
  7. package/dist/avi-BLIH7KKV.js.map +1 -0
  8. package/dist/{avi-S7EY54YA.js → avi-GX2H34IQ.js} +12 -4
  9. package/dist/avi-GX2H34IQ.js.map +1 -0
  10. package/dist/{chunk-2LNXMGT6.js → chunk-5CX7BVVV.js} +5 -5
  11. package/dist/{chunk-2LNXMGT6.js.map → chunk-5CX7BVVV.js.map} +1 -1
  12. package/dist/{chunk-5Y5BTB5D.js → chunk-B76QWPFM.js} +3 -3
  13. package/dist/{chunk-5Y5BTB5D.js.map → chunk-B76QWPFM.js.map} +1 -1
  14. package/dist/{chunk-Z26PXRUY.js → chunk-BN7BRTLY.js} +137 -26
  15. package/dist/chunk-BN7BRTLY.js.map +1 -0
  16. package/dist/{chunk-GJBNLPGI.cjs → chunk-E5MAM2P4.cjs} +9 -9
  17. package/dist/{chunk-GJBNLPGI.cjs.map → chunk-E5MAM2P4.cjs.map} +1 -1
  18. package/dist/{chunk-7EF4VTUS.cjs → chunk-UM6WCSGL.cjs} +141 -30
  19. package/dist/chunk-UM6WCSGL.cjs.map +1 -0
  20. package/dist/{chunk-HBHSUGNI.cjs → chunk-VLI3Y6IJ.cjs} +5 -5
  21. package/dist/{chunk-HBHSUGNI.cjs.map → chunk-VLI3Y6IJ.cjs.map} +1 -1
  22. package/dist/element-browser.js +144 -25
  23. package/dist/element-browser.js.map +1 -1
  24. package/dist/element.cjs +3 -3
  25. package/dist/element.js +2 -2
  26. package/dist/index.cjs +18 -18
  27. package/dist/index.js +6 -6
  28. package/dist/player.cjs +207 -41
  29. package/dist/player.cjs.map +1 -1
  30. package/dist/player.d.cts +1 -0
  31. package/dist/player.d.ts +1 -0
  32. package/dist/player.js +207 -41
  33. package/dist/player.js.map +1 -1
  34. package/dist/{remux-VPKCLHHM.cjs → remux-NSBJFMLG.cjs} +9 -9
  35. package/dist/{remux-VPKCLHHM.cjs.map → remux-NSBJFMLG.cjs.map} +1 -1
  36. package/dist/{remux-7TA4FKTY.js → remux-PHUHO3VV.js} +4 -4
  37. package/dist/{remux-7TA4FKTY.js.map → remux-PHUHO3VV.js.map} +1 -1
  38. package/package.json +1 -1
  39. package/src/element/avbridge-player.ts +87 -15
  40. package/src/probe/avi.ts +34 -2
  41. package/src/strategies/fallback/decoder.ts +148 -19
  42. package/src/strategies/fallback/video-renderer.ts +41 -3
  43. package/src/strategies/hybrid/decoder.ts +34 -9
  44. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
  45. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
  46. package/vendor/libav/avbridge/libav-avbridge.mjs +1 -1
  47. package/dist/avi-EQE6AR75.cjs.map +0 -1
  48. package/dist/avi-NNHH4AAA.js.map +0 -1
  49. package/dist/avi-S7EY54YA.js.map +0 -1
  50. package/dist/avi-Y3N325WZ.cjs.map +0 -1
  51. package/dist/chunk-7EF4VTUS.cjs.map +0 -1
  52. 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 chunk7EF4VTUS_cjs = require('./chunk-7EF4VTUS.cjs');
3
+ var chunkUM6WCSGL_cjs = require('./chunk-UM6WCSGL.cjs');
4
4
  require('./chunk-WRKO6Q42.cjs');
5
- require('./chunk-HBHSUGNI.cjs');
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 chunk7EF4VTUS_cjs.createPlayer({
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-Z26PXRUY.js';
1
+ import { createPlayer } from './chunk-BN7BRTLY.js';
2
2
  import './chunk-EDDWAN2L.js';
3
- import './chunk-2LNXMGT6.js';
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 chunkGJBNLPGI_cjs = require('./chunk-GJBNLPGI.cjs');
4
- var chunk7EF4VTUS_cjs = require('./chunk-7EF4VTUS.cjs');
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 chunkHBHSUGNI_cjs = require('./chunk-HBHSUGNI.cjs');
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-VPKCLHHM.cjs')
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 chunkHBHSUGNI_cjs.probe(source);
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 chunkHBHSUGNI_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
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: chunkGJBNLPGI_cjs.createOutputFormat(mb, outputFormat),
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 = chunkGJBNLPGI_cjs.mimeForFormat(outputFormat);
661
- const filename = chunkGJBNLPGI_cjs.generateFilename(ctx.name, outputFormat);
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 chunkGJBNLPGI_cjs.remux; }
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 chunk7EF4VTUS_cjs.FALLBACK_AUDIO_CODECS; }
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 chunk7EF4VTUS_cjs.FALLBACK_VIDEO_CODECS; }
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 chunk7EF4VTUS_cjs.NATIVE_AUDIO_CODECS; }
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 chunk7EF4VTUS_cjs.NATIVE_VIDEO_CODECS; }
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 chunk7EF4VTUS_cjs.UnifiedPlayer; }
782
+ get: function () { return chunkUM6WCSGL_cjs.UnifiedPlayer; }
783
783
  });
784
784
  Object.defineProperty(exports, "classify", {
785
785
  enumerable: true,
786
- get: function () { return chunk7EF4VTUS_cjs.classifyContext; }
786
+ get: function () { return chunkUM6WCSGL_cjs.classifyContext; }
787
787
  });
788
788
  Object.defineProperty(exports, "createPlayer", {
789
789
  enumerable: true,
790
- get: function () { return chunk7EF4VTUS_cjs.createPlayer; }
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 chunkHBHSUGNI_cjs.probe; }
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-5Y5BTB5D.js';
2
- export { remux } from './chunk-5Y5BTB5D.js';
3
- export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext as classify, createPlayer } from './chunk-Z26PXRUY.js';
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-2LNXMGT6.js';
6
- export { probe } from './chunk-2LNXMGT6.js';
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-7TA4FKTY.js')
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-Y3N325WZ.cjs');
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-Y3N325WZ.cjs');
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-Y3N325WZ.cjs');
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
- /** True once at least one frame has been enqueued. */
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.framesPainted > 0;
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 dropThresholdUs = audioNowUs - frameDurationUs * 2;
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
- console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
2136
+ bsfRequiredButMissing = true;
2112
2137
  bsfCtx = null;
2113
2138
  }
2114
2139
  } catch (err) {
2115
- console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
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
- await libav.av_bsf_send_packet(bsfCtx, 0);
2142
- while (true) {
2143
- const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2144
- if (err < 0) break;
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
- console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
2800
+ bsfRequiredButMissing = true;
2766
2801
  bsfCtx = null;
2767
2802
  }
2768
2803
  } catch (err) {
2769
- console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
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
- await libav.av_bsf_send_packet(bsfCtx, 0);
2796
- while (true) {
2797
- const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2798
- if (err < 0) break;
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
- while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
2889
- await new Promise((r) => setTimeout(r, 50));
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 ts = syntheticVideoUs;
2934
- syntheticVideoUs += videoFrameStepUs;
2935
- return ts;
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 lines = [
6046
- `Container: ${d.container ?? "?"}`,
6047
- `Video: ${d.videoCodec ?? "?"} ${d.width ?? "?"}\xD7${d.height ?? "?"}`,
6048
- `Audio: ${d.audioCodec ?? "none"}`,
6049
- `Strategy: ${d.strategy ?? "?"} Class: ${d.strategyClass ?? "?"}`,
6050
- `Transport: ${d.transport ?? "?"} Range: ${d.rangeSupported ?? "?"}`,
6051
- `Duration: ${typeof d.duration === "number" ? d.duration.toFixed(1) + "s" : "?"}`
6052
- ];
6053
- if (rt.framesDecoded != null) lines.push(`Frames: ${rt.framesDecoded} decoded, ${rt.framesDropped ?? 0} dropped`);
6054
- if (rt.framesPainted != null) lines.push(`Painted: ${rt.framesPainted} Late: ${rt.framesDroppedLate ?? 0} Overflow: ${rt.framesDroppedOverflow ?? 0}`);
6055
- if (rt.videoFramesDecoded != null) lines.push(`Video decoded: ${rt.videoFramesDecoded} Chunks fed: ${rt.videoChunksFed ?? "?"}`);
6056
- if (rt.audioFramesDecoded != null) lines.push(`Audio decoded: ${rt.audioFramesDecoded}`);
6057
- if (rt.packetsRead != null) lines.push(`Packets read: ${rt.packetsRead}`);
6058
- if (rt.bsfApplied && rt.bsfApplied.length > 0) lines.push(`BSF: ${rt.bsfApplied.join(", ")}`);
6059
- if (rt.audioState != null) lines.push(`Audio state: ${rt.audioState} Clock: ${rt.clockMode ?? "?"}`);
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() {