avbridge 2.12.0 → 2.13.0

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 (55) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/README.md +33 -0
  3. package/dist/{avi-EQE6AR75.cjs → avi-32UABODO.cjs} +12 -4
  4. package/dist/avi-32UABODO.cjs.map +1 -0
  5. package/dist/{avi-Y3N325WZ.cjs → avi-5BPR6QUX.cjs} +12 -4
  6. package/dist/avi-5BPR6QUX.cjs.map +1 -0
  7. package/dist/{avi-NNHH4AAA.js → avi-BLIH7KKV.js} +12 -4
  8. package/dist/avi-BLIH7KKV.js.map +1 -0
  9. package/dist/{avi-S7EY54YA.js → avi-GX2H34IQ.js} +12 -4
  10. package/dist/avi-GX2H34IQ.js.map +1 -0
  11. package/dist/{chunk-2LNXMGT6.js → chunk-5CX7BVVV.js} +5 -5
  12. package/dist/{chunk-2LNXMGT6.js.map → chunk-5CX7BVVV.js.map} +1 -1
  13. package/dist/{chunk-5Y5BTB5D.js → chunk-B76QWPFM.js} +3 -3
  14. package/dist/{chunk-5Y5BTB5D.js.map → chunk-B76QWPFM.js.map} +1 -1
  15. package/dist/{chunk-GJBNLPGI.cjs → chunk-E5MAM2P4.cjs} +9 -9
  16. package/dist/{chunk-GJBNLPGI.cjs.map → chunk-E5MAM2P4.cjs.map} +1 -1
  17. package/dist/{chunk-7EF4VTUS.cjs → chunk-OFJYEITB.cjs} +489 -113
  18. package/dist/chunk-OFJYEITB.cjs.map +1 -0
  19. package/dist/{chunk-HBHSUGNI.cjs → chunk-VLI3Y6IJ.cjs} +5 -5
  20. package/dist/{chunk-HBHSUGNI.cjs.map → chunk-VLI3Y6IJ.cjs.map} +1 -1
  21. package/dist/{chunk-Z26PXRUY.js → chunk-VOC24LYF.js} +486 -110
  22. package/dist/chunk-VOC24LYF.js.map +1 -0
  23. package/dist/element-browser.js +492 -130
  24. package/dist/element-browser.js.map +1 -1
  25. package/dist/element.cjs +3 -3
  26. package/dist/element.js +2 -2
  27. package/dist/index.cjs +18 -18
  28. package/dist/index.js +6 -6
  29. package/dist/player.cjs +658 -170
  30. package/dist/player.cjs.map +1 -1
  31. package/dist/player.d.cts +36 -4
  32. package/dist/player.d.ts +36 -4
  33. package/dist/player.js +658 -170
  34. package/dist/player.js.map +1 -1
  35. package/dist/{remux-VPKCLHHM.cjs → remux-NSBJFMLG.cjs} +9 -9
  36. package/dist/{remux-VPKCLHHM.cjs.map → remux-NSBJFMLG.cjs.map} +1 -1
  37. package/dist/{remux-7TA4FKTY.js → remux-PHUHO3VV.js} +4 -4
  38. package/dist/{remux-7TA4FKTY.js.map → remux-PHUHO3VV.js.map} +1 -1
  39. package/package.json +1 -1
  40. package/src/element/avbridge-player.ts +223 -43
  41. package/src/probe/avi.ts +34 -2
  42. package/src/strategies/fallback/audio-output.ts +164 -35
  43. package/src/strategies/fallback/decoder.ts +467 -60
  44. package/src/strategies/fallback/video-renderer.ts +209 -29
  45. package/src/strategies/hybrid/decoder.ts +56 -28
  46. package/src/strategies/remux/pipeline.ts +12 -3
  47. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
  48. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
  49. package/vendor/libav/avbridge/libav-avbridge.mjs +1 -1
  50. package/dist/avi-EQE6AR75.cjs.map +0 -1
  51. package/dist/avi-NNHH4AAA.js.map +0 -1
  52. package/dist/avi-S7EY54YA.js.map +0 -1
  53. package/dist/avi-Y3N325WZ.cjs.map +0 -1
  54. package/dist/chunk-7EF4VTUS.cjs.map +0 -1
  55. package/dist/chunk-Z26PXRUY.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkWRKO6Q42_cjs = require('./chunk-WRKO6Q42.cjs');
4
- var chunkHBHSUGNI_cjs = require('./chunk-HBHSUGNI.cjs');
4
+ var chunkVLI3Y6IJ_cjs = require('./chunk-VLI3Y6IJ.cjs');
5
5
  var chunk2IJ66NTD_cjs = require('./chunk-2IJ66NTD.cjs');
6
6
  var chunkHZUVMXBN_cjs = require('./chunk-HZUVMXBN.cjs');
7
7
  var chunkG4APZMCP_cjs = require('./chunk-G4APZMCP.cjs');
@@ -734,12 +734,12 @@ async function createRemuxPipeline(ctx, video) {
734
734
  const mb = await import('mediabunny');
735
735
  const videoTrackInfo = ctx.videoTracks[0];
736
736
  if (!videoTrackInfo) throw new Error("remux: source has no video track");
737
- const mbVideoCodec = chunkHBHSUGNI_cjs.avbridgeVideoToMediabunny(videoTrackInfo.codec);
737
+ const mbVideoCodec = chunkVLI3Y6IJ_cjs.avbridgeVideoToMediabunny(videoTrackInfo.codec);
738
738
  if (!mbVideoCodec) {
739
739
  throw new Error(`remux: video codec "${videoTrackInfo.codec}" is not supported by mediabunny output`);
740
740
  }
741
741
  const input = new mb.Input({
742
- source: await chunkHBHSUGNI_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
742
+ source: await chunkVLI3Y6IJ_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
743
743
  formats: mb.ALL_FORMATS
744
744
  });
745
745
  const allTracks = await input.getTracks();
@@ -771,7 +771,7 @@ async function createRemuxPipeline(ctx, video) {
771
771
  throw new Error("remux: audio track not found in input");
772
772
  }
773
773
  inputAudio = newInput;
774
- mbAudioCodec = chunkHBHSUGNI_cjs.avbridgeAudioToMediabunny(trackInfo.codec);
774
+ mbAudioCodec = chunkVLI3Y6IJ_cjs.avbridgeAudioToMediabunny(trackInfo.codec);
775
775
  audioSink = new mb.EncodedPacketSink(newInput);
776
776
  audioConfig = await newInput.getDecoderConfig();
777
777
  }
@@ -791,22 +791,25 @@ async function createRemuxPipeline(ctx, video) {
791
791
  }
792
792
  }
793
793
  let mimePromise = null;
