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/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ All notable changes to **avbridge.js** are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.10.0]
8
+
9
+ Settings UI overhaul + playback rate on all strategies.
10
+
11
+ ### Added
12
+
13
+ - **Bottom-sheet settings panel** replacing the popup menu. Slides up
14
+ from the controls bar with a scrim overlay. Each section uses a
15
+ native `<select>` picker overlaid on a styled row — the OS picker
16
+ renders outside the player bounds (intentional for small players).
17
+ Rows show label left, current value right. Tapping anywhere outside
18
+ the sheet or pressing Escape dismisses it.
19
+ - **Consumer extensibility API**: `player.addSettingsSection({ id,
20
+ label, items, onSelect })` / `player.removeSettingsSection(id)`.
21
+ Custom sections render after built-in ones using the same native
22
+ `<select>` pattern. New `SettingsSectionConfig` type exported.
23
+ - **Playback rate on hybrid + fallback strategies.** `playbackRate`
24
+ was a no-op on canvas strategies because the inner `<video>` has
25
+ no `src`. Now patched via `Object.defineProperty` — drives the
26
+ `AudioOutput` clock speed + `AudioBufferSourceNode.playbackRate`
27
+ for pitch-shifted audio. Video renderer follows automatically
28
+ since it syncs to `audio.now()`. The `ratechange` event fires.
29
+
30
+ ### Fixed
31
+
32
+ - **Settings menu sizing** — JS-measured max-height (70% of player)
33
+ replaces the broken CSS percentage approach.
34
+ - **Blue tap-highlight flash** suppressed on `<avbridge-player>`.
35
+ - **`cursor: pointer` removed** from the player container — the
36
+ video surface isn't a button.
37
+
7
38
  ## [2.9.0]
8
39
 
9
40
  Player chrome ergonomics — four changes driven by explorer integration
@@ -1374,6 +1374,10 @@ var AudioOutput = class {
1374
1374
  _volume = 1;
1375
1375
  /** User-set muted flag. When true, gain is forced to 0. */
1376
1376
  _muted = false;
1377
+ /** Playback rate. Scales the media clock and each AudioBufferSourceNode's
1378
+ * playbackRate so audio pitches up/down accordingly (same as native
1379
+ * <video>.playbackRate). Default 1. */
1380
+ _rate = 1;
1377
1381
  constructor() {
1378
1382
  this.ctx = new AudioContext();
1379
1383
  this.gain = this.ctx.createGain();
@@ -1395,6 +1399,20 @@ var AudioOutput = class {
1395
1399
  getMuted() {
1396
1400
  return this._muted;
1397
1401
  }
1402
+ /** Set playback rate. Scales the media clock and pitches audio output
1403
+ * (same as native <video>.playbackRate — speed without pitch correction).
1404
+ * Rebases the anchor so the clock transition is seamless. */
1405
+ setPlaybackRate(rate) {
1406
+ if (rate === this._rate) return;
1407
+ const t = this.now();
1408
+ this.mediaTimeOfAnchor = t;
1409
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
1410
+ this.wallAnchorMs = performance.now();
1411
+ this._rate = rate;
1412
+ }
1413
+ getPlaybackRate() {
1414
+ return this._rate;
1415
+ }
1398
1416
  applyGain() {
1399
1417
  const target = this._muted ? 0 : this._volume;
1400
1418
  try {
@@ -1415,12 +1433,12 @@ var AudioOutput = class {
1415
1433
  now() {
1416
1434
  if (this.noAudio) {
1417
1435
  if (this.state === "playing") {
1418
- return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3;
1436
+ return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3 * this._rate;
1419
1437
  }
1420
1438
  return this.mediaTimeOfAnchor;
1421
1439
  }
1422
1440
  if (this.state === "playing") {
1423
- return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor);
1441
+ return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
1424
1442
  }
1425
1443
  return this.mediaTimeOfAnchor;
1426
1444
  }
@@ -1472,7 +1490,8 @@ var AudioOutput = class {
1472
1490
  const node = this.ctx.createBufferSource();
1473
1491
  node.buffer = buffer;
1474
1492
  node.connect(this.gain);
1475
- let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
1493
+ if (this._rate !== 1) node.playbackRate.value = this._rate;
1494
+ let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
1476
1495
  if (ctxStart < this.ctx.currentTime) {
1477
1496
  this.ctxTimeAtAnchor = this.ctx.currentTime;
1478
1497
  this.mediaTimeOfAnchor = this.mediaTimeOfNext;
@@ -2120,6 +2139,14 @@ async function createHybridSession(ctx, target, transport) {
2120
2139
  get: () => ctx.duration ?? NaN
2121
2140
  });
2122
2141
  }
2142
+ Object.defineProperty(target, "playbackRate", {
2143
+ configurable: true,
2144
+ get: () => audio.getPlaybackRate(),
2145
+ set: (v) => {
2146
+ audio.setPlaybackRate(v);
2147
+ target.dispatchEvent(new Event("ratechange"));
2148
+ }
2149
+ });
2123
2150
  Object.defineProperty(target, "readyState", {
2124
2151
  configurable: true,
2125
2152
  get: () => {
@@ -2230,6 +2257,7 @@ async function createHybridSession(ctx, target, transport) {
2230
2257
  delete target.muted;
2231
2258
  delete target.readyState;
2232
2259
  delete target.seekable;
2260
+ delete target.playbackRate;
2233
2261
  } catch {
2234
2262
  }
2235
2263
  },
@@ -2765,6 +2793,14 @@ async function createFallbackSession(ctx, target, transport) {
2765
2793
  get: () => ctx.duration ?? NaN
2766
2794
  });
2767
2795
  }
2796
+ Object.defineProperty(target, "playbackRate", {
2797
+ configurable: true,
2798
+ get: () => audio.getPlaybackRate(),
2799
+ set: (v) => {
2800
+ audio.setPlaybackRate(v);
2801
+ target.dispatchEvent(new Event("ratechange"));
2802
+ }
2803
+ });
2768
2804
  Object.defineProperty(target, "readyState", {
2769
2805
  configurable: true,
2770
2806
  get: () => {
@@ -2896,6 +2932,7 @@ async function createFallbackSession(ctx, target, transport) {
2896
2932
  delete target.muted;
2897
2933
  delete target.readyState;
2898
2934
  delete target.seekable;
2935
+ delete target.playbackRate;
2899
2936
  } catch {
2900
2937
  }
2901
2938
  },
@@ -3498,5 +3535,5 @@ function defaultFallbackChain(strategy) {
3498
3535
  }
3499
3536
 
3500
3537
  export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext, createPlayer };
3501
- //# sourceMappingURL=chunk-SN4WZE24.js.map
3502
- //# sourceMappingURL=chunk-SN4WZE24.js.map
3538
+ //# sourceMappingURL=chunk-3GKM5DFM.js.map
3539
+ //# sourceMappingURL=chunk-3GKM5DFM.js.map