avbridge 2.9.0 → 2.11.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 (50) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/{chunk-EY6DZEDT.cjs → chunk-37UOSAVI.cjs} +55 -10
  3. package/dist/chunk-37UOSAVI.cjs.map +1 -0
  4. package/dist/{chunk-5KVLE6YI.js → chunk-EDDWAN2L.js} +3 -2
  5. package/dist/chunk-EDDWAN2L.js.map +1 -0
  6. package/dist/{chunk-SN4WZE24.js → chunk-IHNHHEA2.js} +51 -6
  7. package/dist/chunk-IHNHHEA2.js.map +1 -0
  8. package/dist/{chunk-S4WAZC2T.cjs → chunk-WRKO6Q42.cjs} +3 -2
  9. package/dist/chunk-WRKO6Q42.cjs.map +1 -0
  10. package/dist/element-browser.js +63 -4
  11. package/dist/element-browser.js.map +1 -1
  12. package/dist/element.cjs +18 -5
  13. package/dist/element.cjs.map +1 -1
  14. package/dist/element.d.cts +1 -1
  15. package/dist/element.d.ts +1 -1
  16. package/dist/element.js +17 -4
  17. package/dist/element.js.map +1 -1
  18. package/dist/index.cjs +10 -10
  19. package/dist/index.d.cts +2 -2
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +2 -2
  22. package/dist/{player-DEcidWk6.d.cts → player-DDdNVFDv.d.cts} +23 -1
  23. package/dist/{player-DEcidWk6.d.ts → player-DDdNVFDv.d.ts} +23 -1
  24. package/dist/player.cjs +329 -109
  25. package/dist/player.cjs.map +1 -1
  26. package/dist/player.d.cts +42 -0
  27. package/dist/player.d.ts +42 -0
  28. package/dist/player.js +325 -105
  29. package/dist/player.js.map +1 -1
  30. package/dist/subtitles-5H24MEBJ.js +4 -0
  31. package/dist/{subtitles-4T74JRGT.js.map → subtitles-5H24MEBJ.js.map} +1 -1
  32. package/dist/subtitles-HMVGWTU2.cjs +29 -0
  33. package/dist/{subtitles-QUH4LPI4.cjs.map → subtitles-HMVGWTU2.cjs.map} +1 -1
  34. package/package.json +1 -1
  35. package/src/element/avbridge-player.ts +235 -78
  36. package/src/element/avbridge-subtitles.ts +273 -0
  37. package/src/element/avbridge-video.ts +21 -1
  38. package/src/element/player-styles.ts +85 -35
  39. package/src/index.ts +1 -0
  40. package/src/strategies/fallback/audio-output.ts +39 -4
  41. package/src/strategies/fallback/index.ts +12 -0
  42. package/src/strategies/hybrid/index.ts +9 -0
  43. package/src/subtitles/index.ts +2 -0
  44. package/src/types.ts +25 -0
  45. package/dist/chunk-5KVLE6YI.js.map +0 -1
  46. package/dist/chunk-EY6DZEDT.cjs.map +0 -1
  47. package/dist/chunk-S4WAZC2T.cjs.map +0 -1
  48. package/dist/chunk-SN4WZE24.js.map +0 -1
  49. package/dist/subtitles-4T74JRGT.js +0 -4
  50. package/dist/subtitles-QUH4LPI4.cjs +0 -29
