avbridge 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31978,6 +31978,10 @@ async function createRemuxPipeline(ctx, video) {
31978
31978
  console.error("[avbridge] remux pipeline reseek failed:", err);
31979
31979
  });
31980
31980
  },
31981
+ setAutoPlay(autoPlay) {
31982
+ pendingAutoPlay = autoPlay;
31983
+ if (sink) sink.setPlayOnSeek(autoPlay);
31984
+ },
31981
31985
  async destroy() {
31982
31986
  destroyed = true;
31983
31987
  pumpToken++;
@@ -32018,7 +32022,11 @@ async function createRemuxSession(context, video) {
32018
32022
  await pipeline.start(video.currentTime || 0, true);
32019
32023
  return;
32020
32024
  }
32021
- await video.play();
32025
+ pipeline.setAutoPlay(true);
32026
+ try {
32027
+ await video.play();
32028
+ } catch {
32029
+ }
32022
32030
  },
32023
32031
  pause() {
32024
32032
  wantPlay = false;
@@ -32066,7 +32074,7 @@ var VideoRenderer = class {
32066
32074
  this.resolveFirstFrame = resolve;
32067
32075
  });
32068
32076
  this.canvas = document.createElement("canvas");
32069
- this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
32077
+ this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
32070
32078
  const parent = target.parentElement ?? target.parentNode;
32071
32079
  if (parent && parent instanceof HTMLElement) {
32072
32080
  if (getComputedStyle(parent).position === "static") {
@@ -32308,9 +32316,13 @@ var AudioOutput = class {
32308
32316
  const node3 = this.ctx.createBufferSource();
32309
32317
  node3.buffer = buffer;
32310
32318
  node3.connect(this.gain);
32311
- const ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
32312
- const safeStart = Math.max(ctxStart, this.ctx.currentTime);
32313
- node3.start(safeStart);
32319
+ let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
32320
+ if (ctxStart < this.ctx.currentTime) {
32321
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
32322
+ this.mediaTimeOfAnchor = this.mediaTimeOfNext;
32323
+ ctxStart = this.ctx.currentTime;
32324
+ }
32325
+ node3.start(ctxStart);
32314
32326
  this.mediaTimeOfNext += frameCount / sampleRate;
32315
32327
  this.framesScheduled++;
32316
32328
  }
@@ -32905,6 +32917,10 @@ async function createHybridSession(ctx, target) {
32905
32917
  void doSeek(v);
32906
32918
  }
32907
32919
  });
