avbridge 2.8.4 → 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 (77) hide show
  1. package/CHANGELOG.md +133 -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-YX4AGLNF.cjs → chunk-EY6DZEDT.cjs} +89 -15
  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-KBWQRGHS.js → chunk-SN4WZE24.js} +79 -5
  26. package/dist/chunk-SN4WZE24.js.map +1 -0
  27. package/dist/element-browser.js +104 -7
  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-BptSJPfn.d.cts → player-DEcidWk6.d.cts} +1 -1
  44. package/dist/{player-BptSJPfn.d.ts → player-DEcidWk6.d.ts} +1 -1
  45. package/dist/player.cjs +187 -23
  46. package/dist/player.cjs.map +1 -1
  47. package/dist/player.d.cts +17 -11
  48. package/dist/player.d.ts +17 -11
  49. package/dist/player.js +187 -23
  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 +2 -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/probe/avi.ts +2 -0
  61. package/src/strategies/fallback/decoder.ts +30 -0
  62. package/src/strategies/fallback/index.ts +30 -0
  63. package/src/strategies/hybrid/decoder.ts +35 -0
  64. package/src/strategies/hybrid/index.ts +17 -0
  65. package/src/strategies/remux/index.ts +8 -0
  66. package/src/types.ts +6 -0
  67. package/src/util/libav-demux.ts +26 -0
  68. package/dist/avi-2JPBSHGA.js.map +0 -1
  69. package/dist/avi-F6WZJK5T.cjs.map +0 -1
  70. package/dist/avi-NJXAXUXK.js.map +0 -1
  71. package/dist/avi-W6L3BTWU.cjs.map +0 -1
  72. package/dist/chunk-CPZ7PXAM.cjs.map +0 -1
  73. package/dist/chunk-KBWQRGHS.js.map +0 -1
  74. package/dist/chunk-X2K3GIWE.js.map +0 -1
  75. package/dist/chunk-YX4AGLNF.cjs.map +0 -1
  76. package/dist/libav-demux-H2GS46GH.cjs +0 -27
  77. package/dist/libav-demux-OWZ4T2YW.js +0 -6