package/CHANGELOG.md CHANGED
@@ -4,6 +4,71 @@ 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.11.0]
8
+
9
+ Subtitle panel, interaction fixes, and quality-of-life.
10
+
11
+ ### Added
12
+
13
+ - **`<avbridge-subtitles>` element** — scrollable cue timeline panel.
14
+ Connects to a player via `for` attribute, renders all subtitle cues
15
+ as timestamped rows, highlights the active cue, auto-scrolls to
16
+ follow playback, and click-to-seek. Shadow DOM with dark styles.
17
+ - **Frame-by-frame keyboard shortcuts** (`,` / `.`) — YouTube-style.
18
+ Pauses if playing, then steps back/forward one frame (1/fps).
19
+ - **Real-time scrub seeking on narrow seekbars** — when the seekbar
20
+ is <400px wide, dragging seeks in real-time (throttled to 4 Hz)
21
+ instead of preview-only. Immediate video feedback on small players.
22
+ - **`controls-timeout` attribute** on `<avbridge-player>`. Customize
23
+ the auto-hide duration (default 3000ms). Set to `"0"` to disable
24
+ auto-hide entirely (always-visible controls).
25
+
26
+ ### Fixed
27
+
28
+ - **Subtitles not showing** — `addSubtitle()` didn't dispatch
29
+ `trackschange` (settings sheet never showed the new track) and the
30
+ `<track>` element was created with `mode="disabled"` (never
31
+ rendered on native strategy). Now dispatches + auto-enables.
32
+ - **Audio bleed on pause** for hybrid/fallback — already-scheduled
33
+ AudioBufferSourceNodes kept playing ~200ms after pause. Now
34
+ disconnects the gain node immediately (same pattern as seek/reset).
35
+ - **Double-tap fires both ff/rw AND fullscreen on touch** — browser's
36
+ synthetic `dblclick` after two rapid taps was calling fullscreen
37
+ on top of the touch handler's ff/rw. Blocked via a consumed flag.
38
+ - **Settings sheet didn't show active audio/subtitle selection** —
39
+ always displayed "Track 1" / "Off" regardless of actual state.
40
+
41
+ ## [2.10.0]
42
+
43
+ Settings UI overhaul + playback rate on all strategies.
44
+
45
+ ### Added
46
+
47
+ - **Bottom-sheet settings panel** replacing the popup menu. Slides up
48
+ from the controls bar with a scrim overlay. Each section uses a
49
+ native `<select>` picker overlaid on a styled row — the OS picker
50
+ renders outside the player bounds (intentional for small players).
51
+ Rows show label left, current value right. Tapping anywhere outside
52
+ the sheet or pressing Escape dismisses it.
53
+ - **Consumer extensibility API**: `player.addSettingsSection({ id,
54
+ label, items, onSelect })` / `player.removeSettingsSection(id)`.
55
+ Custom sections render after built-in ones using the same native
56
+ `<select>` pattern. New `SettingsSectionConfig` type exported.
57
+ - **Playback rate on hybrid + fallback strategies.** `playbackRate`
58
+ was a no-op on canvas strategies because the inner `<video>` has
59
+ no `src`. Now patched via `Object.defineProperty` — drives the
60
+ `AudioOutput` clock speed + `AudioBufferSourceNode.playbackRate`
61
+ for pitch-shifted audio. Video renderer follows automatically
62
+ since it syncs to `audio.now()`. The `ratechange` event fires.
63
+
64
+ ### Fixed
65
+
66
+ - **Settings menu sizing** — JS-measured max-height (70% of player)
67
+ replaces the broken CSS percentage approach.
68
+ - **Blue tap-highlight flash** suppressed on `<avbridge-player>`.
69
+ - **`cursor: pointer` removed** from the player container — the
70
+ video surface isn't a button.
71
+
7
72
  ## [2.9.0]
8
73
 
9
74
  Player chrome ergonomics — four changes driven by explorer integration
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkS4WAZC2T_cjs = require('./chunk-S4WAZC2T.cjs');
3
+ var chunkWRKO6Q42_cjs = require('./chunk-WRKO6Q42.cjs');
4
4
  var chunkBYGZN4Z5_cjs = require('./chunk-BYGZN4Z5.cjs');
5
5
  var chunk2IJ66NTD_cjs = require('./chunk-2IJ66NTD.cjs');
6
6
  var chunkL7A3ECI2_cjs = require('./chunk-L7A3ECI2.cjs');
@@ -1055,7 +1055,7 @@ var VideoRenderer = class {
1055
1055
  }
1056
1056
  target.style.visibility = "hidden";
1057
1057
  const overlayParent = parent instanceof HTMLElement ? parent : document.body;
