avbridge 2.7.0 → 2.8.2

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.js CHANGED
@@ -494,6 +494,22 @@ function classifyContext(ctx) {
494
494
  };
495
495
  }
496
496
  if (REMUXABLE_CONTAINERS.has(ctx.container)) {
497
+ const mime = mp4MimeFor(video, audio);
498
+ if (mime && typeof MediaSource !== "undefined" && !mseSupports(mime)) {
499
+ if (webCodecsAvailable()) {
500
+ return {
501
+ class: "HYBRID_CANDIDATE",
502
+ strategy: "hybrid",
503
+ reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime \u2014 routing to WebCodecs hardware decode`,
504
+ fallbackChain: ["fallback"]
505
+ };
506
+ }
507
+ return {
508
+ class: "FALLBACK_REQUIRED",
509
+ strategy: "fallback",
510
+ reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime and WebCodecs is unavailable \u2014 falling back to WASM decode`
511
+ };
512
+ }
497
513
  return {
498
514
  class: "REMUX_CANDIDATE",
499
515
  strategy: "remux",
@@ -1237,7 +1253,7 @@ var VideoRenderer = class {
1237
1253
  this.resolveFirstFrame = resolve;
1238
1254
  });
1239
1255
  this.canvas = document.createElement("canvas");
1240
- this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
1256
+ this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:var(--avbridge-fit, contain);";
1241
1257
  const parent = target.parentElement ?? target.parentNode;
1242
1258
  if (parent && parent instanceof HTMLElement) {
1243
1259
  if (getComputedStyle(parent).position === "static") {
@@ -2580,7 +2596,7 @@ async function createHybridSession(ctx, target, transport) {
2580
2596
 
2581
2597
  // src/strategies/fallback/decoder.ts
2582
2598
  async function startDecoder(opts) {
2583
- const variant = pickLibavVariant(opts.context);
2599
+ const variant = "avbridge";
2584
2600
  const libav = await loadLibav(variant);
2585
2601
  const bridge = await loadBridge2();
2586
2602
  const { prepareLibavInput } = await import('./libav-http-reader-WXG3Z7AI.js');
@@ -2636,9 +2652,8 @@ async function startDecoder(opts) {
2636
2652
  videoStream ? `video: ${opts.context.videoTracks[0]?.codec ?? "unknown"}` : null,
2637
2653
  audioStream ? `audio: ${opts.context.audioTracks[0]?.codec ?? "unknown"}` : null
2638
2654
  ].filter(Boolean).join(", ");
2639
- const hint = variant === "webcodecs" ? ` The "${variant}" libav variant does not include software decoders for these codecs. Try the custom "avbridge" variant (scripts/build-libav.sh) for broader codec support, or use a lighter strategy (native, remux, hybrid) instead.` : "";
2640
2655
  throw new Error(
2641
- `fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
2656
+ `fallback decoder: could not initialize any libav decoders (${codecs}). The "${variant}" libav variant lacks software decoders for these codecs \u2014 rebuild with scripts/build-libav.sh including the missing decoder, or use a lighter strategy (native, remux, hybrid) instead.`
2642
2657
  );
2643
2658
  }
2644
2659
  let bsfCtx = null;
@@ -3765,6 +3780,8 @@ var PREFERRED_STRATEGY_VALUES = /* @__PURE__ */ new Set([
3765
3780
  "hybrid",
3766
3781
  "fallback"
3767
3782
  ]);
3783
+ var FIT_VALUES = /* @__PURE__ */ new Set(["contain", "cover", "fill"]);
3784
+ var DEFAULT_FIT = "contain";
3768
3785
  var FORWARDED_VIDEO_EVENTS = [
3769
3786
  "loadstart",
3770
3787
  "loadedmetadata",
@@ -3799,7 +3816,9 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
3799
3816
  "crossorigin",
3800
3817
  "disableremoteplayback",
3801
3818
  "diagnostics",
3802
- "preferstrategy"
3819
+ "preferstrategy",
3820
+ "fit",
3821
+ "no-orientation-lock"
3803
3822
  ];
3804
3823
  // ── Internal state ─────────────────────────────────────────────────────
3805
3824
  /** The shadow DOM `<video>` element that strategies render into. */
@@ -3851,23 +3870,38 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
3851
3870
  * native fails.
3852
3871
  */
3853
3872
  _preferredStrategy = "auto";
3873
+ /** Current fit mode. Applied to the inner `<video>` via object-fit, and
3874
+ * to the fallback canvas via the `--avbridge-fit` CSS custom property on
3875
+ * the stage wrapper (see `src/strategies/fallback/video-renderer.ts`). */
3876
+ _fit = DEFAULT_FIT;
3877
+ /** The stage wrapper — the element the canvas attaches into, and where
3878
+ * the `--avbridge-fit` CSS custom property lives. */
3879
+ _stageEl;
3854
3880
  /** Set if currentTime was assigned before the player was ready. */
3855
3881
  _pendingSeek = null;
3856
3882
  /** Set if play() was called before the player was ready. */
3857
3883
  _pendingPlay = false;
3858
3884
  /** MutationObserver tracking light-DOM `<track>` children. */
3859
3885
  _trackObserver = null;
3886
+ /** Document-level fullscreenchange handler — installed while connected so
3887
+ * the element can lock/unlock screen orientation to match the video's
3888
+ * intrinsic aspect. */
3889
+ _fullscreenChangeHandler = null;
3890
+ /** True if we successfully called screen.orientation.lock() on the last
3891
+ * fullscreen entry. Used to know whether to unlock on exit. */
3892
+ _orientationLocked = false;
3860
3893
  // ── Construction & lifecycle ───────────────────────────────────────────
3861
3894
  constructor() {
3862
3895
  super();
3863
3896
  const root = this.attachShadow({ mode: "open" });
3864
3897
  const stage = document.createElement("div");
3865
3898
  stage.setAttribute("part", "stage");
3866
- stage.style.cssText = "position:relative;width:100%;height:100%;display:block;";
3899
+ stage.style.cssText = `position:relative;width:100%;height:100%;display:block;--avbridge-fit:${DEFAULT_FIT};`;
3867
3900
  root.appendChild(stage);
3901
+ this._stageEl = stage;
3868
3902
  this._videoEl = document.createElement("video");
3869
3903
  this._videoEl.setAttribute("part", "video");
3870
- this._videoEl.style.cssText = "width:100%;height:100%;display:block;background:#000;";
3904
+ this._videoEl.style.cssText = `width:100%;height:100%;display:block;background:#000;object-fit:var(--avbridge-fit, ${DEFAULT_FIT});`;
3871
3905
  this._videoEl.playsInline = true;
3872
3906
  stage.appendChild(this._videoEl);
3873
3907
  this._videoEl.addEventListener("progress", () => {
@@ -3888,6 +3922,10 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
3888
3922
  this._trackObserver = new MutationObserver(() => this._syncTextTracks());
3889
3923
  this._trackObserver.observe(this, { childList: true, subtree: false });
3890
3924
  }
3925
+ if (!this._fullscreenChangeHandler) {
3926
+ this._fullscreenChangeHandler = () => this._onFullscreenChange();
3927
+ document.addEventListener("fullscreenchange", this._fullscreenChangeHandler);
3928
+ }
3891
3929
  const source = this._activeSource();
3892
3930
  if (source != null) {
3893
3931
  void this._bootstrap(source);
@@ -3899,6 +3937,11 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
3899
3937
  this._trackObserver.disconnect();
3900
3938
  this._trackObserver = null;
3901
3939
  }
3940
+ if (this._fullscreenChangeHandler) {
3941
+ document.removeEventListener("fullscreenchange", this._fullscreenChangeHandler);
3942
+ this._fullscreenChangeHandler = null;
3943
+ }
3944
+ this._releaseOrientationLock();
3902
3945
  this._bootstrapId++;
3903
3946
  void this._teardown();
3904
3947
  }
@@ -3932,6 +3975,14 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
3932
3975
  this._preferredStrategy = "auto";
3933
3976
  }
3934
3977
  break;
3978
+ case "fit": {
3979
+ const next = newValue && FIT_VALUES.has(newValue) ? newValue : DEFAULT_FIT;
3980
+ if (next === this._fit) break;
3981
+ this._fit = next;
3982
+ this._stageEl.style.setProperty("--avbridge-fit", next);
3983
+ this._dispatch("fitchange", { fit: next });
3984
+ break;
3985
+ }
3935
3986
  }
3936
3987
  }
3937
3988
  // ── Source handling ────────────────────────────────────────────────────
@@ -4173,6 +4224,13 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4173
4224
  if (value) this.setAttribute("diagnostics", "");
4174
4225
  else this.removeAttribute("diagnostics");
4175
4226
  }
4227
+ get fit() {
4228
+ return this._fit;
4229
+ }
4230
+ set fit(value) {
4231
+ if (!FIT_VALUES.has(value)) return;
4232
+ this.setAttribute("fit", value);
4233
+ }
4176
4234
  get preferredStrategy() {
4177
4235
  return this._preferredStrategy;
4178
4236
  }
@@ -4346,6 +4404,87 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4346
4404
  }
4347
4405
  );
4348
4406
  }
