avbridge 2.12.1 → 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.
@@ -1,7 +1,7 @@
1
1
  import { SubtitleResourceBag, discoverSidecars, attachSubtitleTracks, SubtitleOverlay } from './chunk-EDDWAN2L.js';
2
2
  import { probe, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, avbridgeAudioToMediabunny } from './chunk-5CX7BVVV.js';
3
3
  import { AvbridgeError, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-CPJLFFCC.js';
4
- import { packetPtsSec, sanitizePacketTimestamp, sanitizeFrameTimestamp, libavFrameToInterleavedFloat32 } from './chunk-3YKWU4FM.js';
4
+ import { packetPtsSec, sanitizePacketTimestamp, libavFrameToInterleavedFloat32 } from './chunk-3YKWU4FM.js';
5
5
  import { dbg, loadLibav } from './chunk-5DMTJVIU.js';
6
6
  import { pickLibavVariant } from './chunk-5YAWWKA3.js';
7
7
 
@@ -789,22 +789,25 @@ async function createRemuxPipeline(ctx, video) {
789
789
  }
790
790
  }
791
791
  let mimePromise = null;
792
+ const myToken = pumpToken;
792
793
  const writable = new WritableStream({
793
794
  write: async (chunk) => {
794
- if (destroyed) return;
795
+ if (destroyed || pumpToken !== myToken) return;
795
796
  if (!sink) {
796
797
  const mime = await (mimePromise ??= output.getMimeType());
798
+ if (destroyed || pumpToken !== myToken) return;
797
799
  sink = new MseSink({ mime, video });
798
800
  await sink.ready();
801
+ if (destroyed || pumpToken !== myToken) return;
799
802
  if (pendingStartTime > 0) {
800
803
  sink.invalidate(pendingStartTime);
801
804
  }
802
805
  sink.setPlayOnSeek(pendingAutoPlay);
803
806
  }
804
- while (sink && !destroyed && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
807
+ while (sink && !destroyed && pumpToken === myToken && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
805
808
  await new Promise((r) => setTimeout(r, 500));
806
809
  }
807
- if (destroyed) return;
810
+ if (destroyed || pumpToken !== myToken) return;
808
811
  sink.append(chunk.data);
809
812
  stats.bytesWritten += chunk.data.byteLength;
810
813
  stats.fragments++;
@@ -1071,7 +1074,21 @@ var VideoRenderer = class {
1071
1074
  framesPainted = 0;
1072
1075
  framesDroppedLate = 0;
1073
1076
  framesDroppedOverflow = 0;
1077
+ /** True once the head frame has been painted as a pre-roll poster
1078
+ * since the last flush. Used to ensure pre-roll paints exactly one
1079
+ * frame (held static) during the post-seek discard window. */
1074
1080
  prerolled = false;
1081
+ /** PTS (µs) of the most recently painted frame. Used as the calibration
1082
+ * reference on the first post-flush snap: the pre-roll path paints one
1083
+ * frame *before* PTS-based playback starts, so the queue head's PTS at
1084
+ * first PTS-based paint is the *next* frame, off by one frameDur from
1085
+ * the actually-displayed frame. Calibrating against the painted frame
1086
+ * instead of the queue head removes that one-frame offset and yields
1087
+ * calib ≈ 0 instead of +frameDur. */
1088
+ lastPaintedPtsUs = 0;
1089
+ hasLastPaintedPts = false;
1090
+ /** Audio-clock reading (ms) at the previous paint, for overlay Δaud. */
1091
+ lastPaintAudMs = 0;
1075
1092
  /** Wall-clock time of the last paint, in ms (performance.now()). */
1076
1093
  lastPaintWall = 0;
1077
1094
  /** Minimum ms between paints — paces video at roughly source fps. */
@@ -1121,13 +1138,17 @@ var VideoRenderer = class {
1121
1138
  return this.queue.length;
1122
1139
  }
1123
1140
  /**
1124
- * Soft cap for decoder backpressure. The decoder pump throttles when
1125
- * `queueDepth() >= queueHighWater`. Set high enough that normal decode
1126
- * bursts don't trigger the renderer's overflow-drop loop (which runs at
1127
- * every paint), but low enough that the decoder doesn't run unboundedly
1128
- * ahead. The hard cap in `enqueue()` is 64.
1141
+ * Cap the decoder may fill the queue up to. Used by the decoder's
1142
+ * enqueue-side discard logic (it closes new frames instead of pushing
1143
+ * them when this is reached). Sized so a long post-seek catch-up
1144
+ * fits the decoder produces frames at PTS T_kf onwards rapidly
1145
+ * while the demuxer is chewing through pre-target audio; if the
1146
+ * queue can hold the whole post-seek burst, the renderer plays
1147
+ * smoothly from pre-roll without a frozen-video gap when audio.start
1148
+ * fires. At ~340 KB per SD frame the cap is ~85 MB peak; at HD it's
1149
+ * larger but still bounded.
1129
1150
  */
1130
- queueHighWater = 30;
1151
+ queueHighWater = 256;
1131
1152
  enqueue(frame) {
1132
1153
  if (this.destroyed) {
1133
1154
  frame.close();
@@ -1138,7 +1159,7 @@ var VideoRenderer = class {
1138
1159
  if (this.queue.length === 1 && this.framesPainted === 0) {
1139
1160
  this.resolveFirstFrame();
1140
1161
  }
1141
- while (this.queue.length > 60) {
1162
+ while (this.queue.length > this.queueHighWater + 8) {
1142
1163
  this.queue.shift()?.close();
1143
1164
  this.framesDroppedOverflow++;
1144
1165
  }
@@ -1220,12 +1241,9 @@ var VideoRenderer = class {
1220
1241
  if (this.queue.length === 0) return;
1221
1242
  const playing = this.clock.isPlaying();
1222
1243
  if (!playing) {
1223
- if (!this.prerolled) {
1224
- const head = this.queue.shift();
1225
- this.paint(head);
1226
- head.close();
1244
+ if (!this.prerolled && this.queue.length > 0) {
1227
1245
  this.prerolled = true;
1228
- this.lastPaintWall = performance.now();
1246
+ this.paint(this.queue[0]);
1229
1247
  }
1230
1248
  return;
1231
1249
  }
@@ -1234,14 +1252,29 @@ var VideoRenderer = class {
1234
1252
  const hasPts = headTs > 0 || this.queue.length > 1;
1235
1253
  if (hasPts) {
1236
1254
  const wallNow2 = performance.now();
1237
- if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
1238
- this.ptsCalibrationUs = headTs - rawAudioNowUs;
1255
+ if (!this.ptsCalibrated) {
1256
+ const anchorUs = (this.clock.anchorTime?.() ?? this.clock.now()) * 1e6;
1257
+ const referencePtsUs = this.hasLastPaintedPts ? this.lastPaintedPtsUs : headTs;
1258
+ this.ptsCalibrationUs = referencePtsUs - anchorUs;
1239
1259
  this.ptsCalibrated = true;
1240
1260
  this.lastCalibrationWall = wallNow2;
1261
+ if (isDebug()) {
1262
+ console.log(
1263
+ `[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`
1264
+ );
1265
+ }
1266
+ } else if (wallNow2 - this.lastCalibrationWall > 1e4) {
1267
+ const oldCalib = this.ptsCalibrationUs;
1268
+ this.ptsCalibrationUs = headTs - rawAudioNowUs;
1269
+ this.lastCalibrationWall = wallNow2;
1270
+ if (isDebug()) {
1271
+ console.log(
1272
+ `[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)`
1273
+ );
1274
+ }
1241
1275
  }
1242
1276
  const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
1243
- const frameDurationUs = this.paintIntervalMs * 1e3;
1244
- const deadlineUs = audioNowUs + frameDurationUs;
1277
+ const deadlineUs = audioNowUs;
1245
1278
  let bestIdx = -1;
1246
1279
  for (let i = 0; i < this.queue.length; i++) {
1247
1280
  const ts = this.queue[i].timestamp ?? 0;
@@ -1269,19 +1302,20 @@ var VideoRenderer = class {
1269
1302
  return;
1270
1303
  }
1271
1304
  const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
1272
- const dropThresholdUs = _relaxDrop ? audioNowUs - 60 * 1e6 : audioNowUs - frameDurationUs * 2;
1273
1305
  let dropped = 0;
1274
- while (bestIdx > 0) {
1275
- const ts = this.queue[0].timestamp ?? 0;
1276
- if (ts < dropThresholdUs) {
1306
+ const initialBestIdx = bestIdx;
1307
+ if (!_relaxDrop) {
1308
+ while (bestIdx > 0) {
1277
1309
  this.queue.shift()?.close();
1278
1310
  this.framesDroppedLate++;
1279
1311
  bestIdx--;
1280
1312
  dropped++;
1281
- } else {
1282
- break;
1283
1313
  }
1284
1314
  }
1315
+ const paintTs = this.queue[0]?.timestamp ?? 0;
1316
+ if (isDebug()) {
1317
+ 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)}`);
1318
+ }
1285
1319
  this.ticksPainted++;
1286
1320
  if (isDebug()) {
1287
1321
  const now = performance.now();
@@ -1317,6 +1351,33 @@ var VideoRenderer = class {
1317
1351
  }
1318
1352
  try {
1319
1353
  this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
1354
+ if (isDebug()) {
1355
+ const wallNow = performance.now();
1356
+ const audNowMs = this.clock.now() * 1e3;
1357
+ const ptsMs = (frame.timestamp ?? 0) / 1e3;
1358
+ const dWall = this.lastPaintWall > 0 ? wallNow - this.lastPaintWall : 0;
1359
+ const dAud = this.lastPaintAudMs > 0 ? audNowMs - this.lastPaintAudMs : 0;
1360
+ const dPts = this.hasLastPaintedPts ? ptsMs - this.lastPaintedPtsUs / 1e3 : 0;
1361
+ this.ctx.save();
1362
+ this.ctx.font = "bold 18px monospace";
1363
+ const lines = [
1364
+ `#${this.framesPainted + 1} pts=${ptsMs.toFixed(0)} aud=${audNowMs.toFixed(0)} wall=${wallNow.toFixed(0)}`,
1365
+ `\u0394pts=${dPts.toFixed(0)} \u0394aud=${dAud.toFixed(0)} \u0394wall=${dWall.toFixed(0)}`
1366
+ ];
1367
+ const lineHeight = 22;
1368
+ const padTop = 6;
1369
+ const stripH = padTop + lineHeight * lines.length;
1370
+ this.ctx.fillStyle = "rgba(0,0,0,0.7)";
1371
+ this.ctx.fillRect(0, 0, this.canvas.width, stripH);
1372
+ this.ctx.fillStyle = "#0f0";
1373
+ for (let i = 0; i < lines.length; i++) {
1374
+ this.ctx.fillText(lines[i], 8, padTop + lineHeight * (i + 1) - 4);
1375
+ }
1376
+ this.ctx.restore();
1377
+ }
1378
+ this.lastPaintedPtsUs = frame.timestamp ?? 0;
1379
+ this.hasLastPaintedPts = true;
1380
+ this.lastPaintAudMs = this.clock.now() * 1e3;
1320
1381
  this.framesPainted++;
1321
1382
  } catch (err) {
1322
1383
  if (this.framesPainted === 0 && this.framesDroppedLate === 0) {
@@ -1329,6 +1390,7 @@ var VideoRenderer = class {
1329
1390
  const count = this.queue.length;
1330
1391
  while (this.queue.length > 0) this.queue.shift()?.close();
1331
1392
  this.prerolled = false;
1393
+ this.hasLastPaintedPts = false;
1332
1394
  this.ptsCalibrated = false;
1333
1395
  this.hasEverEnqueuedSinceFlush = false;
1334
1396
  if (isDebug() && count > 0) {
@@ -1369,6 +1431,9 @@ var VideoRenderer = class {
1369
1431
  };
1370
1432
 
1371
1433
  // src/strategies/fallback/audio-output.ts
1434
+ function isDebug2() {
1435
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
1436
+ }
1372
1437
  var AudioOutput = class {
1373
1438
  ctx;
1374
1439
  gain;
@@ -1390,6 +1455,16 @@ var AudioOutput = class {
1390
1455
  mediaTimeOfNext = 0;
1391
1456
  /** Anchor: media time `mediaTimeOfAnchor` corresponds to ctx time `ctxTimeAtAnchor`. */
1392
1457
  mediaTimeOfAnchor = 0;
1458
+ /**
1459
+ * Ctx time at which the first audible chunk will start playing. `-1`
1460
+ * before any chunk has been scheduled successfully (clock is frozen);
1461
+ * the actual ctx time once one has. The renderer's `clock.now()` uses
1462
+ * this to avoid advancing during the silent-gap window between
1463
+ * `audio.start()` and the first chunk that schedules without being
1464
+ * dropped — that gap is what produces the "audio-less fast-forward"
1465
+ * the user sees post-seek when the gate releases on video-only grace.
1466
+ */
1467
+ firstAudibleCtxStart = -1;
1393
1468
  ctxTimeAtAnchor = 0;
1394
1469
  pendingQueue = [];
1395
1470
  framesScheduled = 0;
@@ -1462,10 +1537,16 @@ var AudioOutput = class {
1462
1537
  return this.mediaTimeOfAnchor;
1463
1538
  }
1464
1539
  if (this.state === "playing") {
1540
+ if (this.firstAudibleCtxStart < 0) {
1541
+ return this.mediaTimeOfAnchor;
1542
+ }
1465
1543
  return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
1466
1544
  }
1467
1545
  return this.mediaTimeOfAnchor;
1468
1546
  }
1547
+ anchorTime() {
1548
+ return this.mediaTimeOfAnchor;
1549
+ }
1469
1550
  isPlaying() {
1470
1551
  return this.state === "playing";
1471
1552
  }
@@ -1492,18 +1573,81 @@ var AudioOutput = class {
1492
1573
  * Schedule a chunk of decoded samples. Queues internally while idle (cold
1493
1574
  * start or post-seek), schedules directly to the audio graph while playing.
1494
1575
  * In wall-clock mode, samples are silently discarded.
1576
+ *
1577
+ * `ptsSec` is the chunk's source-domain content PTS in seconds, from
1578
+ * the demuxer. When provided, the chunk plays at the ctx-time
1579
+ * corresponding to that PTS — so pre-target audio after a seek
1580
+ * naturally drops (its computed `ctxStart` falls in the past) and
1581
+ * post-target audio plays at its true content time, without any
1582
+ * external trim or anchor rebase. When `ptsSec` is null (cold start
1583
+ * with no PTS yet, or codecs whose packet→frame mapping isn't 1:1),
1584
+ * the chunk is scheduled sequentially after `mediaTimeOfNext` — the
1585
+ * pre-refactor behavior.
1495
1586
  */
1496
- schedule(samples, channels, sampleRate) {
1587
+ schedule(samples, channels, sampleRate, ptsSec) {
1497
1588
  if (this.destroyed || this.noAudio) return;
1498
1589
  const frameCount = samples.length / channels;
1499
1590
  const durationSec = frameCount / sampleRate;
1591
+ const hasPts = ptsSec != null && Number.isFinite(ptsSec);
1592
+ if (hasPts && ptsSec + durationSec / this._rate < this.mediaTimeOfAnchor) {
1593
+ return;
1594
+ }
1500
1595
  if (this.state === "idle" || this.state === "paused") {
1501
- this.pendingQueue.push({ samples, channels, sampleRate, frameCount, durationSec });
1596
+ this.pendingQueue.push({
1597
+ samples,
1598
+ channels,
1599
+ sampleRate,
1600
+ frameCount,
1601
+ durationSec,
1602
+ ptsSec: hasPts ? ptsSec : null
1603
+ });
1502
1604
  return;
1503
1605
  }
1504
- this.scheduleNow(samples, channels, sampleRate, frameCount);
1606
+ this.scheduleNow(
1607
+ samples,
1608
+ channels,
1609
+ sampleRate,
1610
+ frameCount,
1611
+ hasPts ? ptsSec : null
1612
+ );
1505
1613
  }
1506
- scheduleNow(samples, channels, sampleRate, frameCount) {
1614
+ scheduleNow(samples, channels, sampleRate, frameCount, ptsSec) {
1615
+ const durationSec = frameCount / sampleRate;
1616
+ let ctxStart;
1617
+ if (ptsSec != null) {
1618
+ ctxStart = this.ctxTimeAtAnchor + (ptsSec - this.mediaTimeOfAnchor) / this._rate;
1619
+ if (isDebug2()) {
1620
+ 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}`);
1621
+ }
1622
+ if (ctxStart < this.ctx.currentTime - 1e-3) {
1623
+ if (isDebug2()) {
1624
+ console.log(`[TRACE-AUD] DROP late chunk pts=${ptsSec.toFixed(3)} ctxStart=${ctxStart.toFixed(4)} < ctxNow=${this.ctx.currentTime.toFixed(4)}`);
1625
+ }
1626
+ return;
1627
+ }
1628
+ if (this.firstAudibleCtxStart < 0) {
1629
+ this.firstAudibleCtxStart = ctxStart;
1630
+ this.mediaTimeOfAnchor = ptsSec;
1631
+ this.ctxTimeAtAnchor = ctxStart;
1632
+ if (isDebug2()) {
1633
+ 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)}`);
1634
+ }
1635
+ }
1636
+ const endMediaTime = ptsSec + durationSec / this._rate;
1637
+ if (endMediaTime > this.mediaTimeOfNext) {
1638
+ this.mediaTimeOfNext = endMediaTime;
1639
+ }
1640
+ } else {
1641
+ ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
1642
+ console.warn(`[TRACE-AUD] LEGACY (no PTS) sched dur=${durationSec.toFixed(4)} ctxStart=${ctxStart.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)}`);
1643
+ if (ctxStart < this.ctx.currentTime) {
1644
+ 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)}`);
1645
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
1646
+ this.mediaTimeOfAnchor = this.mediaTimeOfNext;
1647
+ ctxStart = this.ctx.currentTime;
1648
+ }
1649
+ this.mediaTimeOfNext += durationSec;
1650
+ }
1507
1651
  const buffer = this.ctx.createBuffer(channels, frameCount, sampleRate);
1508
1652
  for (let ch = 0; ch < channels; ch++) {
1509
1653
  const channelData = buffer.getChannelData(ch);
@@ -1515,14 +1659,7 @@ var AudioOutput = class {
1515
1659
  node.buffer = buffer;
1516
1660
  node.connect(this.gain);
1517
1661
  if (this._rate !== 1) node.playbackRate.value = this._rate;
1518
- let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
1519
- if (ctxStart < this.ctx.currentTime) {
1520
- this.ctxTimeAtAnchor = this.ctx.currentTime;
1521
- this.mediaTimeOfAnchor = this.mediaTimeOfNext;
1522
- ctxStart = this.ctx.currentTime;
1523
- }
1524
1662
  node.start(ctxStart);
1525
- this.mediaTimeOfNext += frameCount / sampleRate;
1526
1663
  this.framesScheduled++;
1527
1664
  }
1528
1665
  // ── Lifecycle ─────────────────────────────────────────────────────────
@@ -1547,12 +1684,15 @@ var AudioOutput = class {
1547
1684
  } catch {
1548
1685
  }
1549
1686
  if (this.state === "paused") {
1687
+ if (isDebug2()) {
1688
+ 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}`);
1689
+ }
1550
1690
  this.ctxTimeAtAnchor = this.ctx.currentTime;
1551
1691
  this.state = "playing";
1552
1692
  const drain2 = this.pendingQueue;
1553
1693
  this.pendingQueue = [];
1554
1694
  for (const c of drain2) {
1555
- this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
1695
+ this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
1556
1696
  }
1557
1697
  return;
1558
1698
  }
@@ -1560,10 +1700,13 @@ var AudioOutput = class {
1560
1700
  this.ctxTimeAtAnchor = this.ctx.currentTime + STARTUP_DELAY;
1561
1701
  this.mediaTimeOfNext = this.mediaTimeOfAnchor;
1562
1702
  this.state = "playing";
1703
+ if (isDebug2()) {
1704
+ 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}`);
1705
+ }
1563
1706
  const drain = this.pendingQueue;
1564
1707
  this.pendingQueue = [];
1565
1708
  for (const c of drain) {
1566
- this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
1709
+ this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
1567
1710
  }
1568
1711
  }
1569
1712
  /** Pause playback. Suspends the audio context. */
@@ -1589,6 +1732,9 @@ var AudioOutput = class {
1589
1732
  * supplying new samples) and then call `start()` to resume playback.
1590
1733
  */
1591
1734
  async reset(newMediaTime) {
1735
+ if (isDebug2()) {
1736
+ 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}`);
1737
+ }
1592
1738
  if (this.noAudio) {
1593
1739
  this.pendingQueue = [];
1594
1740
  this.mediaTimeOfAnchor = newMediaTime;
@@ -1607,6 +1753,7 @@ var AudioOutput = class {
1607
1753
  this.mediaTimeOfAnchor = newMediaTime;
1608
1754
  this.mediaTimeOfNext = newMediaTime;
1609
1755
  this.ctxTimeAtAnchor = this.ctx.currentTime;
1756
+ this.firstAudibleCtxStart = -1;
1610
1757
  this.state = "idle";
1611
1758
  if (this.ctx.state === "running") {
1612
1759
  await this.ctx.suspend();
@@ -1775,7 +1922,6 @@ async function startHybridDecoder(opts) {
1775
1922
  let videoChunksFed = 0;
1776
1923
  let bufferedUntilSec = 0;
1777
1924
  let syntheticVideoUs = 0;
1778
- let syntheticAudioUs = 0;
1779
1925
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
1780
1926
  const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
1781
1927
  const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
@@ -1807,7 +1953,13 @@ async function startHybridDecoder(opts) {
1807
1953
  }
1808
1954
  }
1809
1955
  if (audioDec && audioPackets && audioPackets.length > 0) {
1810
- await decodeAudioBatch(audioPackets, myToken);
1956
+ await decodeAudioBatch(
1957
+ audioPackets,
1958
+ myToken,
1959
+ /*flush*/
1960
+ false,
1961
+ audioTimeBase
1962
+ );
1811
1963
  }
1812
1964
  if (myToken !== pumpToken || destroyed) return;
1813
1965
  await new Promise((r) => setTimeout(r, 0));
@@ -1854,8 +2006,11 @@ async function startHybridDecoder(opts) {
1854
2006
  }
1855
2007
  }
1856
2008
  }
1857
- async function decodeAudioBatch(pkts, myToken, flush = false) {
2009
+ async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
1858
2010
  if (!audioDec || destroyed || myToken !== pumpToken) return;
2011
+ const pktPtsSec = pkts.map(
2012
+ (p) => tb ? packetPtsSec(p, tb) : null
2013
+ );
1859
2014
  const AUDIO_SUB_BATCH = 4;
1860
2015
  let allFrames = [];
1861
2016
  for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
@@ -1893,22 +2048,13 @@ async function startHybridDecoder(opts) {
1893
2048
  }
1894
2049
  if (myToken !== pumpToken || destroyed) return;
1895
2050
  const frames = allFrames;
1896
- for (const f of frames) {
2051
+ for (let i = 0; i < frames.length; i++) {
1897
2052
  if (myToken !== pumpToken || destroyed) return;
1898
- sanitizeFrameTimestamp(
1899
- f,
1900
- () => {
1901
- const ts = syntheticAudioUs;
1902
- const samples2 = f.nb_samples ?? 1024;
1903
- const sampleRate = f.sample_rate ?? 44100;
1904
- syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
1905
- return ts;
1906
- },
1907
- audioTimeBase
1908
- );
2053
+ const f = frames[i];
1909
2054
  const samples = libavFrameToInterleavedFloat32(f);
1910
2055
  if (samples) {
1911
- opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
2056
+ const pts = pktPtsSec[i] ?? null;
2057
+ opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
1912
2058
  audioFramesDecoded++;
1913
2059
  }
1914
2060
  }
@@ -2018,7 +2164,6 @@ async function startHybridDecoder(opts) {
2018
2164
  }
2019
2165
  await flushBSF();
2020
2166
  syntheticVideoUs = Math.round(timeSec * 1e6);
2021
- syntheticAudioUs = Math.round(timeSec * 1e6);
2022
2167
  pumpRunning = pumpLoop(newToken).catch(
2023
2168
  (err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
2024
2169
  );
@@ -2057,7 +2202,6 @@ async function startHybridDecoder(opts) {
2057
2202
  }
2058
2203
  await flushBSF();
2059
2204
  syntheticVideoUs = Math.round(timeSec * 1e6);
2060
- syntheticAudioUs = Math.round(timeSec * 1e6);
2061
2205
  pumpRunning = pumpLoop(newToken).catch(
2062
2206
  (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
2063
2207
  );
@@ -2310,6 +2454,9 @@ async function createHybridSession(ctx, target, transport) {
2310
2454
  }
2311
2455
 
2312
2456
  // src/strategies/fallback/decoder.ts
2457
+ function isDebug3() {
2458
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
2459
+ }
2313
2460
  async function startDecoder(opts) {
2314
2461
  const variant = "avbridge";
2315
2462
  const libav = await loadLibav(variant);
@@ -2441,8 +2588,15 @@ async function startDecoder(opts) {
2441
2588
  let watchdogSlowSinceMs = 0;
2442
2589
  let watchdogSlowWarned = false;
2443
2590
  let watchdogOverflowWarned = false;
2444
- let syntheticVideoUs = 0;
2445
- let syntheticAudioUs = 0;
2591
+ let lastContentUs = -1;
2592
+ let firstValidPtsLoggedSinceSeek = false;
2593
+ let seenFirstAudioPacketSinceSeek = false;
2594
+ let seekTargetSec = 0;
2595
+ let diagPktsLoggedSinceSeek = 0;
2596
+ let diagFramesLoggedSinceSeek = 0;
2597
+ let diagFrameKeysDumped = false;
2598
+ const DIAG_MAX_PKTS = 100;
2599
+ const DIAG_MAX_FRAMES = 300;
2446
2600
  let videoDecodeMsTotal = 0;
2447
2601
  let audioDecodeMsTotal = 0;
2448
2602
  let videoDecodeBatches = 0;
@@ -2481,6 +2635,18 @@ async function startDecoder(opts) {
2481
2635
  for (const pkt of videoPackets) {
2482
2636
  const sec = packetPtsSec(pkt, videoTimeBase);
2483
2637
  if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2638
+ if (isDebug3() && diagPktsLoggedSinceSeek < DIAG_MAX_PKTS) {
2639
+ const rawHi = pkt.ptshi ?? 0;
2640
+ const rawLo = pkt.pts ?? 0;
2641
+ const isInvalidPts = rawHi === -2147483648 && rawLo === 0;
2642
+ const rawPts64 = isInvalidPts ? null : rawHi * 4294967296 + rawLo;
2643
+ const rawSec = rawPts64 != null && videoTimeBase ? rawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
2644
+ const pktKeys = diagPktsLoggedSinceSeek === 0 ? `[keys: ${Object.keys(pkt).join(",")}]` : "";
2645
+ console.log(
2646
+ `[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
2647
+ );
2648
+ diagPktsLoggedSinceSeek++;
2649
+ }
2484
2650
  }
2485
2651
  }
2486
2652
  if (audioPackets && audioTimeBase) {
@@ -2488,9 +2654,25 @@ async function startDecoder(opts) {
2488
2654
  const sec = packetPtsSec(pkt, audioTimeBase);
2489
2655
  if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2490
2656
  }
2657
+ if (!seenFirstAudioPacketSinceSeek && audioPackets.length > 0) {
2658
+ const firstSec = packetPtsSec(audioPackets[0], audioTimeBase);
2659
+ if (firstSec != null && Number.isFinite(firstSec)) {
2660
+ seenFirstAudioPacketSinceSeek = true;
2661
+ dbg.info(
2662
+ "av-anchor",
2663
+ `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)`
2664
+ );
2665
+ }
2666
+ }
2491
2667
  }
2492
2668
  if (audioDec && audioPackets && audioPackets.length > 0) {
2493
- await decodeAudioBatch(audioPackets, myToken);
2669
+ await decodeAudioBatch(
2670
+ audioPackets,
2671
+ myToken,
2672
+ /*flush*/
2673
+ false,
2674
+ audioTimeBase
2675
+ );
2494
2676
  }
2495
2677
  if (myToken !== pumpToken || destroyed) return;
2496
2678
  if (videoDec && videoPackets && videoPackets.length > 0) {
@@ -2534,7 +2716,7 @@ async function startDecoder(opts) {
2534
2716
  {
2535
2717
  const _throttleStart = performance.now();
2536
2718
  let _throttled = false;
2537
- while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
2719
+ while (!destroyed && myToken === pumpToken && opts.audio.bufferAhead() > 2) {
2538
2720
  _throttled = true;
2539
2721
  await new Promise((r) => setTimeout(r, 50));
2540
2722
  }
@@ -2589,18 +2771,83 @@ async function startDecoder(opts) {
2589
2771
  if (myToken !== pumpToken || destroyed) return;
2590
2772
  for (const f of frames) {
2591
2773
  if (myToken !== pumpToken || destroyed) return;
2592
- sanitizeFrameTimestamp(
2593
- f,
2594
- () => {
2595
- const base = lastEmittedPtsUs >= 0 ? lastEmittedPtsUs + videoFrameStepUs : syntheticVideoUs;
2596
- syntheticVideoUs = base + videoFrameStepUs;
2597
- return base;
2598
- },
2599
- videoTimeBase
2600
- );
2601
- const _fPts = (f.ptshi ?? 0) * 4294967296 + (f.pts ?? 0);
2774
+ const _diagShouldLog = isDebug3() && diagFramesLoggedSinceSeek < DIAG_MAX_FRAMES;
2775
+ const _diagRawHi = f.ptshi ?? 0;
2776
+ const _diagRawLo = f.pts ?? 0;
2777
+ const _diagInvalid = _diagRawHi === -2147483648 && _diagRawLo === 0;
2778
+ const _diagRawPts64 = _diagInvalid ? null : _diagRawHi * 4294967296 + _diagRawLo;
2779
+ const _diagRawSec = _diagRawPts64 != null && videoTimeBase ? _diagRawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
2780
+ if (_diagShouldLog && !diagFrameKeysDumped) {
2781
+ diagFrameKeysDumped = true;
2782
+ const allKeys = Object.keys(f);
2783
+ const fieldDump = {};
2784
+ for (const k of allKeys) {
2785
+ const v = f[k];
2786
+ if (k === "data") continue;
2787
+ if (typeof v === "object" && v !== null && "length" in v) continue;
2788
+ fieldDump[k] = v;
2789
+ }
2790
+ console.log(`[DIAG-FRAME] FIRST FRAME post-seek \u2014 all keys: ${allKeys.join(",")}`);
2791
+ console.log(`[DIAG-FRAME] FIRST FRAME field dump:`, fieldDump);
2792
+ }
2793
+ let rawUs = null;
2794
+ if (!_diagInvalid && _diagRawPts64 != null) {
2795
+ const tb = videoTimeBase ?? [1, 1e6];
2796
+ const us = Math.round(_diagRawPts64 * 1e6 * tb[0] / tb[1]);
2797
+ if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2798
+ rawUs = us;
2799
+ }
2800
+ }
2801
+ const _diagLog = (decision, finalPtsUs, sanFallback) => {
2802
+ if (!_diagShouldLog) return;
2803
+ const ptsSrc = sanFallback ? `SYNTHETIC(${_diagInvalid ? "NOPTS" : "invalid-range"})` : "LIBAV";
2804
+ console.log(
2805
+ `[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}`
2806
+ );
2807
+ diagFramesLoggedSinceSeek++;
2808
+ };
2809
+ let _diagSanFallbackFired = false;
2810
+ const seekTargetUs = Math.round(seekTargetSec * 1e6);
2811
+ if (lastContentUs < 0) {
2812
+ if (rawUs == null) {
2813
+ const isColdStartKeyframe = seekTargetSec === 0 && f.key_frame === 1;
2814
+ if (isColdStartKeyframe) {
2815
+ lastContentUs = 0;
2816
+ _diagSanFallbackFired = true;
2817
+ } else {
2818
+ _diagLog("PRE-ANCHOR-DROP", 0, true);
2819
+ continue;
2820
+ }
2821
+ } else {
2822
+ lastContentUs = rawUs;
2823
+ if (!firstValidPtsLoggedSinceSeek) {
2824
+ firstValidPtsLoggedSinceSeek = true;
2825
+ if (isDebug3()) {
2826
+ console.log(
2827
+ `[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)`
2828
+ );
2829
+ }
2830
+ if (rawUs >= seekTargetUs) {
2831
+ console.warn(
2832
+ `[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.`
2833
+ );
2834
+ }
2835
+ }
2836
+ }
2837
+ } else {
2838
+ if (rawUs != null) {
2839
+ lastContentUs = rawUs;
2840
+ } else {
2841
+ lastContentUs += videoFrameStepUs;
2842
+ _diagSanFallbackFired = true;
2843
+ }
2844
+ }
2845
+ f.pts = lastContentUs;
2846
+ f.ptshi = lastContentUs < 0 ? -1 : 0;
2847
+ const _fPts = lastContentUs;
2602
2848
  if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
2603
2849
  if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
2850
+ _diagLog("REGRESSED-DROP", _fPts, _diagSanFallbackFired);
2604
2851
  ptsRegressions++;
2605
2852
  const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
2606
2853
  if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
@@ -2612,19 +2859,34 @@ async function startDecoder(opts) {
2612
2859
  continue;
2613
2860
  }
2614
2861
  lastEmittedPtsUs = _fPts;
2862
+ const targetUs = Math.round(seekTargetSec * 1e6);
2863
+ if (_fPts < targetUs - videoFrameStepUs) {
2864
+ _diagLog("PRE-TARGET-DROP", _fPts, _diagSanFallbackFired);
2865
+ continue;
2866
+ }
2615
2867
  try {
2616
2868
  const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
2617
- opts.renderer.enqueue(vf);
2869
+ if (opts.renderer.queueDepth() >= opts.renderer.queueHighWater) {
2870
+ vf.close();
2871
+ _diagLog("OVERFLOW-DROP", _fPts, _diagSanFallbackFired);
2872
+ } else {
2873
+ opts.renderer.enqueue(vf);
2874
+ _diagLog("ENQUEUED", _fPts, _diagSanFallbackFired);
2875
+ }
2618
2876
  videoFramesDecoded++;
2619
2877
  } catch (err) {
2620
2878
  if (videoFramesDecoded === 0) {
2621
2879
  console.warn("[avbridge] laFrameToVideoFrame failed:", err);
2622
2880
  }
2881
+ _diagLog("BRIDGE-ERROR", _fPts, _diagSanFallbackFired);
2623
2882
  }
2624
2883
  }
2625
2884
  }
2626
- async function decodeAudioBatch(pkts, myToken, flush = false) {
2885
+ async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
2627
2886
  if (!audioDec || destroyed || myToken !== pumpToken) return;
2887
+ const pktPtsSec = pkts.map(
2888
+ (p) => tb ? packetPtsSec(p, tb) : null
2889
+ );
2628
2890
  let frames;
2629
2891
  const _t0 = performance.now();
2630
2892
  try {
@@ -2642,22 +2904,17 @@ async function startDecoder(opts) {
2642
2904
  audioDecodeMsTotal += performance.now() - _t0;
2643
2905
  audioDecodeBatches++;
2644
2906
  if (myToken !== pumpToken || destroyed) return;
2645
- for (const f of frames) {
2907
+ for (let i = 0; i < frames.length; i++) {
2646
2908
  if (myToken !== pumpToken || destroyed) return;
2647
- sanitizeFrameTimestamp(
2648
- f,
2649
- () => {
2650
- const ts = syntheticAudioUs;
2651
- const samples2 = f.nb_samples ?? 1024;
2652
- const sampleRate = f.sample_rate ?? 44100;
2653
- syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
2654
- return ts;
2655
- },
2656
- audioTimeBase
2657
- );
2909
+ const f = frames[i];
2658
2910
  const samples = libavFrameToInterleavedFloat32(f);
2659
2911
  if (samples) {
2660
- opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
2912
+ const pts = pktPtsSec[i] ?? null;
2913
+ if (isDebug3()) {
2914
+ const dur = samples.data.length / samples.channels / samples.sampleRate;
2915
+ 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}`);
2916
+ }
2917
+ opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
2661
2918
  audioFramesDecoded++;
2662
2919
  }
2663
2920
  }
@@ -2760,14 +3017,17 @@ async function startDecoder(opts) {
2760
3017
  } catch {
2761
3018
  }
2762
3019
  await flushBSF();
2763
- syntheticVideoUs = Math.round(timeSec * 1e6);
2764
- syntheticAudioUs = Math.round(timeSec * 1e6);
3020
+ lastContentUs = -1;
2765
3021
  lastEmittedPtsUs = -1;
3022
+ firstValidPtsLoggedSinceSeek = false;
2766
3023
  pumpRunning = pumpLoop(newToken).catch(
2767
3024
  (err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
2768
3025
  );
2769
3026
  },
2770
3027
  async seek(timeSec) {
3028
+ if (isDebug3()) {
3029
+ console.log(`[SEEK] target=${timeSec.toFixed(3)}s (${(timeSec * 1e3).toFixed(0)}ms) wall=${performance.now().toFixed(0)}`);
3030
+ }
2771
3031
  const newToken = ++pumpToken;
2772
3032
  if (pumpRunning) {
2773
3033
  try {
@@ -2798,9 +3058,14 @@ async function startDecoder(opts) {
2798
3058
  } catch {
2799
3059
  }
2800
3060
  await flushBSF();
2801
- syntheticVideoUs = Math.round(timeSec * 1e6);
2802
- syntheticAudioUs = Math.round(timeSec * 1e6);
3061
+ lastContentUs = -1;
2803
3062
  lastEmittedPtsUs = -1;
3063
+ firstValidPtsLoggedSinceSeek = false;
3064
+ seenFirstAudioPacketSinceSeek = false;
3065
+ seekTargetSec = timeSec;
3066
+ diagPktsLoggedSinceSeek = 0;
3067
+ diagFramesLoggedSinceSeek = 0;
3068
+ diagFrameKeysDumped = false;
2804
3069
  pumpRunning = pumpLoop(newToken).catch(
2805
3070
  (err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
2806
3071
  );
@@ -3654,5 +3919,5 @@ function defaultFallbackChain(strategy) {
3654
3919
  }
3655
3920
 
3656
3921
  export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext, createPlayer };
3657
- //# sourceMappingURL=chunk-BN7BRTLY.js.map
3658
- //# sourceMappingURL=chunk-BN7BRTLY.js.map
3922
+ //# sourceMappingURL=chunk-VOC24LYF.js.map
3923
+ //# sourceMappingURL=chunk-VOC24LYF.js.map