avbridge 2.8.3 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +74 -1
  3. package/dist/{avi-F6WZJK5T.cjs → avi-2ILLBNPQ.cjs} +8 -2
  4. package/dist/avi-2ILLBNPQ.cjs.map +1 -0
  5. package/dist/{avi-W6L3BTWU.cjs → avi-B5CQYB7L.cjs} +8 -2
  6. package/dist/avi-B5CQYB7L.cjs.map +1 -0
  7. package/dist/{avi-2JPBSHGA.js → avi-JXU4GQL2.js} +8 -2
  8. package/dist/avi-JXU4GQL2.js.map +1 -0
  9. package/dist/{avi-NJXAXUXK.js → avi-RWWPN2PR.js} +8 -2
  10. package/dist/avi-RWWPN2PR.js.map +1 -0
  11. package/dist/{chunk-X2K3GIWE.js → chunk-2NSOOMXW.js} +14 -3
  12. package/dist/chunk-2NSOOMXW.js.map +1 -0
  13. package/dist/{chunk-ZCUXHW55.cjs → chunk-BYGZN4Z5.cjs} +5 -5
  14. package/dist/{chunk-ZCUXHW55.cjs.map → chunk-BYGZN4Z5.cjs.map} +1 -1
  15. package/dist/{chunk-SMH6IOP2.js → chunk-CL6UEUQF.js} +4 -4
  16. package/dist/{chunk-SMH6IOP2.js.map → chunk-CL6UEUQF.js.map} +1 -1
  17. package/dist/{chunk-IUSFLVLJ.cjs → chunk-EY6DZEDT.cjs} +149 -24
  18. package/dist/chunk-EY6DZEDT.cjs.map +1 -0
  19. package/dist/{chunk-SR3MPV4D.js → chunk-GYIJU44C.js} +5 -5
  20. package/dist/{chunk-SR3MPV4D.js.map → chunk-GYIJU44C.js.map} +1 -1
  21. package/dist/{chunk-CPZ7PXAM.cjs → chunk-L7A3ECI2.cjs} +14 -2
  22. package/dist/chunk-L7A3ECI2.cjs.map +1 -0
  23. package/dist/{chunk-Q2VUO52Z.cjs → chunk-OTFS7DC4.cjs} +12 -12
  24. package/dist/{chunk-Q2VUO52Z.cjs.map → chunk-OTFS7DC4.cjs.map} +1 -1
  25. package/dist/{chunk-JSQOBUQB.js → chunk-SN4WZE24.js} +139 -14
  26. package/dist/chunk-SN4WZE24.js.map +1 -0
  27. package/dist/element-browser.js +164 -16
  28. package/dist/element-browser.js.map +1 -1
  29. package/dist/element.cjs +16 -10
  30. package/dist/element.cjs.map +1 -1
  31. package/dist/element.d.cts +11 -6
  32. package/dist/element.d.ts +11 -6
  33. package/dist/element.js +15 -9
  34. package/dist/element.js.map +1 -1
  35. package/dist/index.cjs +20 -20
  36. package/dist/index.d.cts +2 -2
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.js +8 -8
  39. package/dist/libav-demux-3N5Y3VQA.cjs +31 -0
  40. package/dist/{libav-demux-H2GS46GH.cjs.map → libav-demux-3N5Y3VQA.cjs.map} +1 -1
  41. package/dist/libav-demux-JXD4OTLM.js +6 -0
  42. package/dist/{libav-demux-OWZ4T2YW.js.map → libav-demux-JXD4OTLM.js.map} +1 -1
  43. package/dist/{player-DXEKOky8.d.cts → player-DEcidWk6.d.cts} +8 -1
  44. package/dist/{player-DXEKOky8.d.ts → player-DEcidWk6.d.ts} +8 -1
  45. package/dist/player.cjs +266 -36
  46. package/dist/player.cjs.map +1 -1
  47. package/dist/player.d.cts +37 -11
  48. package/dist/player.d.ts +37 -11
  49. package/dist/player.js +266 -36
  50. package/dist/player.js.map +1 -1
  51. package/dist/{remux-WBYIZBBX.js → remux-56V7LDAD.js} +5 -5
  52. package/dist/{remux-WBYIZBBX.js.map → remux-56V7LDAD.js.map} +1 -1
  53. package/dist/{remux-OBSMIENG.cjs → remux-KUS5GIL6.cjs} +10 -10
  54. package/dist/{remux-OBSMIENG.cjs.map → remux-KUS5GIL6.cjs.map} +1 -1
  55. package/package.json +1 -1
  56. package/src/classify/rules.ts +11 -0
  57. package/src/element/avbridge-player.ts +22 -11
  58. package/src/element/avbridge-video.ts +22 -6
  59. package/src/element/player-styles.ts +68 -3
  60. package/src/player.ts +96 -8
  61. package/src/probe/avi.ts +2 -0
  62. package/src/strategies/fallback/decoder.ts +30 -0
  63. package/src/strategies/fallback/index.ts +30 -0
  64. package/src/strategies/hybrid/decoder.ts +35 -0
  65. package/src/strategies/hybrid/index.ts +17 -0
  66. package/src/strategies/remux/index.ts +8 -0
  67. package/src/types.ts +6 -0
  68. package/src/util/libav-demux.ts +26 -0
  69. package/dist/avi-2JPBSHGA.js.map +0 -1
  70. package/dist/avi-F6WZJK5T.cjs.map +0 -1
  71. package/dist/avi-NJXAXUXK.js.map +0 -1
  72. package/dist/avi-W6L3BTWU.cjs.map +0 -1
  73. package/dist/chunk-CPZ7PXAM.cjs.map +0 -1
  74. package/dist/chunk-IUSFLVLJ.cjs.map +0 -1
  75. package/dist/chunk-JSQOBUQB.js.map +0 -1
  76. package/dist/chunk-X2K3GIWE.js.map +0 -1
  77. package/dist/libav-demux-H2GS46GH.cjs +0 -27
  78. package/dist/libav-demux-OWZ4T2YW.js +0 -6