4407
+ /**
4408
+ * Disable the automatic `screen.orientation.lock()` that runs on
4409
+ * fullscreen entry. Set when you want to honor the device's native
4410
+ * auto-rotate instead of matching the video's intrinsic orientation.
4411
+ */
4412
+ get noOrientationLock() {
4413
+ return this.hasAttribute("no-orientation-lock");
4414
+ }
4415
+ set noOrientationLock(value) {
4416
+ if (value) this.setAttribute("no-orientation-lock", "");
4417
+ else this.removeAttribute("no-orientation-lock");
4418
+ }
4419
+ // ── Fullscreen orientation lock ────────────────────────────────────────
4420
+ /** Called whenever `document.fullscreenchange` fires. If this element (or
4421
+ * any of its ancestors) is now fullscreen, derive the target orientation
4422
+ * from the video's intrinsic size and call `screen.orientation.lock()`.
4423
+ * On exit, release the lock we took. iOS Safari rejects `lock()` — we
4424
+ * swallow the rejection so nothing breaks on that path. */
4425
+ _onFullscreenChange() {
4426
+ if (this._destroyed) return;
4427
+ const fsEl = document.fullscreenElement;
4428
+ const nowFullscreen = fsEl != null && this._isInsideOrEquals(fsEl);
4429
+ if (nowFullscreen && !this._orientationLocked) {
4430
+ if (this.noOrientationLock) return;
4431
+ const target = this._desiredOrientation();
4432
+ if (!target) return;
4433
+ void this._lockOrientation(target);
4434
+ } else if (!nowFullscreen && this._orientationLocked) {
4435
+ this._releaseOrientationLock();
4436
+ }
4437
+ }
4438
+ /** Walk composed-tree ancestors to see if `target` is this element or
4439
+ * any ancestor across shadow boundaries. `Node.contains()` can't cross
4440
+ * shadow roots, so when `<avbridge-player>` (the fullscreen element)
4441
+ * hosts this `<avbridge-video>` inside its shadow DOM, `contains()`
4442
+ * returns false. */
4443
+ _isInsideOrEquals(target) {
4444
+ let node = this;
4445
+ while (node) {
4446
+ if (node === target) return true;
4447
+ const parent = node.parentNode;
4448
+ if (parent instanceof ShadowRoot) node = parent.host;
4449
+ else node = parent;
4450
+ }
4451
+ return false;
4452
+ }
4453
+ /** Derive "landscape" / "portrait" from the intrinsic video dimensions.
4454
+ * Returns null when dimensions aren't known yet or the video is square.
4455
+ * Uses `videoWidth` / `videoHeight` from the inner `<video>`, which the
4456
+ * browser sets to the display-aspect-corrected size (so anamorphic
4457
+ * content is judged by its display aspect, not pixel aspect). */
4458
+ _desiredOrientation() {
4459
+ const w = this._videoEl.videoWidth;
4460
+ const h = this._videoEl.videoHeight;
4461
+ if (!w || !h) return null;
4462
+ if (w === h) return null;
4463
+ return w > h ? "landscape" : "portrait";
4464
+ }
4465
+ /** Attempt to lock screen orientation. Swallows rejections — iOS Safari
4466
+ * doesn't implement `lock()`, and desktop / non-fullscreen contexts will
4467
+ * reject too. Records success so we know whether to unlock on exit. */
4468
+ async _lockOrientation(target) {
4469
+ const so = screen.orientation;
4470
+ if (!so || typeof so.lock !== "function") return;
4471
+ try {
4472
+ await so.lock(target);
4473
+ this._orientationLocked = true;
4474
+ } catch {
4475
+ }
4476
+ }
4477
+ _releaseOrientationLock() {
4478
+ if (!this._orientationLocked) return;
4479
+ this._orientationLocked = false;
4480
+ const so = screen.orientation;
4481
+ if (so && typeof so.unlock === "function") {
4482
+ try {
4483
+ so.unlock();
4484
+ } catch {
4485
+ }
4486
+ }
4487
+ }
4349
4488
  // ── Public methods ─────────────────────────────────────────────────────
