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.d.cts CHANGED
@@ -345,6 +345,8 @@ declare class AvbridgePlayerElement extends HTMLElement {
345
345
  private _state;
346
346
  private _controlsTimer;
347
347
  private _settingsOpen;
348
+ private _activeAudioTrackId;
349
+ private _activeSubtitleTrackId;
348
350
  private _userSeeking;
349
351
  private _holdTimer;
350
352
  private _holdSpeedActive;
@@ -369,6 +371,10 @@ declare class AvbridgePlayerElement extends HTMLElement {
369
371
  private _onSeekCommit;
370
372
  /** Linear click-to-time mapping across the full track width (no edge clamping). */
371
373
  private _timeFromSeekPointer;
374
+ /** Seekbar width below which drag-to-scrub seeks in real-time (vs
375
+ * preview-only). On narrow bars precise positioning is hard, so
376
+ * immediate video feedback is more useful than a time tooltip. */
377
+ private static readonly SCRUB_WIDTH_THRESHOLD;
372
378
  private _onSeekPointerDown;
373
379
  private _onSeekHover;
374
380
  private _updateSeekTooltip;
@@ -399,8 +405,14 @@ declare class AvbridgePlayerElement extends HTMLElement {
399
405
  showControls(durationMs?: number): void;
400
406
  private _showControls;
401
407
  private _scheduleHide;
408
+ /** Read the controls-timeout attribute. 0 or negative = never hide.
409
+ * Unset = default 3000ms. */
410
+ private _getControlsTimeout;
402
411
  /** Track whether the last interaction was touch so click handler can skip. */
403
412
  private _lastPointerTypeWasTouch;
413
+ /** True for ~50ms after a touch double-tap was handled, so the
414
+ * synthetic dblclick from the browser doesn't also fire fullscreen. */
415
+ private _touchDoubleTapConsumed;
404
416
  /** True if the event's composed path passes through consumer-slotted
405
417
  * content (toolbar or content-overlay). Slotted content lives in the
406
418
  * light DOM so `.closest(".avp-toolbar-top")` on the event target won't
@@ -412,6 +424,9 @@ declare class AvbridgePlayerElement extends HTMLElement {
412
424
  private _onPointerUp;
413
425
  private _cancelHold;
414
426
  private _doDoubleTap;
427
+ /** Duration of one frame in seconds, derived from diagnostics fps or
428
+ * a 30fps default. Used for frame-step shortcuts (`,` / `.`). */
429
+ private _frameDuration;
415
430
  private _onKeydown;
416
431
  private _clearTimers;
417
432
  get src(): string;
package/dist/player.d.ts CHANGED
@@ -345,6 +345,8 @@ declare class AvbridgePlayerElement extends HTMLElement {
345
345
  private _state;
346
346
  private _controlsTimer;
347
347
  private _settingsOpen;
348
+ private _activeAudioTrackId;
349
+ private _activeSubtitleTrackId;
348
350
  private _userSeeking;
349
351
  private _holdTimer;
350
352
  private _holdSpeedActive;
@@ -369,6 +371,10 @@ declare class AvbridgePlayerElement extends HTMLElement {
369
371
  private _onSeekCommit;
370
372
  /** Linear click-to-time mapping across the full track width (no edge clamping). */
371
373
  private _timeFromSeekPointer;
374
+ /** Seekbar width below which drag-to-scrub seeks in real-time (vs
375
+ * preview-only). On narrow bars precise positioning is hard, so
376
+ * immediate video feedback is more useful than a time tooltip. */
377
+ private static readonly SCRUB_WIDTH_THRESHOLD;
372
378
  private _onSeekPointerDown;
373
379
  private _onSeekHover;
374
380
  private _updateSeekTooltip;
@@ -399,8 +405,14 @@ declare class AvbridgePlayerElement extends HTMLElement {
399
405
  showControls(durationMs?: number): void;
400
406
  private _showControls;
401
407
  private _scheduleHide;
408
+ /** Read the controls-timeout attribute. 0 or negative = never hide.
409
+ * Unset = default 3000ms. */
410
+ private _getControlsTimeout;
402
411
  /** Track whether the last interaction was touch so click handler can skip. */
403
412
  private _lastPointerTypeWasTouch;
413
+ /** True for ~50ms after a touch double-tap was handled, so the
414
+ * synthetic dblclick from the browser doesn't also fire fullscreen. */
415
+ private _touchDoubleTapConsumed;
404
416
  /** True if the event's composed path passes through consumer-slotted
405
417
  * content (toolbar or content-overlay). Slotted content lives in the
406
418
  * light DOM so `.closest(".avp-toolbar-top")` on the event target won't
@@ -412,6 +424,9 @@ declare class AvbridgePlayerElement extends HTMLElement {
412
424
  private _onPointerUp;
413
425
  private _cancelHold;
414
426
  private _doDoubleTap;
427
+ /** Duration of one frame in seconds, derived from diagnostics fps or
428
+ * a 30fps default. Used for frame-step shortcuts (`,` / `.`). */
429
+ private _frameDuration;
415
430
  private _onKeydown;
416
431
  private _clearTimers;
417
432
  get src(): string;
package/dist/player.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AvbridgeError, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, normalizeSource, sniffNormalizedSource, ERR_PROBE_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_LIBAV_NOT_REACHABLE, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-E76AMWI4.js';
2
2
  import { dbg, loadLibav } from './chunk-IAYKFGFG.js';
3
3
  import './chunk-DCSOQH2N.js';
4
- import { SubtitleResourceBag, discoverSidecars, attachSubtitleTracks, SubtitleOverlay } from './chunk-5KVLE6YI.js';
4
+ import { SubtitleResourceBag, discoverSidecars, attachSubtitleTracks, SubtitleOverlay } from './chunk-EDDWAN2L.js';
5
5
  import './chunk-LUFA47FP.js';
6
6
 
7
7
  // src/events.ts
@@ -1749,6 +1749,10 @@ var AudioOutput = class {
1749
1749
  if (this.ctx.state === "suspended") {
1750
1750
  await this.ctx.resume();
1751
1751
  }
1752
+ try {
1753
+ this.gain.connect(this.ctx.destination);
1754
+ } catch {
1755
+ }
1752
1756
  if (this.state === "paused") {
1753
1757
  this.ctxTimeAtAnchor = this.ctx.currentTime;
1754
1758
  this.state = "playing";
@@ -1775,6 +1779,10 @@ var AudioOutput = class {
1775
1779
  this.mediaTimeOfAnchor = this.now();
1776
1780
  this.state = "paused";
1777
1781
  if (this.noAudio) return;
1782
+ try {
1783
+ this.gain.disconnect();
1784
+ } catch {
1785
+ }
1778
1786
  if (this.ctx.state === "running") {
1779
1787
  await this.ctx.suspend();
1780
1788
  }
@@ -4565,7 +4573,7 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4565
4573
  * strategies pick up the new track via their textTracks watcher.
4566
4574
  */
4567
4575
  async addSubtitle(subtitle) {
4568
- const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-4T74JRGT.js');
4576
+ const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-5H24MEBJ.js');
4569
4577
  const format = subtitle.format ?? (subtitle.url.endsWith(".srt") ? "srt" : "vtt");
4570
4578
  const track = {
4571
4579
  id: this._subtitleTracks.length,
@@ -4574,14 +4582,27 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4574
4582
  sidecarUrl: subtitle.url
4575
4583
  };
4576
4584
  this._subtitleTracks.push(track);
4585
+ console.log(`[avbridge:subs] addSubtitle id=${track.id} format=${format} lang=${subtitle.language ?? "?"}`);
4577
4586
  await attachSubtitleTracks2(
4578
4587
  this._videoEl,
4579
4588
  this._subtitleTracks,
4580
4589
  void 0,
4581
4590
  (err, t) => {
4582
- console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
4591
+ console.warn(`[avbridge:subs] subtitle ${t.id} failed: ${err.message}`);
4583
4592
  }
4584
4593
  );
4594
+ const textTracks = this._videoEl.textTracks;
4595
+ for (let i = 0; i < textTracks.length; i++) {
4596
+ if (textTracks[i].label === (subtitle.language ?? `Subtitle ${track.id}`)) {
4597
+ textTracks[i].mode = "showing";
4598
+ console.log(`[avbridge:subs] enabled textTrack[${i}] mode=showing`);
4599
+ break;
4600
+ }
4601
+ }
4602
+ this._dispatch("trackschange", {
4603
+ audioTracks: this._audioTracks,
4604
+ subtitleTracks: this.subtitleTracks
4605
+ });
4585
4606
  }
4586
4607
  /**
4587
4608
  * Disable the automatic `screen.orientation.lock()` that runs on
@@ -5395,7 +5416,7 @@ function formatTime(sec) {
5395
5416
  return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
5396
5417
  }
5397
5418
  var PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
5398
- var CONTROLS_HIDE_MS = 3e3;
5419
+ var DEFAULT_CONTROLS_HIDE_MS = 3e3;
5399
5420
  var FORWARDED_EVENTS = [
5400
5421
  "ready",
5401
5422
  "error",
@@ -5438,7 +5459,7 @@ var PROXY_ATTRIBUTES = [
5438
5459
  ];
5439
5460
  var PLAYER_ATTRIBUTES = ["show-fit"];
5440
5461
  var FIT_MODES = ["contain", "cover", "fill"];
5441
- var AvbridgePlayerElement = class extends HTMLElement {
5462
+ var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
5442
5463
  static observedAttributes = [...PROXY_ATTRIBUTES, ...PLAYER_ATTRIBUTES];
5443
5464
  // ── Internal DOM refs ──────────────────────────────────────────────────
5444
5465
  _video;
@@ -5466,6 +5487,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
5466
5487
  _state = "idle";
5467
5488
  _controlsTimer = null;
5468
5489
  _settingsOpen = false;
5490
+ _activeAudioTrackId = null;
5491
+ _activeSubtitleTrackId = null;
5469
5492
  _userSeeking = false;
5470
5493
  _holdTimer = null;
5471
5494
  _holdSpeedActive = false;
@@ -5729,6 +5752,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
5729
5752
  const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
5730
5753
  return frac * (this._video.duration || 0);
5731
5754
  }
5755
+ /** Seekbar width below which drag-to-scrub seeks in real-time (vs
5756
+ * preview-only). On narrow bars precise positioning is hard, so
5757
+ * immediate video feedback is more useful than a time tooltip. */
5758
+ static SCRUB_WIDTH_THRESHOLD = 400;
5732
5759
  _onSeekPointerDown(e) {
5733
5760
  if (e.button !== 0 && e.pointerType === "mouse") return;
5734
5761
  e.preventDefault();
@@ -5736,15 +5763,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
5736
5763
  const seekBar = this.shadowRoot.querySelector(".avp-seek");
5737
5764
  seekBar.setPointerCapture(e.pointerId);
5738
5765
  seekBar.setAttribute("data-seeking", "");
5766
+ const scrubMode = seekBar.getBoundingClientRect().width < _AvbridgePlayerElement.SCRUB_WIDTH_THRESHOLD;
5767
+ let lastScrubCommit = 0;
5739
5768
  const initial = this._timeFromSeekPointer(e.clientX);
5740
5769
  this._seekInput.value = String(initial);
5741
5770
  this._onSeekInput();
5742
5771
  this._updateSeekTooltip(e.clientX);
5772
+ if (scrubMode) this._onSeekCommit();
5743
5773
  const onMove = (ev) => {
5744
5774
  const t = this._timeFromSeekPointer(ev.clientX);
5745
5775
  this._seekInput.value = String(t);
5746
5776
  this._onSeekInput();
5747
5777
  this._updateSeekTooltip(ev.clientX);
5778
+ if (scrubMode) {
5779
+ const now = performance.now();
5780
+ if (now - lastScrubCommit > 250) {
5781
+ lastScrubCommit = now;
5782
+ this._onSeekCommit();
5783
+ this._userSeeking = true;
5784
+ }
5785
+ }
5748
5786
  };
5749
5787
  const onUp = (ev) => {
5750
5788
  const t = this._timeFromSeekPointer(ev.clientX);
@@ -5845,19 +5883,27 @@ var AvbridgePlayerElement = class extends HTMLElement {
5845
5883
  sections.push(selectRow("Speed", speedValue, speedOpts, `data-action="speed"`));
5846
5884
  const audios = this._video.audioTracks ?? [];
5847
5885
  if (audios.length > 1) {
5886
+ const activeAudioId = this._activeAudioTrackId ?? audios[0]?.id;
5887
+ const activeAudio = audios.find((t) => t.id === activeAudioId) ?? audios[0];
5888
+ const audioValue = activeAudio?.language ?? `Track ${activeAudio?.id ?? 1}`;
5848
5889
  let audioOpts = "";
5849
5890
  for (const t of audios) {
5850
- audioOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
5891
+ const sel = t.id === activeAudioId ? " selected" : "";
5892
+ audioOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
5851
5893
  }
5852
- sections.push(selectRow("Audio", audios[0]?.language ?? "Track 1", audioOpts, `data-action="audio"`));
5894
+ sections.push(selectRow("Audio", audioValue, audioOpts, `data-action="audio"`));
5853
5895
  }
5854
5896
  const subs = this._video.subtitleTracks ?? [];
5855
5897
  if (subs.length > 0) {
5856
- let subOpts = `<option value="-1" selected>Off</option>`;
5898
+ const activeSubId = this._activeSubtitleTrackId;
5899
+ const activeSub = activeSubId != null ? subs.find((t) => t.id === activeSubId) : null;
5900
+ const subValue = activeSub ? activeSub.language ?? `Track ${activeSub.id}` : "Off";
5901
+ let subOpts = `<option value="-1"${activeSubId == null ? " selected" : ""}>Off</option>`;
5857
5902
  for (const t of subs) {
5858
- subOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
5903
+ const sel = t.id === activeSubId ? " selected" : "";
5904
+ subOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
5859
5905
  }
5860
- sections.push(selectRow("Subtitles", "Off", subOpts, `data-action="subtitle"`));
5906
+ sections.push(selectRow("Subtitles", subValue, subOpts, `data-action="subtitle"`));
5861
5907
  }
5862
5908
  if (this.hasAttribute("show-fit")) {
5863
5909
  const currentFit = this._video.fit ?? "contain";
@@ -5897,11 +5943,15 @@ var AvbridgePlayerElement = class extends HTMLElement {
5897
5943
  this._video.playbackRate = Number(val);
5898
5944
  break;
5899
5945
  case "audio":
5946
+ this._activeAudioTrackId = Number(val);
5900
5947
  void this._video.setAudioTrack(Number(val));
5901
5948
  break;
5902
- case "subtitle":
5903
- void this._video.setSubtitleTrack(Number(val) >= 0 ? Number(val) : null);
5949
+ case "subtitle": {
5950
+ const subId = Number(val);
5951
+ this._activeSubtitleTrackId = subId >= 0 ? subId : null;
5952
+ void this._video.setSubtitleTrack(subId >= 0 ? subId : null);
5904
5953
  break;
5954
+ }
5905
5955
  case "fit":
5906
5956
  this.setAttribute("fit", val);
5907
5957
  break;
@@ -5996,16 +6046,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
5996
6046
  _showControls() {
5997
6047
  this.showControls();
5998
6048
  }
5999
- _scheduleHide(durationMs = CONTROLS_HIDE_MS) {
6049
+ _scheduleHide(durationMs) {
6050
+ const ms = durationMs ?? this._getControlsTimeout();
6000
6051
  if (this._controlsTimer) clearTimeout(this._controlsTimer);
6001
6052
  if (this._state !== "playing" && this._state !== "buffering") return;
6002
6053
  if (this._settingsOpen) return;
6054
+ if (ms <= 0) return;
6003
6055
  this._controlsTimer = setTimeout(() => {
6004
6056
  if (this._state === "playing") {
6005
6057
  this.setAttribute("data-controls-hidden", "");
6006
6058
  this._toolbarTop.setAttribute("data-visible", "false");
6007
6059
  }
6008
- }, durationMs);
6060
+ }, ms);
6061
+ }
6062
+ /** Read the controls-timeout attribute. 0 or negative = never hide.
6063
+ * Unset = default 3000ms. */
6064
+ _getControlsTimeout() {
6065
+ const attr = this.getAttribute("controls-timeout");
6066
+ if (attr == null) return DEFAULT_CONTROLS_HIDE_MS;
6067
+ const n = Number(attr);
6068
+ return Number.isFinite(n) ? n : DEFAULT_CONTROLS_HIDE_MS;
6009
6069
  }
6010
6070
  // Strategy is visible in Stats for Nerds, no badge in controls bar.
6011
6071
  // ── Click / tap handling (YouTube delayed-tap pattern) ──────────────────
@@ -6017,6 +6077,9 @@ var AvbridgePlayerElement = class extends HTMLElement {
6017
6077
  // it's treated as a double-click and the single-click action is cancelled.
6018
6078
  /** Track whether the last interaction was touch so click handler can skip. */
6019
6079
  _lastPointerTypeWasTouch = false;
6080
+ /** True for ~50ms after a touch double-tap was handled, so the
6081
+ * synthetic dblclick from the browser doesn't also fire fullscreen. */
6082
+ _touchDoubleTapConsumed = false;
6020
6083
  /** True if the event's composed path passes through consumer-slotted
6021
6084
  * content (toolbar or content-overlay). Slotted content lives in the
6022
6085
  * light DOM so `.closest(".avp-toolbar-top")` on the event target won't
@@ -6050,6 +6113,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
6050
6113
  _onContainerDblClick(e) {
6051
6114
  if (e.target.closest?.(".avp-controls, .avp-settings")) return;
6052
6115
  if (this._isSlottedContentEvent(e)) return;
6116
+ if (this._touchDoubleTapConsumed) return;
6053
6117
  if (this._tapTimer) {
6054
6118
  clearTimeout(this._tapTimer);
6055
6119
  this._tapTimer = null;
@@ -6091,6 +6155,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
6091
6155
  } else {
6092
6156
  this._toggleFullscreen();
6093
6157
  }
6158
+ this._touchDoubleTapConsumed = true;
6159
+ setTimeout(() => {
6160
+ this._touchDoubleTapConsumed = false;
6161
+ }, 100);
6094
6162
  this._lastTapTime = 0;
6095
6163
  return;
6096
6164
  }
@@ -6124,6 +6192,13 @@ var AvbridgePlayerElement = class extends HTMLElement {
6124
6192
  this._video.currentTime = Math.max(0, this._video.currentTime + delta);
6125
6193
  }
6126
6194
  // ── Keyboard shortcuts ─────────────────────────────────────────────────
6195
+ /** Duration of one frame in seconds, derived from diagnostics fps or
6196
+ * a 30fps default. Used for frame-step shortcuts (`,` / `.`). */
6197
+ _frameDuration() {
6198
+ const diag = this._video.getDiagnostics();
6199
+ const fps = diag?.fps && diag.fps > 0 ? diag.fps : 30;
6200
+ return 1 / fps;
6201
+ }
6127
6202
  _onKeydown(e) {
6128
6203
  if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
6129
6204
  switch (e.key) {
@@ -6168,6 +6243,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
6168
6243
  this._video.playbackRate = Math.max(0.25, this._video.playbackRate - 0.25);
6169
6244
  this._buildSettingsMenu();
6170
6245
  break;
6246
+ case ",":
6247
+ e.preventDefault();
6248
+ if (!this._video.paused) this._video.pause();
6249
+ this._video.currentTime = Math.max(0, this._video.currentTime - this._frameDuration());
6250
+ break;
6251
+ case ".":
6252
+ e.preventDefault();
6253
+ if (!this._video.paused) this._video.pause();
6254
+ this._video.currentTime = Math.min(
6255
+ this._video.duration || 0,
6256
+ this._video.currentTime + this._frameDuration()
6257
+ );
6258
+ break;
6171
6259
  case "Escape":
6172
6260
  if (this._settingsOpen) {
6173
6261
  e.preventDefault();