avbridge 2.10.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 (39) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/{chunk-NQULEIA3.cjs → chunk-37UOSAVI.cjs} +15 -7
  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-3GKM5DFM.js → chunk-IHNHHEA2.js} +11 -3
  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 +23 -1
  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.js +17 -4
  15. package/dist/element.js.map +1 -1
  16. package/dist/index.cjs +10 -10
  17. package/dist/index.js +2 -2
  18. package/dist/player.cjs +106 -18
  19. package/dist/player.cjs.map +1 -1
  20. package/dist/player.d.cts +15 -0
  21. package/dist/player.d.ts +15 -0
  22. package/dist/player.js +102 -14
  23. package/dist/player.js.map +1 -1
  24. package/dist/subtitles-5H24MEBJ.js +4 -0
  25. package/dist/{subtitles-4T74JRGT.js.map → subtitles-5H24MEBJ.js.map} +1 -1
  26. package/dist/subtitles-HMVGWTU2.cjs +29 -0
  27. package/dist/{subtitles-QUH4LPI4.cjs.map → subtitles-HMVGWTU2.cjs.map} +1 -1
  28. package/package.json +1 -1
  29. package/src/element/avbridge-player.ts +92 -10
  30. package/src/element/avbridge-subtitles.ts +273 -0
  31. package/src/element/avbridge-video.ts +21 -1
  32. package/src/strategies/fallback/audio-output.ts +10 -0
  33. package/src/subtitles/index.ts +2 -0
  34. package/dist/chunk-3GKM5DFM.js.map +0 -1
  35. package/dist/chunk-5KVLE6YI.js.map +0 -1
  36. package/dist/chunk-NQULEIA3.cjs.map +0 -1
  37. package/dist/chunk-S4WAZC2T.cjs.map +0 -1
  38. package/dist/subtitles-4T74JRGT.js +0 -4
  39. package/dist/subtitles-QUH4LPI4.cjs +0 -29
package/dist/player.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  var chunk2XW2O3YI_cjs = require('./chunk-2XW2O3YI.cjs');
4
4
  var chunkNNVOHKXJ_cjs = require('./chunk-NNVOHKXJ.cjs');
5
5
  require('./chunk-Z33SBWL5.cjs');
6
- var chunkS4WAZC2T_cjs = require('./chunk-S4WAZC2T.cjs');
6
+ var chunkWRKO6Q42_cjs = require('./chunk-WRKO6Q42.cjs');
7
7
  require('./chunk-QDJLQR53.cjs');
8
8
 
9
9
  // src/events.ts
@@ -1286,7 +1286,7 @@ var VideoRenderer = class {
1286
1286
  }
1287
1287
  target.style.visibility = "hidden";
1288
1288
  const overlayParent = parent instanceof HTMLElement ? parent : document.body;
1289
- this.subtitleOverlay = new chunkS4WAZC2T_cjs.SubtitleOverlay(overlayParent);
1289
+ this.subtitleOverlay = new chunkWRKO6Q42_cjs.SubtitleOverlay(overlayParent);
1290
1290
  this.watchTextTracks(target);
1291
1291
  const ctx = this.canvas.getContext("2d");
1292
1292
  if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