package/dist/player.cjs CHANGED
@@ -239,7 +239,7 @@ async function probe(source, transport) {
239
239
  const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
240
240
  if (hasUnknownCodec) {
241
241
  try {
242
- const { probeWithLibav } = await import('./avi-F6WZJK5T.cjs');
242
+ const { probeWithLibav } = await import('./avi-2ILLBNPQ.cjs');
243
243
  return await probeWithLibav(normalized, sniffed);
244
244
  } catch {
245
245
  return result;
@@ -252,7 +252,7 @@ async function probe(source, transport) {
252
252
  mediabunnyErr.message
253
253
  );
254
254
  try {
255
- const { probeWithLibav } = await import('./avi-F6WZJK5T.cjs');
255
+ const { probeWithLibav } = await import('./avi-2ILLBNPQ.cjs');
256
256
  return await probeWithLibav(normalized, sniffed);
257
257
  } catch (libavErr) {
258
258
  const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
@@ -266,7 +266,7 @@ async function probe(source, transport) {
266
266
  }
267
267
  }
268
268
  try {
269
- const { probeWithLibav } = await import('./avi-F6WZJK5T.cjs');
269
+ const { probeWithLibav } = await import('./avi-2ILLBNPQ.cjs');
270
270
  return await probeWithLibav(normalized, sniffed);
271
271
  } catch (err) {
272
272
  const inner = err instanceof Error ? err.message : String(err);
@@ -371,7 +371,13 @@ var FALLBACK_VIDEO_CODECS = /* @__PURE__ */ new Set([
371
371
  "rv40",
372
372
  "mpeg2",
373
373
  "mpeg1",
374
- "theora"
374
+ "theora",
375
+ "dv",
376
+ "hq_hqa",
377
+ "rawvideo",
378
+ "qtrle",
379
+ "png",
380
+ "vp6f"
375
381
  ]);
376
382
  var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
377
383
  "wmav2",
@@ -1207,6 +1213,12 @@ async function createRemuxSession(context, video) {
1207
1213
  }
1208
1214
  const wasPlaying = !video.paused;
1209
1215
  await pipeline.seek(time, wasPlaying || wantPlay);
1216
+ queueMicrotask(() => {
1217
+ try {
1218
+ video.dispatchEvent(new Event("seeked"));
1219
+ } catch {
1220
+ }
1221
+ });
1210
1222
  },
1211
1223
  async setAudioTrack(id) {
1212
1224
  if (!context.audioTracks.some((t) => t.id === id)) {
@@ -1843,6 +1855,17 @@ function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
1843
1855
  pkt.time_base_num = 1;
1844
1856
  pkt.time_base_den = 1e6;
1845
1857
  }
1858
+ function packetPtsSec(pkt, timeBase) {
1859
+ const lo = pkt.pts ?? 0;
1860
+ const hi = pkt.ptshi ?? 0;
1861
+ const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
1862
+ if (isInvalid) return null;
1863
+ const tb = timeBase ?? [1, 1e6];
1864
+ if (!tb[0] || !tb[1]) return null;
1865
+ const pts64 = hi * 4294967296 + lo;
1866
+ const sec = pts64 * tb[0] / tb[1];
1867
+ return Number.isFinite(sec) ? sec : null;
1868
+ }
1846
1869
  var AV_SAMPLE_FMT_U8 = 0;
1847
1870
  var AV_SAMPLE_FMT_S16 = 1;
1848
1871
  var AV_SAMPLE_FMT_S32 = 2;
@@ -2103,6 +2126,7 @@ async function startHybridDecoder(opts) {
2103
2126
  let videoFramesDecoded = 0;
2104
2127
  let audioFramesDecoded = 0;
2105
2128
  let videoChunksFed = 0;
2129
+ let bufferedUntilSec = 0;
2106
2130
  let syntheticVideoUs = 0;
2107
2131
  let syntheticAudioUs = 0;
2108
2132
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
@@ -2123,6 +2147,18 @@ async function startHybridDecoder(opts) {
2123
2147
  if (myToken !== pumpToken || destroyed) return;
2124
2148
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
2125
2149
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
2150
+ if (videoPackets && videoTimeBase) {
2151
+ for (const pkt of videoPackets) {
2152
+ const sec = packetPtsSec(pkt, videoTimeBase);
2153
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2154
+ }
2155
+ }
2156
+ if (audioPackets && audioTimeBase) {
2157
+ for (const pkt of audioPackets) {
2158
+ const sec = packetPtsSec(pkt, audioTimeBase);
2159
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2160
+ }
2161
+ }
2126
2162
  if (audioDec && audioPackets && audioPackets.length > 0) {
2127
2163
  await decodeAudioBatch(audioPackets, myToken);
2128
2164
  }
@@ -2379,6 +2415,9 @@ async function startHybridDecoder(opts) {
2379
2415
  (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
2380
2416
  );
2381
2417
  },
2418
+ bufferedUntilSec() {
2419
+ return bufferedUntilSec;
2420
+ },
2382
2421
  stats() {
2383
2422
  return {
2384
2423
  decoderType: "webcodecs-hybrid",
@@ -2506,6 +2545,13 @@ async function createHybridSession(ctx, target, transport) {
2506
2545
  configurable: true,
2507
2546
  get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
2508
2547
  });
2548
+ Object.defineProperty(target, "buffered", {
2549
+ configurable: true,
2550
+ get: () => {
2551
+ const end = handles.bufferedUntilSec();
2552
+ return makeTimeRanges(end > 0 ? [[0, end]] : []);
2553
+ }
2554
+ });
2509
2555
  async function waitForBuffer() {
2510
2556
  const start = performance.now();
2511
2557
  while (true) {
@@ -2519,6 +2565,7 @@ async function createHybridSession(ctx, target, transport) {
2519
2565
  }
2520
2566
  async function doSeek(timeSec) {
2521
2567
  const wasPlaying = audio.isPlaying();
2568
+ target.dispatchEvent(new Event("seeking"));
2522
2569
  await audio.pause().catch(() => {
2523
2570
  });
2524
2571
  await handles.seek(timeSec).catch(
@@ -2530,7 +2577,14 @@ async function createHybridSession(ctx, target, transport) {
2530
2577
  await waitForBuffer();
2531
2578
  await audio.start();
2532
2579
  }
2580
+ target.dispatchEvent(new Event("seeked"));
2533
2581
  }
2582
+ queueMicrotask(() => {
2583
+ try {
2584
+ target.dispatchEvent(new Event("loadedmetadata"));
2585
+ } catch {
2586
+ }
2587
+ });
2534
2588
  let fatalErrorHandler = null;
2535
2589
  handles.onFatalError((reason) => fatalErrorHandler?.(reason));
2536
2590
  return {
@@ -2715,6 +2769,7 @@ async function startDecoder(opts) {
2715
2769
  let pumpRunning = null;
2716
2770
  let packetsRead = 0;
2717
2771
  let videoFramesDecoded = 0;
2772
+ let bufferedUntilSec = 0;
2718
2773
  let audioFramesDecoded = 0;
2719
2774
  let watchdogFirstFrameMs = 0;
2720
2775
  let watchdogSlowSinceMs = 0;
@@ -2740,6 +2795,18 @@ async function startDecoder(opts) {
2740
2795
  if (myToken !== pumpToken || destroyed) return;
2741
2796
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
2742
2797
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
2798
+ if (videoPackets && videoTimeBase) {
2799
+ for (const pkt of videoPackets) {
2800
+ const sec = packetPtsSec(pkt, videoTimeBase);
2801
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2802
+ }
2803
+ }
2804
+ if (audioPackets && audioTimeBase) {
2805
+ for (const pkt of audioPackets) {
2806
+ const sec = packetPtsSec(pkt, audioTimeBase);
2807
+ if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
2808
+ }
2809
+ }
2743
2810
  if (audioDec && audioPackets && audioPackets.length > 0) {
2744
2811
  await decodeAudioBatch(audioPackets, myToken);
2745
2812
  }
@@ -3021,6 +3088,9 @@ async function startDecoder(opts) {
3021
3088
  (err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
3022
3089
  );
3023
3090
  },
3091
+ bufferedUntilSec() {
3092
+ return bufferedUntilSec;
3093
+ },
3024
3094
  stats() {
3025
3095
  return {
3026
3096
  decoderType: "libav-wasm",
@@ -3120,6 +3190,13 @@ async function createFallbackSession(ctx, target, transport) {
3120
3190
  configurable: true,
3121
3191
  get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
3122
3192
  });
3193
+ Object.defineProperty(target, "buffered", {
3194
+ configurable: true,
3195
+ get: () => {
3196
+ const end = handles.bufferedUntilSec();
3197
+ return makeTimeRanges(end > 0 ? [[0, end]] : []);
3198
+ }
3199
+ });
3123
3200
  async function waitForBuffer() {
3124
3201
  const start = performance.now();
3125
3202
  let firstFrameAtMs = 0;
@@ -3159,6 +3236,7 @@ async function createFallbackSession(ctx, target, transport) {
3159
3236
  }
3160
3237
  async function doSeek(timeSec) {
3161
3238
  const wasPlaying = audio.isPlaying();
3239
+ target.dispatchEvent(new Event("seeking"));
3162
3240
  await audio.pause().catch(() => {
3163
3241
  });
3164
3242
  await handles.seek(timeSec).catch(
@@ -3170,7 +3248,14 @@ async function createFallbackSession(ctx, target, transport) {
3170
3248
  await waitForBuffer();
3171
3249
  await audio.start();
3172
3250
  }
3251
+ target.dispatchEvent(new Event("seeked"));
3173
3252
  }
3253
+ queueMicrotask(() => {
3254
+ try {
3255
+ target.dispatchEvent(new Event("loadedmetadata"));
3256
+ } catch {
3257
+ }
3258
+ });
3174
3259
  return {
3175
3260
  strategy: "fallback",
3176
3261
  async play() {
@@ -4250,9 +4335,10 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4250
4335
  else this.removeAttribute("autoplay");
4251
4336
  }
4252
4337
  get muted() {
4253
- return this.hasAttribute("muted");
4338
+ return this._videoEl.muted;
4254
4339
  }
4255
4340
  set muted(value) {
4341
+ this._videoEl.muted = value;
4256
4342
  if (value) this.setAttribute("muted", "");
4257
4343
  else this.removeAttribute("muted");
4258
4344
  }
@@ -4317,11 +4403,16 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4317
4403
  }
4318
4404
  /**
4319
4405
  * Buffered time ranges for the active source. Mirrors the standard
4320
- * `<video>.buffered` `TimeRanges` API. For the native and remux strategies
4321
- * this reflects the underlying SourceBuffer / progressive download state.
4322
- * For the hybrid and fallback (canvas-rendered) strategies it currently
4323
- * returns an empty TimeRanges; a future release will synthesize a coarse
4324
- * range from the decoder's read position.
4406
+ * `<video>.buffered` `TimeRanges` API.
4407
+ *
4408
+ * - **Native / remux:** pass-through to the real `<video>.buffered`
4409
+ * (reflects the browser's SourceBuffer / progressive-download state).
4410
+ * - **Hybrid / fallback:** a single `[0, frontier]` range synthesized
4411
+ * from the demuxer's read progress — "how far libav has ever pumped
4412
+ * packets through." Monotonic; does not shrink on seek. This is an
4413
+ * approximation, not MSE-fidelity: decoded frames on canvas strategies
4414
+ * are consumed in flight, so we can't report per-range availability
4415
+ * the way MSE does. Enough for a seek-bar buffered indicator.
4325
4416
  */
4326
4417
  get buffered() {
4327
4418
  return this._videoEl.buffered;
@@ -4625,11 +4716,18 @@ var PLAYER_STYLES = (
4625
4716
 
4626
4717
  /* \u2500\u2500 Container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4627
4718
 
4719
+ :host {
4720
+ -webkit-tap-highlight-color: transparent;
4721
+ outline: none;
4722
+ }
4723
+
4628
4724
  .avp {
4629
4725
  position: relative;
4630
4726
  width: 100%;
4631
4727
  height: 100%;
4632
4728
  cursor: pointer;
4729
+ -webkit-tap-highlight-color: transparent;
4730
+ user-select: none;
4633
4731
  }
4634
4732
 
4635
4733
  .avp avbridge-video {
@@ -4828,7 +4926,14 @@ var PLAYER_STYLES = (
4828
4926
  pointer-events: auto;
4829
4927
  }
4830
4928
 
4831
- .avp-toolbar-top-right { margin-left: auto; }
4929
+ /* Left slot fills remaining space so slotted text/content can grow.
4930
+ min-width: 0 prevents flex children from overflowing the toolbar. */
4931
+ .avp-toolbar-top-left {
4932
+ flex: 1;
4933
+ min-width: 0;
4934
+ }
4935
+
4936
+ .avp-toolbar-top-right { margin-left: auto; flex-shrink: 0; }
4832
4937
 
4833
4938
  /* Hide the gradient band when no consumer has slotted anything \u2014 we
4834
4939
  toggle data-toolbar-empty from JS via slotchange. */
@@ -4841,6 +4946,30 @@ var PLAYER_STYLES = (
4841
4946
  pointer-events: none;
4842
4947
  }
4843
4948
 
4949
+ /* \u2500\u2500 Content overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4950
+ /* Consumer-provided rich content (tweet cards, media info, annotations).
4951
+ Sits above the video, below the play-button overlay and controls in
4952
+ z-order. Auto-hides with the chrome. The wrapper is pointer-events:none
4953
+ so taps fall through to the video; consumers opt in on their content
4954
+ with pointer-events:auto. */
4955
+
4956
+ .avp-content-overlay {
4957
+ position: absolute;
4958
+ inset: 0;
4959
+ z-index: 1;
4960
+ pointer-events: none;
4961
+ opacity: 1;
4962
+ transition: opacity 0.25s;
4963
+ }
4964
+
4965
+ .avp-content-overlay ::slotted(*) {
4966
+ pointer-events: auto;
4967
+ }
4968
+
4969
+ :host([data-controls-hidden]) .avp-content-overlay {
4970
+ opacity: 0;
4971
+ }
4972
+
4844
4973
  /* \u2500\u2500 Seek bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4845
4974
 
4846
4975
  .avp-seek {
@@ -4927,6 +5056,15 @@ var PLAYER_STYLES = (
4927
5056
 
4928
5057
  .avp-seek:hover .avp-seek-tooltip { display: block; }
4929
5058
 
5059
+ /* Show tooltip during active drag (touch or mouse). The JS side sets
5060
+ data-seeking on .avp-seek while the user is scrubbing. */
5061
+ .avp-seek[data-seeking] .avp-seek-tooltip { display: block; }
5062
+
5063
+ /* Enlarge thumb while scrubbing. */
5064
+ .avp-seek[data-seeking] .avp-seek-thumb {
5065
+ transform: translate(-50%, -50%) scale(1.4);
5066
+ }
5067
+
4930
5068
  /* \u2500\u2500 Bottom row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4931
5069
 
4932
5070
  .avp-bottom {
@@ -5046,7 +5184,10 @@ var PLAYER_STYLES = (
5046
5184
  background: rgba(28, 28, 28, 0.95);
5047
5185
  border-radius: 8px;
5048
5186
  min-width: 220px;
5049
- max-height: 300px;
5187
+ /* Fit within the player: leave room for the controls bar (52px bottom)
5188
+ and a small top margin (8px). On tall players this caps at 300px;
5189
+ on short players it shrinks to whatever fits. */
5190
+ max-height: min(300px, calc(100% - 52px - 8px));
5050
5191
  overflow-y: auto;
5051
5192
  display: none;
5052
5193
  z-index: 10;
@@ -5118,9 +5259,24 @@ var PLAYER_STYLES = (
5118
5259
  @media (pointer: coarse) {
5119
5260
  .avp-btn svg { width: 28px; height: 28px; }
5120
5261
  .avp-btn { padding: 8px; }
5262
+
5263
+ /* Taller touch target on mobile (44px, matching YouTube Mobile)
5264
+ while keeping the visual track thin. Negative margin collapses
5265
+ the extra space so the controls layout doesn't shift. */
5266
+ .avp-seek { height: 44px; margin-top: -12px; margin-bottom: -12px; }
5121
5267
  .avp-seek-track { height: 4px; }
5122
5268
  .avp-seek:hover .avp-seek-track { height: 4px; }
5123
- .avp-seek-thumb { transform: translate(-50%, -50%) scale(1); }
5269
+ .avp-seek-thumb {
5270
+ transform: translate(-50%, -50%) scale(1);
5271
+ width: 16px;
5272
+ height: 16px;
5273
+ }
5274
+ .avp-seek[data-seeking] .avp-seek-thumb {
5275
+ transform: translate(-50%, -50%) scale(1.5);
5276
+ }
5277
+ /* Move tooltip above the taller touch zone. */
5278
+ .avp-seek-tooltip { bottom: 32px; }
5279
+
5124
5280
  .avp-volume:hover .avp-volume-slider { width: 0; }
5125
5281
  .avp-overlay-btn { width: 56px; height: 56px; }
5126
5282
  .avp-overlay-btn svg { width: 30px; height: 30px; }
@@ -5278,6 +5434,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
5278
5434
  <div part="toolbar-top-left" class="avp-toolbar-top-left"><slot name="top-left"></slot></div>
5279
5435
  <div part="toolbar-top-right" class="avp-toolbar-top-right"><slot name="top-right"></slot></div>
5280
5436
  </div>
5437
+ <div part="content-overlay" class="avp-content-overlay"><slot name="content-overlay"></slot></div>
5281
5438
  <div part="overlay" class="avp-overlay">
5282
5439
  <button class="avp-overlay-btn" aria-label="Play">${ICON_PLAY}</button>
5283
5440
  <div class="avp-spinner"></div>
@@ -5493,19 +5650,23 @@ var AvbridgePlayerElement = class extends HTMLElement {
5493
5650
  this._userSeeking = true;
5494
5651
  const seekBar = this.shadowRoot.querySelector(".avp-seek");
5495
5652
  seekBar.setPointerCapture(e.pointerId);
5653
+ seekBar.setAttribute("data-seeking", "");
5496
5654
  const initial = this._timeFromSeekPointer(e.clientX);
5497
5655
  this._seekInput.value = String(initial);
5498
5656
  this._onSeekInput();
5657
+ this._updateSeekTooltip(e.clientX);
5499
5658
  const onMove = (ev) => {
5500
5659
  const t = this._timeFromSeekPointer(ev.clientX);
5501
5660
  this._seekInput.value = String(t);
5502
5661
  this._onSeekInput();
5662
+ this._updateSeekTooltip(ev.clientX);
5503
5663
  };
5504
5664
  const onUp = (ev) => {
5505
5665
  const t = this._timeFromSeekPointer(ev.clientX);
5506
5666
  this._seekInput.value = String(t);
5507
5667
  this._onSeekCommit();
5508
5668
  this._seekInput.focus();
5669
+ seekBar.removeAttribute("data-seeking");
5509
5670
  seekBar.removeEventListener("pointermove", onMove);
5510
5671
  seekBar.removeEventListener("pointerup", onUp);
5511
5672
  seekBar.removeEventListener("pointercancel", onUp);
@@ -5519,8 +5680,11 @@ var AvbridgePlayerElement = class extends HTMLElement {
5519
5680
  seekBar.addEventListener("pointercancel", onUp);
5520
5681
  }
5521
5682
  _onSeekHover(e) {
5683
+ this._updateSeekTooltip(e.clientX);
5684
+ }
5685
+ _updateSeekTooltip(clientX) {
5522
5686
  const rect = this._seekInput.getBoundingClientRect();
5523
- const frac = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
5687
+ const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
5524
5688
  const t = frac * (this._video.duration || 0);
5525
5689
  this._seekTooltip.textContent = formatTime(t);
5526
5690
  this._seekTooltip.style.left = `${frac * 100}%`;
@@ -5740,19 +5904,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
5740
5904
  // it's treated as a double-click and the single-click action is cancelled.
5741
5905
  /** Track whether the last interaction was touch so click handler can skip. */
5742
5906
  _lastPointerTypeWasTouch = false;
5743
- /** True if the event's composed path passes through consumer-slotted toolbar
5744
- * content. Slotted content lives in the light DOM so `.closest(".avp-toolbar-top")`
5745
- * on the event target won't find the shadow-DOM wrapper — `composedPath()`
5746
- * does. */
5747
- _isToolbarEvent(e) {
5907
+ /** True if the event's composed path passes through consumer-slotted
5908
+ * content (toolbar or content-overlay). Slotted content lives in the
5909
+ * light DOM so `.closest(".avp-toolbar-top")` on the event target won't
5910
+ * find the shadow-DOM wrapper — `composedPath()` does. */
5911
+ _isSlottedContentEvent(e) {
5748
5912
  for (const node of e.composedPath()) {
5749
- if (node instanceof HTMLElement && node.classList.contains("avp-toolbar-top")) return true;
5913
+ if (node instanceof HTMLElement && (node.classList.contains("avp-toolbar-top") || node.classList.contains("avp-content-overlay"))) return true;
5750
5914
  }
5751
5915
  return false;
5752
5916
  }
5753
5917
  _onContainerClick(e) {
5754
5918
  if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
5755
- if (this._isToolbarEvent(e)) return;
5919
+ if (this._isSlottedContentEvent(e)) return;
5756
5920
  if (this._lastPointerTypeWasTouch) {
5757
5921
  this._lastPointerTypeWasTouch = false;
5758
5922
  return;
@@ -5768,7 +5932,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
5768
5932
  }
5769
5933
  _onContainerDblClick(e) {
5770
5934
  if (e.target.closest?.(".avp-controls, .avp-settings")) return;
5771
- if (this._isToolbarEvent(e)) return;
5935
+ if (this._isSlottedContentEvent(e)) return;
5772
5936
  if (this._tapTimer) {
5773
5937
  clearTimeout(this._tapTimer);
5774
5938
  this._tapTimer = null;
@@ -5790,7 +5954,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
5790
5954
  if (e.pointerType !== "touch") return;
5791
5955
  this._lastPointerTypeWasTouch = true;
5792
5956
  if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
5793
- if (this._isToolbarEvent(e)) return;
5957
+ if (this._isSlottedContentEvent(e)) return;
5794
5958
  const now = Date.now();
5795
5959
  if (now - this._lastTapTime < 300) {
5796
5960
  if (this._tapTimer) {