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.
- package/CHANGELOG.md +34 -0
- package/dist/{chunk-NQULEIA3.cjs → chunk-37UOSAVI.cjs} +15 -7
- package/dist/chunk-37UOSAVI.cjs.map +1 -0
- package/dist/{chunk-5KVLE6YI.js → chunk-EDDWAN2L.js} +3 -2
- package/dist/chunk-EDDWAN2L.js.map +1 -0
- package/dist/{chunk-3GKM5DFM.js → chunk-IHNHHEA2.js} +11 -3
- package/dist/chunk-IHNHHEA2.js.map +1 -0
- package/dist/{chunk-S4WAZC2T.cjs → chunk-WRKO6Q42.cjs} +3 -2
- package/dist/chunk-WRKO6Q42.cjs.map +1 -0
- package/dist/element-browser.js +23 -1
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +18 -5
- package/dist/element.cjs.map +1 -1
- package/dist/element.js +17 -4
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +10 -10
- package/dist/index.js +2 -2
- package/dist/player.cjs +106 -18
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +15 -0
- package/dist/player.d.ts +15 -0
- package/dist/player.js +102 -14
- package/dist/player.js.map +1 -1
- package/dist/subtitles-5H24MEBJ.js +4 -0
- package/dist/{subtitles-4T74JRGT.js.map → subtitles-5H24MEBJ.js.map} +1 -1
- package/dist/subtitles-HMVGWTU2.cjs +29 -0
- package/dist/{subtitles-QUH4LPI4.cjs.map → subtitles-HMVGWTU2.cjs.map} +1 -1
- package/package.json +1 -1
- package/src/element/avbridge-player.ts +92 -10
- package/src/element/avbridge-subtitles.ts +273 -0
- package/src/element/avbridge-video.ts +21 -1
- package/src/strategies/fallback/audio-output.ts +10 -0
- package/src/subtitles/index.ts +2 -0
- package/dist/chunk-3GKM5DFM.js.map +0 -1
- package/dist/chunk-5KVLE6YI.js.map +0 -1
- package/dist/chunk-NQULEIA3.cjs.map +0 -1
- package/dist/chunk-S4WAZC2T.cjs.map +0 -1
- package/dist/subtitles-4T74JRGT.js +0 -4
- 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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
|
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
|
-
},
|
|
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();
|