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
@@ -30030,6 +30030,12 @@ function ffmpegToAvbridgeVideo(name) {
30030
30030
  return "rv30";
30031
30031
  case "rv40":
30032
30032
  return "rv40";
30033
+ case "dvvideo":
30034
+ return "dv";
30035
+ // DV / DVCPRO (camcorder, MiniDV)
30036
+ case "hq_hqa":
30037
+ return "hq_hqa";
30038
+ // Canopus HQ / HQA (Grass Valley)
30033
30039
  default:
30034
30040
  return name;
30035
30041
  }
@@ -31564,7 +31570,13 @@ var FALLBACK_VIDEO_CODECS = /* @__PURE__ */ new Set([
31564
31570
  "rv40",
31565
31571
  "mpeg2",
31566
31572
  "mpeg1",
31567
- "theora"
31573
+ "theora",
31574
+ "dv",
31575
+ "hq_hqa",
31576
+ "rawvideo",
31577
+ "qtrle",
31578
+ "png",
31579
+ "vp6f"
31568
31580
  ]);
31569
31581
  var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
31570
31582
  "wmav2",
@@ -31705,10 +31717,12 @@ function classifyContext(ctx) {
31705
31717
  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`
31706
31718
  };
31707
31719
  }
31720
+ const fallbackChain = webCodecsAvailable() ? ["hybrid", "fallback"] : ["fallback"];
31708
31721
  return {
31709
31722
  class: "REMUX_CANDIDATE",
31710
31723
  strategy: "remux",
31711
- reason: `${ctx.container} container with native-supported codecs \u2014 remux to fragmented MP4 for reliable playback`
31724
+ reason: `${ctx.container} container with native-supported codecs \u2014 remux to fragmented MP4 for reliable playback`,
31725
+ fallbackChain
31712
31726
  };
31713
31727
  }
31714
31728
  if (webCodecsAvailable()) {
@@ -32399,6 +32413,12 @@ async function createRemuxSession(context, video) {
32399
32413
  }
32400
32414
  const wasPlaying = !video.paused;
32401
32415
  await pipeline.seek(time, wasPlaying || wantPlay);
32416
+ queueMicrotask(() => {
32417
+ try {
32418
+ video.dispatchEvent(new Event("seeked"));
32419
+ } catch {
32420
+ }
32421
+ });
32402
32422
  },
32403
32423
  async setAudioTrack(id) {
32404
32424
  if (!context.audioTracks.some((t) => t.id === id)) {
@@ -33040,6 +33060,17 @@ function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
33040
33060
  pkt.time_base_num = 1;
33041
33061
  pkt.time_base_den = 1e6;
33042
33062
  }
33063
+ function packetPtsSec(pkt, timeBase) {
33064
+ const lo = pkt.pts ?? 0;
33065
+ const hi = pkt.ptshi ?? 0;
33066
+ const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
33067
+ if (isInvalid) return null;
33068
+ const tb = timeBase ?? [1, 1e6];
33069
+ if (!tb[0] || !tb[1]) return null;
33070
+ const pts64 = hi * 4294967296 + lo;
33071
+ const sec = pts64 * tb[0] / tb[1];
33072
+ return Number.isFinite(sec) ? sec : null;
33073
+ }
33043
33074
  var AV_SAMPLE_FMT_U8 = 0;
33044
33075
  var AV_SAMPLE_FMT_S16 = 1;
33045
33076
  var AV_SAMPLE_FMT_S32 = 2;
@@ -33300,6 +33331,7 @@ async function startHybridDecoder(opts) {
33300
33331
  let videoFramesDecoded = 0;
33301
33332
  let audioFramesDecoded = 0;
33302
33333
  let videoChunksFed = 0;
33334
+ let bufferedUntilSec = 0;
33303
33335
  let syntheticVideoUs = 0;
33304
33336
  let syntheticAudioUs = 0;
33305
33337
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
@@ -33320,6 +33352,18 @@ async function startHybridDecoder(opts) {
33320
33352
  if (myToken !== pumpToken || destroyed) return;
33321
33353
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
33322
33354
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
33355
+ if (videoPackets && videoTimeBase) {
33356
+ for (const pkt of videoPackets) {
33357
+ const sec = packetPtsSec(pkt, videoTimeBase);
33358
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
33359
+ }
33360
+ }
33361
+ if (audioPackets && audioTimeBase) {
33362
+ for (const pkt of audioPackets) {
33363
+ const sec = packetPtsSec(pkt, audioTimeBase);
33364
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
33365
+ }
33366
+ }
33323
33367
  if (audioDec && audioPackets && audioPackets.length > 0) {
33324
33368
  await decodeAudioBatch(audioPackets, myToken);
33325
33369
  }
@@ -33576,6 +33620,9 @@ async function startHybridDecoder(opts) {
33576
33620
  (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
33577
33621
  );
33578
33622
  },
33623
+ bufferedUntilSec() {
33624
+ return bufferedUntilSec;
33625
+ },
33579
33626
  stats() {
33580
33627
  return {
33581
33628
  decoderType: "webcodecs-hybrid",
@@ -33703,6 +33750,13 @@ async function createHybridSession(ctx, target, transport) {
33703
33750
  configurable: true,
33704
33751
  get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
33705
33752
  });
33753
+ Object.defineProperty(target, "buffered", {
33754
+ configurable: true,
33755
+ get: () => {
33756
+ const end = handles.bufferedUntilSec();
33757
+ return makeTimeRanges(end > 0 ? [[0, end]] : []);
33758
+ }
33759
+ });
33706
33760
  async function waitForBuffer() {
33707
33761
  const start = performance.now();
33708
33762
  while (true) {
@@ -33716,6 +33770,7 @@ async function createHybridSession(ctx, target, transport) {
33716
33770
  }
33717
33771
  async function doSeek(timeSec) {
33718
33772
  const wasPlaying = audio.isPlaying();
33773
+ target.dispatchEvent(new Event("seeking"));
33719
33774
  await audio.pause().catch(() => {
33720
33775
  });
33721
33776
  await handles.seek(timeSec).catch(
@@ -33727,7 +33782,14 @@ async function createHybridSession(ctx, target, transport) {
33727
33782
  await waitForBuffer();
33728
33783
  await audio.start();
33729
33784
  }
33785
+ target.dispatchEvent(new Event("seeked"));
33730
33786
  }
33787
+ queueMicrotask(() => {
33788
+ try {
33789
+ target.dispatchEvent(new Event("loadedmetadata"));
33790
+ } catch {
33791
+ }
33792
+ });
33731
33793
  let fatalErrorHandler = null;
33732
33794
  handles.onFatalError((reason) => fatalErrorHandler?.(reason));
33733
33795
  return {
@@ -33914,6 +33976,7 @@ async function startDecoder(opts) {
33914
33976
  let pumpRunning = null;
33915
33977
  let packetsRead = 0;
33916
33978
  let videoFramesDecoded = 0;
33979
+ let bufferedUntilSec = 0;
33917
33980
  let audioFramesDecoded = 0;
33918
33981
  let watchdogFirstFrameMs = 0;
33919
33982
  let watchdogSlowSinceMs = 0;
@@ -33939,6 +34002,18 @@ async function startDecoder(opts) {
33939
34002
  if (myToken !== pumpToken || destroyed) return;
33940
34003
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
33941
34004
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
34005
+ if (videoPackets && videoTimeBase) {
34006
+ for (const pkt of videoPackets) {
34007
+ const sec = packetPtsSec(pkt, videoTimeBase);
34008
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
34009
+ }
34010
+ }
34011
+ if (audioPackets && audioTimeBase) {
34012
+ for (const pkt of audioPackets) {
34013
+ const sec = packetPtsSec(pkt, audioTimeBase);
34014
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
34015
+ }
34016
+ }
33942
34017
  if (audioDec && audioPackets && audioPackets.length > 0) {
33943
34018
  await decodeAudioBatch(audioPackets, myToken);
33944
34019
  }
@@ -34220,6 +34295,9 @@ async function startDecoder(opts) {
34220
34295
  (err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
34221
34296
  );
34222
34297
  },
34298
+ bufferedUntilSec() {
34299
+ return bufferedUntilSec;
34300
+ },
34223
34301
  stats() {
34224
34302
  return {
34225
34303
  decoderType: "libav-wasm",
@@ -34320,6 +34398,13 @@ async function createFallbackSession(ctx, target, transport) {
34320
34398
  configurable: true,
34321
34399
  get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
34322
34400
  });
34401
+ Object.defineProperty(target, "buffered", {
34402
+ configurable: true,
34403
+ get: () => {
34404
+ const end = handles.bufferedUntilSec();
34405
+ return makeTimeRanges(end > 0 ? [[0, end]] : []);
34406
+ }
34407
+ });
34323
34408
  async function waitForBuffer() {
34324
34409
  const start = performance.now();
34325
34410
  let firstFrameAtMs = 0;
@@ -34359,6 +34444,7 @@ async function createFallbackSession(ctx, target, transport) {
34359
34444
  }
34360
34445
  async function doSeek(timeSec) {
34361
34446
  const wasPlaying = audio.isPlaying();
34447
+ target.dispatchEvent(new Event("seeking"));
34362
34448
  await audio.pause().catch(() => {
34363
34449
  });
34364
34450
  await handles.seek(timeSec).catch(
@@ -34370,7 +34456,14 @@ async function createFallbackSession(ctx, target, transport) {
34370
34456
  await waitForBuffer();
34371
34457
  await audio.start();
34372
34458
  }
34459
+ target.dispatchEvent(new Event("seeked"));
34373
34460
  }
34461
+ queueMicrotask(() => {
34462
+ try {
34463
+ target.dispatchEvent(new Event("loadedmetadata"));
34464
+ } catch {
34465
+ }
34466
+ });
34374
34467
  return {
34375
34468
  strategy: "fallback",
34376
34469
  async play() {
@@ -34465,6 +34558,29 @@ function registerBuiltins(registry) {
34465
34558
  init_subtitles2();
34466
34559
  init_debug();
34467
34560
  init_errors();
34561
+ function readDecodedFrameCount(target) {
34562
+ if (typeof HTMLVideoElement === "undefined" || !(target instanceof HTMLVideoElement)) return 0;
34563
+ const vq = target.getVideoPlaybackQuality;
34564
+ if (typeof vq === "function") {
34565
+ try {
34566
+ return vq.call(target).totalVideoFrames;
34567
+ } catch {
34568
+ }
34569
+ }
34570
+ const legacy = target.webkitDecodedFrameCount;
34571
+ return typeof legacy === "number" ? legacy : 0;
34572
+ }
34573
+ function evaluateDecodeHealth(input) {
34574
+ const timeThreshold = input.timeStallThresholdMs ?? 5e3;
34575
+ const frameThreshold = input.frameStallThresholdMs ?? 3e3;
34576
+ if (!input.timeAdvanced && input.now - input.lastProgressTime > timeThreshold) {
34577
+ return { escalate: true, kind: "time-stall" };
34578
+ }
34579
+ if (input.hasVideoTrack && input.timeAdvanced && !input.framesAdvanced && input.now - input.lastFrameProgressTime > frameThreshold) {
34580
+ return { escalate: true, kind: "silent-video" };
34581
+ }
34582
+ return { escalate: false };
34583
+ }
34468
34584
  var UnifiedPlayer = class _UnifiedPlayer {
34469
34585
  /**
34470
34586
  * @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
@@ -34490,6 +34606,13 @@ var UnifiedPlayer = class _UnifiedPlayer {
34490
34606
  stallTimer = null;
34491
34607
  lastProgressTime = 0;
34492
34608
  lastProgressPosition = -1;
34609
+ /** Last observed `HTMLVideoElement.getVideoPlaybackQuality().totalVideoFrames`
34610
+ * (or `webkitDecodedFrameCount` fallback). Used by the silent-video
34611
+ * watchdog — catches cases where `currentTime` advances (audio plays)
34612
+ * but the decoder produces no frames, e.g. Firefox claiming `hev1.*`
34613
+ * via MSE when the decoder actually can't decode HEVC. */
34614
+ lastVideoFrameCount = 0;
34615
+ lastVideoFrameProgressTime = 0;
34493
34616
  errorListener = null;
34494
34617
  // Bound so we can removeEventListener in destroy(); without this the
34495
34618
  // listener outlives the player and accumulates on elements that swap
@@ -34734,22 +34857,41 @@ var UnifiedPlayer = class _UnifiedPlayer {
34734
34857
  if (strategy === "native" || strategy === "remux") {
34735
34858
  this.lastProgressPosition = this.options.target.currentTime;
34736
34859
  this.lastProgressTime = performance.now();
34860
+ this.lastVideoFrameCount = readDecodedFrameCount(this.options.target);
34861
+ this.lastVideoFrameProgressTime = performance.now();
34862
+ const hasVideoTrack = (this.mediaContext?.videoTracks.length ?? 0) > 0;
34737
34863
  this.stallTimer = setInterval(() => {
34738
34864
  const t = this.options.target;
34865
+ const now = performance.now();
34739
34866
  if (t.paused || t.ended || t.readyState < 2) {
34740
34867
  this.lastProgressPosition = t.currentTime;
34741
- this.lastProgressTime = performance.now();
34868
+ this.lastProgressTime = now;
34869
+ this.lastVideoFrameCount = readDecodedFrameCount(t);
34870
+ this.lastVideoFrameProgressTime = now;
34742
34871
  return;
34743
34872
  }
34744
- if (t.currentTime !== this.lastProgressPosition) {
34873
+ const timeAdvanced = t.currentTime !== this.lastProgressPosition;
34874
+ const frames = readDecodedFrameCount(t);
34875
+ const framesAdvanced = frames > this.lastVideoFrameCount;
34876
+ const health = evaluateDecodeHealth({
34877
+ hasVideoTrack,
34878
+ timeAdvanced,
34879
+ framesAdvanced,
34880
+ now,
34881
+ lastProgressTime: this.lastProgressTime,
34882
+ lastFrameProgressTime: this.lastVideoFrameProgressTime
34883
+ });
34884
+ if (timeAdvanced) {
34745
34885
  this.lastProgressPosition = t.currentTime;
34746
- this.lastProgressTime = performance.now();
34747
- return;
34886
+ this.lastProgressTime = now;
34748
34887
  }
34749
- if (performance.now() - this.lastProgressTime > 5e3) {
34750
- void this.escalate(
34751
- `${strategy} strategy stalled for 5s at ${t.currentTime.toFixed(1)}s`
34752
- );
34888
+ if (framesAdvanced) {
34889
+ this.lastVideoFrameCount = frames;
34890
+ this.lastVideoFrameProgressTime = now;
34891
+ }
34892
+ if (health.escalate) {
34893
+ 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`;
34894
+ void this.escalate(reason);
34753
34895
  }