4350
4489
  /** Force a (re-)bootstrap if a source is currently set. */
4351
4490
  async load() {
@@ -4605,6 +4744,50 @@ var PLAYER_STYLES = (
4605
4744
 
4606
4745
  :host([data-controls-hidden]) { cursor: none; }
4607
4746
 
4747
+ /* \u2500\u2500 Top toolbar (slotted consumer chrome) \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
4748
+ Two named slots (top-left, top-right) let consumers place back / title /
4749
+ translate buttons inside the auto-hide chrome. Wrapper has
4750
+ pointer-events:none so empty slots don't block container clicks; each
4751
+ side re-enables pointer-events so real buttons remain interactive. */
4752
+
4753
+ .avp-toolbar-top {
4754
+ position: absolute;
4755
+ top: 0;
4756
+ left: 0;
4757
+ right: 0;
4758
+ z-index: 5;
4759
+ padding: 8px 12px 24px;
4760
+ background: linear-gradient(rgba(0, 0, 0, 0.6), transparent);
4761
+ display: flex;
4762
+ align-items: flex-start;
4763
+ justify-content: space-between;
4764
+ gap: 8px;
4765
+ opacity: 1;
4766
+ pointer-events: none;
4767
+ transition: opacity 0.25s;
4768
+ }
4769
+
4770
+ .avp-toolbar-top-left,
4771
+ .avp-toolbar-top-right {
4772
+ display: flex;
4773
+ align-items: center;
4774
+ gap: 8px;
4775
+ pointer-events: auto;
4776
+ }
4777
+
4778
+ .avp-toolbar-top-right { margin-left: auto; }
4779
+
4780
+ /* Hide the gradient band when no consumer has slotted anything \u2014 we
4781
+ toggle data-toolbar-empty from JS via slotchange. */
4782
+ :host([data-toolbar-empty]) .avp-toolbar-top {
4783
+ background: none;
4784
+ }
4785
+
4786
+ :host([data-controls-hidden]) .avp-toolbar-top {
4787
+ opacity: 0;
4788
+ pointer-events: none;
4789
+ }
4790
+
4608
4791
  /* \u2500\u2500 Seek bar \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\u2500\u2500\u2500\u2500\u2500 */
4609
4792
 
4610
4793
  .avp-seek {
@@ -4956,10 +5139,13 @@ var PROXY_ATTRIBUTES = [
4956
5139
  "playsinline",
4957
5140
  "crossorigin",
4958
5141
  "disableremoteplayback",
4959
- "preferstrategy"
5142
+ "preferstrategy",
5143
+ "fit"
4960
5144
  ];
5145
+ var PLAYER_ATTRIBUTES = ["show-fit"];
5146
+ var FIT_MODES = ["contain", "cover", "fill"];
4961
5147
  var AvbridgePlayerElement = class extends HTMLElement {
4962
- static observedAttributes = [...PROXY_ATTRIBUTES];
5148
+ static observedAttributes = [...PROXY_ATTRIBUTES, ...PLAYER_ATTRIBUTES];
4963
5149
  // ── Internal DOM refs ──────────────────────────────────────────────────
4964
5150
  _video;
4965
5151
  _playBtn;
@@ -4994,6 +5180,9 @@ var AvbridgePlayerElement = class extends HTMLElement {
4994
5180
  _statsEl;
4995
5181
  _statsInterval = null;
4996
5182
  _eventCleanup = [];
5183
+ _updateToolbarEmpty = () => {
5184
+ };
5185
+ _toolbarTop;
4997
5186
  // ── Constructor ────────────────────────────────────────────────────────
4998
5187
  constructor() {
4999
5188
  super();
@@ -5017,12 +5206,25 @@ var AvbridgePlayerElement = class extends HTMLElement {
5017
5206
  this._statsEl = shadow.querySelector(".avp-stats");
5018
5207
  this._rippleLeft = shadow.querySelector(".avp-ripple-left");
5019
5208
  this._rippleRight = shadow.querySelector(".avp-ripple-right");
5209
+ this._toolbarTop = shadow.querySelector('[part="toolbar-top"]');
5210
+ this._toolbarTop.setAttribute("data-visible", "true");
5211
+ const slots = shadow.querySelectorAll('slot[name="top-left"], slot[name="top-right"]');
5212
+ this._updateToolbarEmpty = () => {
5213
+ const hasContent = Array.from(slots).some((s) => s.assignedNodes({ flatten: true }).length > 0);
5214
+ if (hasContent) this.removeAttribute("data-toolbar-empty");
5215
+ else this.setAttribute("data-toolbar-empty", "");
5216
+ };
5217
+ for (const s of slots) s.addEventListener("slotchange", this._updateToolbarEmpty);
5020
5218
  this._bindEvents();
5021
5219
  }
5022
5220
  _template() {
5023
5221
  return `
5024
5222
  <div part="container" class="avp">
5025
5223
  <avbridge-video part="video"></avbridge-video>
5224
+ <div part="toolbar-top" class="avp-toolbar-top">
5225
+ <div part="toolbar-top-left" class="avp-toolbar-top-left"><slot name="top-left"></slot></div>
5226
+ <div part="toolbar-top-right" class="avp-toolbar-top-right"><slot name="top-right"></slot></div>
5227
+ </div>
5026
5228
  <div part="overlay" class="avp-overlay">
5027
5229
  <button class="avp-overlay-btn" aria-label="Play">${ICON_PLAY}</button>
5028
5230
  <div class="avp-spinner"></div>
@@ -5175,19 +5377,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
5175
5377
  });
5176
5378
  });
5177
5379
  on(this, "keydown", (e) => this._onKeydown(e));
5178
- if (!this.hasAttribute("tabindex")) {
5179
- this.setAttribute("tabindex", "0");
5180
- }
5181
5380
  }
5182
5381
  // ── Lifecycle ──────────────────────────────────────────────────────────
5183
5382
  connectedCallback() {
5184
5383
  this._setState("idle");
5384
+ if (!this.hasAttribute("tabindex")) {
5385
+ this.setAttribute("tabindex", "0");
5386
+ }
5387
+ this._updateToolbarEmpty();
5185
5388
  }
5186
5389
  disconnectedCallback() {
5187
5390
  this._clearTimers();
5188
5391
  }
5189
5392
  attributeChangedCallback(name, _old, value) {
5190
5393
  if (!this._video) return;
5394
+ if (PLAYER_ATTRIBUTES.includes(name)) {
5395
+ if (name === "show-fit" && this._settingsOpen) {
5396
+ this._buildSettingsMenu();
5397
+ }
5398
+ return;
5399
+ }
5191
5400
  if (value == null) this._video.removeAttribute(name);
5192
5401
  else this._video.setAttribute(name, value);
5193
5402
  }
@@ -5310,6 +5519,16 @@ var AvbridgePlayerElement = class extends HTMLElement {
5310
5519
  }
5311
5520
  _buildSettingsMenu() {
5312
5521
  const sections = [];
5522
+ if (this.hasAttribute("show-fit")) {
5523
+ const currentFit = this._video.fit ?? "contain";
5524
+ let fitItems = "";
5525
+ for (const mode of FIT_MODES) {
5526
+ const active = mode === currentFit;
5527
+ const label = mode[0].toUpperCase() + mode.slice(1);
5528
+ fitItems += `<div class="avp-settings-item${active ? " active" : ""}" data-fit="${mode}">${label}</div>`;
5529
+ }
5530
+ sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Fit</div>${fitItems}</div>`);
5531
+ }
5313
5532
  const currentRate = this._video.playbackRate ?? 1;
5314
5533
  let speedItems = "";
5315
5534
  for (const spd of PLAYBACK_SPEEDS) {
@@ -5336,6 +5555,14 @@ var AvbridgePlayerElement = class extends HTMLElement {
5336
5555
  }
5337
5556
  sections.push(`<div class="avp-settings-section"><div class="avp-settings-item" data-stats>Stats for nerds</div></div>`);
5338
5557
  this._settingsMenu.innerHTML = sections.join("");
5558
+ for (const item of this._settingsMenu.querySelectorAll("[data-fit]")) {
5559
+ item.addEventListener("click", (e) => {
5560
+ e.stopPropagation();
5561
+ const mode = item.dataset.fit;
5562
+ this.setAttribute("fit", mode);
5563
+ this._buildSettingsMenu();
5564
+ });
5565
+ }
5339
5566
  for (const item of this._settingsMenu.querySelectorAll("[data-speed]")) {
5340
5567
  item.addEventListener("click", (e) => {
5341
5568
  e.stopPropagation();
@@ -5421,6 +5648,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
5421
5648
  // ── Controls: auto-hide ────────────────────────────────────────────────
5422
5649
  _showControls() {
5423
5650
  this.removeAttribute("data-controls-hidden");
5651
+ this._toolbarTop.setAttribute("data-visible", "true");
5424
5652
  this._scheduleHide();
5425
5653
  }
5426
5654
  _scheduleHide() {
@@ -5430,6 +5658,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
5430
5658
  this._controlsTimer = setTimeout(() => {
5431
5659
  if (this._state === "playing") {
5432
5660
  this.setAttribute("data-controls-hidden", "");
5661
+ this._toolbarTop.setAttribute("data-visible", "false");
5433
5662
  }
5434
5663
  }, CONTROLS_HIDE_MS);
5435
5664
  }
@@ -5443,8 +5672,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
5443
5672
  // it's treated as a double-click and the single-click action is cancelled.
5444
5673
  /** Track whether the last interaction was touch so click handler can skip. */
5445
5674
  _lastPointerTypeWasTouch = false;
5675
+ /** True if the event's composed path passes through consumer-slotted toolbar
5676
+ * content. Slotted content lives in the light DOM so `.closest(".avp-toolbar-top")`
5677
+ * on the event target won't find the shadow-DOM wrapper — `composedPath()`
5678
+ * does. */
5679
+ _isToolbarEvent(e) {
5680
+ for (const node of e.composedPath()) {
5681
+ if (node instanceof HTMLElement && node.classList.contains("avp-toolbar-top")) return true;
5682
+ }
5683
+ return false;
5684
+ }
5446
5685
  _onContainerClick(e) {
5447
5686
  if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
5687
+ if (this._isToolbarEvent(e)) return;
5448
5688
  if (this._lastPointerTypeWasTouch) {
5449
5689
  this._lastPointerTypeWasTouch = false;
5450
5690
  return;
@@ -5460,6 +5700,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
5460
5700
  }
5461
5701
  _onContainerDblClick(e) {
5462
5702
  if (e.target.closest?.(".avp-controls, .avp-settings")) return;
5703
+ if (this._isToolbarEvent(e)) return;
5463
5704
  if (this._tapTimer) {
5464
5705
  clearTimeout(this._tapTimer);
5465
5706
  this._tapTimer = null;
@@ -5481,6 +5722,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
5481
5722
  if (e.pointerType !== "touch") return;
5482
5723
  this._lastPointerTypeWasTouch = true;
5483
5724
  if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
5725
+ if (this._isToolbarEvent(e)) return;
5484
5726
  const now = Date.now();
5485
5727
  if (now - this._lastTapTime < 300) {
5486
5728
  if (this._tapTimer) {