32920
+ Object.defineProperty(target, "paused", {
32921
+ configurable: true,
32922
+ get: () => !audio.isPlaying()
32923
+ });
32908
32924
  if (ctx.duration && Number.isFinite(ctx.duration)) {
32909
32925
  Object.defineProperty(target, "duration", {
32910
32926
  configurable: true,
@@ -32969,6 +32985,7 @@ async function createHybridSession(ctx, target) {
32969
32985
  try {
32970
32986
  delete target.currentTime;
32971
32987
  delete target.duration;
32988
+ delete target.paused;
32972
32989
  } catch {
32973
32990
  }
32974
32991
  },
@@ -33049,7 +33066,8 @@ async function startDecoder(opts) {
33049
33066
  let audioFramesDecoded = 0;
33050
33067
  let watchdogFirstFrameMs = 0;
33051
33068
  let watchdogSlowSinceMs = 0;
33052
- let watchdogWarned = false;
33069
+ let watchdogSlowWarned = false;
33070
+ let watchdogOverflowWarned = false;
33053
33071
  let syntheticVideoUs = 0;
33054
33072
  let syntheticAudioUs = 0;
33055
33073
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
@@ -33061,7 +33079,7 @@ async function startDecoder(opts) {
33061
33079
  let packets;
33062
33080
  try {
33063
33081
  [readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
33064
- limit: 64 * 1024
33082
+ limit: 16 * 1024
33065
33083
  });
33066
33084
  } catch (err) {
33067
33085
  console.error("[avbridge] ff_read_frame_multi failed:", err);
@@ -33070,26 +33088,26 @@ async function startDecoder(opts) {
33070
33088
  if (myToken !== pumpToken || destroyed) return;
33071
33089
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
33072
33090
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
33073
- if (videoDec && videoPackets && videoPackets.length > 0) {
33074
- await decodeVideoBatch(videoPackets, myToken);
33075
- }
33076
- if (myToken !== pumpToken || destroyed) return;
33077
33091
  if (audioDec && audioPackets && audioPackets.length > 0) {
33078
33092
  await decodeAudioBatch(audioPackets, myToken);
33079
33093
  }
33094
+ if (myToken !== pumpToken || destroyed) return;
33095
+ if (videoDec && videoPackets && videoPackets.length > 0) {
33096
+ await decodeVideoBatch(videoPackets, myToken);
33097
+ }
33080
33098
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
33081
33099
  if (videoFramesDecoded > 0) {
33082
33100
  if (watchdogFirstFrameMs === 0) {
33083
33101
  watchdogFirstFrameMs = performance.now();
33084
33102
  }
33085
33103
  const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
33086
- if (elapsedSinceFirst > 1 && !watchdogWarned) {
33104
+ if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
33087
33105
  const expectedFrames = elapsedSinceFirst * videoFps;
33088
33106
  const ratio = videoFramesDecoded / expectedFrames;
33089
33107
  if (ratio < 0.6) {
33090
33108
  if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
33091
33109
  if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
33092
- watchdogWarned = true;
33110
+ watchdogSlowWarned = true;
33093
33111
  console.warn(
33094
33112
  "[avbridge:decode-rate]",
33095
33113
  `decoder is running slower than realtime: ${videoFramesDecoded} frames in ${elapsedSinceFirst.toFixed(1)}s (${(videoFramesDecoded / elapsedSinceFirst).toFixed(1)} fps vs ${videoFps} fps source \u2014 ${(ratio * 100).toFixed(0)}% of realtime). Playback will stutter. Typical causes: software decode of a codec with no WebCodecs support (rv40, mpeg4 @ 720p+, wmv3), or a resolution too large for single-threaded WASM to keep up with.`
@@ -33099,6 +33117,17 @@ async function startDecoder(opts) {
33099
33117
  watchdogSlowSinceMs = 0;
33100
33118
  }
33101
33119
  }
33120
+ if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
33121
+ const rendererStats = opts.renderer.stats();
33122
+ const overflow = rendererStats.framesDroppedOverflow ?? 0;
33123
+ if (overflow / videoFramesDecoded > 0.1) {
33124
+ watchdogOverflowWarned = true;
33125
+ console.warn(
33126
+ "[avbridge:overflow-drop]",
33127
+ `renderer is dropping ${overflow}/${videoFramesDecoded} frames (${(overflow / videoFramesDecoded * 100).toFixed(0)}%) because the decoder is producing bursts faster than the canvas can drain. Symptom: choppy playback despite decoder keeping up on average. Fix would be smaller read batches in the pump loop or a lower queueHighWater cap \u2014 see src/strategies/fallback/decoder.ts.`
33128
+ );
33129
+ }
33130
+ }
33102
33131
  }
33103
33132
  while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
33104
33133
  await new Promise((r) => setTimeout(r, 50));
@@ -33456,6 +33485,10 @@ async function createFallbackSession(ctx, target) {
33456
33485
  void doSeek(v);
33457
33486
  }
33458
33487
  });
33488
+ Object.defineProperty(target, "paused", {
33489
+ configurable: true,
33490
+ get: () => !audio.isPlaying()
33491
+ });
33459
33492
  if (ctx.duration && Number.isFinite(ctx.duration)) {
33460
33493
  Object.defineProperty(target, "duration", {
33461
33494
  configurable: true,
@@ -33464,25 +33497,35 @@ async function createFallbackSession(ctx, target) {
33464
33497
  }
33465
33498
  async function waitForBuffer() {
33466
33499
  const start = performance.now();
33500
+ let firstFrameAtMs = 0;
33467
33501
  dbg.info(
33468
33502
  "cold-start",
33469
- `gate entry: need audio >= ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
33503
+ `gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
33470
33504
  );
33471
33505
  while (true) {
33472
33506
  const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
33473
33507
  const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
33474
33508
  const hasFrames = renderer.hasFrames();
33509
+ const nowMs = performance.now();
33510
+ if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
33475
33511
  if (audioReady && hasFrames) {
33476
33512
  dbg.info(
33477
33513
  "cold-start",
33478
- `gate satisfied in ${(performance.now() - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
33514
+ `gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
33479
33515
  );
33480
33516
  return;
33481
33517
  }
33482
- if ((performance.now() - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
33518
+ if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
33519
+ dbg.info(
33520
+ "cold-start",
33521
+ `gate released on video-only grace at ${(nowMs - start).toFixed(0)}ms (frames=${renderer.queueDepth()}, audio=${(audioAhead * 1e3).toFixed(0)}ms \u2014 demuxer hasn't delivered audio packets yet, starting anyway and letting the audio scheduler catch up at its media-time anchor)`
33522
+ );
33523
+ return;
33524
+ }
33525
+ if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
33483
33526
  dbg.diag(
33484
33527
  "cold-start",
33485
- `gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651). Software decoder is producing output slower than realtime \u2014 playback will stutter. Check getDiagnostics().runtime for the decode rate.`
33528
+ `gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651). Decoder produced nothing in ${READY_TIMEOUT_SECONDS2}s \u2014 either a corrupt source, a missing codec, or WASM is catastrophically slow on this file. Check getDiagnostics().runtime for decode counters.`
33486
33529
  );
33487
33530
  return;
33488
33531
  }
@@ -33531,6 +33574,7 @@ async function createFallbackSession(ctx, target) {
33531
33574
  try {
33532
33575
  delete target.currentTime;
33533
33576
  delete target.duration;
33577
+ delete target.paused;
33534
33578
  } catch {
33535
33579
  }
33536
33580
  },
@@ -33704,6 +33748,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
33704
33748
  lastProgressTime = 0;
33705
33749
  lastProgressPosition = -1;
33706
33750
  errorListener = null;
33751
+ // Bound so we can removeEventListener in destroy(); without this the
33752
+ // listener outlives the player and accumulates on elements that swap
33753
+ // source (e.g. <avbridge-video>).
33754
+ endedListener = null;
33707
33755
  // Serializes escalation / setStrategy calls
33708
33756
  switchingPromise = Promise.resolve();
33709
33757
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
@@ -33789,7 +33837,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
33789
33837
  subtitle: ctx.subtitleTracks
33790
33838
  });
33791
33839
  this.startTimeupdateLoop();
33792
- this.options.target.addEventListener("ended", () => this.emitter.emit("ended", void 0));
33840
+ this.endedListener = () => this.emitter.emit("ended", void 0);
33841
+ this.options.target.addEventListener("ended", this.endedListener);
33793
33842
  this.emitter.emitSticky("ready", void 0);
33794
33843
  const bootstrapElapsed = performance.now() - bootstrapStart;
33795
33844
  dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
@@ -34075,6 +34124,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
34075
34124
  this.timeupdateInterval = null;
34076
34125
  }
34077
34126
  this.clearSupervisor();
34127
+ if (this.endedListener) {
34128
+ this.options.target.removeEventListener("ended", this.endedListener);
34129
+ this.endedListener = null;
34130
+ }
34078
34131
  if (this.session) {
34079
34132
  await this.session.destroy();
34080
34133
  this.session = null;
@@ -34089,11 +34142,13 @@ async function createPlayer(options) {
34089
34142
  function buildInitialDecision(initial, ctx) {
34090
34143
  const natural = classifyContext(ctx);
34091
34144
  const cls = strategyToClass(initial, natural);
34145
+ const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
34146
+ const fallbackChain = inherited.filter((s) => s !== initial);
34092
34147
  return {
34093
34148
  class: cls,
34094
34149
  strategy: initial,
34095
34150
  reason: `initial strategy "${initial}" requested via options.initialStrategy`,
34096
- fallbackChain: natural.fallbackChain ?? defaultFallbackChain(initial)
34151
+ fallbackChain
34097
34152
  };
34098
34153
  }
34099
34154
  function strategyToClass(strategy, natural) {