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.
- package/CHANGELOG.md +65 -0
- package/dist/{chunk-EY6DZEDT.cjs → chunk-37UOSAVI.cjs} +55 -10
- 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-SN4WZE24.js → chunk-IHNHHEA2.js} +51 -6
- 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 +63 -4
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +18 -5
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +17 -4
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +10 -10
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{player-DEcidWk6.d.cts → player-DDdNVFDv.d.cts} +23 -1
- package/dist/{player-DEcidWk6.d.ts → player-DDdNVFDv.d.ts} +23 -1
- package/dist/player.cjs +329 -109
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +42 -0
- package/dist/player.d.ts +42 -0
- package/dist/player.js +325 -105
- 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 +235 -78
- package/src/element/avbridge-subtitles.ts +273 -0
- package/src/element/avbridge-video.ts +21 -1
- package/src/element/player-styles.ts +85 -35
- package/src/index.ts +1 -0
- package/src/strategies/fallback/audio-output.ts +39 -4
- package/src/strategies/fallback/index.ts +12 -0
- package/src/strategies/hybrid/index.ts +9 -0
- package/src/subtitles/index.ts +2 -0
- package/src/types.ts +25 -0
- package/dist/chunk-5KVLE6YI.js.map +0 -1
- package/dist/chunk-EY6DZEDT.cjs.map +0 -1
- package/dist/chunk-S4WAZC2T.cjs.map +0 -1
- package/dist/chunk-SN4WZE24.js.map +0 -1
- package/dist/subtitles-4T74JRGT.js +0 -4
- 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
|
|
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
|
|
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");
|
|
@@ -1607,6 +1607,10 @@ var AudioOutput = class {
|
|
|
1607
1607
|
_volume = 1;
|
|
1608
1608
|
/** User-set muted flag. When true, gain is forced to 0. */
|
|
1609
1609
|
_muted = false;
|
|
1610
|
+
/** Playback rate. Scales the media clock and each AudioBufferSourceNode's
|
|
1611
|
+
* playbackRate so audio pitches up/down accordingly (same as native
|
|
1612
|
+
* <video>.playbackRate). Default 1. */
|
|
1613
|
+
_rate = 1;
|
|
1610
1614
|
constructor() {
|
|
1611
1615
|
this.ctx = new AudioContext();
|
|
1612
1616
|
this.gain = this.ctx.createGain();
|
|
@@ -1628,6 +1632,20 @@ var AudioOutput = class {
|
|
|
1628
1632
|
getMuted() {
|
|
1629
1633
|
return this._muted;
|
|
1630
1634
|
}
|
|
1635
|
+
/** Set playback rate. Scales the media clock and pitches audio output
|
|
1636
|
+
* (same as native <video>.playbackRate — speed without pitch correction).
|
|
1637
|
+
* Rebases the anchor so the clock transition is seamless. */
|
|
1638
|
+
setPlaybackRate(rate) {
|
|
1639
|
+
if (rate === this._rate) return;
|
|
1640
|
+
const t = this.now();
|
|
1641
|
+
this.mediaTimeOfAnchor = t;
|
|
1642
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1643
|
+
this.wallAnchorMs = performance.now();
|
|
1644
|
+
this._rate = rate;
|
|
1645
|
+
}
|
|
1646
|
+
getPlaybackRate() {
|
|
1647
|
+
return this._rate;
|
|
1648
|
+
}
|
|
1631
1649
|
applyGain() {
|
|
1632
1650
|
const target = this._muted ? 0 : this._volume;
|
|
1633
1651
|
try {
|
|
@@ -1648,12 +1666,12 @@ var AudioOutput = class {
|
|
|
1648
1666
|
now() {
|
|
1649
1667
|
if (this.noAudio) {
|
|
1650
1668
|
if (this.state === "playing") {
|
|
1651
|
-
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3;
|
|
1669
|
+
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3 * this._rate;
|
|
1652
1670
|
}
|
|
1653
1671
|
return this.mediaTimeOfAnchor;
|
|
1654
1672
|
}
|
|
1655
1673
|
if (this.state === "playing") {
|
|
1656
|
-
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor);
|
|
1674
|
+
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
|
|
1657
1675
|
}
|
|
1658
1676
|
return this.mediaTimeOfAnchor;
|
|
1659
1677
|
}
|
|
@@ -1705,7 +1723,8 @@ var AudioOutput = class {
|
|
|
1705
1723
|
const node = this.ctx.createBufferSource();
|
|
1706
1724
|
node.buffer = buffer;
|
|
1707
1725
|
node.connect(this.gain);
|
|
1708
|
-
|
|
1726
|
+
if (this._rate !== 1) node.playbackRate.value = this._rate;
|
|
1727
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
1709
1728
|
if (ctxStart < this.ctx.currentTime) {
|
|
1710
1729
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1711
1730
|
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
@@ -1732,6 +1751,10 @@ var AudioOutput = class {
|
|
|
1732
1751
|
if (this.ctx.state === "suspended") {
|
|
1733
1752
|
await this.ctx.resume();
|
|
1734
1753
|
}
|
|
1754
|
+
try {
|
|
1755
|
+
this.gain.connect(this.ctx.destination);
|
|
1756
|
+
} catch {
|
|
1757
|
+
}
|
|
1735
1758
|
if (this.state === "paused") {
|
|
1736
1759
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1737
1760
|
this.state = "playing";
|
|
@@ -1758,6 +1781,10 @@ var AudioOutput = class {
|
|
|
1758
1781
|
this.mediaTimeOfAnchor = this.now();
|
|
1759
1782
|
this.state = "paused";
|
|
1760
1783
|
if (this.noAudio) return;
|
|
1784
|
+
try {
|
|
1785
|
+
this.gain.disconnect();
|
|
1786
|
+
} catch {
|
|
1787
|
+
}
|
|
1761
1788
|
if (this.ctx.state === "running") {
|
|
1762
1789
|
await this.ctx.suspend();
|
|
1763
1790
|
}
|
|
@@ -2533,6 +2560,14 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2533
2560
|
get: () => ctx.duration ?? NaN
|
|
2534
2561
|
});
|
|
2535
2562
|
}
|
|
2563
|
+
Object.defineProperty(target, "playbackRate", {
|
|
2564
|
+
configurable: true,
|
|
2565
|
+
get: () => audio.getPlaybackRate(),
|
|
2566
|
+
set: (v) => {
|
|
2567
|
+
audio.setPlaybackRate(v);
|
|
2568
|
+
target.dispatchEvent(new Event("ratechange"));
|
|
2569
|
+
}
|
|
2570
|
+
});
|
|
2536
2571
|
Object.defineProperty(target, "readyState", {
|
|
2537
2572
|
configurable: true,
|
|
2538
2573
|
get: () => {
|
|
@@ -2643,6 +2678,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2643
2678
|
delete target.muted;
|
|
2644
2679
|
delete target.readyState;
|
|
2645
2680
|
delete target.seekable;
|
|
2681
|
+
delete target.playbackRate;
|
|
2646
2682
|
} catch {
|
|
2647
2683
|
}
|
|
2648
2684
|
},
|
|
@@ -3178,6 +3214,14 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3178
3214
|
get: () => ctx.duration ?? NaN
|
|
3179
3215
|
});
|
|
3180
3216
|
}
|
|
3217
|
+
Object.defineProperty(target, "playbackRate", {
|
|
3218
|
+
configurable: true,
|
|
3219
|
+
get: () => audio.getPlaybackRate(),
|
|
3220
|
+
set: (v) => {
|
|
3221
|
+
audio.setPlaybackRate(v);
|
|
3222
|
+
target.dispatchEvent(new Event("ratechange"));
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3181
3225
|
Object.defineProperty(target, "readyState", {
|
|
3182
3226
|
configurable: true,
|
|
3183
3227
|
get: () => {
|
|
@@ -3309,6 +3353,7 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3309
3353
|
delete target.muted;
|
|
3310
3354
|
delete target.readyState;
|
|
3311
3355
|
delete target.seekable;
|
|
3356
|
+
delete target.playbackRate;
|
|
3312
3357
|
} catch {
|
|
3313
3358
|
}
|
|
3314
3359
|
},
|
|
@@ -3419,7 +3464,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3419
3464
|
switchingPromise = Promise.resolve();
|
|
3420
3465
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
3421
3466
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
3422
|
-
subtitleResources = new
|
|
3467
|
+
subtitleResources = new chunkWRKO6Q42_cjs.SubtitleResourceBag();
|
|
3423
3468
|
// Transport config extracted from CreatePlayerOptions. Threaded to probe,
|
|
3424
3469
|
// subtitle fetches, and strategy session creators. Not stored on MediaContext
|
|
3425
3470
|
// because it's runtime config, not media analysis.
|
|
@@ -3465,7 +3510,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3465
3510
|
}
|
|
3466
3511
|
}
|
|
3467
3512
|
if (this.options.directory && this.options.source instanceof File) {
|
|
3468
|
-
const found = await
|
|
3513
|
+
const found = await chunkWRKO6Q42_cjs.discoverSidecars(this.options.source, this.options.directory);
|
|
3469
3514
|
for (const s of found) {
|
|
3470
3515
|
this.subtitleResources.track(s.url);
|
|
3471
3516
|
ctx.subtitleTracks.push({
|
|
@@ -3488,7 +3533,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3488
3533
|
reason: decision.reason
|
|
3489
3534
|
});
|
|
3490
3535
|
await this.startSession(decision.strategy, decision.reason);
|
|
3491
|
-
await
|
|
3536
|
+
await chunkWRKO6Q42_cjs.attachSubtitleTracks(
|
|
3492
3537
|
this.options.target,
|
|
3493
3538
|
ctx.subtitleTracks,
|
|
3494
3539
|
this.subtitleResources,
|
|
@@ -4530,7 +4575,7 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4530
4575
|
* strategies pick up the new track via their textTracks watcher.
|
|
4531
4576
|
*/
|
|
4532
4577
|
async addSubtitle(subtitle) {
|
|
4533
|
-
const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-
|
|
4578
|
+
const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-HMVGWTU2.cjs');
|
|
4534
4579
|
const format = subtitle.format ?? (subtitle.url.endsWith(".srt") ? "srt" : "vtt");
|
|
4535
4580
|
const track = {
|
|
4536
4581
|
id: this._subtitleTracks.length,
|
|
@@ -4539,14 +4584,27 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4539
4584
|
sidecarUrl: subtitle.url
|
|
4540
4585
|
};
|
|
4541
4586
|
this._subtitleTracks.push(track);
|
|
4587
|
+
console.log(`[avbridge:subs] addSubtitle id=${track.id} format=${format} lang=${subtitle.language ?? "?"}`);
|
|
4542
4588
|
await attachSubtitleTracks2(
|
|
4543
4589
|
this._videoEl,
|
|
4544
4590
|
this._subtitleTracks,
|
|
4545
4591
|
void 0,
|
|
4546
4592
|
(err, t) => {
|
|
4547
|
-
console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
|
|
4593
|
+
console.warn(`[avbridge:subs] subtitle ${t.id} failed: ${err.message}`);
|
|
4548
4594
|
}
|
|
4549
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
|
+
});
|
|
4550
4608
|
}
|
|
4551
4609
|
/**
|
|
4552
4610
|
* Disable the automatic `screen.orientation.lock()` that runs on
|
|
@@ -4725,7 +4783,6 @@ var PLAYER_STYLES = (
|
|
|
4725
4783
|
position: relative;
|
|
4726
4784
|
width: 100%;
|
|
4727
4785
|
height: 100%;
|
|
4728
|
-
cursor: pointer;
|
|
4729
4786
|
-webkit-tap-highlight-color: transparent;
|
|
4730
4787
|
user-select: none;
|
|
4731
4788
|
}
|
|
@@ -5175,62 +5232,113 @@ var PLAYER_STYLES = (
|
|
|
5175
5232
|
|
|
5176
5233
|
.avp-spacer { flex: 1; }
|
|
5177
5234
|
|
|
5178
|
-
/* \u2500\u2500 Settings
|
|
5235
|
+
/* \u2500\u2500 Settings bottom sheet \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 */
|
|
5236
|
+
|
|
5237
|
+
/* Scrim \u2014 semi-transparent overlay behind the sheet, above the video.
|
|
5238
|
+
Tapping it dismisses the sheet. */
|
|
5239
|
+
.avp-settings-scrim {
|
|
5240
|
+
position: absolute;
|
|
5241
|
+
inset: 0;
|
|
5242
|
+
z-index: 9;
|
|
5243
|
+
background: rgba(0, 0, 0, 0.4);
|
|
5244
|
+
opacity: 0;
|
|
5245
|
+
pointer-events: none;
|
|
5246
|
+
transition: opacity 0.2s;
|
|
5247
|
+
}
|
|
5179
5248
|
|
|
5249
|
+
.avp-settings-scrim.open {
|
|
5250
|
+
opacity: 1;
|
|
5251
|
+
pointer-events: auto;
|
|
5252
|
+
}
|
|
5253
|
+
|
|
5254
|
+
/* Sheet container \u2014 slides up from the bottom. Height is content-driven
|
|
5255
|
+
up to a JS-measured max (set on open via style.maxHeight). */
|
|
5180
5256
|
.avp-settings {
|
|
5181
5257
|
position: absolute;
|
|
5182
|
-
bottom:
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
border-radius: 8px;
|
|
5186
|
-
min-width: 220px;
|
|
5187
|
-
/* Fit within the player: leave room for the controls bar (52px bottom)
|
|
5188
|
-
and a small top margin (8px). On tall players this caps at 300px;
|
|
5189
|
-
on short players it shrinks to whatever fits. */
|
|
5190
|
-
max-height: min(300px, calc(100% - 52px - 8px));
|
|
5191
|
-
overflow-y: auto;
|
|
5192
|
-
display: none;
|
|
5258
|
+
bottom: 0;
|
|
5259
|
+
left: 0;
|
|
5260
|
+
right: 0;
|
|
5193
5261
|
z-index: 10;
|
|
5194
|
-
|
|
5262
|
+
background: rgba(28, 28, 28, 0.97);
|
|
5263
|
+
border-radius: 12px 12px 0 0;
|
|
5264
|
+
overflow-y: auto;
|
|
5265
|
+
overscroll-behavior: contain;
|
|
5266
|
+
transform: translateY(100%);
|
|
5267
|
+
transition: transform 0.2s ease-out;
|
|
5268
|
+
max-height: 70%;
|
|
5269
|
+
padding-bottom: 52px;
|
|
5270
|
+
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
|
|
5195
5271
|
}
|
|
5196
5272
|
|
|
5197
|
-
.avp-settings.open {
|
|
5273
|
+
.avp-settings.open {
|
|
5274
|
+
transform: translateY(0);
|
|
5275
|
+
}
|
|
5198
5276
|
|
|
5199
|
-
.
|
|
5200
|
-
|
|
5201
|
-
|
|
5277
|
+
/* Drag handle indicator at top of sheet. */
|
|
5278
|
+
.avp-settings-handle {
|
|
5279
|
+
width: 36px;
|
|
5280
|
+
height: 4px;
|
|
5281
|
+
border-radius: 2px;
|
|
5282
|
+
background: rgba(255, 255, 255, 0.3);
|
|
5283
|
+
margin: 8px auto 4px;
|
|
5202
5284
|
}
|
|
5203
5285
|
|
|
5204
|
-
|
|
5286
|
+
/* \u2500\u2500 Accordion sections \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 */
|
|
5205
5287
|
|
|
5206
|
-
.avp-settings-
|
|
5207
|
-
|
|
5208
|
-
font-size: 11px;
|
|
5209
|
-
text-transform: uppercase;
|
|
5210
|
-
letter-spacing: 0.5px;
|
|
5211
|
-
opacity: 0.5;
|
|
5288
|
+
.avp-settings-section {
|
|
5289
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
5212
5290
|
}
|
|
5213
5291
|
|
|
5214
|
-
.avp-settings-
|
|
5292
|
+
.avp-settings-section:last-child { border-bottom: none; }
|
|
5293
|
+
|
|
5294
|
+
/* Section header \u2014 clickable row showing label + current value. */
|
|
5295
|
+
.avp-settings-header {
|
|
5296
|
+
position: relative;
|
|
5215
5297
|
display: flex;
|
|
5216
5298
|
align-items: center;
|
|
5217
|
-
|
|
5218
|
-
|
|
5299
|
+
justify-content: space-between;
|
|
5300
|
+
padding: 12px 16px;
|
|
5219
5301
|
cursor: pointer;
|
|
5302
|
+
font-size: 14px;
|
|
5220
5303
|
transition: background 0.1s;
|
|
5221
5304
|
}
|
|
5222
5305
|
|
|
5223
|
-
.avp-settings-
|
|
5306
|
+
.avp-settings-header:hover { background: rgba(255, 255, 255, 0.06); }
|
|
5307
|
+
|
|
5308
|
+
.avp-settings-header-label {
|
|
5309
|
+
display: flex;
|
|
5310
|
+
align-items: center;
|
|
5311
|
+
gap: 8px;
|
|
5312
|
+
font-weight: 500;
|
|
5313
|
+
}
|
|
5314
|
+
|
|
5315
|
+
.avp-settings-header-value {
|
|
5316
|
+
margin-left: auto;
|
|
5317
|
+
opacity: 0.6;
|
|
5318
|
+
font-size: 13px;
|
|
5319
|
+
text-align: right;
|
|
5320
|
+
}
|
|
5224
5321
|
|
|
5225
|
-
.
|
|
5226
|
-
|
|
5322
|
+
/* Invisible native <select> layered over the value portion of the row.
|
|
5323
|
+
Covers from the value text to the right edge so tapping the value
|
|
5324
|
+
opens the OS picker. The label side remains inert. */
|
|
5325
|
+
.avp-settings-select {
|
|
5326
|
+
position: absolute;
|
|
5327
|
+
top: 0;
|
|
5328
|
+
right: 0;
|
|
5329
|
+
bottom: 0;
|
|
5330
|
+
width: 50%;
|
|
5331
|
+
opacity: 0;
|
|
5332
|
+
cursor: pointer;
|
|
5333
|
+
font-size: 16px;
|
|
5334
|
+
direction: rtl;
|
|
5227
5335
|
}
|
|
5228
5336
|
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
font-weight: bold;
|
|
5337
|
+
/* Toggle-style rows (Stats for Nerds) \u2014 no select, just clickable. */
|
|
5338
|
+
.avp-settings-toggle {
|
|
5339
|
+
cursor: pointer;
|
|
5233
5340
|
}
|
|
5341
|
+
.avp-settings-toggle:hover { background: rgba(255, 255, 255, 0.06); }
|
|
5234
5342
|
|
|
5235
5343
|
/* \u2500\u2500 Stats for nerds \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 */
|
|
5236
5344
|
|
|
@@ -5310,7 +5418,7 @@ function formatTime(sec) {
|
|
|
5310
5418
|
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
|
|
5311
5419
|
}
|
|
5312
5420
|
var PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
|
5313
|
-
var
|
|
5421
|
+
var DEFAULT_CONTROLS_HIDE_MS = 3e3;
|
|
5314
5422
|
var FORWARDED_EVENTS = [
|
|
5315
5423
|
"ready",
|
|
5316
5424
|
"error",
|
|
@@ -5353,7 +5461,7 @@ var PROXY_ATTRIBUTES = [
|
|
|
5353
5461
|
];
|
|
5354
5462
|
var PLAYER_ATTRIBUTES = ["show-fit"];
|
|
5355
5463
|
var FIT_MODES = ["contain", "cover", "fill"];
|
|
5356
|
-
var AvbridgePlayerElement = class extends HTMLElement {
|
|
5464
|
+
var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
5357
5465
|
static observedAttributes = [...PROXY_ATTRIBUTES, ...PLAYER_ATTRIBUTES];
|
|
5358
5466
|
// ── Internal DOM refs ──────────────────────────────────────────────────
|
|
5359
5467
|
_video;
|
|
@@ -5369,6 +5477,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5369
5477
|
_volumeInput;
|
|
5370
5478
|
_settingsBtn;
|
|
5371
5479
|
_settingsMenu;
|
|
5480
|
+
_settingsScrim;
|
|
5481
|
+
_customSections = [];
|
|
5372
5482
|
_fullscreenBtn;
|
|
5373
5483
|
// Strategy badge removed — visible in Stats for Nerds instead.
|
|
5374
5484
|
// Spinner is rendered but driven entirely by CSS :host([data-state]) selectors.
|
|
@@ -5379,6 +5489,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5379
5489
|
_state = "idle";
|
|
5380
5490
|
_controlsTimer = null;
|
|
5381
5491
|
_settingsOpen = false;
|
|
5492
|
+
_activeAudioTrackId = null;
|
|
5493
|
+
_activeSubtitleTrackId = null;
|
|
5382
5494
|
_userSeeking = false;
|
|
5383
5495
|
_holdTimer = null;
|
|
5384
5496
|
_holdSpeedActive = false;
|
|
@@ -5410,6 +5522,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5410
5522
|
this._volumeInput = shadow.querySelector(".avp-volume-input");
|
|
5411
5523
|
this._settingsBtn = shadow.querySelector(".avp-settings-btn");
|
|
5412
5524
|
this._settingsMenu = shadow.querySelector(".avp-settings");
|
|
5525
|
+
this._settingsScrim = shadow.querySelector(".avp-settings-scrim");
|
|
5413
5526
|
this._fullscreenBtn = shadow.querySelector(".avp-fullscreen");
|
|
5414
5527
|
this._speedIndicator = shadow.querySelector(".avp-speed-indicator");
|
|
5415
5528
|
this._statsEl = shadow.querySelector(".avp-stats");
|
|
@@ -5466,7 +5579,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5466
5579
|
<button class="avp-btn avp-settings-btn" part="settings-button" aria-label="Settings">${ICON_SETTINGS}</button>
|
|
5467
5580
|
<button class="avp-btn avp-fullscreen" part="fullscreen-button" aria-label="Fullscreen">${ICON_FULLSCREEN}</button>
|
|
5468
5581
|
</div>
|
|
5469
|
-
<div class="avp-settings
|
|
5582
|
+
<div class="avp-settings-scrim"></div>
|
|
5583
|
+
<div class="avp-settings" part="settings-menu"><div class="avp-settings-handle"></div></div>
|
|
5470
5584
|
</div>
|
|
5471
5585
|
</div>`;
|
|
5472
5586
|
}
|
|
@@ -5538,6 +5652,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5538
5652
|
e.stopPropagation();
|
|
5539
5653
|
this._toggleSettings();
|
|
5540
5654
|
});
|
|
5655
|
+
on(this._settingsScrim, "click", () => this._closeSettings());
|
|
5541
5656
|
on(this._fullscreenBtn, "click", (e) => {
|
|
5542
5657
|
e.stopPropagation();
|
|
5543
5658
|
this._toggleFullscreen();
|
|
@@ -5546,11 +5661,6 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5546
5661
|
const container = this.shadowRoot.querySelector(".avp");
|
|
5547
5662
|
on(container, "click", (e) => this._onContainerClick(e));
|
|
5548
5663
|
on(container, "dblclick", (e) => this._onContainerDblClick(e));
|
|
5549
|
-
on(container, "click", (e) => {
|
|
5550
|
-
if (this._settingsOpen && !e.target.closest?.(".avp-settings-btn, .avp-settings")) {
|
|
5551
|
-
this._closeSettings();
|
|
5552
|
-
}
|
|
5553
|
-
});
|
|
5554
5664
|
on(document, "click", (e) => {
|
|
5555
5665
|
if (this._settingsOpen && !this.contains(e.target)) {
|
|
5556
5666
|
this._closeSettings();
|
|
@@ -5644,6 +5754,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5644
5754
|
const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
5645
5755
|
return frac * (this._video.duration || 0);
|
|
5646
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;
|
|
5647
5761
|
_onSeekPointerDown(e) {
|
|
5648
5762
|
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
5649
5763
|
e.preventDefault();
|
|
@@ -5651,15 +5765,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5651
5765
|
const seekBar = this.shadowRoot.querySelector(".avp-seek");
|
|
5652
5766
|
seekBar.setPointerCapture(e.pointerId);
|
|
5653
5767
|
seekBar.setAttribute("data-seeking", "");
|
|
5768
|
+
const scrubMode = seekBar.getBoundingClientRect().width < _AvbridgePlayerElement.SCRUB_WIDTH_THRESHOLD;
|
|
5769
|
+
let lastScrubCommit = 0;
|
|
5654
5770
|
const initial = this._timeFromSeekPointer(e.clientX);
|
|
5655
5771
|
this._seekInput.value = String(initial);
|
|
5656
5772
|
this._onSeekInput();
|
|
5657
5773
|
this._updateSeekTooltip(e.clientX);
|
|
5774
|
+
if (scrubMode) this._onSeekCommit();
|
|
5658
5775
|
const onMove = (ev) => {
|
|
5659
5776
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5660
5777
|
this._seekInput.value = String(t);
|
|
5661
5778
|
this._onSeekInput();
|
|
5662
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
|
+
}
|
|
5663
5788
|
};
|
|
5664
5789
|
const onUp = (ev) => {
|
|
5665
5790
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
@@ -5728,83 +5853,123 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5728
5853
|
_toggleSettings() {
|
|
5729
5854
|
this._settingsOpen = !this._settingsOpen;
|
|
5730
5855
|
this._settingsMenu.classList.toggle("open", this._settingsOpen);
|
|
5731
|
-
|
|
5856
|
+
this._settingsScrim.classList.toggle("open", this._settingsOpen);
|
|
5857
|
+
if (this._settingsOpen) {
|
|
5858
|
+
this._fitSettingsToPlayer();
|
|
5859
|
+
this._showControls();
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
_fitSettingsToPlayer() {
|
|
5863
|
+
const container = this.shadowRoot?.querySelector(".avp");
|
|
5864
|
+
if (!container) return;
|
|
5865
|
+
const rect = container.getBoundingClientRect();
|
|
5866
|
+
const maxH = Math.max(120, Math.floor(rect.height * 0.7));
|
|
5867
|
+
this._settingsMenu.style.maxHeight = `${maxH}px`;
|
|
5732
5868
|
}
|
|
5733
5869
|
_closeSettings() {
|
|
5734
5870
|
this._settingsOpen = false;
|
|
5735
5871
|
this._settingsMenu.classList.remove("open");
|
|
5872
|
+
this._settingsScrim.classList.remove("open");
|
|
5736
5873
|
}
|
|
5737
5874
|
_buildSettingsMenu() {
|
|
5738
5875
|
const sections = [];
|
|
5739
|
-
|
|
5740
|
-
const currentFit = this._video.fit ?? "contain";
|
|
5741
|
-
let fitItems = "";
|
|
5742
|
-
for (const mode of FIT_MODES) {
|
|
5743
|
-
const active = mode === currentFit;
|
|
5744
|
-
const label = mode[0].toUpperCase() + mode.slice(1);
|
|
5745
|
-
fitItems += `<div class="avp-settings-item${active ? " active" : ""}" data-fit="${mode}">${label}</div>`;
|
|
5746
|
-
}
|
|
5747
|
-
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Fit</div>${fitItems}</div>`);
|
|
5748
|
-
}
|
|
5876
|
+
const selectRow = (label, currentValue, options, selectAttrs) => `<div class="avp-settings-section"><div class="avp-settings-header"><span class="avp-settings-header-label">${label}</span><span class="avp-settings-header-value">${currentValue}</span><select class="avp-settings-select" ${selectAttrs}>${options}</select></div></div>`;
|
|
5749
5877
|
const currentRate = this._video.playbackRate ?? 1;
|
|
5750
|
-
|
|
5878
|
+
const speedValue = Math.abs(currentRate - 1) < 0.01 ? "Normal" : `${currentRate}x`;
|
|
5879
|
+
let speedOpts = "";
|
|
5751
5880
|
for (const spd of PLAYBACK_SPEEDS) {
|
|
5752
|
-
const
|
|
5881
|
+
const sel = Math.abs(spd - currentRate) < 0.01 ? " selected" : "";
|
|
5753
5882
|
const label = spd === 1 ? "Normal" : `${spd}x`;
|
|
5754
|
-
|
|
5883
|
+
speedOpts += `<option value="${spd}"${sel}>${label}</option>`;
|
|
5884
|
+
}
|
|
5885
|
+
sections.push(selectRow("Speed", speedValue, speedOpts, `data-action="speed"`));
|
|
5886
|
+
const audios = this._video.audioTracks ?? [];
|
|
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}`;
|
|
5891
|
+
let audioOpts = "";
|
|
5892
|
+
for (const t of audios) {
|
|
5893
|
+
const sel = t.id === activeAudioId ? " selected" : "";
|
|
5894
|
+
audioOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
|
|
5895
|
+
}
|
|
5896
|
+
sections.push(selectRow("Audio", audioValue, audioOpts, `data-action="audio"`));
|
|
5755
5897
|
}
|
|
5756
|
-
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Speed</div>${speedItems}</div>`);
|
|
5757
5898
|
const subs = this._video.subtitleTracks ?? [];
|
|
5758
5899
|
if (subs.length > 0) {
|
|
5759
|
-
|
|
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>`;
|
|
5760
5904
|
for (const t of subs) {
|
|
5761
|
-
|
|
5905
|
+
const sel = t.id === activeSubId ? " selected" : "";
|
|
5906
|
+
subOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
|
|
5762
5907
|
}
|
|
5763
|
-
sections.push(
|
|
5908
|
+
sections.push(selectRow("Subtitles", subValue, subOpts, `data-action="subtitle"`));
|
|
5764
5909
|
}
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5910
|
+
if (this.hasAttribute("show-fit")) {
|
|
5911
|
+
const currentFit = this._video.fit ?? "contain";
|
|
5912
|
+
const fitValue = currentFit[0].toUpperCase() + currentFit.slice(1);
|
|
5913
|
+
let fitOpts = "";
|
|
5914
|
+
for (const mode of FIT_MODES) {
|
|
5915
|
+
const sel = mode === currentFit ? " selected" : "";
|
|
5916
|
+
const label = mode[0].toUpperCase() + mode.slice(1);
|
|
5917
|
+
fitOpts += `<option value="${mode}"${sel}>${label}</option>`;
|
|
5770
5918
|
}
|
|
5771
|
-
sections.push(
|
|
5919
|
+
sections.push(selectRow("Fit", fitValue, fitOpts, `data-action="fit"`));
|
|
5772
5920
|
}
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
item.
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
});
|
|
5921
|
+
for (const cfg of this._customSections) {
|
|
5922
|
+
const activeItem = cfg.items.find((i) => i.active);
|
|
5923
|
+
let customOpts = "";
|
|
5924
|
+
for (const item of cfg.items) {
|
|
5925
|
+
const sel = item.active ? " selected" : "";
|
|
5926
|
+
customOpts += `<option value="${item.id}"${sel}>${item.label}</option>`;
|
|
5927
|
+
}
|
|
5928
|
+
sections.push(selectRow(cfg.label, activeItem?.label ?? "", customOpts, `data-action="custom" data-custom-id="${cfg.id}"`));
|
|
5782
5929
|
}
|
|
5783
|
-
|
|
5784
|
-
|
|
5930
|
+
sections.push(
|
|
5931
|
+
`<div class="avp-settings-section"><div class="avp-settings-header avp-settings-toggle" data-stats><span class="avp-settings-header-label">Stats for Nerds</span></div></div>`
|
|
5932
|
+
);
|
|
5933
|
+
const handle = this._settingsMenu.querySelector(".avp-settings-handle");
|
|
5934
|
+
this._settingsMenu.innerHTML = "";
|
|
5935
|
+
if (handle) this._settingsMenu.appendChild(handle);
|
|
5936
|
+
else this._settingsMenu.insertAdjacentHTML("afterbegin", `<div class="avp-settings-handle"></div>`);
|
|
5937
|
+
this._settingsMenu.insertAdjacentHTML("beforeend", sections.join(""));
|
|
5938
|
+
for (const sel of this._settingsMenu.querySelectorAll(".avp-settings-select")) {
|
|
5939
|
+
sel.addEventListener("change", (e) => {
|
|
5785
5940
|
e.stopPropagation();
|
|
5786
|
-
|
|
5941
|
+
const action = sel.dataset.action;
|
|
5942
|
+
const val = sel.value;
|
|
5943
|
+
switch (action) {
|
|
5944
|
+
case "speed":
|
|
5945
|
+
this._video.playbackRate = Number(val);
|
|
5946
|
+
break;
|
|
5947
|
+
case "audio":
|
|
5948
|
+
this._activeAudioTrackId = Number(val);
|
|
5949
|
+
void this._video.setAudioTrack(Number(val));
|
|
5950
|
+
break;
|
|
5951
|
+
case "subtitle": {
|
|
5952
|
+
const subId = Number(val);
|
|
5953
|
+
this._activeSubtitleTrackId = subId >= 0 ? subId : null;
|
|
5954
|
+
void this._video.setSubtitleTrack(subId >= 0 ? subId : null);
|
|
5955
|
+
break;
|
|
5956
|
+
}
|
|
5957
|
+
case "fit":
|
|
5958
|
+
this.setAttribute("fit", val);
|
|
5959
|
+
break;
|
|
5960
|
+
case "custom": {
|
|
5961
|
+
const cfgId = sel.dataset.customId;
|
|
5962
|
+
const cfg = this._customSections.find((s) => s.id === cfgId);
|
|
5963
|
+
cfg?.onSelect(val);
|
|
5964
|
+
break;
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5787
5967
|
this._buildSettingsMenu();
|
|
5788
5968
|
});
|
|
5789
5969
|
}
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
const id = Number(item.dataset.subtitle);
|
|
5794
|
-
void this._video.setSubtitleTrack(id >= 0 ? id : null);
|
|
5795
|
-
this._closeSettings();
|
|
5796
|
-
});
|
|
5797
|
-
}
|
|
5798
|
-
for (const item of this._settingsMenu.querySelectorAll("[data-audio]")) {
|
|
5799
|
-
item.addEventListener("click", (e) => {
|
|
5800
|
-
e.stopPropagation();
|
|
5801
|
-
void this._video.setAudioTrack(Number(item.dataset.audio));
|
|
5802
|
-
this._closeSettings();
|
|
5803
|
-
});
|
|
5804
|
-
}
|
|
5805
|
-
const statsItem = this._settingsMenu.querySelector("[data-stats]");
|
|
5806
|
-
if (statsItem) {
|
|
5807
|
-
statsItem.addEventListener("click", (e) => {
|
|
5970
|
+
const statsRow = this._settingsMenu.querySelector("[data-stats]");
|
|
5971
|
+
if (statsRow) {
|
|
5972
|
+
statsRow.addEventListener("click", (e) => {
|
|
5808
5973
|
e.stopPropagation();
|
|
5809
5974
|
this._toggleStats();
|
|
5810
5975
|
this._closeSettings();
|
|
@@ -5883,16 +6048,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5883
6048
|
_showControls() {
|
|
5884
6049
|
this.showControls();
|
|
5885
6050
|
}
|
|
5886
|
-
_scheduleHide(durationMs
|
|
6051
|
+
_scheduleHide(durationMs) {
|
|
6052
|
+
const ms = durationMs ?? this._getControlsTimeout();
|
|
5887
6053
|
if (this._controlsTimer) clearTimeout(this._controlsTimer);
|
|
5888
6054
|
if (this._state !== "playing" && this._state !== "buffering") return;
|
|
5889
6055
|
if (this._settingsOpen) return;
|
|
6056
|
+
if (ms <= 0) return;
|
|
5890
6057
|
this._controlsTimer = setTimeout(() => {
|
|
5891
6058
|
if (this._state === "playing") {
|
|
5892
6059
|
this.setAttribute("data-controls-hidden", "");
|
|
5893
6060
|
this._toolbarTop.setAttribute("data-visible", "false");
|
|
5894
6061
|
}
|
|
5895
|
-
},
|
|
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;
|
|
5896
6071
|
}
|
|
5897
6072
|
// Strategy is visible in Stats for Nerds, no badge in controls bar.
|
|
5898
6073
|
// ── Click / tap handling (YouTube delayed-tap pattern) ──────────────────
|
|
@@ -5904,6 +6079,9 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5904
6079
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
5905
6080
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
5906
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;
|
|
5907
6085
|
/** True if the event's composed path passes through consumer-slotted
|
|
5908
6086
|
* content (toolbar or content-overlay). Slotted content lives in the
|
|
5909
6087
|
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
@@ -5917,6 +6095,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5917
6095
|
_onContainerClick(e) {
|
|
5918
6096
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5919
6097
|
if (this._isSlottedContentEvent(e)) return;
|
|
6098
|
+
if (this._settingsOpen) {
|
|
6099
|
+
this._closeSettings();
|
|
6100
|
+
return;
|
|
6101
|
+
}
|
|
5920
6102
|
if (this._lastPointerTypeWasTouch) {
|
|
5921
6103
|
this._lastPointerTypeWasTouch = false;
|
|
5922
6104
|
return;
|
|
@@ -5933,6 +6115,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5933
6115
|
_onContainerDblClick(e) {
|
|
5934
6116
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
5935
6117
|
if (this._isSlottedContentEvent(e)) return;
|
|
6118
|
+
if (this._touchDoubleTapConsumed) return;
|
|
5936
6119
|
if (this._tapTimer) {
|
|
5937
6120
|
clearTimeout(this._tapTimer);
|
|
5938
6121
|
this._tapTimer = null;
|
|
@@ -5955,6 +6138,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5955
6138
|
this._lastPointerTypeWasTouch = true;
|
|
5956
6139
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5957
6140
|
if (this._isSlottedContentEvent(e)) return;
|
|
6141
|
+
if (this._settingsOpen) {
|
|
6142
|
+
this._closeSettings();
|
|
6143
|
+
return;
|
|
6144
|
+
}
|
|
5958
6145
|
const now = Date.now();
|
|
5959
6146
|
if (now - this._lastTapTime < 300) {
|
|
5960
6147
|
if (this._tapTimer) {
|
|
@@ -5970,6 +6157,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5970
6157
|
} else {
|
|
5971
6158
|
this._toggleFullscreen();
|
|
5972
6159
|
}
|
|
6160
|
+
this._touchDoubleTapConsumed = true;
|
|
6161
|
+
setTimeout(() => {
|
|
6162
|
+
this._touchDoubleTapConsumed = false;
|
|
6163
|
+
}, 100);
|
|
5973
6164
|
this._lastTapTime = 0;
|
|
5974
6165
|
return;
|
|
5975
6166
|
}
|
|
@@ -6003,6 +6194,13 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6003
6194
|
this._video.currentTime = Math.max(0, this._video.currentTime + delta);
|
|
6004
6195
|
}
|
|
6005
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
|
+
}
|
|
6006
6204
|
_onKeydown(e) {
|
|
6007
6205
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
6008
6206
|
switch (e.key) {
|
|
@@ -6047,6 +6245,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6047
6245
|
this._video.playbackRate = Math.max(0.25, this._video.playbackRate - 0.25);
|
|
6048
6246
|
this._buildSettingsMenu();
|
|
6049
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;
|
|
6050
6261
|
case "Escape":
|
|
6051
6262
|
if (this._settingsOpen) {
|
|
6052
6263
|
e.preventDefault();
|
|
@@ -6204,6 +6415,15 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6204
6415
|
async setAudioTrack(id) {
|
|
6205
6416
|
return this._video.setAudioTrack(id);
|
|
6206
6417
|
}
|
|
6418
|
+
addSettingsSection(config) {
|
|
6419
|
+
this._customSections = this._customSections.filter((s) => s.id !== config.id);
|
|
6420
|
+
this._customSections.push(config);
|
|
6421
|
+
this._buildSettingsMenu();
|
|
6422
|
+
}
|
|
6423
|
+
removeSettingsSection(id) {
|
|
6424
|
+
this._customSections = this._customSections.filter((s) => s.id !== id);
|
|
6425
|
+
this._buildSettingsMenu();
|
|
6426
|
+
}
|
|
6207
6427
|
async setSubtitleTrack(id) {
|
|
6208
6428
|
return this._video.setSubtitleTrack(id);
|
|
6209
6429
|
}
|