avbridge 2.9.0 → 2.10.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.
package/dist/player.cjs CHANGED
@@ -1607,6 +1607,10 @@ var AudioOutput = class {
1607
1607
  _volume = 1;
1608
1608
  /** User-set muted flag. When true, gain is forced to 0. */
1609
1609
  _muted = false;
1610
+ /** Playback rate. Scales the media clock and each AudioBufferSourceNode's
1611
+ * playbackRate so audio pitches up/down accordingly (same as native
1612
+ * <video>.playbackRate). Default 1. */
1613
+ _rate = 1;
1610
1614
  constructor() {
1611
1615
  this.ctx = new AudioContext();
1612
1616
  this.gain = this.ctx.createGain();
@@ -1628,6 +1632,20 @@ var AudioOutput = class {
1628
1632
  getMuted() {
1629
1633
  return this._muted;
1630
1634
  }
1635
+ /** Set playback rate. Scales the media clock and pitches audio output
1636
+ * (same as native <video>.playbackRate — speed without pitch correction).
1637
+ * Rebases the anchor so the clock transition is seamless. */
1638
+ setPlaybackRate(rate) {
1639
+ if (rate === this._rate) return;
1640
+ const t = this.now();
1641
+ this.mediaTimeOfAnchor = t;
1642
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
1643
+ this.wallAnchorMs = performance.now();
1644
+ this._rate = rate;
1645
+ }
1646
+ getPlaybackRate() {
1647
+ return this._rate;
1648
+ }
1631
1649
  applyGain() {
1632
1650
  const target = this._muted ? 0 : this._volume;
1633
1651
  try {
@@ -1648,12 +1666,12 @@ var AudioOutput = class {
1648
1666
  now() {
1649
1667
  if (this.noAudio) {
1650
1668
  if (this.state === "playing") {
1651
- return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3;
1669
+ return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3 * this._rate;
1652
1670
  }
1653
1671
  return this.mediaTimeOfAnchor;
1654
1672
  }
1655
1673
  if (this.state === "playing") {
1656
- return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor);
1674
+ return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
1657
1675
  }
1658
1676
  return this.mediaTimeOfAnchor;
1659
1677
  }
@@ -1705,7 +1723,8 @@ var AudioOutput = class {
1705
1723
  const node = this.ctx.createBufferSource();
1706
1724
  node.buffer = buffer;
1707
1725
  node.connect(this.gain);
1708
- let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
1726
+ if (this._rate !== 1) node.playbackRate.value = this._rate;
1727
+ let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
1709
1728
  if (ctxStart < this.ctx.currentTime) {
1710
1729
  this.ctxTimeAtAnchor = this.ctx.currentTime;
1711
1730
  this.mediaTimeOfAnchor = this.mediaTimeOfNext;
@@ -2533,6 +2552,14 @@ async function createHybridSession(ctx, target, transport) {
2533
2552
  get: () => ctx.duration ?? NaN
2534
2553
  });
2535
2554
  }
2555
+ Object.defineProperty(target, "playbackRate", {
2556
+ configurable: true,
2557
+ get: () => audio.getPlaybackRate(),
2558
+ set: (v) => {
2559
+ audio.setPlaybackRate(v);
2560
+ target.dispatchEvent(new Event("ratechange"));
2561
+ }
2562
+ });
2536
2563
  Object.defineProperty(target, "readyState", {
2537
2564
  configurable: true,
2538
2565
  get: () => {
@@ -2643,6 +2670,7 @@ async function createHybridSession(ctx, target, transport) {
2643
2670
  delete target.muted;
2644
2671
  delete target.readyState;
2645
2672
  delete target.seekable;
2673
+ delete target.playbackRate;
2646
2674
  } catch {
2647
2675
  }
2648
2676
  },
@@ -3178,6 +3206,14 @@ async function createFallbackSession(ctx, target, transport) {
3178
3206
  get: () => ctx.duration ?? NaN
3179
3207
  });
3180
3208
  }
3209
+ Object.defineProperty(target, "playbackRate", {
3210
+ configurable: true,
3211
+ get: () => audio.getPlaybackRate(),
3212
+ set: (v) => {
3213
+ audio.setPlaybackRate(v);
3214
+ target.dispatchEvent(new Event("ratechange"));
3215
+ }
3216
+ });
3181
3217
  Object.defineProperty(target, "readyState", {
3182
3218
  configurable: true,
3183
3219
  get: () => {
@@ -3309,6 +3345,7 @@ async function createFallbackSession(ctx, target, transport) {
3309
3345
  delete target.muted;
3310
3346
  delete target.readyState;
3311
3347
  delete target.seekable;
3348
+ delete target.playbackRate;
3312
3349
  } catch {
3313
3350
  }
3314
3351
  },
@@ -4725,7 +4762,6 @@ var PLAYER_STYLES = (
4725
4762
  position: relative;
4726
4763
  width: 100%;
4727
4764
  height: 100%;
4728
- cursor: pointer;
4729
4765
  -webkit-tap-highlight-color: transparent;
4730
4766
  user-select: none;
4731
4767
  }
@@ -5175,63 +5211,114 @@ var PLAYER_STYLES = (
5175
5211
 
5176
5212
  .avp-spacer { flex: 1; }
5177
5213
 
5178
- /* \u2500\u2500 Settings menu \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 */
5214
+ /* \u2500\u2500 Settings bottom sheet \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 */
5179
5215
 
5216
+ /* Scrim \u2014 semi-transparent overlay behind the sheet, above the video.
5217
+ Tapping it dismisses the sheet. */
5218
+ .avp-settings-scrim {
5219
+ position: absolute;
5220
+ inset: 0;
5221
+ z-index: 9;
5222
+ background: rgba(0, 0, 0, 0.4);
5223
+ opacity: 0;
5224
+ pointer-events: none;
5225
+ transition: opacity 0.2s;
5226
+ }
5227
+
5228
+ .avp-settings-scrim.open {
5229
+ opacity: 1;
5230
+ pointer-events: auto;
5231
+ }
5232
+
5233
+ /* Sheet container \u2014 slides up from the bottom. Height is content-driven
5234
+ up to a JS-measured max (set on open via style.maxHeight). */
5180
5235
  .avp-settings {
5181
5236
  position: absolute;
5182
- bottom: 52px;
5183
- right: 12px;
5184
- background: rgba(28, 28, 28, 0.95);
5185
- border-radius: 8px;
5186
- min-width: 220px;
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));
5191
- overflow-y: auto;
5192
- display: none;
5237
+ bottom: 0;
5238
+ left: 0;
5239
+ right: 0;
5193
5240
  z-index: 10;
5194
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
5241
+ background: rgba(28, 28, 28, 0.97);
5242
+ border-radius: 12px 12px 0 0;
5243
+ overflow-y: auto;
5244
+ overscroll-behavior: contain;
5245
+ transform: translateY(100%);
5246
+ transition: transform 0.2s ease-out;
5247
+ max-height: 70%;
5248
+ padding-bottom: 52px;
5249
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
5195
5250
  }
5196
5251
 
5197
- .avp-settings.open { display: block; }
5252
+ .avp-settings.open {
5253
+ transform: translateY(0);
5254
+ }
5198
5255
 
5199
- .avp-settings-section {
5200
- padding: 8px 0;
5201
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
5256
+ /* Drag handle indicator at top of sheet. */
5257
+ .avp-settings-handle {
5258
+ width: 36px;
5259
+ height: 4px;
5260
+ border-radius: 2px;
5261
+ background: rgba(255, 255, 255, 0.3);
5262
+ margin: 8px auto 4px;
5202
5263
  }
5203
5264
 
5204
- .avp-settings-section:last-child { border-bottom: none; }
5265
+ /* \u2500\u2500 Accordion sections \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 */
5205
5266
 
5206
- .avp-settings-label {
5207
- padding: 4px 16px;
5208
- font-size: 11px;
5209
- text-transform: uppercase;
5210
- letter-spacing: 0.5px;
5211
- opacity: 0.5;
5267
+ .avp-settings-section {
5268
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
5212
5269
  }
5213
5270
 
5214
- .avp-settings-item {
5271
+ .avp-settings-section:last-child { border-bottom: none; }
5272
+
5273
+ /* Section header \u2014 clickable row showing label + current value. */
5274
+ .avp-settings-header {
5275
+ position: relative;
5215
5276
  display: flex;
5216
5277
  align-items: center;
5217
- padding: 8px 16px;
5218
- font-size: 13px;
5278
+ justify-content: space-between;
5279
+ padding: 12px 16px;
5219
5280
  cursor: pointer;
5281
+ font-size: 14px;
5220
5282
  transition: background 0.1s;
5221
5283
  }
5222
5284
 
5223
- .avp-settings-item:hover { background: rgba(255, 255, 255, 0.1); }
5285
+ .avp-settings-header:hover { background: rgba(255, 255, 255, 0.06); }
5224
5286
 
5225
- .avp-settings-item.active {
5226
- color: #3ea6ff;
5287
+ .avp-settings-header-label {
5288
+ display: flex;
5289
+ align-items: center;
5290
+ gap: 8px;
5291
+ font-weight: 500;
5227
5292
  }
5228
5293
 
5229
- .avp-settings-item.active::before {
5230
- content: "\\2713";
5231
- margin-right: 8px;
5232
- font-weight: bold;
5294
+ .avp-settings-header-value {
5295
+ margin-left: auto;
5296
+ opacity: 0.6;
5297
+ font-size: 13px;
5298
+ text-align: right;
5233
5299
  }
5234
5300
 
5301
+ /* Invisible native <select> layered over the value portion of the row.
5302
+ Covers from the value text to the right edge so tapping the value
5303
+ opens the OS picker. The label side remains inert. */
5304
+ .avp-settings-select {
5305
+ position: absolute;
5306
+ top: 0;
5307
+ right: 0;
5308
+ bottom: 0;
5309
+ width: 50%;
5310
+ opacity: 0;
5311
+ cursor: pointer;
5312
+ font-size: 16px;
5313
+ direction: rtl;
5314
+ }
5315
+
5316
+ /* Toggle-style rows (Stats for Nerds) \u2014 no select, just clickable. */
5317
+ .avp-settings-toggle {
5318
+ cursor: pointer;
5319
+ }
5320
+ .avp-settings-toggle:hover { background: rgba(255, 255, 255, 0.06); }
5321
+
5235
5322
  /* \u2500\u2500 Stats for nerds \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 */
5236
5323
 
5237
5324
  .avp-stats {
@@ -5369,6 +5456,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
5369
5456
  _volumeInput;
5370
5457
  _settingsBtn;
5371
5458
  _settingsMenu;
5459
+ _settingsScrim;
5460
+ _customSections = [];
5372
5461
  _fullscreenBtn;
5373
5462
  // Strategy badge removed — visible in Stats for Nerds instead.
5374
5463
  // Spinner is rendered but driven entirely by CSS :host([data-state]) selectors.
@@ -5410,6 +5499,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
5410
5499
  this._volumeInput = shadow.querySelector(".avp-volume-input");
5411
5500
  this._settingsBtn = shadow.querySelector(".avp-settings-btn");
5412
5501
  this._settingsMenu = shadow.querySelector(".avp-settings");
5502
+ this._settingsScrim = shadow.querySelector(".avp-settings-scrim");
5413
5503
  this._fullscreenBtn = shadow.querySelector(".avp-fullscreen");
5414
5504
  this._speedIndicator = shadow.querySelector(".avp-speed-indicator");
5415
5505
  this._statsEl = shadow.querySelector(".avp-stats");
@@ -5466,7 +5556,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
5466
5556
  <button class="avp-btn avp-settings-btn" part="settings-button" aria-label="Settings">${ICON_SETTINGS}</button>
5467
5557
  <button class="avp-btn avp-fullscreen" part="fullscreen-button" aria-label="Fullscreen">${ICON_FULLSCREEN}</button>
5468
5558
  </div>
5469
- <div class="avp-settings" part="settings-menu"></div>
5559
+ <div class="avp-settings-scrim"></div>
5560
+ <div class="avp-settings" part="settings-menu"><div class="avp-settings-handle"></div></div>
5470
5561
  </div>
5471
5562
  </div>`;
5472
5563
  }
@@ -5538,6 +5629,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
5538
5629
  e.stopPropagation();
5539
5630
  this._toggleSettings();
5540
5631
  });
5632
+ on(this._settingsScrim, "click", () => this._closeSettings());
5541
5633
  on(this._fullscreenBtn, "click", (e) => {
5542
5634
  e.stopPropagation();
5543
5635
  this._toggleFullscreen();
@@ -5546,11 +5638,6 @@ var AvbridgePlayerElement = class extends HTMLElement {
5546
5638
  const container = this.shadowRoot.querySelector(".avp");
5547
5639
  on(container, "click", (e) => this._onContainerClick(e));
5548
5640
  on(container, "dblclick", (e) => this._onContainerDblClick(e));
5549
- on(container, "click", (e) => {
5550
- if (this._settingsOpen && !e.target.closest?.(".avp-settings-btn, .avp-settings")) {
5551
- this._closeSettings();
5552
- }
5553
- });
5554
5641
  on(document, "click", (e) => {
5555
5642
  if (this._settingsOpen && !this.contains(e.target)) {
5556
5643
  this._closeSettings();
@@ -5728,83 +5815,111 @@ var AvbridgePlayerElement = class extends HTMLElement {
5728
5815
  _toggleSettings() {
5729
5816
  this._settingsOpen = !this._settingsOpen;
5730
5817
  this._settingsMenu.classList.toggle("open", this._settingsOpen);
5731
- if (this._settingsOpen) this._showControls();
5818
+ this._settingsScrim.classList.toggle("open", this._settingsOpen);
5819
+ if (this._settingsOpen) {
5820
+ this._fitSettingsToPlayer();
5821
+ this._showControls();
5822
+ }
5823
+ }
5824
+ _fitSettingsToPlayer() {
5825
+ const container = this.shadowRoot?.querySelector(".avp");
5826
+ if (!container) return;
5827
+ const rect = container.getBoundingClientRect();
5828
+ const maxH = Math.max(120, Math.floor(rect.height * 0.7));
5829
+ this._settingsMenu.style.maxHeight = `${maxH}px`;
5732
5830
  }
5733
5831
  _closeSettings() {
5734
5832
  this._settingsOpen = false;
5735
5833
  this._settingsMenu.classList.remove("open");
5834
+ this._settingsScrim.classList.remove("open");
5736
5835
  }
5737
5836
  _buildSettingsMenu() {
5738
5837
  const sections = [];
5739
- if (this.hasAttribute("show-fit")) {
5740
- const currentFit = this._video.fit ?? "contain";
5741
- let fitItems = "";
5742
- for (const mode of FIT_MODES) {
5743
- const active = mode === currentFit;
5744
- const label = mode[0].toUpperCase() + mode.slice(1);
5745
- fitItems += `<div class="avp-settings-item${active ? " active" : ""}" data-fit="${mode}">${label}</div>`;
5746
- }
5747
- sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Fit</div>${fitItems}</div>`);
5748
- }
5838
+ const selectRow = (label, currentValue, options, selectAttrs) => `<div class="avp-settings-section"><div class="avp-settings-header"><span class="avp-settings-header-label">${label}</span><span class="avp-settings-header-value">${currentValue}</span><select class="avp-settings-select" ${selectAttrs}>${options}</select></div></div>`;
5749
5839
  const currentRate = this._video.playbackRate ?? 1;
5750
- let speedItems = "";
5840
+ const speedValue = Math.abs(currentRate - 1) < 0.01 ? "Normal" : `${currentRate}x`;
5841
+ let speedOpts = "";
5751
5842
  for (const spd of PLAYBACK_SPEEDS) {
5752
- const active = Math.abs(spd - currentRate) < 0.01;
5843
+ const sel = Math.abs(spd - currentRate) < 0.01 ? " selected" : "";
5753
5844
  const label = spd === 1 ? "Normal" : `${spd}x`;
5754
- speedItems += `<div class="avp-settings-item${active ? " active" : ""}" data-speed="${spd}">${label}</div>`;
5845
+ speedOpts += `<option value="${spd}"${sel}>${label}</option>`;
5846
+ }
5847
+ sections.push(selectRow("Speed", speedValue, speedOpts, `data-action="speed"`));
5848
+ const audios = this._video.audioTracks ?? [];
5849
+ if (audios.length > 1) {
5850
+ let audioOpts = "";
5851
+ for (const t of audios) {
5852
+ audioOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
5853
+ }
5854
+ sections.push(selectRow("Audio", audios[0]?.language ?? "Track 1", audioOpts, `data-action="audio"`));
5755
5855
  }
5756
- sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Speed</div>${speedItems}</div>`);
5757
5856
  const subs = this._video.subtitleTracks ?? [];
5758
5857
  if (subs.length > 0) {
5759
- let subItems = `<div class="avp-settings-item" data-subtitle="-1">Off</div>`;
5858
+ let subOpts = `<option value="-1" selected>Off</option>`;
5760
5859
  for (const t of subs) {
5761
- subItems += `<div class="avp-settings-item" data-subtitle="${t.id}">${t.language ?? `Track ${t.id}`}</div>`;
5860
+ subOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
5762
5861
  }
5763
- sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Subtitles</div>${subItems}</div>`);
5862
+ sections.push(selectRow("Subtitles", "Off", subOpts, `data-action="subtitle"`));
5764
5863
  }
5765
- const audios = this._video.audioTracks ?? [];
5766
- if (audios.length > 1) {
5767
- let audioItems = "";
5768
- for (const t of audios) {
5769
- audioItems += `<div class="avp-settings-item" data-audio="${t.id}">${t.language ?? `Track ${t.id}`}</div>`;
5864
+ if (this.hasAttribute("show-fit")) {
5865
+ const currentFit = this._video.fit ?? "contain";
5866
+ const fitValue = currentFit[0].toUpperCase() + currentFit.slice(1);
5867
+ let fitOpts = "";
5868
+ for (const mode of FIT_MODES) {
5869
+ const sel = mode === currentFit ? " selected" : "";
5870
+ const label = mode[0].toUpperCase() + mode.slice(1);
5871
+ fitOpts += `<option value="${mode}"${sel}>${label}</option>`;
5770
5872
  }
5771
- sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Audio</div>${audioItems}</div>`);
5873
+ sections.push(selectRow("Fit", fitValue, fitOpts, `data-action="fit"`));
5772
5874
  }
5773
- sections.push(`<div class="avp-settings-section"><div class="avp-settings-item" data-stats>Stats for nerds</div></div>`);
5774
- this._settingsMenu.innerHTML = sections.join("");
5775
- for (const item of this._settingsMenu.querySelectorAll("[data-fit]")) {
5776
- item.addEventListener("click", (e) => {
5777
- e.stopPropagation();
5778
- const mode = item.dataset.fit;
5779
- this.setAttribute("fit", mode);
5780
- this._buildSettingsMenu();
5781
- });
5875
+ for (const cfg of this._customSections) {
5876
+ const activeItem = cfg.items.find((i) => i.active);
5877
+ let customOpts = "";
5878
+ for (const item of cfg.items) {
5879
+ const sel = item.active ? " selected" : "";
5880
+ customOpts += `<option value="${item.id}"${sel}>${item.label}</option>`;
5881
+ }
5882
+ sections.push(selectRow(cfg.label, activeItem?.label ?? "", customOpts, `data-action="custom" data-custom-id="${cfg.id}"`));
5782
5883
  }
5783
- for (const item of this._settingsMenu.querySelectorAll("[data-speed]")) {
5784
- item.addEventListener("click", (e) => {
5884
+ sections.push(
5885
+ `<div class="avp-settings-section"><div class="avp-settings-header avp-settings-toggle" data-stats><span class="avp-settings-header-label">Stats for Nerds</span></div></div>`
5886
+ );
5887
+ const handle = this._settingsMenu.querySelector(".avp-settings-handle");
5888
+ this._settingsMenu.innerHTML = "";
5889
+ if (handle) this._settingsMenu.appendChild(handle);
5890
+ else this._settingsMenu.insertAdjacentHTML("afterbegin", `<div class="avp-settings-handle"></div>`);
5891
+ this._settingsMenu.insertAdjacentHTML("beforeend", sections.join(""));
5892
+ for (const sel of this._settingsMenu.querySelectorAll(".avp-settings-select")) {
5893
+ sel.addEventListener("change", (e) => {
5785
5894
  e.stopPropagation();
5786
- this._video.playbackRate = Number(item.dataset.speed);
5895
+ const action = sel.dataset.action;
5896
+ const val = sel.value;
5897
+ switch (action) {
5898
+ case "speed":
5899
+ this._video.playbackRate = Number(val);
5900
+ break;
5901
+ case "audio":
5902
+ void this._video.setAudioTrack(Number(val));
5903
+ break;
5904
+ case "subtitle":
5905
+ void this._video.setSubtitleTrack(Number(val) >= 0 ? Number(val) : null);
5906
+ break;
5907
+ case "fit":
5908
+ this.setAttribute("fit", val);
5909
+ break;
5910
+ case "custom": {
5911
+ const cfgId = sel.dataset.customId;
5912
+ const cfg = this._customSections.find((s) => s.id === cfgId);
5913
+ cfg?.onSelect(val);
5914
+ break;
5915
+ }
5916
+ }
5787
5917
  this._buildSettingsMenu();
5788
5918
  });
5789
5919
  }
5790
- for (const item of this._settingsMenu.querySelectorAll("[data-subtitle]")) {
5791
- item.addEventListener("click", (e) => {
5792
- e.stopPropagation();
5793
- const id = Number(item.dataset.subtitle);
5794
- void this._video.setSubtitleTrack(id >= 0 ? id : null);
5795
- this._closeSettings();
5796
- });
5797
- }
5798
- for (const item of this._settingsMenu.querySelectorAll("[data-audio]")) {
5799
- item.addEventListener("click", (e) => {
5800
- e.stopPropagation();
5801
- void this._video.setAudioTrack(Number(item.dataset.audio));
5802
- this._closeSettings();
5803
- });
5804
- }
5805
- const statsItem = this._settingsMenu.querySelector("[data-stats]");
5806
- if (statsItem) {
5807
- statsItem.addEventListener("click", (e) => {
5920
+ const statsRow = this._settingsMenu.querySelector("[data-stats]");
5921
+ if (statsRow) {
5922
+ statsRow.addEventListener("click", (e) => {
5808
5923
  e.stopPropagation();
5809
5924
  this._toggleStats();
5810
5925
  this._closeSettings();
@@ -5917,6 +6032,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
5917
6032
  _onContainerClick(e) {
5918
6033
  if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
5919
6034
  if (this._isSlottedContentEvent(e)) return;
6035
+ if (this._settingsOpen) {
6036
+ this._closeSettings();
6037
+ return;
6038
+ }
5920
6039
  if (this._lastPointerTypeWasTouch) {
5921
6040
  this._lastPointerTypeWasTouch = false;
5922
6041
  return;
@@ -5955,6 +6074,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
5955
6074
  this._lastPointerTypeWasTouch = true;
5956
6075
  if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
5957
6076
  if (this._isSlottedContentEvent(e)) return;
6077
+ if (this._settingsOpen) {
6078
+ this._closeSettings();
6079
+ return;
6080
+ }
5958
6081
  const now = Date.now();
5959
6082
  if (now - this._lastTapTime < 300) {
5960
6083
  if (this._tapTimer) {
@@ -6204,6 +6327,15 @@ var AvbridgePlayerElement = class extends HTMLElement {
6204
6327
  async setAudioTrack(id) {
6205
6328
  return this._video.setAudioTrack(id);
6206
6329
  }
6330
+ addSettingsSection(config) {
6331
+ this._customSections = this._customSections.filter((s) => s.id !== config.id);
6332
+ this._customSections.push(config);
6333
+ this._buildSettingsMenu();
6334
+ }
6335
+ removeSettingsSection(id) {
6336
+ this._customSections = this._customSections.filter((s) => s.id !== id);
6337
+ this._buildSettingsMenu();
6338
+ }
6207
6339
  async setSubtitleTrack(id) {
6208
6340
  return this._video.setSubtitleTrack(id);
6209
6341
  }