@@ -1,7 +1,7 @@
1
1
  import { SubtitleResourceBag, discoverSidecars, attachSubtitleTracks, SubtitleOverlay } from './chunk-5KVLE6YI.js';
2
- import { probe, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, avbridgeAudioToMediabunny } from './chunk-SR3MPV4D.js';
2
+ import { probe, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, avbridgeAudioToMediabunny } from './chunk-GYIJU44C.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 { sanitizePacketTimestamp, sanitizeFrameTimestamp, libavFrameToInterleavedFloat32 } from './chunk-X2K3GIWE.js';
4
+ import { packetPtsSec, sanitizePacketTimestamp, sanitizeFrameTimestamp, libavFrameToInterleavedFloat32 } from './chunk-2NSOOMXW.js';
5
5
  import { dbg, loadLibav } from './chunk-5DMTJVIU.js';
6
6
  import { pickLibavVariant } from './chunk-5YAWWKA3.js';
7
7
 
@@ -90,7 +90,13 @@ var FALLBACK_VIDEO_CODECS = /* @__PURE__ */ new Set([
90
90
  "rv40",
91
91
  "mpeg2",
92
92
  "mpeg1",
93
- "theora"
93
+ "theora",
94
+ "dv",
95
+ "hq_hqa",
96
+ "rawvideo",
97
+ "qtrle",
98
+ "png",
99
+ "vp6f"
94
100
  ]);
95
101
  var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
96
102
  "wmav2",