794
+ const myToken = pumpToken;
794
795
  const writable = new WritableStream({
795
796
  write: async (chunk) => {
796
- if (destroyed) return;
797
+ if (destroyed || pumpToken !== myToken) return;
797
798
  if (!sink) {
798
799
  const mime = await (mimePromise ??= output.getMimeType());
800
+ if (destroyed || pumpToken !== myToken) return;
799
801
  sink = new MseSink({ mime, video });
800
802
  await sink.ready();
803
+ if (destroyed || pumpToken !== myToken) return;
801
804
  if (pendingStartTime > 0) {
802
805
  sink.invalidate(pendingStartTime);
803
806
  }
804
807
  sink.setPlayOnSeek(pendingAutoPlay);
805
808
  }
806
- while (sink && !destroyed && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
809
+ while (sink && !destroyed && pumpToken === myToken && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
807
810
  await new Promise((r) => setTimeout(r, 500));
808
811
  }
809
- if (destroyed) return;
812
+ if (destroyed || pumpToken !== myToken) return;
810
813
  sink.append(chunk.data);
811
814
  stats.bytesWritten += chunk.data.byteLength;
812
815
  stats.fragments++;
@@ -1073,7 +1076,21 @@ var VideoRenderer = class {
1073
1076
  framesPainted = 0;
1074
1077
  framesDroppedLate = 0;
1075
1078
  framesDroppedOverflow = 0;
1079
+ /** True once the head frame has been painted as a pre-roll poster
1080
+ * since the last flush. Used to ensure pre-roll paints exactly one
1081
+ * frame (held static) during the post-seek discard window. */
1076
1082
  prerolled = false;
1083
+ /** PTS (µs) of the most recently painted frame. Used as the calibration
1084
+ * reference on the first post-flush snap: the pre-roll path paints one
1085
+ * frame *before* PTS-based playback starts, so the queue head's PTS at
1086
+ * first PTS-based paint is the *next* frame, off by one frameDur from
1087
+ * the actually-displayed frame. Calibrating against the painted frame
1088
+ * instead of the queue head removes that one-frame offset and yields
1089
+ * calib ≈ 0 instead of +frameDur. */
1090
+ lastPaintedPtsUs = 0;
1091
+ hasLastPaintedPts = false;
1092
+ /** Audio-clock reading (ms) at the previous paint, for overlay Δaud. */
1093
+ lastPaintAudMs = 0;
1077
1094
  /** Wall-clock time of the last paint, in ms (performance.now()). */
1078
1095
  lastPaintWall = 0;
1079
1096
  /** Minimum ms between paints — paces video at roughly source fps. */
@@ -1104,32 +1121,47 @@ var VideoRenderer = class {
1104
1121
  /** Resolves once the first decoded frame has been enqueued. */
1105
1122
  firstFrameReady;
1106
1123
  resolveFirstFrame;
1107
- /** True once at least one frame has been enqueued. */
1124
+ /**
1125
+ * True once at least one frame has been enqueued *since the last flush*.
1126
+ * Used by `readyState` — initial cold-start reports HAVE_NOTHING until
1127
+ * any frame has arrived, and after a seek we want the same semantics
1128
+ * (HAVE_NOTHING until post-seek frames arrive), so the cumulative
1129
+ * `framesPainted > 0` that used to live here was wrong: it kept the
1130
+ * state "true forever" after the first frame ever, so post-seek
1131
+ * `waitForBuffer()` would exit immediately with an empty queue and
1132
+ * leave video frozen while audio kept going.
1133
+ */
1108
1134
  hasFrames() {
1109
- return this.queue.length > 0 || this.framesPainted > 0;
1135
+ return this.queue.length > 0 || this.hasEverEnqueuedSinceFlush;
1110
1136
  }
1137
+ hasEverEnqueuedSinceFlush = false;
1111
1138
  /** Current depth of the frame queue. Used by the decoder for backpressure. */
1112
1139
  queueDepth() {
1113
1140
  return this.queue.length;
1114
1141
  }
1115
1142
  /**
1116
- * Soft cap for decoder backpressure. The decoder pump throttles when
1117
- * `queueDepth() >= queueHighWater`. Set high enough that normal decode
1118
- * bursts don't trigger the renderer's overflow-drop loop (which runs at
1119
- * every paint), but low enough that the decoder doesn't run unboundedly
1120
- * ahead. The hard cap in `enqueue()` is 64.
1143
+ * Cap the decoder may fill the queue up to. Used by the decoder's
1144
+ * enqueue-side discard logic (it closes new frames instead of pushing
1145
+ * them when this is reached). Sized so a long post-seek catch-up
1146
+ * fits the decoder produces frames at PTS T_kf onwards rapidly
1147
+ * while the demuxer is chewing through pre-target audio; if the
1148
+ * queue can hold the whole post-seek burst, the renderer plays
1149
+ * smoothly from pre-roll without a frozen-video gap when audio.start
1150
+ * fires. At ~340 KB per SD frame the cap is ~85 MB peak; at HD it's
1151
+ * larger but still bounded.
1121
1152
  */
1122
- queueHighWater = 30;
1153
+ queueHighWater = 256;
1123
1154
  enqueue(frame) {
1124
1155
  if (this.destroyed) {
1125
1156
  frame.close();
1126
1157
  return;
1127
1158
  }
1128
1159
  this.queue.push(frame);
1160
+ this.hasEverEnqueuedSinceFlush = true;
1129
1161
  if (this.queue.length === 1 && this.framesPainted === 0) {
1130
1162
  this.resolveFirstFrame();
1131
1163
  }
1132
- while (this.queue.length > 60) {
1164
+ while (this.queue.length > this.queueHighWater + 8) {
1133
1165
  this.queue.shift()?.close();
1134
1166
  this.framesDroppedOverflow++;
1135
1167
  }
@@ -1211,12 +1243,9 @@ var VideoRenderer = class {
1211
1243
  if (this.queue.length === 0) return;
1212
1244
  const playing = this.clock.isPlaying();
1213
1245
  if (!playing) {
1214
- if (!this.prerolled) {
1215
- const head = this.queue.shift();
1216
- this.paint(head);
1217
- head.close();
1246
+ if (!this.prerolled && this.queue.length > 0) {
1218
1247
  this.prerolled = true;
1219
- this.lastPaintWall = performance.now();
1248
+ this.paint(this.queue[0]);
1220
1249
  }
1221
1250
  return;
1222
1251
  }
@@ -1225,14 +1254,29 @@ var VideoRenderer = class {
1225
1254
  const hasPts = headTs > 0 || this.queue.length > 1;
1226
1255
  if (hasPts) {
1227
1256
  const wallNow2 = performance.now();
1228
- if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
1229
- this.ptsCalibrationUs = headTs - rawAudioNowUs;
1257
+ if (!this.ptsCalibrated) {
1258
+ const anchorUs = (this.clock.anchorTime?.() ?? this.clock.now()) * 1e6;
1259
+ const referencePtsUs = this.hasLastPaintedPts ? this.lastPaintedPtsUs : headTs;
1260
+ this.ptsCalibrationUs = referencePtsUs - anchorUs;
1230
1261
  this.ptsCalibrated = true;
1231
1262
  this.lastCalibrationWall = wallNow2;
1263
+ if (isDebug()) {
1264
+ console.log(
1265
+ `[avbridge:renderer] CALIB-FIRST audioAnchor=${(anchorUs / 1e3).toFixed(1)}ms prerolledPTS=${this.hasLastPaintedPts ? (this.lastPaintedPtsUs / 1e3).toFixed(1) : "n/a"}ms queueHeadPTS=${(headTs / 1e3).toFixed(1)}ms rawAudioNow=${(rawAudioNowUs / 1e3).toFixed(1)}ms \u2192 calib=${(this.ptsCalibrationUs / 1e3).toFixed(1)}ms`
1266
+ );
1267
+ }
1268
+ } else if (wallNow2 - this.lastCalibrationWall > 1e4) {
1269
+ const oldCalib = this.ptsCalibrationUs;
1270
+ this.ptsCalibrationUs = headTs - rawAudioNowUs;
1271
+ this.lastCalibrationWall = wallNow2;
1272
+ if (isDebug()) {
1273
+ console.log(
1274
+ `[avbridge:renderer] CALIB-RESNAP headPTS=${(headTs / 1e3).toFixed(1)}ms rawAudioNow=${(rawAudioNowUs / 1e3).toFixed(1)}ms calib ${(oldCalib / 1e3).toFixed(1)}ms \u2192 ${(this.ptsCalibrationUs / 1e3).toFixed(1)}ms (\u0394=${((this.ptsCalibrationUs - oldCalib) / 1e3).toFixed(1)}ms after 10s)`
1275
+ );
1276
+ }
1232
1277
  }
1233
1278
  const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
1234
- const frameDurationUs = this.paintIntervalMs * 1e3;
1235
- const deadlineUs = audioNowUs + frameDurationUs;
1279
+ const deadlineUs = audioNowUs;
1236
1280
  let bestIdx = -1;
1237
1281
  for (let i = 0; i < this.queue.length; i++) {
1238
1282
  const ts = this.queue[i].timestamp ?? 0;
@@ -1259,19 +1303,21 @@ var VideoRenderer = class {
1259
1303
  }
1260
1304
  return;
1261
1305
  }
1262
- const dropThresholdUs = audioNowUs - frameDurationUs * 2;
1306
+ const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
1263
1307
  let dropped = 0;
1264
- while (bestIdx > 0) {
1265
- const ts = this.queue[0].timestamp ?? 0;
1266
- if (ts < dropThresholdUs) {
1308
+ const initialBestIdx = bestIdx;
1309
+ if (!_relaxDrop) {
1310
+ while (bestIdx > 0) {
1267
1311
  this.queue.shift()?.close();
1268
1312
  this.framesDroppedLate++;
1269
1313
  bestIdx--;
1270
1314
  dropped++;
1271
- } else {
1272
- break;
1273
1315
  }
1274
1316
  }
1317
+ const paintTs = this.queue[0]?.timestamp ?? 0;
1318
+ if (isDebug()) {
1319
+ console.log(`[TRACE] PAINT bestIdx_initial=${initialBestIdx} dropped=${dropped} paintPts=${(paintTs / 1e3).toFixed(1)}ms audioNow=${(audioNowUs / 1e3).toFixed(1)}ms deadline=${(deadlineUs / 1e3).toFixed(1)}ms queueLen=${this.queue.length} wall=${performance.now().toFixed(0)}`);
1320
+ }
1275
1321
  this.ticksPainted++;
1276
1322
  if (isDebug()) {
1277
1323
  const now = performance.now();
@@ -1307,6 +1353,33 @@ var VideoRenderer = class {
1307
1353
  }
1308
1354
  try {
1309
1355
  this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
1356
+ if (isDebug()) {
1357
+ const wallNow = performance.now();
1358
+ const audNowMs = this.clock.now() * 1e3;
1359
+ const ptsMs = (frame.timestamp ?? 0) / 1e3;
1360
+ const dWall = this.lastPaintWall > 0 ? wallNow - this.lastPaintWall : 0;
1361
+ const dAud = this.lastPaintAudMs > 0 ? audNowMs - this.lastPaintAudMs : 0;
1362
+ const dPts = this.hasLastPaintedPts ? ptsMs - this.lastPaintedPtsUs / 1e3 : 0;
1363
+ this.ctx.save();
1364
+ this.ctx.font = "bold 18px monospace";
1365
+ const lines = [
1366
+ `#${this.framesPainted + 1} pts=${ptsMs.toFixed(0)} aud=${audNowMs.toFixed(0)} wall=${wallNow.toFixed(0)}`,
1367
+ `\u0394pts=${dPts.toFixed(0)} \u0394aud=${dAud.toFixed(0)} \u0394wall=${dWall.toFixed(0)}`
1368
+ ];
1369
+ const lineHeight = 22;
1370
+ const padTop = 6;
1371
+ const stripH = padTop + lineHeight * lines.length;
1372
+ this.ctx.fillStyle = "rgba(0,0,0,0.7)";
1373
+ this.ctx.fillRect(0, 0, this.canvas.width, stripH);
1374
+ this.ctx.fillStyle = "#0f0";
1375
+ for (let i = 0; i < lines.length; i++) {
1376
+ this.ctx.fillText(lines[i], 8, padTop + lineHeight * (i + 1) - 4);
1377
+ }
1378
+ this.ctx.restore();
1379
+ }
1380
+ this.lastPaintedPtsUs = frame.timestamp ?? 0;
1381
+ this.hasLastPaintedPts = true;
1382
+ this.lastPaintAudMs = this.clock.now() * 1e3;
1310
1383
  this.framesPainted++;
1311
1384
  } catch (err) {
1312
1385
  if (this.framesPainted === 0 && this.framesDroppedLate === 0) {
@@ -1319,17 +1392,30 @@ var VideoRenderer = class {
1319
1392
  const count = this.queue.length;
1320
1393
  while (this.queue.length > 0) this.queue.shift()?.close();
1321
1394
  this.prerolled = false;
1395
+ this.hasLastPaintedPts = false;
1322
1396
  this.ptsCalibrated = false;
1397
+ this.hasEverEnqueuedSinceFlush = false;
1323
1398
  if (isDebug() && count > 0) {
1324
1399
  console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
1325
1400
  }
1326
1401
  }
1327
1402
  stats() {
1403
+ let queueSpanMs = 0;
1404
+ let queueHeadMs = 0;
1405
+ let queueTailMs = 0;
1406
+ if (this.queue.length > 0) {
1407
+ queueHeadMs = Math.round((this.queue[0].timestamp ?? 0) / 1e3);
1408
+ queueTailMs = Math.round((this.queue[this.queue.length - 1].timestamp ?? 0) / 1e3);
1409
+ queueSpanMs = Math.max(0, queueTailMs - queueHeadMs);
1410
+ }
1328
1411
  return {
1329
1412
  framesPainted: this.framesPainted,
1330
1413
  framesDroppedLate: this.framesDroppedLate,
1331
1414
  framesDroppedOverflow: this.framesDroppedOverflow,
1332
- queueDepth: this.queue.length
1415
+ queueDepth: this.queue.length,
1416
+ queueHeadMs,
1417
+ queueTailMs,
1418
+ queueSpanMs
1333
1419
  };
1334
1420
  }
1335
1421
  destroy() {
@@ -1347,6 +1433,9 @@ var VideoRenderer = class {
1347
1433
  };
1348
1434
 
1349
1435
  // src/strategies/fallback/audio-output.ts
1436
+ function isDebug2() {
1437
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
1438
+ }
1350
1439
  var AudioOutput = class {
1351
1440
  ctx;
1352
1441
  gain;
@@ -1368,6 +1457,16 @@ var AudioOutput = class {
1368
1457
  mediaTimeOfNext = 0;
1369
1458
  /** Anchor: media time `mediaTimeOfAnchor` corresponds to ctx time `ctxTimeAtAnchor`. */
1370
1459
  mediaTimeOfAnchor = 0;
1460
+ /**
1461
+ * Ctx time at which the first audible chunk will start playing. `-1`
1462
+ * before any chunk has been scheduled successfully (clock is frozen);
1463
+ * the actual ctx time once one has. The renderer's `clock.now()` uses
1464
+ * this to avoid advancing during the silent-gap window between
1465
+ * `audio.start()` and the first chunk that schedules without being
1466
+ * dropped — that gap is what produces the "audio-less fast-forward"
1467
+ * the user sees post-seek when the gate releases on video-only grace.
1468
+ */
1469
+ firstAudibleCtxStart = -1;
1371
1470
  ctxTimeAtAnchor = 0;
1372
1471
  pendingQueue = [];
1373
1472
  framesScheduled = 0;
@@ -1440,10 +1539,16 @@ var AudioOutput = class {
1440
1539
  return this.mediaTimeOfAnchor;
1441
1540
  }
1442
1541
  if (this.state === "playing") {
1542
+ if (this.firstAudibleCtxStart < 0) {
1543
+ return this.mediaTimeOfAnchor;
1544
+ }
1443
1545
  return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
1444
1546
  }
1445
1547
  return this.mediaTimeOfAnchor;
1446
1548
  }
1549
+ anchorTime() {
1550
+ return this.mediaTimeOfAnchor;
1551
+ }
1447
1552
  isPlaying() {
1448
1553
  return this.state === "playing";
1449
1554
  }
@@ -1470,18 +1575,81 @@ var AudioOutput = class {
1470
1575
  * Schedule a chunk of decoded samples. Queues internally while idle (cold
1471
1576
  * start or post-seek), schedules directly to the audio graph while playing.
1472
1577
  * In wall-clock mode, samples are silently discarded.
1578
+ *
1579
+ * `ptsSec` is the chunk's source-domain content PTS in seconds, from
1580
+ * the demuxer. When provided, the chunk plays at the ctx-time
1581
+ * corresponding to that PTS — so pre-target audio after a seek
1582
+ * naturally drops (its computed `ctxStart` falls in the past) and
1583
+ * post-target audio plays at its true content time, without any
1584
+ * external trim or anchor rebase. When `ptsSec` is null (cold start
1585
+ * with no PTS yet, or codecs whose packet→frame mapping isn't 1:1),
1586
+ * the chunk is scheduled sequentially after `mediaTimeOfNext` — the
1587
+ * pre-refactor behavior.
1473
1588
  */
1474
- schedule(samples, channels, sampleRate) {
1589
+ schedule(samples, channels, sampleRate, ptsSec) {
1475
1590
  if (this.destroyed || this.noAudio) return;
1476
1591
  const frameCount = samples.length / channels;
1477
1592
  const durationSec = frameCount / sampleRate;
1593
+ const hasPts = ptsSec != null && Number.isFinite(ptsSec);
1594
+ if (hasPts && ptsSec + durationSec / this._rate < this.mediaTimeOfAnchor) {
1595
+ return;
1596
+ }
1478
1597
  if (this.state === "idle" || this.state === "paused") {
1479
- this.pendingQueue.push({ samples, channels, sampleRate, frameCount, durationSec });
1598
+ this.pendingQueue.push({
1599
+ samples,
1600
+ channels,
1601
+ sampleRate,
1602
+ frameCount,
1603
+ durationSec,
1604
+ ptsSec: hasPts ? ptsSec : null
1605
+ });
1480
1606
  return;
1481
1607
  }
1482
- this.scheduleNow(samples, channels, sampleRate, frameCount);
1608
+ this.scheduleNow(
1609
+ samples,
1610
+ channels,
1611
+ sampleRate,
1612
+ frameCount,
1613
+ hasPts ? ptsSec : null
1614
+ );
1483
1615
  }
1484
- scheduleNow(samples, channels, sampleRate, frameCount) {
1616
+ scheduleNow(samples, channels, sampleRate, frameCount, ptsSec) {
1617
+ const durationSec = frameCount / sampleRate;
1618
+ let ctxStart;
1619
+ if (ptsSec != null) {
1620
+ ctxStart = this.ctxTimeAtAnchor + (ptsSec - this.mediaTimeOfAnchor) / this._rate;
1621
+ if (isDebug2()) {
1622
+ console.log(`[TRACE-AUD] PTS sched #${this.framesScheduled} pts=${ptsSec.toFixed(3)} dur=${durationSec.toFixed(4)} ctxStart=${ctxStart.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)} anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} mtNext=${this.mediaTimeOfNext.toFixed(3)} rate=${this._rate}`);
1623
+ }
1624
+ if (ctxStart < this.ctx.currentTime - 1e-3) {
1625
+ if (isDebug2()) {
1626
+ console.log(`[TRACE-AUD] DROP late chunk pts=${ptsSec.toFixed(3)} ctxStart=${ctxStart.toFixed(4)} < ctxNow=${this.ctx.currentTime.toFixed(4)}`);
1627
+ }
1628
+ return;
1629
+ }
1630
+ if (this.firstAudibleCtxStart < 0) {
1631
+ this.firstAudibleCtxStart = ctxStart;
1632
+ this.mediaTimeOfAnchor = ptsSec;
1633
+ this.ctxTimeAtAnchor = ctxStart;
1634
+ if (isDebug2()) {
1635
+ console.log(`[TRACE-AUD] UNFREEZE clock \u2014 first audible chunk pts=${ptsSec.toFixed(3)} ctxStart=${ctxStart.toFixed(4)} \u2192 anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)}`);
1636
+ }
1637
+ }
1638
+ const endMediaTime = ptsSec + durationSec / this._rate;
1639
+ if (endMediaTime > this.mediaTimeOfNext) {
1640
+ this.mediaTimeOfNext = endMediaTime;
1641
+ }
1642
+ } else {
1643
+ ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
1644
+ console.warn(`[TRACE-AUD] LEGACY (no PTS) sched dur=${durationSec.toFixed(4)} ctxStart=${ctxStart.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)}`);
1645
+ if (ctxStart < this.ctx.currentTime) {
1646
+ console.warn(`[TRACE-AUD] REBASE anchor was=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor was=${this.ctxTimeAtAnchor.toFixed(4)} \u2192 anchor=${this.mediaTimeOfNext.toFixed(3)} ctxAnchor=${this.ctx.currentTime.toFixed(4)}`);
1647
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
1648
+ this.mediaTimeOfAnchor = this.mediaTimeOfNext;
1649
+ ctxStart = this.ctx.currentTime;
1650
+ }
1651
+ this.mediaTimeOfNext += durationSec;
1652
+ }
1485
1653
  const buffer = this.ctx.createBuffer(channels, frameCount, sampleRate);
1486
1654
  for (let ch = 0; ch < channels; ch++) {
1487
1655
  const channelData = buffer.getChannelData(ch);
@@ -1493,14 +1661,7 @@ var AudioOutput = class {
1493
1661
  node.buffer = buffer;
1494
1662
  node.connect(this.gain);
1495
1663
  if (this._rate !== 1) node.playbackRate.value = this._rate;
1496
- let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
1497
- if (ctxStart < this.ctx.currentTime) {
1498
- this.ctxTimeAtAnchor = this.ctx.currentTime;
1499
- this.mediaTimeOfAnchor = this.mediaTimeOfNext;
1500
- ctxStart = this.ctx.currentTime;
1501
- }
1502
1664
  node.start(ctxStart);
1503
- this.mediaTimeOfNext += frameCount / sampleRate;
1504
1665
  this.framesScheduled++;
1505
1666
  }
1506
1667
  // ── Lifecycle ─────────────────────────────────────────────────────────
@@ -1525,12 +1686,15 @@ var AudioOutput = class {
1525
1686
  } catch {
1526
1687
  }
1527
1688
  if (this.state === "paused") {
1689
+ if (isDebug2()) {
1690
+ console.log(`[TRACE-AUD] START(resume) anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} \u2192 ctxAnchor=${this.ctx.currentTime.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)} pendingCount=${this.pendingQueue.length}`);
1691
+ }
1528
1692
  this.ctxTimeAtAnchor = this.ctx.currentTime;
1529
1693
  this.state = "playing";
1530
1694
  const drain2 = this.pendingQueue;
1531
1695
  this.pendingQueue = [];
1532
1696
  for (const c of drain2) {
1533
- this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
1697
+ this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
1534
1698
  }
1535
1699
  return;
1536
1700
  }
@@ -1538,10 +1702,13 @@ var AudioOutput = class {
1538
1702
  this.ctxTimeAtAnchor = this.ctx.currentTime + STARTUP_DELAY;
1539
1703
  this.mediaTimeOfNext = this.mediaTimeOfAnchor;
1540
1704
  this.state = "playing";
1705
+ if (isDebug2()) {
1706
+ console.log(`[TRACE-AUD] START(cold) anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} mtNext=${this.mediaTimeOfNext.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(4)} pendingCount=${this.pendingQueue.length}`);
1707
+ }
1541
1708
  const drain = this.pendingQueue;
1542
1709
  this.pendingQueue = [];
1543
1710
  for (const c of drain) {
1544
- this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
1711
+ this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
1545
1712
  }
1546
1713
  }
1547
1714
  /** Pause playback. Suspends the audio context. */
@@ -1567,6 +1734,9 @@ var AudioOutput = class {
1567
1734
  * supplying new samples) and then call `start()` to resume playback.
1568
1735
  */
1569
1736
  async reset(newMediaTime) {
1737
+ if (isDebug2()) {
1738
+ console.log(`[TRACE-AUD] RESET to=${newMediaTime.toFixed(3)} prev_anchor=${this.mediaTimeOfAnchor.toFixed(3)} prev_mtNext=${this.mediaTimeOfNext.toFixed(3)} prev_ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)} state=${this.state}`);
1739
+ }
1570
1740
  if (this.noAudio) {
1571
1741
  this.pendingQueue = [];
1572
1742
  this.mediaTimeOfAnchor = newMediaTime;
@@ -1585,6 +1755,7 @@ var AudioOutput = class {
1585
1755
  this.mediaTimeOfAnchor = newMediaTime;
1586
1756
  this.mediaTimeOfNext = newMediaTime;
1587
1757
  this.ctxTimeAtAnchor = this.ctx.currentTime;
1758
+ this.firstAudibleCtxStart = -1;
1588
1759
  this.state = "idle";
1589
1760
  if (this.ctx.state === "running") {
1590
1761
  await this.ctx.suspend();
@@ -1687,6 +1858,7 @@ async function startHybridDecoder(opts) {
1687
1858
  }
1688
1859
  let bsfCtx = null;
1689
1860
  let bsfPkt = null;
1861
+ let bsfRequiredButMissing = false;
1690
1862
  if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
1691
1863
  try {
1692
1864
  bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
@@ -1697,13 +1869,19 @@ async function startHybridDecoder(opts) {
1697
1869
  bsfPkt = await libav.av_packet_alloc();
1698
1870
  chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
1699
1871
  } else {
1700
- console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
1872
+ bsfRequiredButMissing = true;
1701
1873
  bsfCtx = null;
1702
1874
  }
1703
1875
  } catch (err) {
1704
- console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
1876
+ bsfRequiredButMissing = true;
1705
1877
  bsfCtx = null;
1706
1878
  bsfPkt = null;
1879
+ chunkG4APZMCP_cjs.dbg.warn("bsf", `hybrid: mpeg4_unpack_bframes BSF init failed: ${err.message}`);
1880
+ }
1881
+ if (bsfRequiredButMissing) {
1882
+ console.error(
1883
+ "[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."
1884
+ );
1707
1885
  }
1708
1886
  }
1709
1887
  async function applyBSF(packets) {
@@ -1713,7 +1891,6 @@ async function startHybridDecoder(opts) {
1713
1891
  await libav.ff_copyin_packet(bsfPkt, pkt);
1714
1892
  const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
1715
1893
  if (sendErr < 0) {
1716
- out.push(pkt);
1717
1894
  continue;
1718
1895
  }
1719
1896
  while (true) {
@@ -1727,10 +1904,13 @@ async function startHybridDecoder(opts) {
1727
1904
  async function flushBSF() {
1728
1905
  if (!bsfCtx || !bsfPkt) return;
1729
1906
  try {
1730
- await libav.av_bsf_send_packet(bsfCtx, 0);
1731
- while (true) {
1732
- const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1733
- if (err < 0) break;
1907
+ if (libav.av_bsf_flush) {
1908
+ await libav.av_bsf_flush(bsfCtx);
1909
+ } else {
1910
+ while (true) {
1911
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1912
+ if (err < 0) break;
1913
+ }
1734
1914
  }
1735
1915
  } catch {
1736
1916
  }
@@ -1744,7 +1924,6 @@ async function startHybridDecoder(opts) {
1744
1924
  let videoChunksFed = 0;
1745
1925
  let bufferedUntilSec = 0;
1746
1926
  let syntheticVideoUs = 0;
1747
- let syntheticAudioUs = 0;
1748
1927
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
1749
1928
  const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
1750
1929
  const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
@@ -1776,7 +1955,13 @@ async function startHybridDecoder(opts) {
1776
1955
  }
1777
1956
  }
1778
1957
  if (audioDec && audioPackets && audioPackets.length > 0) {
1779
- await decodeAudioBatch(audioPackets, myToken);
1958
+ await decodeAudioBatch(
1959
+ audioPackets,
1960
+ myToken,
1961
+ /*flush*/
1962
+ false,
1963
+ audioTimeBase
1964
+ );
1780
1965
  }
1781
1966
  if (myToken !== pumpToken || destroyed) return;
1782
1967
  await new Promise((r) => setTimeout(r, 0));
@@ -1823,8 +2008,11 @@ async function startHybridDecoder(opts) {
1823
2008
  }
1824
2009
  }
1825
2010
  }
1826
- async function decodeAudioBatch(pkts, myToken, flush = false) {
2011
+ async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
1827
2012
  if (!audioDec || destroyed || myToken !== pumpToken) return;
2013
+ const pktPtsSec = pkts.map(
2014
+ (p) => tb ? chunkHZUVMXBN_cjs.packetPtsSec(p, tb) : null
2015
+ );
1828
2016
  const AUDIO_SUB_BATCH = 4;
1829
2017
  let allFrames = [];
1830
2018
  for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
@@ -1862,22 +2050,13 @@ async function startHybridDecoder(opts) {
1862
2050
  }
1863
2051
  if (myToken !== pumpToken || destroyed) return;
1864
2052
  const frames = allFrames;
1865
- for (const f of frames) {
2053
+ for (let i = 0; i < frames.length; i++) {
1866
2054
  if (myToken !== pumpToken || destroyed) return;
1867
- chunkHZUVMXBN_cjs.sanitizeFrameTimestamp(
1868
- f,
1869
- () => {
1870
- const ts = syntheticAudioUs;
1871
- const samples2 = f.nb_samples ?? 1024;
1872
- const sampleRate = f.sample_rate ?? 44100;
1873
- syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
1874
- return ts;
1875
- },
1876
- audioTimeBase
1877
- );
2055
+ const f = frames[i];
1878
2056
  const samples = chunkHZUVMXBN_cjs.libavFrameToInterleavedFloat32(f);
1879
2057
  if (samples) {
1880
- opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
2058
+ const pts = pktPtsSec[i] ?? null;
2059
+ opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
1881
2060
  audioFramesDecoded++;
1882
2061
  }
1883
2062
  }
@@ -1987,7 +2166,6 @@ async function startHybridDecoder(opts) {
1987
2166
  }
1988
2167
  await flushBSF();
1989
2168
  syntheticVideoUs = Math.round(timeSec * 1e6);
1990
- syntheticAudioUs = Math.round(timeSec * 1e6);
1991
2169
  pumpRunning = pumpLoop(newToken).catch(
1992
2170
  (err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
1993
2171
  );
@@ -2026,7 +2204,6 @@ async function startHybridDecoder(opts) {
2026
2204
  }
2027
2205
  await flushBSF();
2028
2206
  syntheticVideoUs = Math.round(timeSec * 1e6);
2029
- syntheticAudioUs = Math.round(timeSec * 1e6);
2030
2207
  pumpRunning = pumpLoop(newToken).catch(
2031
2208
  (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
2032
2209
  );
@@ -2042,6 +2219,7 @@ async function startHybridDecoder(opts) {
2042
2219
  videoChunksFed,
2043
2220
  audioFramesDecoded,
2044
2221
  bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2222
+ bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
2045
2223
  videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
2046
2224
  // Confirmed transport info — see fallback decoder for the pattern.
2047
2225
  _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
@@ -2278,6 +2456,9 @@ async function createHybridSession(ctx, target, transport) {
2278
2456
  }
2279
2457
 
2280
2458
  // src/strategies/fallback/decoder.ts
2459
+ function isDebug3() {
2460
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
2461
+ }
2281
2462
  async function startDecoder(opts) {
2282
2463
  const variant = "avbridge";
2283
2464
  const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
@@ -2341,6 +2522,7 @@ async function startDecoder(opts) {
2341
2522
  }
2342
2523
  let bsfCtx = null;
2343
2524
  let bsfPkt = null;
2525
+ let bsfRequiredButMissing = false;
2344
2526
  if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
2345
2527
  try {
2346
2528
  bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
@@ -2351,13 +2533,19 @@ async function startDecoder(opts) {
2351
2533
  bsfPkt = await libav.av_packet_alloc();
2352
2534
  chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
2353
2535
  } else {
2354
- console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
2536
+ bsfRequiredButMissing = true;
2355
2537
  bsfCtx = null;
2356
2538
  }
2357
2539
  } catch (err) {
2358
- console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
2540
+ bsfRequiredButMissing = true;
2359
2541
  bsfCtx = null;
2360
2542
  bsfPkt = null;
2543
+ chunkG4APZMCP_cjs.dbg.warn("bsf", `mpeg4_unpack_bframes BSF init failed: ${err.message}`);
2544
+ }
2545
+ if (bsfRequiredButMissing) {
2546
+ console.error(
2547
+ "[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."
2548
+ );
2361
2549
  }
2362
2550
  }
2363
2551
  async function applyBSF(packets) {
@@ -2367,7 +2555,6 @@ async function startDecoder(opts) {
2367
2555
  await libav.ff_copyin_packet(bsfPkt, pkt);
2368
2556
  const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
2369
2557
  if (sendErr < 0) {
2370
- out.push(pkt);
2371
2558
  continue;
2372
2559
  }
2373
2560
  while (true) {
@@ -2381,10 +2568,13 @@ async function startDecoder(opts) {
2381
2568
  async function flushBSF() {
2382
2569
  if (!bsfCtx || !bsfPkt) return;
2383
2570
  try {
2384
- await libav.av_bsf_send_packet(bsfCtx, 0);
2385
- while (true) {
2386
- const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2387
- if (err < 0) break;
2571
+ if (libav.av_bsf_flush) {
2572
+ await libav.av_bsf_flush(bsfCtx);
2573
+ } else {
2574
+ while (true) {
2575
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2576
+ if (err < 0) break;
2577
+ }
2388
2578
  }
2389
2579
  } catch {
2390
2580
  }
@@ -2400,8 +2590,28 @@ async function startDecoder(opts) {
2400
2590
  let watchdogSlowSinceMs = 0;
2401
2591
  let watchdogSlowWarned = false;
2402
2592
  let watchdogOverflowWarned = false;
2403
- let syntheticVideoUs = 0;
2404
- let syntheticAudioUs = 0;
2593
+ let lastContentUs = -1;
2594
+ let firstValidPtsLoggedSinceSeek = false;
2595
+ let seenFirstAudioPacketSinceSeek = false;
2596
+ let seekTargetSec = 0;
2597
+ let diagPktsLoggedSinceSeek = 0;
2598
+ let diagFramesLoggedSinceSeek = 0;
2599
+ let diagFrameKeysDumped = false;
2600
+ const DIAG_MAX_PKTS = 100;
2601
+ const DIAG_MAX_FRAMES = 300;
2602
+ let videoDecodeMsTotal = 0;
2603
+ let audioDecodeMsTotal = 0;
2604
+ let videoDecodeBatches = 0;
2605
+ let audioDecodeBatches = 0;
2606
+ let readMsTotal = 0;
2607
+ let readBatches = 0;
2608
+ let pumpThrottleMsTotal = 0;
2609
+ let pumpThrottleEntries = 0;
2610
+ let slowestVideoBatchMs = 0;
2611
+ let newestVideoPtsUs = 0;
2612
+ let lastEmittedPtsUs = -1;
2613
+ let ptsRegressions = 0;
2614
+ let worstPtsRegressionMs = 0;
2405
2615
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
2406
2616
  const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
2407
2617
  const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
@@ -2410,9 +2620,12 @@ async function startDecoder(opts) {
2410
2620
  let readErr;
2411
2621
  let packets;
2412
2622
  try {
2623
+ const _readStart = performance.now();
2413
2624
  [readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
2414
2625
  limit: 16 * 1024
2415
2626
  });
2627
+ readMsTotal += performance.now() - _readStart;
2628
+ readBatches++;
2416
2629
  } catch (err) {
2417
2630
  console.error("[avbridge] ff_read_frame_multi failed:", err);
2418
2631
  return;
@@ -2424,6 +2637,18 @@ async function startDecoder(opts) {
2424
2637
  for (const pkt of videoPackets) {
2425
2638
  const sec = chunkHZUVMXBN_cjs.packetPtsSec(pkt, videoTimeBase);
2426
2639
  if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2640
+ if (isDebug3() && diagPktsLoggedSinceSeek < DIAG_MAX_PKTS) {
2641
+ const rawHi = pkt.ptshi ?? 0;
2642
+ const rawLo = pkt.pts ?? 0;
2643
+ const isInvalidPts = rawHi === -2147483648 && rawLo === 0;
2644
+ const rawPts64 = isInvalidPts ? null : rawHi * 4294967296 + rawLo;
2645
+ const rawSec = rawPts64 != null && videoTimeBase ? rawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
2646
+ const pktKeys = diagPktsLoggedSinceSeek === 0 ? `[keys: ${Object.keys(pkt).join(",")}]` : "";
2647
+ console.log(
2648
+ `[DIAG-PKT] vidx=${diagPktsLoggedSinceSeek} pts=${isInvalidPts ? "NOPTS" : rawPts64} pts_sec=${rawSec != null ? rawSec.toFixed(3) : "n/a"} ptshi=${rawHi} ptslo=${rawLo} flags=0x${(pkt.flags ?? 0).toString(16)} keyframe=${(pkt.flags ?? 0) & 1 ? "Y" : "N"} stream=${pkt.stream_index} dataLen=${pkt.data?.length ?? 0} seekTarget=${seekTargetSec.toFixed(3)} ` + pktKeys
2649
+ );
2650
+ diagPktsLoggedSinceSeek++;
2651
+ }
2427
2652
  }
2428
2653
  }
2429
2654
  if (audioPackets && audioTimeBase) {
@@ -2431,9 +2656,25 @@ async function startDecoder(opts) {
2431
2656
  const sec = chunkHZUVMXBN_cjs.packetPtsSec(pkt, audioTimeBase);
2432
2657
  if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2433
2658
  }
2659
+ if (!seenFirstAudioPacketSinceSeek && audioPackets.length > 0) {
2660
+ const firstSec = chunkHZUVMXBN_cjs.packetPtsSec(audioPackets[0], audioTimeBase);
2661
+ if (firstSec != null && Number.isFinite(firstSec)) {
2662
+ seenFirstAudioPacketSinceSeek = true;
2663
+ chunkG4APZMCP_cjs.dbg.info(
2664
+ "av-anchor",
2665
+ `seek-target=${seekTargetSec.toFixed(3)}s, first-audio-pkt-pts=${firstSec.toFixed(3)}s (\u0394=${((firstSec - seekTargetSec) * 1e3).toFixed(1)}ms \u2014 pre-target packets will be skipped by AudioOutput)`
2666
+ );
2667
+ }
2668
+ }
2434
2669
  }
2435
2670
  if (audioDec && audioPackets && audioPackets.length > 0) {
2436
- await decodeAudioBatch(audioPackets, myToken);
2671
+ await decodeAudioBatch(
2672
+ audioPackets,
2673
+ myToken,
2674
+ /*flush*/
2675
+ false,
2676
+ audioTimeBase
2677
+ );
2437
2678
  }
2438
2679
  if (myToken !== pumpToken || destroyed) return;
2439
2680
  if (videoDec && videoPackets && videoPackets.length > 0) {
@@ -2474,8 +2715,17 @@ async function startDecoder(opts) {
2474
2715
  }
2475
2716
  }
2476
2717
  }
2477
- while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
2478
- await new Promise((r) => setTimeout(r, 50));
2718
+ {
2719
+ const _throttleStart = performance.now();
2720
+ let _throttled = false;
2721
+ while (!destroyed && myToken === pumpToken && opts.audio.bufferAhead() > 2) {
2722
+ _throttled = true;
2723
+ await new Promise((r) => setTimeout(r, 50));
2724
+ }
2725
+ if (_throttled) {
2726
+ pumpThrottleMsTotal += performance.now() - _throttleStart;
2727
+ pumpThrottleEntries++;
2728
+ }
2479
2729
  }
2480
2730
  if (readErr === libav.AVERROR_EOF) {
2481
2731
  if (videoDec) await decodeVideoBatch(
@@ -2501,6 +2751,7 @@ async function startDecoder(opts) {
2501
2751
  async function decodeVideoBatch(pkts, myToken, flush = false) {
2502
2752
  if (!videoDec || destroyed || myToken !== pumpToken) return;
2503
2753
  let frames;
2754
+ const _t0 = performance.now();
2504
2755
  try {
2505
2756
  frames = await libav.ff_decode_multi(
2506
2757
  videoDec.c,
@@ -2513,32 +2764,133 @@ async function startDecoder(opts) {
2513
2764
  console.error("[avbridge] video decode batch failed:", err);
2514
2765
  return;
2515
2766
  }
2767
+ {
2768
+ const _dt = performance.now() - _t0;
2769
+ videoDecodeMsTotal += _dt;
2770
+ videoDecodeBatches++;
2771
+ if (_dt > slowestVideoBatchMs) slowestVideoBatchMs = _dt;
2772
+ }
2516
2773
  if (myToken !== pumpToken || destroyed) return;
2517
2774
  for (const f of frames) {
2518
2775
  if (myToken !== pumpToken || destroyed) return;
2519
- chunkHZUVMXBN_cjs.sanitizeFrameTimestamp(
2520
- f,
2521
- () => {
2522
- const ts = syntheticVideoUs;
2523
- syntheticVideoUs += videoFrameStepUs;
2524
- return ts;
2525
- },
2526
- videoTimeBase
2527
- );
2776
+ const _diagShouldLog = isDebug3() && diagFramesLoggedSinceSeek < DIAG_MAX_FRAMES;
2777
+ const _diagRawHi = f.ptshi ?? 0;
2778
+ const _diagRawLo = f.pts ?? 0;
2779
+ const _diagInvalid = _diagRawHi === -2147483648 && _diagRawLo === 0;
2780
+ const _diagRawPts64 = _diagInvalid ? null : _diagRawHi * 4294967296 + _diagRawLo;
2781
+ const _diagRawSec = _diagRawPts64 != null && videoTimeBase ? _diagRawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
2782
+ if (_diagShouldLog && !diagFrameKeysDumped) {
2783
+ diagFrameKeysDumped = true;
2784
+ const allKeys = Object.keys(f);
2785
+ const fieldDump = {};
2786
+ for (const k of allKeys) {
2787
+ const v = f[k];
2788
+ if (k === "data") continue;
2789
+ if (typeof v === "object" && v !== null && "length" in v) continue;
2790
+ fieldDump[k] = v;
2791
+ }
2792
+ console.log(`[DIAG-FRAME] FIRST FRAME post-seek \u2014 all keys: ${allKeys.join(",")}`);
2793
+ console.log(`[DIAG-FRAME] FIRST FRAME field dump:`, fieldDump);
2794
+ }
2795
+ let rawUs = null;
2796
+ if (!_diagInvalid && _diagRawPts64 != null) {
2797
+ const tb = videoTimeBase ?? [1, 1e6];
2798
+ const us = Math.round(_diagRawPts64 * 1e6 * tb[0] / tb[1]);
2799
+ if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2800
+ rawUs = us;
2801
+ }
2802
+ }
2803
+ const _diagLog = (decision, finalPtsUs, sanFallback) => {
2804
+ if (!_diagShouldLog) return;
2805
+ const ptsSrc = sanFallback ? `SYNTHETIC(${_diagInvalid ? "NOPTS" : "invalid-range"})` : "LIBAV";
2806
+ console.log(
2807
+ `[DIAG-FRAME] vidx=${diagFramesLoggedSinceSeek} raw_pts=${_diagInvalid ? "NOPTS" : _diagRawPts64} raw_pts_sec=${_diagRawSec != null ? _diagRawSec.toFixed(3) : "n/a"} pts_src=${ptsSrc} final_pts_us=${finalPtsUs} final_pts_sec=${(finalPtsUs / 1e6).toFixed(3)} seekTarget=${seekTargetSec.toFixed(3)} offset_to_target_ms=${(finalPtsUs / 1e3 - seekTargetSec * 1e3).toFixed(1)} lastEmittedPts_us=${lastEmittedPtsUs} decision=${decision}`
2808
+ );
2809
+ diagFramesLoggedSinceSeek++;
2810
+ };
2811
+ let _diagSanFallbackFired = false;
2812
+ const seekTargetUs = Math.round(seekTargetSec * 1e6);
2813
+ if (lastContentUs < 0) {
2814
+ if (rawUs == null) {
2815
+ const isColdStartKeyframe = seekTargetSec === 0 && f.key_frame === 1;
2816
+ if (isColdStartKeyframe) {
2817
+ lastContentUs = 0;
2818
+ _diagSanFallbackFired = true;
2819
+ } else {
2820
+ _diagLog("PRE-ANCHOR-DROP", 0, true);
2821
+ continue;
2822
+ }
2823
+ } else {
2824
+ lastContentUs = rawUs;
2825
+ if (!firstValidPtsLoggedSinceSeek) {
2826
+ firstValidPtsLoggedSinceSeek = true;
2827
+ if (isDebug3()) {
2828
+ console.log(
2829
+ `[avbridge:decoder] post-seek anchor established: first valid raw pts = ${(rawUs / 1e3).toFixed(1)}ms (seekTarget = ${(seekTargetSec * 1e3).toFixed(1)}ms, \u0394 = ${((rawUs - seekTargetUs) / 1e3).toFixed(1)}ms)`
2830
+ );
2831
+ }
2832
+ if (rawUs >= seekTargetUs) {
2833
+ console.warn(
2834
+ `[avbridge:decoder] first valid raw pts \u2265 seek target \u2014 pre-anchor NOPTS frames may have straddled the target and been mis-discarded. First painted frame may be late by up to one keyframe interval.`
2835
+ );
2836
+ }
2837
+ }
2838
+ }
2839
+ } else {
2840
+ if (rawUs != null) {
2841
+ lastContentUs = rawUs;
2842
+ } else {
2843
+ lastContentUs += videoFrameStepUs;
2844
+ _diagSanFallbackFired = true;
2845
+ }
2846
+ }
2847
+ f.pts = lastContentUs;
2848
+ f.ptshi = lastContentUs < 0 ? -1 : 0;
2849
+ const _fPts = lastContentUs;
2850
+ if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
2851
+ if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
2852
+ _diagLog("REGRESSED-DROP", _fPts, _diagSanFallbackFired);
2853
+ ptsRegressions++;
2854
+ const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
2855
+ if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
2856
+ if (ptsRegressions <= 10) {
2857
+ console.warn(
2858
+ `[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.`
2859
+ );
2860
+ }
2861
+ continue;
2862
+ }
2863
+ lastEmittedPtsUs = _fPts;
2864
+ const targetUs = Math.round(seekTargetSec * 1e6);
2865
+ if (_fPts < targetUs - videoFrameStepUs) {
2866
+ _diagLog("PRE-TARGET-DROP", _fPts, _diagSanFallbackFired);
2867
+ continue;
2868
+ }
2528
2869
  try {
2529
2870
  const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
2530
- opts.renderer.enqueue(vf);
2871
+ if (opts.renderer.queueDepth() >= opts.renderer.queueHighWater) {
2872
+ vf.close();
2873
+ _diagLog("OVERFLOW-DROP", _fPts, _diagSanFallbackFired);
2874
+ } else {
2875
+ opts.renderer.enqueue(vf);
2876
+ _diagLog("ENQUEUED", _fPts, _diagSanFallbackFired);
2877
+ }
2531
2878
  videoFramesDecoded++;
2532
2879
  } catch (err) {
2533
2880
  if (videoFramesDecoded === 0) {
2534
2881
  console.warn("[avbridge] laFrameToVideoFrame failed:", err);
2535
2882
  }
2883
+ _diagLog("BRIDGE-ERROR", _fPts, _diagSanFallbackFired);
2536
2884
  }
2537
2885
  }
2538
2886
  }
2539
- async function decodeAudioBatch(pkts, myToken, flush = false) {
2887
+ async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
2540
2888
  if (!audioDec || destroyed || myToken !== pumpToken) return;
2889
+ const pktPtsSec = pkts.map(
2890
+ (p) => tb ? chunkHZUVMXBN_cjs.packetPtsSec(p, tb) : null
2891
+ );
2541
2892
  let frames;
2893
+ const _t0 = performance.now();
2542
2894
  try {
2543
2895
  frames = await libav.ff_decode_multi(
2544
2896
  audioDec.c,
@@ -2551,23 +2903,20 @@ async function startDecoder(opts) {
2551
2903
  console.error("[avbridge] audio decode batch failed:", err);
2552
2904
  return;
2553
2905
  }
2906
+ audioDecodeMsTotal += performance.now() - _t0;
2907
+ audioDecodeBatches++;
2554
2908
  if (myToken !== pumpToken || destroyed) return;
2555
- for (const f of frames) {
2909
+ for (let i = 0; i < frames.length; i++) {
2556
2910
  if (myToken !== pumpToken || destroyed) return;
2557
- chunkHZUVMXBN_cjs.sanitizeFrameTimestamp(
2558
- f,
2559
- () => {
2560
- const ts = syntheticAudioUs;
2561
- const samples2 = f.nb_samples ?? 1024;
2562
- const sampleRate = f.sample_rate ?? 44100;
2563
- syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
2564
- return ts;
2565
- },
2566
- audioTimeBase
2567
- );
2911
+ const f = frames[i];
2568
2912
  const samples = chunkHZUVMXBN_cjs.libavFrameToInterleavedFloat32(f);
2569
2913
  if (samples) {
2570
- opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
2914
+ const pts = pktPtsSec[i] ?? null;
2915
+ if (isDebug3()) {
2916
+ const dur = samples.data.length / samples.channels / samples.sampleRate;
2917
+ console.log(`[TRACE-DEC] audio frame #${audioFramesDecoded} pts=${pts != null ? pts.toFixed(4) : "NULL"} dur=${dur.toFixed(4)} samples=${samples.data.length / samples.channels} sr=${samples.sampleRate} ch=${samples.channels} pktsIn=${pkts.length} framesOut=${frames.length}`);
2918
+ }
2919
+ opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
2571
2920
  audioFramesDecoded++;
2572
2921
  }
2573
2922
  }
@@ -2670,13 +3019,17 @@ async function startDecoder(opts) {
2670
3019
  } catch {
2671
3020
  }
2672
3021
  await flushBSF();
2673
- syntheticVideoUs = Math.round(timeSec * 1e6);
2674
- syntheticAudioUs = Math.round(timeSec * 1e6);
3022
+ lastContentUs = -1;
3023
+ lastEmittedPtsUs = -1;
3024
+ firstValidPtsLoggedSinceSeek = false;
2675
3025
  pumpRunning = pumpLoop(newToken).catch(
2676
3026
  (err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
2677
3027
  );
2678
3028
  },
2679
3029
  async seek(timeSec) {
3030
+ if (isDebug3()) {
3031
+ console.log(`[SEEK] target=${timeSec.toFixed(3)}s (${(timeSec * 1e3).toFixed(0)}ms) wall=${performance.now().toFixed(0)}`);
3032
+ }
2680
3033
  const newToken = ++pumpToken;
2681
3034
  if (pumpRunning) {
2682
3035
  try {
@@ -2707,8 +3060,14 @@ async function startDecoder(opts) {
2707
3060
  } catch {
2708
3061
  }
2709
3062
  await flushBSF();
2710
- syntheticVideoUs = Math.round(timeSec * 1e6);
2711
- syntheticAudioUs = Math.round(timeSec * 1e6);
3063
+ lastContentUs = -1;
3064
+ lastEmittedPtsUs = -1;
3065
+ firstValidPtsLoggedSinceSeek = false;
3066
+ seenFirstAudioPacketSinceSeek = false;
3067
+ seekTargetSec = timeSec;
3068
+ diagPktsLoggedSinceSeek = 0;
3069
+ diagFramesLoggedSinceSeek = 0;
3070
+ diagFrameKeysDumped = false;
2712
3071
  pumpRunning = pumpLoop(newToken).catch(
2713
3072
  (err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
2714
3073
  );
@@ -2722,7 +3081,24 @@ async function startDecoder(opts) {
2722
3081
  packetsRead,
2723
3082
  videoFramesDecoded,
2724
3083
  audioFramesDecoded,
3084
+ // Throughput instrumentation — the stats panel turns these into
3085
+ // "decode fps actual / realtime target" and shows slowest batch
3086
+ // + producer throttle share.
3087
+ videoDecodeMsTotal,
3088
+ videoDecodeBatches,
3089
+ audioDecodeMsTotal,
3090
+ audioDecodeBatches,
3091
+ readMsTotal,
3092
+ readBatches,
3093
+ pumpThrottleMsTotal,
3094
+ pumpThrottleEntries,
3095
+ slowestVideoBatchMs,
3096
+ newestVideoPtsMs: Math.round(newestVideoPtsUs / 1e3),
3097
+ ptsRegressions,
3098
+ worstPtsRegressionMs,
3099
+ sourceFps: videoFps,
2725
3100
  bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
3101
+ bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
2726
3102
  // Confirmed transport info: once prepareLibavInput returns
2727
3103
  // successfully, we *know* whether the source is http-range (probe
2728
3104
  // succeeded and returned 206) or in-memory blob. Diagnostics hoists
@@ -3081,7 +3457,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3081
3457
  const bootstrapStart = performance.now();
3082
3458
  try {
3083
3459
  chunkG4APZMCP_cjs.dbg.info("bootstrap", "start");
3084
- const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => chunkHBHSUGNI_cjs.probe(this.options.source, this.transport));
3460
+ const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => chunkVLI3Y6IJ_cjs.probe(this.options.source, this.transport));
3085
3461
  chunkG4APZMCP_cjs.dbg.info(
3086
3462
  "probe",
3087
3463
  `container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
@@ -3551,5 +3927,5 @@ exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
3551
3927
  exports.UnifiedPlayer = UnifiedPlayer;
3552
3928
  exports.classifyContext = classifyContext;
3553
3929
  exports.createPlayer = createPlayer;
3554
- //# sourceMappingURL=chunk-7EF4VTUS.cjs.map
3555
- //# sourceMappingURL=chunk-7EF4VTUS.cjs.map
3930
+ //# sourceMappingURL=chunk-OFJYEITB.cjs.map
3931
+ //# sourceMappingURL=chunk-OFJYEITB.cjs.map