34754
34896
  }, 1e3);
34755
34897
  const onError = () => {
@@ -35404,9 +35546,10 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
35404
35546
  else this.removeAttribute("autoplay");
35405
35547
  }
35406
35548
  get muted() {
35407
- return this.hasAttribute("muted");
35549
+ return this._videoEl.muted;
35408
35550
  }
35409
35551
  set muted(value) {
35552
+ this._videoEl.muted = value;
35410
35553
  if (value) this.setAttribute("muted", "");
35411
35554
  else this.removeAttribute("muted");
35412
35555
  }
@@ -35471,11 +35614,16 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
35471
35614
  }
35472
35615
  /**
35473
35616
  * Buffered time ranges for the active source. Mirrors the standard
35474
- * `<video>.buffered` `TimeRanges` API. For the native and remux strategies
35475
- * this reflects the underlying SourceBuffer / progressive download state.
35476
- * For the hybrid and fallback (canvas-rendered) strategies it currently
35477
- * returns an empty TimeRanges; a future release will synthesize a coarse
35478
- * range from the decoder's read position.
35617
+ * `<video>.buffered` `TimeRanges` API.
35618
+ *
35619
+ * - **Native / remux:** pass-through to the real `<video>.buffered`
35620
+ * (reflects the browser's SourceBuffer / progressive-download state).
35621
+ * - **Hybrid / fallback:** a single `[0, frontier]` range synthesized
35622
+ * from the demuxer's read progress — "how far libav has ever pumped
35623
+ * packets through." Monotonic; does not shrink on seek. This is an
35624
+ * approximation, not MSE-fidelity: decoded frames on canvas strategies
35625
+ * are consumed in flight, so we can't report per-range availability
35626
+ * the way MSE does. Enough for a seek-bar buffered indicator.
35479
35627
  */
35480
35628
  get buffered() {
35481
35629
  return this._videoEl.buffered;