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.
@@ -1072,6 +1072,10 @@ async function createRemuxPipeline(ctx, video) {
1072
1072
  console.error("[avbridge] remux pipeline reseek failed:", err);
1073
1073
  });
1074
1074
  },
1075
+ setAutoPlay(autoPlay) {
1076
+ pendingAutoPlay = autoPlay;
1077
+ if (sink) sink.setPlayOnSeek(autoPlay);
1078
+ },
1075
1079
  async destroy() {
1076
1080
  destroyed = true;
1077
1081
  pumpToken++;
@@ -1112,7 +1116,11 @@ async function createRemuxSession(context, video) {
1112
1116
  await pipeline.start(video.currentTime || 0, true);
1113
1117
  return;
1114
1118
  }
1115
- await video.play();
1119
+ pipeline.setAutoPlay(true);
1120
+ try {
1121
+ await video.play();
1122
+ } catch {
1123
+ }
1116
1124
  },
1117
1125
  pause() {
1118
1126
  wantPlay = false;
@@ -1160,7 +1168,7 @@ var VideoRenderer = class {
1160
1168
  this.resolveFirstFrame = resolve;
1161
1169
  });
1162
1170
  this.canvas = document.createElement("canvas");
1163
- this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
1171
+ this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
1164
1172
  const parent = target.parentElement ?? target.parentNode;
1165
1173
  if (parent && parent instanceof HTMLElement) {
1166
1174
  if (getComputedStyle(parent).position === "static") {
@@ -1402,9 +1410,13 @@ var AudioOutput = class {
1402
1410
  const node = this.ctx.createBufferSource();
1403
1411
  node.buffer = buffer;
1404
1412
  node.connect(this.gain);
1405
- const ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
1406
- const safeStart = Math.max(ctxStart, this.ctx.currentTime);
1407
- node.start(safeStart);
1413
+ let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
1414
+ if (ctxStart < this.ctx.currentTime) {
1415
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
1416
+ this.mediaTimeOfAnchor = this.mediaTimeOfNext;
1417
+ ctxStart = this.ctx.currentTime;
1418
+ }
1419
+ node.start(ctxStart);
1408
1420
  this.mediaTimeOfNext += frameCount / sampleRate;
1409
1421
  this.framesScheduled++;
1410
1422
  }
@@ -1972,6 +1984,10 @@ async function createHybridSession(ctx, target) {
1972
1984
  void doSeek(v);
1973
1985
  }
1974
1986
  });