@@ -1751,6 +1751,10 @@ var AudioOutput = class {
1751
1751
  if (this.ctx.state === "suspended") {
1752
1752
  await this.ctx.resume();
1753
1753
  }
1754
+ try {
1755
+ this.gain.connect(this.ctx.destination);
1756
+ } catch {
1757
+ }
1754
1758
  if (this.state === "paused") {
1755
1759
  this.ctxTimeAtAnchor = this.ctx.currentTime;
1756
1760
  this.state = "playing";
@@ -1777,6 +1781,10 @@ var AudioOutput = class {
1777
1781
  this.mediaTimeOfAnchor = this.now();
1778
1782
  this.state = "paused";
1779
1783
  if (this.noAudio) return;
1784
+ try {
1785
+ this.gain.disconnect();
1786
+ } catch {
1787
+ }
1780
1788
  if (this.ctx.state === "running") {
1781
1789
  await this.ctx.suspend();
1782
1790
  }
@@ -3456,7 +3464,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3456
3464
  switchingPromise = Promise.resolve();
3457
3465
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
3458
3466
  // Revoked at destroy() so repeated source swaps don't leak.
3459
- subtitleResources = new chunkS4WAZC2T_cjs.SubtitleResourceBag();
3467
+ subtitleResources = new chunkWRKO6Q42_cjs.SubtitleResourceBag();
3460
3468
  // Transport config extracted from CreatePlayerOptions. Threaded to probe,
3461
3469
  // subtitle fetches, and strategy session creators. Not stored on MediaContext
3462
3470
  // because it's runtime config, not media analysis.
@@ -3502,7 +3510,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3502
3510
  }
3503
3511
  }
3504
3512
  if (this.options.directory && this.options.source instanceof File) {
3505
- const found = await chunkS4WAZC2T_cjs.discoverSidecars(this.options.source, this.options.directory);
3513
+ const found = await chunkWRKO6Q42_cjs.discoverSidecars(this.options.source, this.options.directory);
3506
3514
  for (const s of found) {
3507
3515
  this.subtitleResources.track(s.url);
3508
3516
  ctx.subtitleTracks.push({
@@ -3525,7 +3533,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3525
3533
  reason: decision.reason
3526
3534
  });
3527
3535
  await this.startSession(decision.strategy, decision.reason);
3528
- await chunkS4WAZC2T_cjs.attachSubtitleTracks(
3536
+ await chunkWRKO6Q42_cjs.attachSubtitleTracks(
3529
3537
  this.options.target,
3530
3538
  ctx.subtitleTracks,
3531
3539
  this.subtitleResources,
@@ -4567,7 +4575,7 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4567
4575
  * strategies pick up the new track via their textTracks watcher.
4568
4576
  */
4569
4577
  async addSubtitle(subtitle) {
4570
- const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-QUH4LPI4.cjs');
4578
+ const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-HMVGWTU2.cjs');
4571
4579
  const format = subtitle.format ?? (subtitle.url.endsWith(".srt") ? "srt" : "vtt");
4572
4580
  const track = {
4573
4581
  id: this._subtitleTracks.length,
@@ -4576,14 +4584,27 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4576
4584
  sidecarUrl: subtitle.url
4577
4585
  };
4578
4586
  this._subtitleTracks.push(track);
4587
+ console.log(`[avbridge:subs] addSubtitle id=${track.id} format=${format} lang=${subtitle.language ?? "?"}`);
4579
4588
  await attachSubtitleTracks2(
4580
4589
  this._videoEl,
4581
4590
  this._subtitleTracks,
4582
4591
  void 0,
4583
4592
  (err, t) => {
4584
- console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
4593
+ console.warn(`[avbridge:subs] subtitle ${t.id} failed: ${err.message}`);
4585
4594
  }
4586
4595
  );
4596
+ const textTracks = this._videoEl.textTracks;
4597
+ for (let i = 0; i < textTracks.length; i++) {
4598
+ if (textTracks[i].label === (subtitle.language ?? `Subtitle ${track.id}`)) {
4599
+ textTracks[i].mode = "showing";
4600
+ console.log(`[avbridge:subs] enabled textTrack[${i}] mode=showing`);
4601
+ break;
4602
+ }
4603
+ }
4604
+ this._dispatch("trackschange", {
4605
+ audioTracks: this._audioTracks,
4606
+ subtitleTracks: this.subtitleTracks
4607
+ });
4587
4608
  }
4588
4609
  /**
4589
4610
  * Disable the automatic `screen.orientation.lock()` that runs on
@@ -5397,7 +5418,7 @@ function formatTime(sec) {
5397
5418
  return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
5398
5419
  }
5399
5420
  var PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
5400
- var CONTROLS_HIDE_MS = 3e3;
5421
+ var DEFAULT_CONTROLS_HIDE_MS = 3e3;
5401
5422
  var FORWARDED_EVENTS = [
5402
5423
  "ready",
5403
5424
  "error",
@@ -5440,7 +5461,7 @@ var PROXY_ATTRIBUTES = [
5440
5461
  ];
5441
5462
  var PLAYER_ATTRIBUTES = ["show-fit"];
5442
5463
  var FIT_MODES = ["contain", "cover", "fill"];
5443
- var AvbridgePlayerElement = class extends HTMLElement {
5464
+ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
5444
5465
  static observedAttributes = [...PROXY_ATTRIBUTES, ...PLAYER_ATTRIBUTES];
5445
5466
  // ── Internal DOM refs ──────────────────────────────────────────────────
5446
5467
  _video;
@@ -5468,6 +5489,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
5468
5489
  _state = "idle";
5469
5490
  _controlsTimer = null;
5470
5491
  _settingsOpen = false;
5492
+ _activeAudioTrackId = null;
5493
+ _activeSubtitleTrackId = null;
5471
5494
  _userSeeking = false;
5472
5495
  _holdTimer = null;
5473
5496
  _holdSpeedActive = false;
@@ -5731,6 +5754,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
5731
5754
  const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
5732
5755
  return frac * (this._video.duration || 0);
5733
5756
  }
5757
+ /** Seekbar width below which drag-to-scrub seeks in real-time (vs
5758
+ * preview-only). On narrow bars precise positioning is hard, so
5759
+ * immediate video feedback is more useful than a time tooltip. */
5760
+ static SCRUB_WIDTH_THRESHOLD = 400;
5734
5761
  _onSeekPointerDown(e) {
5735
5762
  if (e.button !== 0 && e.pointerType === "mouse") return;
5736
5763
  e.preventDefault();
@@ -5738,15 +5765,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
5738
5765
  const seekBar = this.shadowRoot.querySelector(".avp-seek");
5739
5766
  seekBar.setPointerCapture(e.pointerId);
5740
5767
  seekBar.setAttribute("data-seeking", "");
5768
+ const scrubMode = seekBar.getBoundingClientRect().width < _AvbridgePlayerElement.SCRUB_WIDTH_THRESHOLD;
5769
+ let lastScrubCommit = 0;
5741
5770
  const initial = this._timeFromSeekPointer(e.clientX);
5742
5771
  this._seekInput.value = String(initial);
5743
5772
  this._onSeekInput();
5744
5773
  this._updateSeekTooltip(e.clientX);
5774
+ if (scrubMode) this._onSeekCommit();
5745
5775
  const onMove = (ev) => {
5746
5776
  const t = this._timeFromSeekPointer(ev.clientX);
5747
5777
  this._seekInput.value = String(t);
5748
5778
  this._onSeekInput();
5749
5779
  this._updateSeekTooltip(ev.clientX);
5780
+ if (scrubMode) {
5781
+ const now = performance.now();
5782
+ if (now - lastScrubCommit > 250) {
5783
+ lastScrubCommit = now;
5784
+ this._onSeekCommit();
5785
+ this._userSeeking = true;
5786
+ }
5787
+ }
5750
5788
  };
5751
5789
  const onUp = (ev) => {
5752
5790
  const t = this._timeFromSeekPointer(ev.clientX);
@@ -5847,19 +5885,27 @@ var AvbridgePlayerElement = class extends HTMLElement {
5847
5885
  sections.push(selectRow("Speed", speedValue, speedOpts, `data-action="speed"`));
5848
5886
  const audios = this._video.audioTracks ?? [];
5849
5887
  if (audios.length > 1) {
5888
+ const activeAudioId = this._activeAudioTrackId ?? audios[0]?.id;
5889
+ const activeAudio = audios.find((t) => t.id === activeAudioId) ?? audios[0];
5890
+ const audioValue = activeAudio?.language ?? `Track ${activeAudio?.id ?? 1}`;
5850
5891
  let audioOpts = "";
5851
5892
  for (const t of audios) {
5852
- audioOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
5893
+ const sel = t.id === activeAudioId ? " selected" : "";
5894
+ audioOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
5853
5895
  }
5854
- sections.push(selectRow("Audio", audios[0]?.language ?? "Track 1", audioOpts, `data-action="audio"`));
5896
+ sections.push(selectRow("Audio", audioValue, audioOpts, `data-action="audio"`));
5855
5897
  }
5856
5898
  const subs = this._video.subtitleTracks ?? [];
5857
5899
  if (subs.length > 0) {
5858
- let subOpts = `<option value="-1" selected>Off</option>`;
5900
+ const activeSubId = this._activeSubtitleTrackId;
5901
+ const activeSub = activeSubId != null ? subs.find((t) => t.id === activeSubId) : null;
5902
+ const subValue = activeSub ? activeSub.language ?? `Track ${activeSub.id}` : "Off";
5903
+ let subOpts = `<option value="-1"${activeSubId == null ? " selected" : ""}>Off</option>`;
5859
5904
  for (const t of subs) {
5860
- subOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
5905
+ const sel = t.id === activeSubId ? " selected" : "";
5906
+ subOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
5861
5907
  }
5862
- sections.push(selectRow("Subtitles", "Off", subOpts, `data-action="subtitle"`));
5908
+ sections.push(selectRow("Subtitles", subValue, subOpts, `data-action="subtitle"`));
5863
5909
  }
5864
5910
  if (this.hasAttribute("show-fit")) {
5865
5911
  const currentFit = this._video.fit ?? "contain";
@@ -5899,11 +5945,15 @@ var AvbridgePlayerElement = class extends HTMLElement {
5899
5945
  this._video.playbackRate = Number(val);
5900
5946
  break;
5901
5947
  case "audio":
5948
+ this._activeAudioTrackId = Number(val);
5902
5949
  void this._video.setAudioTrack(Number(val));
5903
5950
  break;
5904
- case "subtitle":
5905
- void this._video.setSubtitleTrack(Number(val) >= 0 ? Number(val) : null);
5951
+ case "subtitle": {
5952
+ const subId = Number(val);
5953
+ this._activeSubtitleTrackId = subId >= 0 ? subId : null;
5954
+ void this._video.setSubtitleTrack(subId >= 0 ? subId : null);
5906
5955
  break;
5956
+ }
5907
5957
  case "fit":
5908
5958
  this.setAttribute("fit", val);
5909
5959
  break;
@@ -5998,16 +6048,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
5998
6048
  _showControls() {
5999
6049
  this.showControls();
6000
6050
  }
6001
- _scheduleHide(durationMs = CONTROLS_HIDE_MS) {
6051
+ _scheduleHide(durationMs) {
6052
+ const ms = durationMs ?? this._getControlsTimeout();
6002
6053
  if (this._controlsTimer) clearTimeout(this._controlsTimer);
6003
6054
  if (this._state !== "playing" && this._state !== "buffering") return;
6004
6055
  if (this._settingsOpen) return;
6056
+ if (ms <= 0) return;
6005
6057
  this._controlsTimer = setTimeout(() => {
6006
6058
  if (this._state === "playing") {
6007
6059
  this.setAttribute("data-controls-hidden", "");
6008
6060
  this._toolbarTop.setAttribute("data-visible", "false");
6009
6061
  }
6010
- }, durationMs);
6062
+ }, ms);
6063
+ }
6064
+ /** Read the controls-timeout attribute. 0 or negative = never hide.
6065
+ * Unset = default 3000ms. */
6066
+ _getControlsTimeout() {
6067
+ const attr = this.getAttribute("controls-timeout");
6068
+ if (attr == null) return DEFAULT_CONTROLS_HIDE_MS;
6069
+ const n = Number(attr);
6070
+ return Number.isFinite(n) ? n : DEFAULT_CONTROLS_HIDE_MS;
6011
6071
  }
6012
6072
  // Strategy is visible in Stats for Nerds, no badge in controls bar.
6013
6073
  // ── Click / tap handling (YouTube delayed-tap pattern) ──────────────────
@@ -6019,6 +6079,9 @@ var AvbridgePlayerElement = class extends HTMLElement {
6019
6079
  // it's treated as a double-click and the single-click action is cancelled.
6020
6080
  /** Track whether the last interaction was touch so click handler can skip. */
6021
6081
  _lastPointerTypeWasTouch = false;
6082
+ /** True for ~50ms after a touch double-tap was handled, so the
6083
+ * synthetic dblclick from the browser doesn't also fire fullscreen. */
6084
+ _touchDoubleTapConsumed = false;
6022
6085
  /** True if the event's composed path passes through consumer-slotted
6023
6086
  * content (toolbar or content-overlay). Slotted content lives in the
6024
6087
  * light DOM so `.closest(".avp-toolbar-top")` on the event target won't
@@ -6052,6 +6115,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
6052
6115
  _onContainerDblClick(e) {
6053
6116
  if (e.target.closest?.(".avp-controls, .avp-settings")) return;
6054
6117
  if (this._isSlottedContentEvent(e)) return;
6118
+ if (this._touchDoubleTapConsumed) return;
6055
6119
  if (this._tapTimer) {
6056
6120
  clearTimeout(this._tapTimer);
6057
6121
  this._tapTimer = null;
@@ -6093,6 +6157,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
6093
6157
  } else {
6094
6158
  this._toggleFullscreen();
6095
6159
  }
6160
+ this._touchDoubleTapConsumed = true;
6161
+ setTimeout(() => {
6162
+ this._touchDoubleTapConsumed = false;
6163
+ }, 100);
6096
6164
  this._lastTapTime = 0;
6097
6165
  return;
6098
6166
  }
@@ -6126,6 +6194,13 @@ var AvbridgePlayerElement = class extends HTMLElement {
6126
6194
  this._video.currentTime = Math.max(0, this._video.currentTime + delta);
6127
6195
  }
6128
6196
  // ── Keyboard shortcuts ─────────────────────────────────────────────────
6197
+ /** Duration of one frame in seconds, derived from diagnostics fps or
6198
+ * a 30fps default. Used for frame-step shortcuts (`,` / `.`). */
6199
+ _frameDuration() {
6200
+ const diag = this._video.getDiagnostics();
6201
+ const fps = diag?.fps && diag.fps > 0 ? diag.fps : 30;
6202
+ return 1 / fps;
6203
+ }
6129
6204
  _onKeydown(e) {
6130
6205
  if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
6131
6206
  switch (e.key) {
@@ -6170,6 +6245,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
6170
6245
  this._video.playbackRate = Math.max(0.25, this._video.playbackRate - 0.25);
6171
6246
  this._buildSettingsMenu();
6172
6247
  break;
6248
+ case ",":
6249
+ e.preventDefault();
6250
+ if (!this._video.paused) this._video.pause();
6251
+ this._video.currentTime = Math.max(0, this._video.currentTime - this._frameDuration());
6252
+ break;
6253
+ case ".":
6254
+ e.preventDefault();
6255
+ if (!this._video.paused) this._video.pause();
6256
+ this._video.currentTime = Math.min(
6257
+ this._video.duration || 0,
6258
+ this._video.currentTime + this._frameDuration()
6259
+ );
6260
+ break;
6173
6261
  case "Escape":
6174
6262
  if (this._settingsOpen) {
6175
6263
  e.preventDefault();