@@ -231,10 +237,12 @@ function classifyContext(ctx) {
231
237
  reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime and WebCodecs is unavailable \u2014 falling back to WASM decode`
232
238
  };
233
239
  }
240
+ const fallbackChain = webCodecsAvailable() ? ["hybrid", "fallback"] : ["fallback"];
234
241
  return {
235
242
  class: "REMUX_CANDIDATE",
236
243
  strategy: "remux",
237
- reason: `${ctx.container} container with native-supported codecs \u2014 remux to fragmented MP4 for reliable playback`
244
+ reason: `${ctx.container} container with native-supported codecs \u2014 remux to fragmented MP4 for reliable playback`,
245
+ fallbackChain
238
246
  };
239
247
  }
240
248
  if (webCodecsAvailable()) {
@@ -972,6 +980,12 @@ async function createRemuxSession(context, video) {
972
980
  }
973
981
  const wasPlaying = !video.paused;
974
982
  await pipeline.seek(time, wasPlaying || wantPlay);
983
+ queueMicrotask(() => {
984
+ try {
985
+ video.dispatchEvent(new Event("seeked"));
986
+ } catch {
987
+ }
988
+ });
975
989
  },
976
990
  async setAudioTrack(id) {
977
991
  if (!context.audioTracks.some((t) => t.id === id)) {
@@ -1699,6 +1713,7 @@ async function startHybridDecoder(opts) {
1699
1713
  let videoFramesDecoded = 0;
1700
1714
  let audioFramesDecoded = 0;
1701
1715
  let videoChunksFed = 0;
1716
+ let bufferedUntilSec = 0;
1702
1717
  let syntheticVideoUs = 0;
1703
1718
  let syntheticAudioUs = 0;
1704
1719
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
@@ -1719,6 +1734,18 @@ async function startHybridDecoder(opts) {
1719
1734
  if (myToken !== pumpToken || destroyed) return;
1720
1735
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
1721
1736
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
1737
+ if (videoPackets && videoTimeBase) {
1738
+ for (const pkt of videoPackets) {
1739
+ const sec = packetPtsSec(pkt, videoTimeBase);
1740
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
1741
+ }
1742
+ }
1743
+ if (audioPackets && audioTimeBase) {
1744
+ for (const pkt of audioPackets) {
1745
+ const sec = packetPtsSec(pkt, audioTimeBase);
1746
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
1747
+ }
1748
+ }
1722
1749
  if (audioDec && audioPackets && audioPackets.length > 0) {
1723
1750
  await decodeAudioBatch(audioPackets, myToken);
1724
1751
  }
@@ -1975,6 +2002,9 @@ async function startHybridDecoder(opts) {
1975
2002
  (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
1976
2003
  );
1977
2004
  },
2005
+ bufferedUntilSec() {
2006
+ return bufferedUntilSec;
2007
+ },
1978
2008
  stats() {
1979
2009
  return {
1980
2010
  decoderType: "webcodecs-hybrid",
@@ -2102,6 +2132,13 @@ async function createHybridSession(ctx, target, transport) {
2102
2132
  configurable: true,
2103
2133
  get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
2104
2134
  });
2135
+ Object.defineProperty(target, "buffered", {
2136
+ configurable: true,
2137
+ get: () => {
2138
+ const end = handles.bufferedUntilSec();
2139
+ return makeTimeRanges(end > 0 ? [[0, end]] : []);
2140
+ }
2141
+ });
2105
2142
  async function waitForBuffer() {
2106
2143
  const start = performance.now();
2107
2144
  while (true) {
@@ -2115,6 +2152,7 @@ async function createHybridSession(ctx, target, transport) {
2115
2152
  }
2116
2153
  async function doSeek(timeSec) {
2117
2154
  const wasPlaying = audio.isPlaying();
2155
+ target.dispatchEvent(new Event("seeking"));
2118
2156
  await audio.pause().catch(() => {
2119
2157
  });
2120
2158
  await handles.seek(timeSec).catch(
@@ -2126,7 +2164,14 @@ async function createHybridSession(ctx, target, transport) {
2126
2164
  await waitForBuffer();
2127
2165
  await audio.start();
2128
2166
  }
2167
+ target.dispatchEvent(new Event("seeked"));
2129
2168
  }
2169
+ queueMicrotask(() => {
2170
+ try {
2171
+ target.dispatchEvent(new Event("loadedmetadata"));
2172
+ } catch {
2173
+ }
2174
+ });
2130
2175
  let fatalErrorHandler = null;
2131
2176
  handles.onFatalError((reason) => fatalErrorHandler?.(reason));
2132
2177
  return {
@@ -2311,6 +2356,7 @@ async function startDecoder(opts) {
2311
2356
  let pumpRunning = null;
2312
2357
  let packetsRead = 0;
2313
2358
  let videoFramesDecoded = 0;
2359
+ let bufferedUntilSec = 0;
2314
2360
  let audioFramesDecoded = 0;
2315
2361
  let watchdogFirstFrameMs = 0;
2316
2362
  let watchdogSlowSinceMs = 0;
@@ -2336,6 +2382,18 @@ async function startDecoder(opts) {
2336
2382
  if (myToken !== pumpToken || destroyed) return;
2337
2383
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
2338
2384
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
2385
+ if (videoPackets && videoTimeBase) {
2386
+ for (const pkt of videoPackets) {
2387
+ const sec = packetPtsSec(pkt, videoTimeBase);
2388
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2389
+ }
2390
+ }
2391
+ if (audioPackets && audioTimeBase) {
2392
+ for (const pkt of audioPackets) {
2393
+ const sec = packetPtsSec(pkt, audioTimeBase);
2394
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2395
+ }
2396
+ }
2339
2397
  if (audioDec && audioPackets && audioPackets.length > 0) {
2340
2398
  await decodeAudioBatch(audioPackets, myToken);
2341
2399
  }
@@ -2617,6 +2675,9 @@ async function startDecoder(opts) {
2617
2675
  (err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
2618
2676
  );
2619
2677
  },
2678
+ bufferedUntilSec() {
2679
+ return bufferedUntilSec;
2680
+ },
2620
2681
  stats() {
2621
2682
  return {
2622
2683
  decoderType: "libav-wasm",
@@ -2716,6 +2777,13 @@ async function createFallbackSession(ctx, target, transport) {
2716
2777
  configurable: true,
2717
2778
  get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
2718
2779
  });
2780
+ Object.defineProperty(target, "buffered", {
2781
+ configurable: true,
2782
+ get: () => {
2783
+ const end = handles.bufferedUntilSec();
2784
+ return makeTimeRanges(end > 0 ? [[0, end]] : []);
2785
+ }
2786
+ });
2719
2787
  async function waitForBuffer() {
2720
2788
  const start = performance.now();
2721
2789
  let firstFrameAtMs = 0;
@@ -2755,6 +2823,7 @@ async function createFallbackSession(ctx, target, transport) {
2755
2823
  }
2756
2824
  async function doSeek(timeSec) {
2757
2825
  const wasPlaying = audio.isPlaying();
2826
+ target.dispatchEvent(new Event("seeking"));
2758
2827
  await audio.pause().catch(() => {
2759
2828
  });
2760
2829
  await handles.seek(timeSec).catch(
@@ -2766,7 +2835,14 @@ async function createFallbackSession(ctx, target, transport) {
2766
2835
  await waitForBuffer();
2767
2836
  await audio.start();
2768
2837
  }
2838
+ target.dispatchEvent(new Event("seeked"));
2769
2839
  }
2840
+ queueMicrotask(() => {
2841
+ try {
2842
+ target.dispatchEvent(new Event("loadedmetadata"));
2843
+ } catch {
2844
+ }
2845
+ });
2770
2846
  return {
2771
2847
  strategy: "fallback",
2772
2848
  async play() {
@@ -2858,6 +2934,29 @@ function registerBuiltins(registry) {
2858
2934
  }
2859
2935
 
2860
2936
  // src/player.ts
2937
+ function readDecodedFrameCount(target) {
2938
+ if (typeof HTMLVideoElement === "undefined" || !(target instanceof HTMLVideoElement)) return 0;
2939
+ const vq = target.getVideoPlaybackQuality;
2940
+ if (typeof vq === "function") {
2941
+ try {
2942
+ return vq.call(target).totalVideoFrames;
2943
+ } catch {
2944
+ }
2945
+ }
2946
+ const legacy = target.webkitDecodedFrameCount;
2947
+ return typeof legacy === "number" ? legacy : 0;
2948
+ }
2949
+ function evaluateDecodeHealth(input) {
2950
+ const timeThreshold = input.timeStallThresholdMs ?? 5e3;
2951
+ const frameThreshold = input.frameStallThresholdMs ?? 3e3;
2952
+ if (!input.timeAdvanced && input.now - input.lastProgressTime > timeThreshold) {
2953
+ return { escalate: true, kind: "time-stall" };
2954
+ }
2955
+ if (input.hasVideoTrack && input.timeAdvanced && !input.framesAdvanced && input.now - input.lastFrameProgressTime > frameThreshold) {
2956
+ return { escalate: true, kind: "silent-video" };
2957
+ }
2958
+ return { escalate: false };
2959
+ }
2861
2960
  var UnifiedPlayer = class _UnifiedPlayer {
2862
2961
  /**
2863
2962
  * @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
@@ -2883,6 +2982,13 @@ var UnifiedPlayer = class _UnifiedPlayer {
2883
2982
  stallTimer = null;
2884
2983
  lastProgressTime = 0;
2885
2984
  lastProgressPosition = -1;
2985
+ /** Last observed `HTMLVideoElement.getVideoPlaybackQuality().totalVideoFrames`
2986
+ * (or `webkitDecodedFrameCount` fallback). Used by the silent-video
2987
+ * watchdog — catches cases where `currentTime` advances (audio plays)
2988
+ * but the decoder produces no frames, e.g. Firefox claiming `hev1.*`
2989
+ * via MSE when the decoder actually can't decode HEVC. */
2990
+ lastVideoFrameCount = 0;
2991
+ lastVideoFrameProgressTime = 0;
2886
2992
  errorListener = null;
2887
2993
  // Bound so we can removeEventListener in destroy(); without this the
2888
2994
  // listener outlives the player and accumulates on elements that swap
@@ -3127,22 +3233,41 @@ var UnifiedPlayer = class _UnifiedPlayer {
3127
3233
  if (strategy === "native" || strategy === "remux") {
3128
3234
  this.lastProgressPosition = this.options.target.currentTime;
3129
3235
  this.lastProgressTime = performance.now();
3236
+ this.lastVideoFrameCount = readDecodedFrameCount(this.options.target);
3237
+ this.lastVideoFrameProgressTime = performance.now();
3238
+ const hasVideoTrack = (this.mediaContext?.videoTracks.length ?? 0) > 0;
3130
3239
  this.stallTimer = setInterval(() => {
3131
3240
  const t = this.options.target;
3241
+ const now = performance.now();
3132
3242
  if (t.paused || t.ended || t.readyState < 2) {
3133
3243
  this.lastProgressPosition = t.currentTime;
3134
- this.lastProgressTime = performance.now();
3244
+ this.lastProgressTime = now;
3245
+ this.lastVideoFrameCount = readDecodedFrameCount(t);
3246
+ this.lastVideoFrameProgressTime = now;
3135
3247
  return;
3136
3248
  }
3137
- if (t.currentTime !== this.lastProgressPosition) {
3249
+ const timeAdvanced = t.currentTime !== this.lastProgressPosition;
3250
+ const frames = readDecodedFrameCount(t);
3251
+ const framesAdvanced = frames > this.lastVideoFrameCount;
3252
+ const health = evaluateDecodeHealth({
3253
+ hasVideoTrack,
3254
+ timeAdvanced,
3255
+ framesAdvanced,
3256
+ now,
3257
+ lastProgressTime: this.lastProgressTime,
3258
+ lastFrameProgressTime: this.lastVideoFrameProgressTime
3259
+ });
3260
+ if (timeAdvanced) {
3138
3261
  this.lastProgressPosition = t.currentTime;
3139
- this.lastProgressTime = performance.now();
3140
- return;
3262
+ this.lastProgressTime = now;
3141
3263
  }
3142
- if (performance.now() - this.lastProgressTime > 5e3) {
3143
- void this.escalate(
3144
- `${strategy} strategy stalled for 5s at ${t.currentTime.toFixed(1)}s`
3145
- );
3264
+ if (framesAdvanced) {
3265
+ this.lastVideoFrameCount = frames;
3266
+ this.lastVideoFrameProgressTime = now;
3267
+ }
3268
+ if (health.escalate) {
3269
+ const reason = health.kind === "time-stall" ? `${strategy} strategy stalled for 5s at ${t.currentTime.toFixed(1)}s` : `${strategy} strategy: audio is advancing but the video decoder has produced no new frames for 3s \u2014 likely a silent codec failure`;
3270
+ void this.escalate(reason);
3146
3271
  }
3147
3272
  }, 1e3);
3148
3273
  const onError = () => {
@@ -3373,5 +3498,5 @@ function defaultFallbackChain(strategy) {
3373
3498
  }
3374
3499
 
3375
3500
  export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext, createPlayer };
3376
- //# sourceMappingURL=chunk-JSQOBUQB.js.map
3377
- //# sourceMappingURL=chunk-JSQOBUQB.js.map
3501
+ //# sourceMappingURL=chunk-SN4WZE24.js.map
3502
+ //# sourceMappingURL=chunk-SN4WZE24.js.map