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.
@@ -32255,22 +32255,25 @@ async function createRemuxPipeline(ctx, video) {
32255
32255
  }
32256
32256
  }
32257
32257
  let mimePromise = null;
32258
+ const myToken = pumpToken;
32258
32259
  const writable = new WritableStream({
32259
32260
  write: async (chunk) => {
32260
- if (destroyed) return;
32261
+ if (destroyed || pumpToken !== myToken) return;
32261
32262
  if (!sink) {
32262
32263
  const mime = await (mimePromise ??= output.getMimeType());
32264
+ if (destroyed || pumpToken !== myToken) return;
32263
32265
  sink = new MseSink({ mime, video });
32264
32266
  await sink.ready();
32267
+ if (destroyed || pumpToken !== myToken) return;
32265
32268
  if (pendingStartTime > 0) {
32266
32269
  sink.invalidate(pendingStartTime);
32267
32270
  }
32268
32271
  sink.setPlayOnSeek(pendingAutoPlay);
32269
32272
  }
32270
- while (sink && !destroyed && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
32273
+ while (sink && !destroyed && pumpToken === myToken && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
32271
32274
  await new Promise((r) => setTimeout(r, 500));
32272
32275
  }
32273
- if (destroyed) return;
32276
+ if (destroyed || pumpToken !== myToken) return;
32274
32277
  sink.append(chunk.data);
32275
32278
  stats.bytesWritten += chunk.data.byteLength;
32276
32279
  stats.fragments++;
@@ -32538,7 +32541,21 @@ var VideoRenderer = class {
32538
32541
  framesPainted = 0;
32539
32542
  framesDroppedLate = 0;
32540
32543
  framesDroppedOverflow = 0;
32544
+ /** True once the head frame has been painted as a pre-roll poster
32545
+ * since the last flush. Used to ensure pre-roll paints exactly one
32546
+ * frame (held static) during the post-seek discard window. */
32541
32547
  prerolled = false;
32548
+ /** PTS (µs) of the most recently painted frame. Used as the calibration
32549
+ * reference on the first post-flush snap: the pre-roll path paints one
32550
+ * frame *before* PTS-based playback starts, so the queue head's PTS at
32551
+ * first PTS-based paint is the *next* frame, off by one frameDur from
32552
+ * the actually-displayed frame. Calibrating against the painted frame
32553
+ * instead of the queue head removes that one-frame offset and yields
32554
+ * calib ≈ 0 instead of +frameDur. */
32555
+ lastPaintedPtsUs = 0;
32556
+ hasLastPaintedPts = false;
32557
+ /** Audio-clock reading (ms) at the previous paint, for overlay Δaud. */
32558
+ lastPaintAudMs = 0;
32542
32559
  /** Wall-clock time of the last paint, in ms (performance.now()). */
32543
32560
  lastPaintWall = 0;
32544
32561
  /** Minimum ms between paints — paces video at roughly source fps. */
@@ -32588,13 +32605,17 @@ var VideoRenderer = class {
32588
32605
  return this.queue.length;
32589
32606
  }
32590
32607
  /**
32591
- * Soft cap for decoder backpressure. The decoder pump throttles when
32592
- * `queueDepth() >= queueHighWater`. Set high enough that normal decode
32593
- * bursts don't trigger the renderer's overflow-drop loop (which runs at
32594
- * every paint), but low enough that the decoder doesn't run unboundedly
32595
- * ahead. The hard cap in `enqueue()` is 64.
32608
+ * Cap the decoder may fill the queue up to. Used by the decoder's
32609
+ * enqueue-side discard logic (it closes new frames instead of pushing
32610
+ * them when this is reached). Sized so a long post-seek catch-up
32611
+ * fits the decoder produces frames at PTS T_kf onwards rapidly
32612
+ * while the demuxer is chewing through pre-target audio; if the
32613
+ * queue can hold the whole post-seek burst, the renderer plays
32614
+ * smoothly from pre-roll without a frozen-video gap when audio.start
32615
+ * fires. At ~340 KB per SD frame the cap is ~85 MB peak; at HD it's
32616
+ * larger but still bounded.
32596
32617
  */
32597
- queueHighWater = 30;
32618
+ queueHighWater = 256;
32598
32619
  enqueue(frame) {
32599
32620
  if (this.destroyed) {
32600
32621
  frame.close();
@@ -32605,7 +32626,7 @@ var VideoRenderer = class {
32605
32626
  if (this.queue.length === 1 && this.framesPainted === 0) {
32606
32627
  this.resolveFirstFrame();
32607
32628
  }
32608
- while (this.queue.length > 60) {
32629
+ while (this.queue.length > this.queueHighWater + 8) {
32609
32630
  this.queue.shift()?.close();
32610
32631
  this.framesDroppedOverflow++;
32611
32632
  }
@@ -32687,12 +32708,9 @@ var VideoRenderer = class {
32687
32708
  if (this.queue.length === 0) return;
32688
32709
  const playing = this.clock.isPlaying();
32689
32710
  if (!playing) {
32690
- if (!this.prerolled) {
32691
- const head = this.queue.shift();
32692
- this.paint(head);
32693
- head.close();
32711
+ if (!this.prerolled && this.queue.length > 0) {
32694
32712
  this.prerolled = true;
32695
- this.lastPaintWall = performance.now();
32713
+ this.paint(this.queue[0]);
32696
32714
  }
32697
32715
  return;
32698
32716
  }
@@ -32701,14 +32719,29 @@ var VideoRenderer = class {
32701
32719
  const hasPts = headTs > 0 || this.queue.length > 1;
32702
32720
  if (hasPts) {
32703
32721
  const wallNow2 = performance.now();
32704
- if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
32705
- this.ptsCalibrationUs = headTs - rawAudioNowUs;
32722
+ if (!this.ptsCalibrated) {
32723
+ const anchorUs = (this.clock.anchorTime?.() ?? this.clock.now()) * 1e6;
32724
+ const referencePtsUs = this.hasLastPaintedPts ? this.lastPaintedPtsUs : headTs;
32725
+ this.ptsCalibrationUs = referencePtsUs - anchorUs;
32706
32726
  this.ptsCalibrated = true;
32707
32727
  this.lastCalibrationWall = wallNow2;
32728
+ if (isDebug()) {
32729
+ console.log(
32730
+ `[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`
32731
+ );
32732
+ }
32733
+ } else if (wallNow2 - this.lastCalibrationWall > 1e4) {
32734
+ const oldCalib = this.ptsCalibrationUs;
32735
+ this.ptsCalibrationUs = headTs - rawAudioNowUs;
32736
+ this.lastCalibrationWall = wallNow2;
32737
+ if (isDebug()) {
32738
+ console.log(
32739
+ `[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)`
32740
+ );
32741
+ }
32708
32742
  }
32709
32743
  const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
32710
- const frameDurationUs = this.paintIntervalMs * 1e3;
32711
- const deadlineUs = audioNowUs + frameDurationUs;
32744
+ const deadlineUs = audioNowUs;
32712
32745
  let bestIdx = -1;
32713
32746
  for (let i = 0; i < this.queue.length; i++) {
32714
32747
  const ts = this.queue[i].timestamp ?? 0;
@@ -32736,19 +32769,20 @@ var VideoRenderer = class {
32736
32769
  return;
32737
32770
  }
32738
32771
  const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
32739
- const dropThresholdUs = _relaxDrop ? audioNowUs - 60 * 1e6 : audioNowUs - frameDurationUs * 2;
32740
32772
  let dropped = 0;
32741
- while (bestIdx > 0) {
32742
- const ts = this.queue[0].timestamp ?? 0;
32743
- if (ts < dropThresholdUs) {
32773
+ const initialBestIdx = bestIdx;
32774
+ if (!_relaxDrop) {
32775
+ while (bestIdx > 0) {
32744
32776
  this.queue.shift()?.close();
32745
32777
  this.framesDroppedLate++;
32746
32778
  bestIdx--;
32747
32779
  dropped++;
32748
- } else {
32749
- break;
32750
32780
  }
32751
32781
  }
32782
+ const paintTs = this.queue[0]?.timestamp ?? 0;
32783
+ if (isDebug()) {
32784
+ 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)}`);
32785
+ }
32752
32786
  this.ticksPainted++;
32753
32787
  if (isDebug()) {
32754
32788
  const now = performance.now();
@@ -32784,6 +32818,33 @@ var VideoRenderer = class {
32784
32818
  }
32785
32819
  try {
32786
32820
  this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
32821
+ if (isDebug()) {
32822
+ const wallNow = performance.now();
32823
+ const audNowMs = this.clock.now() * 1e3;
32824
+ const ptsMs = (frame.timestamp ?? 0) / 1e3;
32825
+ const dWall = this.lastPaintWall > 0 ? wallNow - this.lastPaintWall : 0;
32826
+ const dAud = this.lastPaintAudMs > 0 ? audNowMs - this.lastPaintAudMs : 0;
32827
+ const dPts = this.hasLastPaintedPts ? ptsMs - this.lastPaintedPtsUs / 1e3 : 0;
32828
+ this.ctx.save();
32829
+ this.ctx.font = "bold 18px monospace";
32830
+ const lines = [
32831
+ `#${this.framesPainted + 1} pts=${ptsMs.toFixed(0)} aud=${audNowMs.toFixed(0)} wall=${wallNow.toFixed(0)}`,
32832
+ `\u0394pts=${dPts.toFixed(0)} \u0394aud=${dAud.toFixed(0)} \u0394wall=${dWall.toFixed(0)}`
32833
+ ];
32834
+ const lineHeight = 22;
32835
+ const padTop = 6;
32836
+ const stripH = padTop + lineHeight * lines.length;
32837
+ this.ctx.fillStyle = "rgba(0,0,0,0.7)";
32838
+ this.ctx.fillRect(0, 0, this.canvas.width, stripH);
32839
+ this.ctx.fillStyle = "#0f0";
32840
+ for (let i = 0; i < lines.length; i++) {
32841
+ this.ctx.fillText(lines[i], 8, padTop + lineHeight * (i + 1) - 4);
32842
+ }
32843
+ this.ctx.restore();
32844
+ }
32845
+ this.lastPaintedPtsUs = frame.timestamp ?? 0;
32846
+ this.hasLastPaintedPts = true;
32847
+ this.lastPaintAudMs = this.clock.now() * 1e3;
32787
32848
  this.framesPainted++;
32788
32849
  } catch (err) {
32789
32850
  if (this.framesPainted === 0 && this.framesDroppedLate === 0) {
@@ -32796,6 +32857,7 @@ var VideoRenderer = class {
32796
32857
  const count = this.queue.length;
32797
32858
  while (this.queue.length > 0) this.queue.shift()?.close();
32798
32859
  this.prerolled = false;
32860
+ this.hasLastPaintedPts = false;
32799
32861
  this.ptsCalibrated = false;
32800
32862
  this.hasEverEnqueuedSinceFlush = false;
32801
32863
  if (isDebug() && count > 0) {
@@ -32836,6 +32898,9 @@ var VideoRenderer = class {
32836
32898
  };
32837
32899
 
32838
32900
  // src/strategies/fallback/audio-output.ts
32901
+ function isDebug2() {
32902
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
32903
+ }
32839
32904
  var AudioOutput = class {
32840
32905
  ctx;
32841
32906
  gain;
@@ -32857,6 +32922,16 @@ var AudioOutput = class {
32857
32922
  mediaTimeOfNext = 0;
32858
32923
  /** Anchor: media time `mediaTimeOfAnchor` corresponds to ctx time `ctxTimeAtAnchor`. */
32859
32924
  mediaTimeOfAnchor = 0;
32925
+ /**
32926
+ * Ctx time at which the first audible chunk will start playing. `-1`
32927
+ * before any chunk has been scheduled successfully (clock is frozen);
32928
+ * the actual ctx time once one has. The renderer's `clock.now()` uses
32929
+ * this to avoid advancing during the silent-gap window between
32930
+ * `audio.start()` and the first chunk that schedules without being
32931
+ * dropped — that gap is what produces the "audio-less fast-forward"
32932
+ * the user sees post-seek when the gate releases on video-only grace.
32933
+ */
32934
+ firstAudibleCtxStart = -1;
32860
32935
  ctxTimeAtAnchor = 0;
32861
32936
  pendingQueue = [];
32862
32937
  framesScheduled = 0;
@@ -32929,10 +33004,16 @@ var AudioOutput = class {
32929
33004
  return this.mediaTimeOfAnchor;
32930
33005
  }
32931
33006
  if (this.state === "playing") {
33007
+ if (this.firstAudibleCtxStart < 0) {
33008
+ return this.mediaTimeOfAnchor;
33009
+ }
32932
33010
  return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
32933
33011
  }
32934
33012
  return this.mediaTimeOfAnchor;
32935
33013
  }
33014
+ anchorTime() {
33015
+ return this.mediaTimeOfAnchor;
33016
+ }
32936
33017
  isPlaying() {
32937
33018
  return this.state === "playing";
32938
33019
  }
@@ -32959,18 +33040,81 @@ var AudioOutput = class {
32959
33040
  * Schedule a chunk of decoded samples. Queues internally while idle (cold
32960
33041
  * start or post-seek), schedules directly to the audio graph while playing.
32961
33042
  * In wall-clock mode, samples are silently discarded.
33043
+ *
33044
+ * `ptsSec` is the chunk's source-domain content PTS in seconds, from
33045
+ * the demuxer. When provided, the chunk plays at the ctx-time
33046
+ * corresponding to that PTS — so pre-target audio after a seek
33047
+ * naturally drops (its computed `ctxStart` falls in the past) and
33048
+ * post-target audio plays at its true content time, without any
33049
+ * external trim or anchor rebase. When `ptsSec` is null (cold start
33050
+ * with no PTS yet, or codecs whose packet→frame mapping isn't 1:1),
33051
+ * the chunk is scheduled sequentially after `mediaTimeOfNext` — the
33052
+ * pre-refactor behavior.
32962
33053
  */
32963
- schedule(samples, channels, sampleRate) {
33054
+ schedule(samples, channels, sampleRate, ptsSec) {
32964
33055
  if (this.destroyed || this.noAudio) return;
32965
33056
  const frameCount = samples.length / channels;
32966
33057
  const durationSec = frameCount / sampleRate;
33058
+ const hasPts = ptsSec != null && Number.isFinite(ptsSec);
33059
+ if (hasPts && ptsSec + durationSec / this._rate < this.mediaTimeOfAnchor) {
33060
+ return;
33061
+ }
32967
33062
  if (this.state === "idle" || this.state === "paused") {
32968
- this.pendingQueue.push({ samples, channels, sampleRate, frameCount, durationSec });
33063
+ this.pendingQueue.push({
33064
+ samples,
33065
+ channels,
33066
+ sampleRate,
33067
+ frameCount,
33068
+ durationSec,
33069
+ ptsSec: hasPts ? ptsSec : null
33070
+ });
32969
33071
  return;
32970
33072
  }
32971
- this.scheduleNow(samples, channels, sampleRate, frameCount);
33073
+ this.scheduleNow(
33074
+ samples,
33075
+ channels,
33076
+ sampleRate,
33077
+ frameCount,
33078
+ hasPts ? ptsSec : null
33079
+ );
32972
33080
  }
32973
- scheduleNow(samples, channels, sampleRate, frameCount) {
33081
+ scheduleNow(samples, channels, sampleRate, frameCount, ptsSec) {
33082
+ const durationSec = frameCount / sampleRate;
33083
+ let ctxStart;
33084
+ if (ptsSec != null) {
33085
+ ctxStart = this.ctxTimeAtAnchor + (ptsSec - this.mediaTimeOfAnchor) / this._rate;
33086
+ if (isDebug2()) {
33087
+ 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}`);
33088
+ }
33089
+ if (ctxStart < this.ctx.currentTime - 1e-3) {
33090
+ if (isDebug2()) {
33091
+ console.log(`[TRACE-AUD] DROP late chunk pts=${ptsSec.toFixed(3)} ctxStart=${ctxStart.toFixed(4)} < ctxNow=${this.ctx.currentTime.toFixed(4)}`);
33092
+ }
33093
+ return;
33094
+ }
33095
+ if (this.firstAudibleCtxStart < 0) {
33096
+ this.firstAudibleCtxStart = ctxStart;
33097
+ this.mediaTimeOfAnchor = ptsSec;
33098
+ this.ctxTimeAtAnchor = ctxStart;
33099
+ if (isDebug2()) {
33100
+ 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)}`);
33101
+ }
33102
+ }
33103
+ const endMediaTime = ptsSec + durationSec / this._rate;
33104
+ if (endMediaTime > this.mediaTimeOfNext) {
33105
+ this.mediaTimeOfNext = endMediaTime;
33106
+ }
33107
+ } else {
33108
+ ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
33109
+ console.warn(`[TRACE-AUD] LEGACY (no PTS) sched dur=${durationSec.toFixed(4)} ctxStart=${ctxStart.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)}`);
33110
+ if (ctxStart < this.ctx.currentTime) {
33111
+ 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)}`);
33112
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
33113
+ this.mediaTimeOfAnchor = this.mediaTimeOfNext;
33114
+ ctxStart = this.ctx.currentTime;
33115
+ }
33116
+ this.mediaTimeOfNext += durationSec;
33117
+ }
32974
33118
  const buffer = this.ctx.createBuffer(channels, frameCount, sampleRate);
32975
33119
  for (let ch = 0; ch < channels; ch++) {
32976
33120
  const channelData = buffer.getChannelData(ch);
@@ -32982,14 +33126,7 @@ var AudioOutput = class {
32982
33126
  node3.buffer = buffer;
32983
33127
  node3.connect(this.gain);
32984
33128
  if (this._rate !== 1) node3.playbackRate.value = this._rate;
32985
- let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
32986
- if (ctxStart < this.ctx.currentTime) {
32987
- this.ctxTimeAtAnchor = this.ctx.currentTime;
32988
- this.mediaTimeOfAnchor = this.mediaTimeOfNext;
32989
- ctxStart = this.ctx.currentTime;
32990
- }
32991
33129
  node3.start(ctxStart);
32992
- this.mediaTimeOfNext += frameCount / sampleRate;
32993
33130
  this.framesScheduled++;
32994
33131
  }
32995
33132
  // ── Lifecycle ─────────────────────────────────────────────────────────
@@ -33014,12 +33151,15 @@ var AudioOutput = class {
33014
33151
  } catch {
33015
33152
  }
33016
33153
  if (this.state === "paused") {
33154
+ if (isDebug2()) {
33155
+ 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}`);
33156
+ }
33017
33157
  this.ctxTimeAtAnchor = this.ctx.currentTime;
33018
33158
  this.state = "playing";
33019
33159
  const drain2 = this.pendingQueue;
33020
33160
  this.pendingQueue = [];
33021
33161
  for (const c of drain2) {
33022
- this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
33162
+ this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
33023
33163
  }
33024
33164
  return;
33025
33165
  }
@@ -33027,10 +33167,13 @@ var AudioOutput = class {
33027
33167
  this.ctxTimeAtAnchor = this.ctx.currentTime + STARTUP_DELAY;
33028
33168
  this.mediaTimeOfNext = this.mediaTimeOfAnchor;
33029
33169
  this.state = "playing";
33170
+ if (isDebug2()) {
33171
+ 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}`);
33172
+ }
33030
33173
  const drain = this.pendingQueue;
33031
33174
  this.pendingQueue = [];
33032
33175
  for (const c of drain) {
33033
- this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
33176
+ this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
33034
33177
  }
33035
33178
  }
33036
33179
  /** Pause playback. Suspends the audio context. */
@@ -33056,6 +33199,9 @@ var AudioOutput = class {
33056
33199
  * supplying new samples) and then call `start()` to resume playback.
33057
33200
  */
33058
33201
  async reset(newMediaTime) {
33202
+ if (isDebug2()) {
33203
+ 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}`);
33204
+ }
33059
33205
  if (this.noAudio) {
33060
33206
  this.pendingQueue = [];
33061
33207
  this.mediaTimeOfAnchor = newMediaTime;
@@ -33074,6 +33220,7 @@ var AudioOutput = class {
33074
33220
  this.mediaTimeOfAnchor = newMediaTime;
33075
33221
  this.mediaTimeOfNext = newMediaTime;
33076
33222
  this.ctxTimeAtAnchor = this.ctx.currentTime;
33223
+ this.firstAudibleCtxStart = -1;
33077
33224
  this.state = "idle";
33078
33225
  if (this.ctx.state === "running") {
33079
33226
  await this.ctx.suspend();
@@ -33257,28 +33404,6 @@ function asUint8(x) {
33257
33404
  const ta = x;
33258
33405
  return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
33259
33406
  }
33260
- function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
33261
- const lo = frame.pts ?? 0;
33262
- const hi = frame.ptshi ?? 0;
33263
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
33264
- if (isInvalid) {
33265
- const us2 = nextUs();
33266
- frame.pts = us2;
33267
- frame.ptshi = 0;
33268
- return;
33269
- }
33270
- const tb = fallbackTimeBase ?? [1, 1e6];
33271
- const pts64 = hi * 4294967296 + lo;
33272
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
33273
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
33274
- frame.pts = us;
33275
- frame.ptshi = us < 0 ? -1 : 0;
33276
- return;
33277
- }
33278
- const fallback = nextUs();
33279
- frame.pts = fallback;
33280
- frame.ptshi = 0;
33281
- }
33282
33407
 
33283
33408
  // src/strategies/hybrid/decoder.ts
33284
33409
  async function startHybridDecoder(opts) {
@@ -33426,7 +33551,6 @@ async function startHybridDecoder(opts) {
33426
33551
  let videoChunksFed = 0;
33427
33552
  let bufferedUntilSec = 0;
33428
33553
  let syntheticVideoUs = 0;
33429
- let syntheticAudioUs = 0;
33430
33554
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
33431
33555
  const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
33432
33556
  const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
@@ -33458,7 +33582,13 @@ async function startHybridDecoder(opts) {
33458
33582
  }
33459
33583
  }
33460
33584
  if (audioDec && audioPackets && audioPackets.length > 0) {
33461
- await decodeAudioBatch(audioPackets, myToken);
33585
+ await decodeAudioBatch(
33586
+ audioPackets,
33587
+ myToken,
33588
+ /*flush*/
33589
+ false,
33590
+ audioTimeBase
33591
+ );
33462
33592
  }
33463
33593
  if (myToken !== pumpToken || destroyed) return;
33464
33594
  await new Promise((r) => setTimeout(r, 0));
@@ -33505,8 +33635,11 @@ async function startHybridDecoder(opts) {
33505
33635
  }
33506
33636
  }
33507
33637
  }
33508
- async function decodeAudioBatch(pkts, myToken, flush = false) {
33638
+ async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
33509
33639
  if (!audioDec || destroyed || myToken !== pumpToken) return;
33640
+ const pktPtsSec = pkts.map(
33641
+ (p) => tb ? packetPtsSec(p, tb) : null
33642
+ );
33510
33643
  const AUDIO_SUB_BATCH = 4;
33511
33644
  let allFrames = [];
33512
33645
  for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
@@ -33544,22 +33677,13 @@ async function startHybridDecoder(opts) {
33544
33677
  }
33545
33678
  if (myToken !== pumpToken || destroyed) return;
33546
33679
  const frames = allFrames;
33547
- for (const f of frames) {
33680
+ for (let i = 0; i < frames.length; i++) {
33548
33681
  if (myToken !== pumpToken || destroyed) return;
33549
- sanitizeFrameTimestamp(
33550
- f,
33551
- () => {
33552
- const ts = syntheticAudioUs;
33553
- const samples2 = f.nb_samples ?? 1024;
33554
- const sampleRate = f.sample_rate ?? 44100;
33555
- syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
33556
- return ts;
33557
- },
33558
- audioTimeBase
33559
- );
33682
+ const f = frames[i];
33560
33683
  const samples = libavFrameToInterleavedFloat32(f);
33561
33684
  if (samples) {
33562
- opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
33685
+ const pts = pktPtsSec[i] ?? null;
33686
+ opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
33563
33687
  audioFramesDecoded++;
33564
33688
  }
33565
33689
  }
@@ -33669,7 +33793,6 @@ async function startHybridDecoder(opts) {
33669
33793
  }
33670
33794
  await flushBSF();
33671
33795
  syntheticVideoUs = Math.round(timeSec * 1e6);
33672
- syntheticAudioUs = Math.round(timeSec * 1e6);
33673
33796
  pumpRunning = pumpLoop(newToken).catch(
33674
33797
  (err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
33675
33798
  );
@@ -33708,7 +33831,6 @@ async function startHybridDecoder(opts) {
33708
33831
  }
33709
33832
  await flushBSF();
33710
33833
  syntheticVideoUs = Math.round(timeSec * 1e6);
33711
- syntheticAudioUs = Math.round(timeSec * 1e6);
33712
33834
  pumpRunning = pumpLoop(newToken).catch(
33713
33835
  (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
33714
33836
  );
@@ -33963,6 +34085,9 @@ async function createHybridSession(ctx, target, transport) {
33963
34085
  // src/strategies/fallback/decoder.ts
33964
34086
  init_libav_loader();
33965
34087
  init_debug();
34088
+ function isDebug3() {
34089
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
34090
+ }
33966
34091
  async function startDecoder(opts) {
33967
34092
  const variant = "avbridge";
33968
34093
  const libav = await loadLibav(variant);
@@ -34094,8 +34219,15 @@ async function startDecoder(opts) {
34094
34219
  let watchdogSlowSinceMs = 0;
34095
34220
  let watchdogSlowWarned = false;
34096
34221
  let watchdogOverflowWarned = false;
34097
- let syntheticVideoUs = 0;
34098
- let syntheticAudioUs = 0;
34222
+ let lastContentUs = -1;
34223
+ let firstValidPtsLoggedSinceSeek = false;
34224
+ let seenFirstAudioPacketSinceSeek = false;
34225
+ let seekTargetSec = 0;
34226
+ let diagPktsLoggedSinceSeek = 0;
34227
+ let diagFramesLoggedSinceSeek = 0;
34228
+ let diagFrameKeysDumped = false;
34229
+ const DIAG_MAX_PKTS = 100;
34230
+ const DIAG_MAX_FRAMES = 300;
34099
34231
  let videoDecodeMsTotal = 0;
34100
34232
  let audioDecodeMsTotal = 0;
34101
34233
  let videoDecodeBatches = 0;
@@ -34134,6 +34266,18 @@ async function startDecoder(opts) {
34134
34266
  for (const pkt of videoPackets) {
34135
34267
  const sec = packetPtsSec(pkt, videoTimeBase);
34136
34268
  if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
34269
+ if (isDebug3() && diagPktsLoggedSinceSeek < DIAG_MAX_PKTS) {
34270
+ const rawHi = pkt.ptshi ?? 0;
34271
+ const rawLo = pkt.pts ?? 0;
34272
+ const isInvalidPts = rawHi === -2147483648 && rawLo === 0;
34273
+ const rawPts64 = isInvalidPts ? null : rawHi * 4294967296 + rawLo;
34274
+ const rawSec = rawPts64 != null && videoTimeBase ? rawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
34275
+ const pktKeys = diagPktsLoggedSinceSeek === 0 ? `[keys: ${Object.keys(pkt).join(",")}]` : "";
34276
+ console.log(
34277
+ `[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
34278
+ );
34279
+ diagPktsLoggedSinceSeek++;
34280
+ }
34137
34281
  }
34138
34282
  }
34139
34283
  if (audioPackets && audioTimeBase) {
@@ -34141,9 +34285,25 @@ async function startDecoder(opts) {
34141
34285
  const sec = packetPtsSec(pkt, audioTimeBase);
34142
34286
  if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
34143
34287
  }
34288
+ if (!seenFirstAudioPacketSinceSeek && audioPackets.length > 0) {
34289
+ const firstSec = packetPtsSec(audioPackets[0], audioTimeBase);
34290
+ if (firstSec != null && Number.isFinite(firstSec)) {
34291
+ seenFirstAudioPacketSinceSeek = true;
34292
+ dbg.info(
34293
+ "av-anchor",
34294
+ `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)`
34295
+ );
34296
+ }
34297
+ }
34144
34298
  }
34145
34299
  if (audioDec && audioPackets && audioPackets.length > 0) {
34146
- await decodeAudioBatch(audioPackets, myToken);
34300
+ await decodeAudioBatch(
34301
+ audioPackets,
34302
+ myToken,
34303
+ /*flush*/
34304
+ false,
34305
+ audioTimeBase
34306
+ );
34147
34307
  }
34148
34308
  if (myToken !== pumpToken || destroyed) return;
34149
34309
  if (videoDec && videoPackets && videoPackets.length > 0) {
@@ -34187,7 +34347,7 @@ async function startDecoder(opts) {
34187
34347
  {
34188
34348
  const _throttleStart = performance.now();
34189
34349
  let _throttled = false;
34190
- while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
34350
+ while (!destroyed && myToken === pumpToken && opts.audio.bufferAhead() > 2) {
34191
34351
  _throttled = true;
34192
34352
  await new Promise((r) => setTimeout(r, 50));
34193
34353
  }
@@ -34242,18 +34402,83 @@ async function startDecoder(opts) {
34242
34402
  if (myToken !== pumpToken || destroyed) return;
34243
34403
  for (const f of frames) {
34244
34404
  if (myToken !== pumpToken || destroyed) return;
34245
- sanitizeFrameTimestamp(
34246
- f,
34247
- () => {
34248
- const base = lastEmittedPtsUs >= 0 ? lastEmittedPtsUs + videoFrameStepUs : syntheticVideoUs;
34249
- syntheticVideoUs = base + videoFrameStepUs;
34250
- return base;
34251
- },
34252
- videoTimeBase
34253
- );
34254
- const _fPts = (f.ptshi ?? 0) * 4294967296 + (f.pts ?? 0);
34405
+ const _diagShouldLog = isDebug3() && diagFramesLoggedSinceSeek < DIAG_MAX_FRAMES;
34406
+ const _diagRawHi = f.ptshi ?? 0;
34407
+ const _diagRawLo = f.pts ?? 0;
34408
+ const _diagInvalid = _diagRawHi === -2147483648 && _diagRawLo === 0;
34409
+ const _diagRawPts64 = _diagInvalid ? null : _diagRawHi * 4294967296 + _diagRawLo;
34410
+ const _diagRawSec = _diagRawPts64 != null && videoTimeBase ? _diagRawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
34411
+ if (_diagShouldLog && !diagFrameKeysDumped) {
34412
+ diagFrameKeysDumped = true;
34413
+ const allKeys = Object.keys(f);
34414
+ const fieldDump = {};
34415
+ for (const k of allKeys) {
34416
+ const v = f[k];
34417
+ if (k === "data") continue;
34418
+ if (typeof v === "object" && v !== null && "length" in v) continue;
34419
+ fieldDump[k] = v;
34420
+ }
34421
+ console.log(`[DIAG-FRAME] FIRST FRAME post-seek \u2014 all keys: ${allKeys.join(",")}`);
34422
+ console.log(`[DIAG-FRAME] FIRST FRAME field dump:`, fieldDump);
34423
+ }
34424
+ let rawUs = null;
34425
+ if (!_diagInvalid && _diagRawPts64 != null) {
34426
+ const tb = videoTimeBase ?? [1, 1e6];
34427
+ const us = Math.round(_diagRawPts64 * 1e6 * tb[0] / tb[1]);
34428
+ if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
34429
+ rawUs = us;
34430
+ }
34431
+ }
34432
+ const _diagLog = (decision, finalPtsUs, sanFallback) => {
34433
+ if (!_diagShouldLog) return;
34434
+ const ptsSrc = sanFallback ? `SYNTHETIC(${_diagInvalid ? "NOPTS" : "invalid-range"})` : "LIBAV";
34435
+ console.log(
34436
+ `[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}`
34437
+ );
34438
+ diagFramesLoggedSinceSeek++;
34439
+ };
34440
+ let _diagSanFallbackFired = false;
34441
+ const seekTargetUs = Math.round(seekTargetSec * 1e6);
34442
+ if (lastContentUs < 0) {
34443
+ if (rawUs == null) {
34444
+ const isColdStartKeyframe = seekTargetSec === 0 && f.key_frame === 1;
34445
+ if (isColdStartKeyframe) {
34446
+ lastContentUs = 0;
34447
+ _diagSanFallbackFired = true;
34448
+ } else {
34449
+ _diagLog("PRE-ANCHOR-DROP", 0, true);
34450
+ continue;
34451
+ }
34452
+ } else {
34453
+ lastContentUs = rawUs;
34454
+ if (!firstValidPtsLoggedSinceSeek) {
34455
+ firstValidPtsLoggedSinceSeek = true;
34456
+ if (isDebug3()) {
34457
+ console.log(
34458
+ `[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)`
34459
+ );
34460
+ }
34461
+ if (rawUs >= seekTargetUs) {
34462
+ console.warn(
34463
+ `[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.`
34464
+ );
34465
+ }
34466
+ }
34467
+ }
34468
+ } else {
34469
+ if (rawUs != null) {
34470
+ lastContentUs = rawUs;
34471
+ } else {
34472
+ lastContentUs += videoFrameStepUs;
34473
+ _diagSanFallbackFired = true;
34474
+ }
34475
+ }
34476
+ f.pts = lastContentUs;
34477
+ f.ptshi = lastContentUs < 0 ? -1 : 0;
34478
+ const _fPts = lastContentUs;
34255
34479
  if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
34256
34480
  if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
34481
+ _diagLog("REGRESSED-DROP", _fPts, _diagSanFallbackFired);
34257
34482
  ptsRegressions++;
34258
34483
  const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
34259
34484
  if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
@@ -34265,19 +34490,34 @@ async function startDecoder(opts) {
34265
34490
  continue;
34266
34491
  }
34267
34492
  lastEmittedPtsUs = _fPts;
34493
+ const targetUs = Math.round(seekTargetSec * 1e6);
34494
+ if (_fPts < targetUs - videoFrameStepUs) {
34495
+ _diagLog("PRE-TARGET-DROP", _fPts, _diagSanFallbackFired);
34496
+ continue;
34497
+ }
34268
34498
  try {
34269
34499
  const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
34270
- opts.renderer.enqueue(vf);
34500
+ if (opts.renderer.queueDepth() >= opts.renderer.queueHighWater) {
34501
+ vf.close();
34502
+ _diagLog("OVERFLOW-DROP", _fPts, _diagSanFallbackFired);
34503
+ } else {
34504
+ opts.renderer.enqueue(vf);
34505
+ _diagLog("ENQUEUED", _fPts, _diagSanFallbackFired);
34506
+ }
34271
34507
  videoFramesDecoded++;
34272
34508
  } catch (err) {
34273
34509
  if (videoFramesDecoded === 0) {
34274
34510
  console.warn("[avbridge] laFrameToVideoFrame failed:", err);
34275
34511
  }
34512
+ _diagLog("BRIDGE-ERROR", _fPts, _diagSanFallbackFired);
34276
34513
  }
34277
34514
  }
34278
34515
  }
34279
- async function decodeAudioBatch(pkts, myToken, flush = false) {
34516
+ async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
34280
34517
  if (!audioDec || destroyed || myToken !== pumpToken) return;
34518
+ const pktPtsSec = pkts.map(
34519
+ (p) => tb ? packetPtsSec(p, tb) : null
34520
+ );
34281
34521
  let frames;
34282
34522
  const _t0 = performance.now();
34283
34523
  try {
@@ -34295,22 +34535,17 @@ async function startDecoder(opts) {
34295
34535
  audioDecodeMsTotal += performance.now() - _t0;
34296
34536
  audioDecodeBatches++;
34297
34537
  if (myToken !== pumpToken || destroyed) return;
34298
- for (const f of frames) {
34538
+ for (let i = 0; i < frames.length; i++) {
34299
34539
  if (myToken !== pumpToken || destroyed) return;
34300
- sanitizeFrameTimestamp(
34301
- f,
34302
- () => {
34303
- const ts = syntheticAudioUs;
34304
- const samples2 = f.nb_samples ?? 1024;
34305
- const sampleRate = f.sample_rate ?? 44100;
34306
- syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
34307
- return ts;
34308
- },
34309
- audioTimeBase
34310
- );
34540
+ const f = frames[i];
34311
34541
  const samples = libavFrameToInterleavedFloat32(f);
34312
34542
  if (samples) {
34313
- opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
34543
+ const pts = pktPtsSec[i] ?? null;
34544
+ if (isDebug3()) {
34545
+ const dur = samples.data.length / samples.channels / samples.sampleRate;
34546
+ 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}`);
34547
+ }
34548
+ opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
34314
34549
  audioFramesDecoded++;
34315
34550
  }
34316
34551
  }
@@ -34413,14 +34648,17 @@ async function startDecoder(opts) {
34413
34648
  } catch {
34414
34649
  }
34415
34650
  await flushBSF();
34416
- syntheticVideoUs = Math.round(timeSec * 1e6);
34417
- syntheticAudioUs = Math.round(timeSec * 1e6);
34651
+ lastContentUs = -1;
34418
34652
  lastEmittedPtsUs = -1;
34653
+ firstValidPtsLoggedSinceSeek = false;
34419
34654
  pumpRunning = pumpLoop(newToken).catch(
34420
34655
  (err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
34421
34656
  );
34422
34657
  },
34423
34658
  async seek(timeSec) {
34659
+ if (isDebug3()) {
34660
+ console.log(`[SEEK] target=${timeSec.toFixed(3)}s (${(timeSec * 1e3).toFixed(0)}ms) wall=${performance.now().toFixed(0)}`);
34661
+ }
34424
34662
  const newToken = ++pumpToken;
34425
34663
  if (pumpRunning) {
34426
34664
  try {
@@ -34451,9 +34689,14 @@ async function startDecoder(opts) {
34451
34689
  } catch {
34452
34690
  }
34453
34691
  await flushBSF();
34454
- syntheticVideoUs = Math.round(timeSec * 1e6);
34455
- syntheticAudioUs = Math.round(timeSec * 1e6);
34692
+ lastContentUs = -1;
34456
34693
  lastEmittedPtsUs = -1;
34694
+ firstValidPtsLoggedSinceSeek = false;
34695
+ seenFirstAudioPacketSinceSeek = false;
34696
+ seekTargetSec = timeSec;
34697
+ diagPktsLoggedSinceSeek = 0;
34698
+ diagFramesLoggedSinceSeek = 0;
34699
+ diagFrameKeysDumped = false;
34457
34700
  pumpRunning = pumpLoop(newToken).catch(
34458
34701
  (err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
34459
34702
  );