1987
+ Object.defineProperty(target, "paused", {
1988
+ configurable: true,
1989
+ get: () => !audio.isPlaying()
1990
+ });
1975
1991
  if (ctx.duration && Number.isFinite(ctx.duration)) {
1976
1992
  Object.defineProperty(target, "duration", {
1977
1993
  configurable: true,
@@ -2036,6 +2052,7 @@ async function createHybridSession(ctx, target) {
2036
2052
  try {
2037
2053
  delete target.currentTime;
2038
2054
  delete target.duration;
2055
+ delete target.paused;
2039
2056
  } catch {
2040
2057
  }
2041
2058
  },
@@ -2115,7 +2132,8 @@ async function startDecoder(opts) {
2115
2132
  let audioFramesDecoded = 0;
2116
2133
  let watchdogFirstFrameMs = 0;
2117
2134
  let watchdogSlowSinceMs = 0;
2118
- let watchdogWarned = false;
2135
+ let watchdogSlowWarned = false;
2136
+ let watchdogOverflowWarned = false;
2119
2137
  let syntheticVideoUs = 0;
2120
2138
  let syntheticAudioUs = 0;
2121
2139
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
@@ -2127,7 +2145,7 @@ async function startDecoder(opts) {
2127
2145
  let packets;
2128
2146
  try {
2129
2147
  [readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
2130
- limit: 64 * 1024
2148
+ limit: 16 * 1024
2131
2149
  });
2132
2150
  } catch (err) {
2133
2151
  console.error("[avbridge] ff_read_frame_multi failed:", err);
@@ -2136,26 +2154,26 @@ async function startDecoder(opts) {
2136
2154
  if (myToken !== pumpToken || destroyed) return;
2137
2155
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
2138
2156
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
2139
- if (videoDec && videoPackets && videoPackets.length > 0) {
2140
- await decodeVideoBatch(videoPackets, myToken);
2141
- }
2142
- if (myToken !== pumpToken || destroyed) return;
2143
2157
  if (audioDec && audioPackets && audioPackets.length > 0) {
2144
2158
  await decodeAudioBatch(audioPackets, myToken);
2145
2159
  }
2160
+ if (myToken !== pumpToken || destroyed) return;
2161
+ if (videoDec && videoPackets && videoPackets.length > 0) {
2162
+ await decodeVideoBatch(videoPackets, myToken);
2163
+ }
2146
2164
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
2147
2165
  if (videoFramesDecoded > 0) {
2148
2166
  if (watchdogFirstFrameMs === 0) {
2149
2167
  watchdogFirstFrameMs = performance.now();
2150
2168
  }
2151
2169
  const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
2152
- if (elapsedSinceFirst > 1 && !watchdogWarned) {
2170
+ if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
2153
2171
  const expectedFrames = elapsedSinceFirst * videoFps;
2154
2172
  const ratio = videoFramesDecoded / expectedFrames;
2155
2173
  if (ratio < 0.6) {
2156
2174
  if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
2157
2175
  if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
2158
- watchdogWarned = true;
2176
+ watchdogSlowWarned = true;
2159
2177
  console.warn(
2160
2178
  "[avbridge:decode-rate]",
2161
2179
  `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.`
@@ -2165,6 +2183,17 @@ async function startDecoder(opts) {
2165
2183
  watchdogSlowSinceMs = 0;
2166
2184
  }
2167
2185
  }
2186
+ if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
2187
+ const rendererStats = opts.renderer.stats();
2188
+ const overflow = rendererStats.framesDroppedOverflow ?? 0;
2189
+ if (overflow / videoFramesDecoded > 0.1) {
2190
+ watchdogOverflowWarned = true;
2191
+ console.warn(
2192
+ "[avbridge:overflow-drop]",
2193
+ `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.`
2194
+ );
2195
+ }
2196
+ }
2168
2197
  }
2169
2198
  while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
2170
2199
  await new Promise((r) => setTimeout(r, 50));
@@ -2521,6 +2550,10 @@ async function createFallbackSession(ctx, target) {
2521
2550
  void doSeek(v);
2522
2551
  }
2523
2552
  });
2553
+ Object.defineProperty(target, "paused", {
2554
+ configurable: true,
2555
+ get: () => !audio.isPlaying()
2556
+ });
2524
2557
  if (ctx.duration && Number.isFinite(ctx.duration)) {
2525
2558
  Object.defineProperty(target, "duration", {
2526
2559
  configurable: true,
@@ -2529,25 +2562,35 @@ async function createFallbackSession(ctx, target) {
2529
2562
  }
2530
2563
  async function waitForBuffer() {
2531
2564
  const start = performance.now();
2565
+ let firstFrameAtMs = 0;
2532
2566
  chunkG4APZMCP_cjs.dbg.info(
2533
2567
  "cold-start",
2534
- `gate entry: need audio >= ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
2568
+ `gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
2535
2569
  );
2536
2570
  while (true) {
2537
2571
  const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
2538
2572
  const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
2539
2573
  const hasFrames = renderer.hasFrames();
2574
+ const nowMs = performance.now();
2575
+ if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
2540
2576
  if (audioReady && hasFrames) {
2541
2577
  chunkG4APZMCP_cjs.dbg.info(
2542
2578
  "cold-start",
2543
- `gate satisfied in ${(performance.now() - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
2579
+ `gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
2544
2580
  );
2545
2581
  return;
2546
2582
  }
2547
- if ((performance.now() - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
2583
+ if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
2584
+ chunkG4APZMCP_cjs.dbg.info(
2585
+ "cold-start",
2586
+ `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)`
2587
+ );
2588
+ return;
2589
+ }
2590
+ if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
2548
2591
  chunkG4APZMCP_cjs.dbg.diag(
2549
2592
  "cold-start",
2550
- `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.`
2593
+ `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.`
2551
2594
  );
2552
2595
  return;
2553
2596
  }
@@ -2596,6 +2639,7 @@ async function createFallbackSession(ctx, target) {
2596
2639
  try {
2597
2640
  delete target.currentTime;
2598
2641
  delete target.duration;
2642
+ delete target.paused;
2599
2643
  } catch {
2600
2644
  }
2601
2645
  },
@@ -2738,6 +2782,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2738
2782
  lastProgressTime = 0;
2739
2783
  lastProgressPosition = -1;
2740
2784
  errorListener = null;
2785
+ // Bound so we can removeEventListener in destroy(); without this the
2786
+ // listener outlives the player and accumulates on elements that swap
2787
+ // source (e.g. <avbridge-video>).
2788
+ endedListener = null;
2741
2789
  // Serializes escalation / setStrategy calls
2742
2790
  switchingPromise = Promise.resolve();
2743
2791
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
@@ -2823,7 +2871,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
2823
2871
  subtitle: ctx.subtitleTracks
2824
2872
  });
2825
2873
  this.startTimeupdateLoop();
2826
- this.options.target.addEventListener("ended", () => this.emitter.emit("ended", void 0));
2874
+ this.endedListener = () => this.emitter.emit("ended", void 0);
2875
+ this.options.target.addEventListener("ended", this.endedListener);
2827
2876
  this.emitter.emitSticky("ready", void 0);
2828
2877
  const bootstrapElapsed = performance.now() - bootstrapStart;
2829
2878
  chunkG4APZMCP_cjs.dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
@@ -3109,6 +3158,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
3109
3158
  this.timeupdateInterval = null;
3110
3159
  }
3111
3160
  this.clearSupervisor();
3161
+ if (this.endedListener) {
3162
+ this.options.target.removeEventListener("ended", this.endedListener);
3163
+ this.endedListener = null;
3164
+ }
3112
3165
  if (this.session) {
3113
3166
  await this.session.destroy();
3114
3167
  this.session = null;
@@ -3123,11 +3176,13 @@ async function createPlayer(options) {
3123
3176
  function buildInitialDecision(initial, ctx) {
3124
3177
  const natural = classifyContext(ctx);
3125
3178
  const cls = strategyToClass(initial, natural);
3179
+ const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
3180
+ const fallbackChain = inherited.filter((s) => s !== initial);
3126
3181
  return {
3127
3182
  class: cls,
3128
3183
  strategy: initial,
3129
3184
  reason: `initial strategy "${initial}" requested via options.initialStrategy`,
3130
- fallbackChain: natural.fallbackChain ?? defaultFallbackChain(initial)
3185
+ fallbackChain
3131
3186
  };
3132
3187
  }
3133
3188
  function strategyToClass(strategy, natural) {
@@ -3164,5 +3219,5 @@ exports.classifyContext = classifyContext;
3164
3219
  exports.createPlayer = createPlayer;
3165
3220
  exports.probe = probe;
3166
3221
  exports.srtToVtt = srtToVtt;
3167
- //# sourceMappingURL=chunk-OE66B34H.cjs.map
3168
- //# sourceMappingURL=chunk-OE66B34H.cjs.map
3222
+ //# sourceMappingURL=chunk-UF2N5L63.cjs.map
3223
+ //# sourceMappingURL=chunk-UF2N5L63.cjs.map