1058
- this.subtitleOverlay = new chunkS4WAZC2T_cjs.SubtitleOverlay(overlayParent);
1058
+ this.subtitleOverlay = new chunkWRKO6Q42_cjs.SubtitleOverlay(overlayParent);
1059
1059
  this.watchTextTracks(target);
1060
1060
  const ctx = this.canvas.getContext("2d");
1061
1061
  if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
@@ -1376,6 +1376,10 @@ var AudioOutput = class {
1376
1376
  _volume = 1;
1377
1377
  /** User-set muted flag. When true, gain is forced to 0. */
1378
1378
  _muted = false;
1379
+ /** Playback rate. Scales the media clock and each AudioBufferSourceNode's
1380
+ * playbackRate so audio pitches up/down accordingly (same as native
1381
+ * <video>.playbackRate). Default 1. */
1382
+ _rate = 1;
1379
1383
  constructor() {
1380
1384
  this.ctx = new AudioContext();
1381
1385
  this.gain = this.ctx.createGain();
@@ -1397,6 +1401,20 @@ var AudioOutput = class {
1397
1401
  getMuted() {
1398
1402
  return this._muted;
1399
1403
  }
1404
+ /** Set playback rate. Scales the media clock and pitches audio output
1405
+ * (same as native <video>.playbackRate — speed without pitch correction).
1406
+ * Rebases the anchor so the clock transition is seamless. */
1407
+ setPlaybackRate(rate) {
1408
+ if (rate === this._rate) return;
1409
+ const t = this.now();
1410
+ this.mediaTimeOfAnchor = t;
1411
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
1412
+ this.wallAnchorMs = performance.now();
1413
+ this._rate = rate;
1414
+ }
1415
+ getPlaybackRate() {
1416
+ return this._rate;
1417
+ }
1400
1418
  applyGain() {
1401
1419
  const target = this._muted ? 0 : this._volume;
1402
1420
  try {
@@ -1417,12 +1435,12 @@ var AudioOutput = class {
1417
1435
  now() {
1418
1436
  if (this.noAudio) {
1419
1437
  if (this.state === "playing") {
1420
- return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3;
1438
+ return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3 * this._rate;
1421
1439
  }
1422
1440
  return this.mediaTimeOfAnchor;
1423
1441
  }
1424
1442
  if (this.state === "playing") {
1425
- return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor);
1443
+ return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
1426
1444
  }
1427
1445
  return this.mediaTimeOfAnchor;
1428
1446
  }
@@ -1474,7 +1492,8 @@ var AudioOutput = class {
1474
1492
  const node = this.ctx.createBufferSource();
1475
1493
  node.buffer = buffer;
1476
1494
  node.connect(this.gain);
1477
- let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
1495
+ if (this._rate !== 1) node.playbackRate.value = this._rate;
1496
+ let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
1478
1497
  if (ctxStart < this.ctx.currentTime) {
1479
1498
  this.ctxTimeAtAnchor = this.ctx.currentTime;
1480
1499
  this.mediaTimeOfAnchor = this.mediaTimeOfNext;
@@ -1501,6 +1520,10 @@ var AudioOutput = class {
1501
1520
  if (this.ctx.state === "suspended") {
1502
1521
  await this.ctx.resume();
1503
1522
  }
1523
+ try {
1524
+ this.gain.connect(this.ctx.destination);
1525
+ } catch {
1526
+ }
1504
1527
  if (this.state === "paused") {
1505
1528
  this.ctxTimeAtAnchor = this.ctx.currentTime;
1506
1529
  this.state = "playing";
@@ -1527,6 +1550,10 @@ var AudioOutput = class {
1527
1550
  this.mediaTimeOfAnchor = this.now();
1528
1551
  this.state = "paused";
1529
1552
  if (this.noAudio) return;
1553
+ try {
1554
+ this.gain.disconnect();
1555
+ } catch {
1556
+ }
1530
1557
  if (this.ctx.state === "running") {
1531
1558
  await this.ctx.suspend();
1532
1559
  }
@@ -2122,6 +2149,14 @@ async function createHybridSession(ctx, target, transport) {
2122
2149
  get: () => ctx.duration ?? NaN
2123
2150
  });
2124
2151
  }
2152
+ Object.defineProperty(target, "playbackRate", {
2153
+ configurable: true,
2154
+ get: () => audio.getPlaybackRate(),
2155
+ set: (v) => {
2156
+ audio.setPlaybackRate(v);
2157
+ target.dispatchEvent(new Event("ratechange"));
2158
+ }
2159
+ });
2125
2160
  Object.defineProperty(target, "readyState", {
2126
2161
  configurable: true,
2127
2162
  get: () => {
@@ -2232,6 +2267,7 @@ async function createHybridSession(ctx, target, transport) {
2232
2267
  delete target.muted;
2233
2268
  delete target.readyState;
2234
2269
  delete target.seekable;
2270
+ delete target.playbackRate;
2235
2271
  } catch {
2236
2272
  }
2237
2273
  },
@@ -2767,6 +2803,14 @@ async function createFallbackSession(ctx, target, transport) {
2767
2803
  get: () => ctx.duration ?? NaN
2768
2804
  });
2769
2805
  }
2806
+ Object.defineProperty(target, "playbackRate", {
2807
+ configurable: true,
2808
+ get: () => audio.getPlaybackRate(),
2809
+ set: (v) => {
2810
+ audio.setPlaybackRate(v);
2811
+ target.dispatchEvent(new Event("ratechange"));
2812
+ }
2813
+ });
2770
2814
  Object.defineProperty(target, "readyState", {
2771
2815
  configurable: true,
2772
2816
  get: () => {
@@ -2898,6 +2942,7 @@ async function createFallbackSession(ctx, target, transport) {
2898
2942
  delete target.muted;
2899
2943
  delete target.readyState;
2900
2944
  delete target.seekable;
2945
+ delete target.playbackRate;
2901
2946
  } catch {
2902
2947
  }
2903
2948
  },
@@ -3008,7 +3053,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3008
3053
  switchingPromise = Promise.resolve();
3009
3054
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
3010
3055
  // Revoked at destroy() so repeated source swaps don't leak.
3011
- subtitleResources = new chunkS4WAZC2T_cjs.SubtitleResourceBag();
3056
+ subtitleResources = new chunkWRKO6Q42_cjs.SubtitleResourceBag();
3012
3057
  // Transport config extracted from CreatePlayerOptions. Threaded to probe,
3013
3058
  // subtitle fetches, and strategy session creators. Not stored on MediaContext
3014
3059
  // because it's runtime config, not media analysis.
@@ -3054,7 +3099,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3054
3099
  }
3055
3100
  }
3056
3101
  if (this.options.directory && this.options.source instanceof File) {
3057
- const found = await chunkS4WAZC2T_cjs.discoverSidecars(this.options.source, this.options.directory);
3102
+ const found = await chunkWRKO6Q42_cjs.discoverSidecars(this.options.source, this.options.directory);
3058
3103
  for (const s of found) {
3059
3104
  this.subtitleResources.track(s.url);
3060
3105
  ctx.subtitleTracks.push({
@@ -3077,7 +3122,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3077
3122
  reason: decision.reason
3078
3123
  });
3079
3124
  await this.startSession(decision.strategy, decision.reason);
3080
- await chunkS4WAZC2T_cjs.attachSubtitleTracks(
3125
+ await chunkWRKO6Q42_cjs.attachSubtitleTracks(
3081
3126
  this.options.target,
3082
3127
  ctx.subtitleTracks,
3083
3128
  this.subtitleResources,
@@ -3506,5 +3551,5 @@ exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
3506
3551
  exports.UnifiedPlayer = UnifiedPlayer;
3507
3552
  exports.classifyContext = classifyContext;
3508
3553
  exports.createPlayer = createPlayer;
3509
- //# sourceMappingURL=chunk-EY6DZEDT.cjs.map
3510
- //# sourceMappingURL=chunk-EY6DZEDT.cjs.map
3554
+ //# sourceMappingURL=chunk-37UOSAVI.cjs.map
3555
+ //# sourceMappingURL=chunk-37UOSAVI.cjs.map