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.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
|
|
@@ -1605,6 +1605,10 @@ var AudioOutput = class {
|
|
|
1605
1605
|
_volume = 1;
|
|
1606
1606
|
/** User-set muted flag. When true, gain is forced to 0. */
|
|
1607
1607
|
_muted = false;
|
|
1608
|
+
/** Playback rate. Scales the media clock and each AudioBufferSourceNode's
|
|
1609
|
+
* playbackRate so audio pitches up/down accordingly (same as native
|
|
1610
|
+
* <video>.playbackRate). Default 1. */
|
|
1611
|
+
_rate = 1;
|
|
1608
1612
|
constructor() {
|
|
1609
1613
|
this.ctx = new AudioContext();
|
|
1610
1614
|
this.gain = this.ctx.createGain();
|
|
@@ -1626,6 +1630,20 @@ var AudioOutput = class {
|
|
|
1626
1630
|
getMuted() {
|
|
1627
1631
|
return this._muted;
|
|
1628
1632
|
}
|
|
1633
|
+
/** Set playback rate. Scales the media clock and pitches audio output
|
|
1634
|
+
* (same as native <video>.playbackRate — speed without pitch correction).
|
|
1635
|
+
* Rebases the anchor so the clock transition is seamless. */
|
|
1636
|
+
setPlaybackRate(rate) {
|
|
1637
|
+
if (rate === this._rate) return;
|
|
1638
|
+
const t = this.now();
|
|
1639
|
+
this.mediaTimeOfAnchor = t;
|
|
1640
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1641
|
+
this.wallAnchorMs = performance.now();
|
|
1642
|
+
this._rate = rate;
|
|
1643
|
+
}
|
|
1644
|
+
getPlaybackRate() {
|
|
1645
|
+
return this._rate;
|
|
1646
|
+
}
|
|
1629
1647
|
applyGain() {
|
|
1630
1648
|
const target = this._muted ? 0 : this._volume;
|
|
1631
1649
|
try {
|
|
@@ -1646,12 +1664,12 @@ var AudioOutput = class {
|
|
|
1646
1664
|
now() {
|
|
1647
1665
|
if (this.noAudio) {
|
|
1648
1666
|
if (this.state === "playing") {
|
|
1649
|
-
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3;
|
|
1667
|
+
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3 * this._rate;
|
|
1650
1668
|
}
|
|
1651
1669
|
return this.mediaTimeOfAnchor;
|
|
1652
1670
|
}
|
|
1653
1671
|
if (this.state === "playing") {
|
|
1654
|
-
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor);
|
|
1672
|
+
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
|
|
1655
1673
|
}
|
|
1656
1674
|
return this.mediaTimeOfAnchor;
|
|
1657
1675
|
}
|
|
@@ -1703,7 +1721,8 @@ var AudioOutput = class {
|
|
|
1703
1721
|
const node = this.ctx.createBufferSource();
|
|
1704
1722
|
node.buffer = buffer;
|
|
1705
1723
|
node.connect(this.gain);
|
|
1706
|
-
|
|
1724
|
+
if (this._rate !== 1) node.playbackRate.value = this._rate;
|
|
1725
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
1707
1726
|
if (ctxStart < this.ctx.currentTime) {
|
|
1708
1727
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1709
1728
|
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
@@ -1730,6 +1749,10 @@ var AudioOutput = class {
|
|
|
1730
1749
|
if (this.ctx.state === "suspended") {
|
|
1731
1750
|
await this.ctx.resume();
|
|
1732
1751
|
}
|
|
1752
|
+
try {
|
|
1753
|
+
this.gain.connect(this.ctx.destination);
|
|
1754
|
+
} catch {
|
|
1755
|
+
}
|
|
1733
1756
|
if (this.state === "paused") {
|
|
1734
1757
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1735
1758
|
this.state = "playing";
|
|
@@ -1756,6 +1779,10 @@ var AudioOutput = class {
|
|
|
1756
1779
|
this.mediaTimeOfAnchor = this.now();
|
|
1757
1780
|
this.state = "paused";
|
|
1758
1781
|
if (this.noAudio) return;
|
|
1782
|
+
try {
|
|
1783
|
+
this.gain.disconnect();
|
|
1784
|
+
} catch {
|
|
1785
|
+
}
|
|
1759
1786
|
if (this.ctx.state === "running") {
|
|
1760
1787
|
await this.ctx.suspend();
|
|
1761
1788
|
}
|
|
@@ -2531,6 +2558,14 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2531
2558
|
get: () => ctx.duration ?? NaN
|
|
2532
2559
|
});
|
|
2533
2560
|
}
|
|
2561
|
+
Object.defineProperty(target, "playbackRate", {
|
|
2562
|
+
configurable: true,
|
|
2563
|
+
get: () => audio.getPlaybackRate(),
|
|
2564
|
+
set: (v) => {
|
|
2565
|
+
audio.setPlaybackRate(v);
|
|
2566
|
+
target.dispatchEvent(new Event("ratechange"));
|
|
2567
|
+
}
|
|
2568
|
+
});
|
|
2534
2569
|
Object.defineProperty(target, "readyState", {
|
|
2535
2570
|
configurable: true,
|
|
2536
2571
|
get: () => {
|
|
@@ -2641,6 +2676,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2641
2676
|
delete target.muted;
|
|
2642
2677
|
delete target.readyState;
|
|
2643
2678
|
delete target.seekable;
|
|
2679
|
+
delete target.playbackRate;
|
|
2644
2680
|
} catch {
|
|
2645
2681
|
}
|
|
2646
2682
|
},
|
|
@@ -3176,6 +3212,14 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3176
3212
|
get: () => ctx.duration ?? NaN
|
|
3177
3213
|
});
|
|
3178
3214
|
}
|
|
3215
|
+
Object.defineProperty(target, "playbackRate", {
|
|
3216
|
+
configurable: true,
|
|
3217
|
+
get: () => audio.getPlaybackRate(),
|
|
3218
|
+
set: (v) => {
|
|
3219
|
+
audio.setPlaybackRate(v);
|
|
3220
|
+
target.dispatchEvent(new Event("ratechange"));
|
|
3221
|
+
}
|
|
3222
|
+
});
|
|
3179
3223
|
Object.defineProperty(target, "readyState", {
|
|
3180
3224
|
configurable: true,
|
|
3181
3225
|
get: () => {
|
|
@@ -3307,6 +3351,7 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3307
3351
|
delete target.muted;
|
|
3308
3352
|
delete target.readyState;
|
|
3309
3353
|
delete target.seekable;
|
|
3354
|
+
delete target.playbackRate;
|
|
3310
3355
|
} catch {
|
|
3311
3356
|
}
|
|
3312
3357
|
},
|
|
@@ -4528,7 +4573,7 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4528
4573
|
* strategies pick up the new track via their textTracks watcher.
|
|
4529
4574
|
*/
|
|
4530
4575
|
async addSubtitle(subtitle) {
|
|
4531
|
-
const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-
|
|
4576
|
+
const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-5H24MEBJ.js');
|
|
4532
4577
|
const format = subtitle.format ?? (subtitle.url.endsWith(".srt") ? "srt" : "vtt");
|
|
4533
4578
|
const track = {
|
|
4534
4579
|
id: this._subtitleTracks.length,
|
|
@@ -4537,14 +4582,27 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4537
4582
|
sidecarUrl: subtitle.url
|
|
4538
4583
|
};
|
|
4539
4584
|
this._subtitleTracks.push(track);
|
|
4585
|
+
console.log(`[avbridge:subs] addSubtitle id=${track.id} format=${format} lang=${subtitle.language ?? "?"}`);
|
|
4540
4586
|
await attachSubtitleTracks2(
|
|
4541
4587
|
this._videoEl,
|
|
4542
4588
|
this._subtitleTracks,
|
|
4543
4589
|
void 0,
|
|
4544
4590
|
(err, t) => {
|
|
4545
|
-
console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
|
|
4591
|
+
console.warn(`[avbridge:subs] subtitle ${t.id} failed: ${err.message}`);
|
|
4546
4592
|
}
|
|
4547
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
|
+
});
|
|
4548
4606
|
}
|
|
4549
4607
|
/**
|
|
4550
4608
|
* Disable the automatic `screen.orientation.lock()` that runs on
|
|
@@ -4723,7 +4781,6 @@ var PLAYER_STYLES = (
|
|
|
4723
4781
|
position: relative;
|
|
4724
4782
|
width: 100%;
|
|
4725
4783
|
height: 100%;
|
|
4726
|
-
cursor: pointer;
|
|
4727
4784
|
-webkit-tap-highlight-color: transparent;
|
|
4728
4785
|
user-select: none;
|
|
4729
4786
|
}
|
|
@@ -5173,62 +5230,113 @@ var PLAYER_STYLES = (
|
|
|
5173
5230
|
|
|
5174
5231
|
.avp-spacer { flex: 1; }
|
|
5175
5232
|
|
|
5176
|
-
/* \u2500\u2500 Settings
|
|
5233
|
+
/* \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 */
|
|
5234
|
+
|
|
5235
|
+
/* Scrim \u2014 semi-transparent overlay behind the sheet, above the video.
|
|
5236
|
+
Tapping it dismisses the sheet. */
|
|
5237
|
+
.avp-settings-scrim {
|
|
5238
|
+
position: absolute;
|
|
5239
|
+
inset: 0;
|
|
5240
|
+
z-index: 9;
|
|
5241
|
+
background: rgba(0, 0, 0, 0.4);
|
|
5242
|
+
opacity: 0;
|
|
5243
|
+
pointer-events: none;
|
|
5244
|
+
transition: opacity 0.2s;
|
|
5245
|
+
}
|
|
5177
5246
|
|
|
5247
|
+
.avp-settings-scrim.open {
|
|
5248
|
+
opacity: 1;
|
|
5249
|
+
pointer-events: auto;
|
|
5250
|
+
}
|
|
5251
|
+
|
|
5252
|
+
/* Sheet container \u2014 slides up from the bottom. Height is content-driven
|
|
5253
|
+
up to a JS-measured max (set on open via style.maxHeight). */
|
|
5178
5254
|
.avp-settings {
|
|
5179
5255
|
position: absolute;
|
|
5180
|
-
bottom:
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
border-radius: 8px;
|
|
5184
|
-
min-width: 220px;
|
|
5185
|
-
/* Fit within the player: leave room for the controls bar (52px bottom)
|
|
5186
|
-
and a small top margin (8px). On tall players this caps at 300px;
|
|
5187
|
-
on short players it shrinks to whatever fits. */
|
|
5188
|
-
max-height: min(300px, calc(100% - 52px - 8px));
|
|
5189
|
-
overflow-y: auto;
|
|
5190
|
-
display: none;
|
|
5256
|
+
bottom: 0;
|
|
5257
|
+
left: 0;
|
|
5258
|
+
right: 0;
|
|
5191
5259
|
z-index: 10;
|
|
5192
|
-
|
|
5260
|
+
background: rgba(28, 28, 28, 0.97);
|
|
5261
|
+
border-radius: 12px 12px 0 0;
|
|
5262
|
+
overflow-y: auto;
|
|
5263
|
+
overscroll-behavior: contain;
|
|
5264
|
+
transform: translateY(100%);
|
|
5265
|
+
transition: transform 0.2s ease-out;
|
|
5266
|
+
max-height: 70%;
|
|
5267
|
+
padding-bottom: 52px;
|
|
5268
|
+
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
|
|
5193
5269
|
}
|
|
5194
5270
|
|
|
5195
|
-
.avp-settings.open {
|
|
5271
|
+
.avp-settings.open {
|
|
5272
|
+
transform: translateY(0);
|
|
5273
|
+
}
|
|
5196
5274
|
|
|
5197
|
-
.
|
|
5198
|
-
|
|
5199
|
-
|
|
5275
|
+
/* Drag handle indicator at top of sheet. */
|
|
5276
|
+
.avp-settings-handle {
|
|
5277
|
+
width: 36px;
|
|
5278
|
+
height: 4px;
|
|
5279
|
+
border-radius: 2px;
|
|
5280
|
+
background: rgba(255, 255, 255, 0.3);
|
|
5281
|
+
margin: 8px auto 4px;
|
|
5200
5282
|
}
|
|
5201
5283
|
|
|
5202
|
-
|
|
5284
|
+
/* \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 */
|
|
5203
5285
|
|
|
5204
|
-
.avp-settings-
|
|
5205
|
-
|
|
5206
|
-
font-size: 11px;
|
|
5207
|
-
text-transform: uppercase;
|
|
5208
|
-
letter-spacing: 0.5px;
|
|
5209
|
-
opacity: 0.5;
|
|
5286
|
+
.avp-settings-section {
|
|
5287
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
5210
5288
|
}
|
|
5211
5289
|
|
|
5212
|
-
.avp-settings-
|
|
5290
|
+
.avp-settings-section:last-child { border-bottom: none; }
|
|
5291
|
+
|
|
5292
|
+
/* Section header \u2014 clickable row showing label + current value. */
|
|
5293
|
+
.avp-settings-header {
|
|
5294
|
+
position: relative;
|
|
5213
5295
|
display: flex;
|
|
5214
5296
|
align-items: center;
|
|
5215
|
-
|
|
5216
|
-
|
|
5297
|
+
justify-content: space-between;
|
|
5298
|
+
padding: 12px 16px;
|
|
5217
5299
|
cursor: pointer;
|
|
5300
|
+
font-size: 14px;
|
|
5218
5301
|
transition: background 0.1s;
|
|
5219
5302
|
}
|
|
5220
5303
|
|
|
5221
|
-
.avp-settings-
|
|
5304
|
+
.avp-settings-header:hover { background: rgba(255, 255, 255, 0.06); }
|
|
5305
|
+
|
|
5306
|
+
.avp-settings-header-label {
|
|
5307
|
+
display: flex;
|
|
5308
|
+
align-items: center;
|
|
5309
|
+
gap: 8px;
|
|
5310
|
+
font-weight: 500;
|
|
5311
|
+
}
|
|
5312
|
+
|
|
5313
|
+
.avp-settings-header-value {
|
|
5314
|
+
margin-left: auto;
|
|
5315
|
+
opacity: 0.6;
|
|
5316
|
+
font-size: 13px;
|
|
5317
|
+
text-align: right;
|
|
5318
|
+
}
|
|
5222
5319
|
|
|
5223
|
-
.
|
|
5224
|
-
|
|
5320
|
+
/* Invisible native <select> layered over the value portion of the row.
|
|
5321
|
+
Covers from the value text to the right edge so tapping the value
|
|
5322
|
+
opens the OS picker. The label side remains inert. */
|
|
5323
|
+
.avp-settings-select {
|
|
5324
|
+
position: absolute;
|
|
5325
|
+
top: 0;
|
|
5326
|
+
right: 0;
|
|
5327
|
+
bottom: 0;
|
|
5328
|
+
width: 50%;
|
|
5329
|
+
opacity: 0;
|
|
5330
|
+
cursor: pointer;
|
|
5331
|
+
font-size: 16px;
|
|
5332
|
+
direction: rtl;
|
|
5225
5333
|
}
|
|
5226
5334
|
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
font-weight: bold;
|
|
5335
|
+
/* Toggle-style rows (Stats for Nerds) \u2014 no select, just clickable. */
|
|
5336
|
+
.avp-settings-toggle {
|
|
5337
|
+
cursor: pointer;
|
|
5231
5338
|
}
|
|
5339
|
+
.avp-settings-toggle:hover { background: rgba(255, 255, 255, 0.06); }
|
|
5232
5340
|
|
|
5233
5341
|
/* \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 */
|
|
5234
5342
|
|
|
@@ -5308,7 +5416,7 @@ function formatTime(sec) {
|
|
|
5308
5416
|
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
|
|
5309
5417
|
}
|
|
5310
5418
|
var PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
|
5311
|
-
var
|
|
5419
|
+
var DEFAULT_CONTROLS_HIDE_MS = 3e3;
|
|
5312
5420
|
var FORWARDED_EVENTS = [
|
|
5313
5421
|
"ready",
|
|
5314
5422
|
"error",
|
|
@@ -5351,7 +5459,7 @@ var PROXY_ATTRIBUTES = [
|
|
|
5351
5459
|
];
|
|
5352
5460
|
var PLAYER_ATTRIBUTES = ["show-fit"];
|
|
5353
5461
|
var FIT_MODES = ["contain", "cover", "fill"];
|
|
5354
|
-
var AvbridgePlayerElement = class extends HTMLElement {
|
|
5462
|
+
var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
5355
5463
|
static observedAttributes = [...PROXY_ATTRIBUTES, ...PLAYER_ATTRIBUTES];
|
|
5356
5464
|
// ── Internal DOM refs ──────────────────────────────────────────────────
|
|
5357
5465
|
_video;
|
|
@@ -5367,6 +5475,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5367
5475
|
_volumeInput;
|
|
5368
5476
|
_settingsBtn;
|
|
5369
5477
|
_settingsMenu;
|
|
5478
|
+
_settingsScrim;
|
|
5479
|
+
_customSections = [];
|
|
5370
5480
|
_fullscreenBtn;
|
|
5371
5481
|
// Strategy badge removed — visible in Stats for Nerds instead.
|
|
5372
5482
|
// Spinner is rendered but driven entirely by CSS :host([data-state]) selectors.
|
|
@@ -5377,6 +5487,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5377
5487
|
_state = "idle";
|
|
5378
5488
|
_controlsTimer = null;
|
|
5379
5489
|
_settingsOpen = false;
|
|
5490
|
+
_activeAudioTrackId = null;
|
|
5491
|
+
_activeSubtitleTrackId = null;
|
|
5380
5492
|
_userSeeking = false;
|
|
5381
5493
|
_holdTimer = null;
|
|
5382
5494
|
_holdSpeedActive = false;
|
|
@@ -5408,6 +5520,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5408
5520
|
this._volumeInput = shadow.querySelector(".avp-volume-input");
|
|
5409
5521
|
this._settingsBtn = shadow.querySelector(".avp-settings-btn");
|
|
5410
5522
|
this._settingsMenu = shadow.querySelector(".avp-settings");
|
|
5523
|
+
this._settingsScrim = shadow.querySelector(".avp-settings-scrim");
|
|
5411
5524
|
this._fullscreenBtn = shadow.querySelector(".avp-fullscreen");
|
|
5412
5525
|
this._speedIndicator = shadow.querySelector(".avp-speed-indicator");
|
|
5413
5526
|
this._statsEl = shadow.querySelector(".avp-stats");
|
|
@@ -5464,7 +5577,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5464
5577
|
<button class="avp-btn avp-settings-btn" part="settings-button" aria-label="Settings">${ICON_SETTINGS}</button>
|
|
5465
5578
|
<button class="avp-btn avp-fullscreen" part="fullscreen-button" aria-label="Fullscreen">${ICON_FULLSCREEN}</button>
|
|
5466
5579
|
</div>
|
|
5467
|
-
<div class="avp-settings
|
|
5580
|
+
<div class="avp-settings-scrim"></div>
|
|
5581
|
+
<div class="avp-settings" part="settings-menu"><div class="avp-settings-handle"></div></div>
|
|
5468
5582
|
</div>
|
|
5469
5583
|
</div>`;
|
|
5470
5584
|
}
|
|
@@ -5536,6 +5650,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5536
5650
|
e.stopPropagation();
|
|
5537
5651
|
this._toggleSettings();
|
|
5538
5652
|
});
|
|
5653
|
+
on(this._settingsScrim, "click", () => this._closeSettings());
|
|
5539
5654
|
on(this._fullscreenBtn, "click", (e) => {
|
|
5540
5655
|
e.stopPropagation();
|
|
5541
5656
|
this._toggleFullscreen();
|
|
@@ -5544,11 +5659,6 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5544
5659
|
const container = this.shadowRoot.querySelector(".avp");
|
|
5545
5660
|
on(container, "click", (e) => this._onContainerClick(e));
|
|
5546
5661
|
on(container, "dblclick", (e) => this._onContainerDblClick(e));
|
|
5547
|
-
on(container, "click", (e) => {
|
|
5548
|
-
if (this._settingsOpen && !e.target.closest?.(".avp-settings-btn, .avp-settings")) {
|
|
5549
|
-
this._closeSettings();
|
|
5550
|
-
}
|
|
5551
|
-
});
|
|
5552
5662
|
on(document, "click", (e) => {
|
|
5553
5663
|
if (this._settingsOpen && !this.contains(e.target)) {
|
|
5554
5664
|
this._closeSettings();
|
|
@@ -5642,6 +5752,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5642
5752
|
const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
5643
5753
|
return frac * (this._video.duration || 0);
|
|
5644
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;
|
|
5645
5759
|
_onSeekPointerDown(e) {
|
|
5646
5760
|
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
5647
5761
|
e.preventDefault();
|
|
@@ -5649,15 +5763,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5649
5763
|
const seekBar = this.shadowRoot.querySelector(".avp-seek");
|
|
5650
5764
|
seekBar.setPointerCapture(e.pointerId);
|
|
5651
5765
|
seekBar.setAttribute("data-seeking", "");
|
|
5766
|
+
const scrubMode = seekBar.getBoundingClientRect().width < _AvbridgePlayerElement.SCRUB_WIDTH_THRESHOLD;
|
|
5767
|
+
let lastScrubCommit = 0;
|
|
5652
5768
|
const initial = this._timeFromSeekPointer(e.clientX);
|
|
5653
5769
|
this._seekInput.value = String(initial);
|
|
5654
5770
|
this._onSeekInput();
|
|
5655
5771
|
this._updateSeekTooltip(e.clientX);
|
|
5772
|
+
if (scrubMode) this._onSeekCommit();
|
|
5656
5773
|
const onMove = (ev) => {
|
|
5657
5774
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5658
5775
|
this._seekInput.value = String(t);
|
|
5659
5776
|
this._onSeekInput();
|
|
5660
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
|
+
}
|
|
5661
5786
|
};
|
|
5662
5787
|
const onUp = (ev) => {
|
|
5663
5788
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
@@ -5726,83 +5851,123 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5726
5851
|
_toggleSettings() {
|
|
5727
5852
|
this._settingsOpen = !this._settingsOpen;
|
|
5728
5853
|
this._settingsMenu.classList.toggle("open", this._settingsOpen);
|
|
5729
|
-
|
|
5854
|
+
this._settingsScrim.classList.toggle("open", this._settingsOpen);
|
|
5855
|
+
if (this._settingsOpen) {
|
|
5856
|
+
this._fitSettingsToPlayer();
|
|
5857
|
+
this._showControls();
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
_fitSettingsToPlayer() {
|
|
5861
|
+
const container = this.shadowRoot?.querySelector(".avp");
|
|
5862
|
+
if (!container) return;
|
|
5863
|
+
const rect = container.getBoundingClientRect();
|
|
5864
|
+
const maxH = Math.max(120, Math.floor(rect.height * 0.7));
|
|
5865
|
+
this._settingsMenu.style.maxHeight = `${maxH}px`;
|
|
5730
5866
|
}
|
|
5731
5867
|
_closeSettings() {
|
|
5732
5868
|
this._settingsOpen = false;
|
|
5733
5869
|
this._settingsMenu.classList.remove("open");
|
|
5870
|
+
this._settingsScrim.classList.remove("open");
|
|
5734
5871
|
}
|
|
5735
5872
|
_buildSettingsMenu() {
|
|
5736
5873
|
const sections = [];
|
|
5737
|
-
|
|
5738
|
-
const currentFit = this._video.fit ?? "contain";
|
|
5739
|
-
let fitItems = "";
|
|
5740
|
-
for (const mode of FIT_MODES) {
|
|
5741
|
-
const active = mode === currentFit;
|
|
5742
|
-
const label = mode[0].toUpperCase() + mode.slice(1);
|
|
5743
|
-
fitItems += `<div class="avp-settings-item${active ? " active" : ""}" data-fit="${mode}">${label}</div>`;
|
|
5744
|
-
}
|
|
5745
|
-
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Fit</div>${fitItems}</div>`);
|
|
5746
|
-
}
|
|
5874
|
+
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>`;
|
|
5747
5875
|
const currentRate = this._video.playbackRate ?? 1;
|
|
5748
|
-
|
|
5876
|
+
const speedValue = Math.abs(currentRate - 1) < 0.01 ? "Normal" : `${currentRate}x`;
|
|
5877
|
+
let speedOpts = "";
|
|
5749
5878
|
for (const spd of PLAYBACK_SPEEDS) {
|
|
5750
|
-
const
|
|
5879
|
+
const sel = Math.abs(spd - currentRate) < 0.01 ? " selected" : "";
|
|
5751
5880
|
const label = spd === 1 ? "Normal" : `${spd}x`;
|
|
5752
|
-
|
|
5881
|
+
speedOpts += `<option value="${spd}"${sel}>${label}</option>`;
|
|
5882
|
+
}
|
|
5883
|
+
sections.push(selectRow("Speed", speedValue, speedOpts, `data-action="speed"`));
|
|
5884
|
+
const audios = this._video.audioTracks ?? [];
|
|
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}`;
|
|
5889
|
+
let audioOpts = "";
|
|
5890
|
+
for (const t of audios) {
|
|
5891
|
+
const sel = t.id === activeAudioId ? " selected" : "";
|
|
5892
|
+
audioOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
|
|
5893
|
+
}
|
|
5894
|
+
sections.push(selectRow("Audio", audioValue, audioOpts, `data-action="audio"`));
|
|
5753
5895
|
}
|
|
5754
|
-
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Speed</div>${speedItems}</div>`);
|
|
5755
5896
|
const subs = this._video.subtitleTracks ?? [];
|
|
5756
5897
|
if (subs.length > 0) {
|
|
5757
|
-
|
|
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>`;
|
|
5758
5902
|
for (const t of subs) {
|
|
5759
|
-
|
|
5903
|
+
const sel = t.id === activeSubId ? " selected" : "";
|
|
5904
|
+
subOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
|
|
5760
5905
|
}
|
|
5761
|
-
sections.push(
|
|
5906
|
+
sections.push(selectRow("Subtitles", subValue, subOpts, `data-action="subtitle"`));
|
|
5762
5907
|
}
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5908
|
+
if (this.hasAttribute("show-fit")) {
|
|
5909
|
+
const currentFit = this._video.fit ?? "contain";
|
|
5910
|
+
const fitValue = currentFit[0].toUpperCase() + currentFit.slice(1);
|
|
5911
|
+
let fitOpts = "";
|
|
5912
|
+
for (const mode of FIT_MODES) {
|
|
5913
|
+
const sel = mode === currentFit ? " selected" : "";
|
|
5914
|
+
const label = mode[0].toUpperCase() + mode.slice(1);
|
|
5915
|
+
fitOpts += `<option value="${mode}"${sel}>${label}</option>`;
|
|
5768
5916
|
}
|
|
5769
|
-
sections.push(
|
|
5917
|
+
sections.push(selectRow("Fit", fitValue, fitOpts, `data-action="fit"`));
|
|
5770
5918
|
}
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
item.
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
});
|
|
5919
|
+
for (const cfg of this._customSections) {
|
|
5920
|
+
const activeItem = cfg.items.find((i) => i.active);
|
|
5921
|
+
let customOpts = "";
|
|
5922
|
+
for (const item of cfg.items) {
|
|
5923
|
+
const sel = item.active ? " selected" : "";
|
|
5924
|
+
customOpts += `<option value="${item.id}"${sel}>${item.label}</option>`;
|
|
5925
|
+
}
|
|
5926
|
+
sections.push(selectRow(cfg.label, activeItem?.label ?? "", customOpts, `data-action="custom" data-custom-id="${cfg.id}"`));
|
|
5780
5927
|
}
|
|
5781
|
-
|
|
5782
|
-
|
|
5928
|
+
sections.push(
|
|
5929
|
+
`<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>`
|
|
5930
|
+
);
|
|
5931
|
+
const handle = this._settingsMenu.querySelector(".avp-settings-handle");
|
|
5932
|
+
this._settingsMenu.innerHTML = "";
|
|
5933
|
+
if (handle) this._settingsMenu.appendChild(handle);
|
|
5934
|
+
else this._settingsMenu.insertAdjacentHTML("afterbegin", `<div class="avp-settings-handle"></div>`);
|
|
5935
|
+
this._settingsMenu.insertAdjacentHTML("beforeend", sections.join(""));
|
|
5936
|
+
for (const sel of this._settingsMenu.querySelectorAll(".avp-settings-select")) {
|
|
5937
|
+
sel.addEventListener("change", (e) => {
|
|
5783
5938
|
e.stopPropagation();
|
|
5784
|
-
|
|
5939
|
+
const action = sel.dataset.action;
|
|
5940
|
+
const val = sel.value;
|
|
5941
|
+
switch (action) {
|
|
5942
|
+
case "speed":
|
|
5943
|
+
this._video.playbackRate = Number(val);
|
|
5944
|
+
break;
|
|
5945
|
+
case "audio":
|
|
5946
|
+
this._activeAudioTrackId = Number(val);
|
|
5947
|
+
void this._video.setAudioTrack(Number(val));
|
|
5948
|
+
break;
|
|
5949
|
+
case "subtitle": {
|
|
5950
|
+
const subId = Number(val);
|
|
5951
|
+
this._activeSubtitleTrackId = subId >= 0 ? subId : null;
|
|
5952
|
+
void this._video.setSubtitleTrack(subId >= 0 ? subId : null);
|
|
5953
|
+
break;
|
|
5954
|
+
}
|
|
5955
|
+
case "fit":
|
|
5956
|
+
this.setAttribute("fit", val);
|
|
5957
|
+
break;
|
|
5958
|
+
case "custom": {
|
|
5959
|
+
const cfgId = sel.dataset.customId;
|
|
5960
|
+
const cfg = this._customSections.find((s) => s.id === cfgId);
|
|
5961
|
+
cfg?.onSelect(val);
|
|
5962
|
+
break;
|
|
5963
|
+
}
|
|
5964
|
+
}
|
|
5785
5965
|
this._buildSettingsMenu();
|
|
5786
5966
|
});
|
|
5787
5967
|
}
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
const id = Number(item.dataset.subtitle);
|
|
5792
|
-
void this._video.setSubtitleTrack(id >= 0 ? id : null);
|
|
5793
|
-
this._closeSettings();
|
|
5794
|
-
});
|
|
5795
|
-
}
|
|
5796
|
-
for (const item of this._settingsMenu.querySelectorAll("[data-audio]")) {
|
|
5797
|
-
item.addEventListener("click", (e) => {
|
|
5798
|
-
e.stopPropagation();
|
|
5799
|
-
void this._video.setAudioTrack(Number(item.dataset.audio));
|
|
5800
|
-
this._closeSettings();
|
|
5801
|
-
});
|
|
5802
|
-
}
|
|
5803
|
-
const statsItem = this._settingsMenu.querySelector("[data-stats]");
|
|
5804
|
-
if (statsItem) {
|
|
5805
|
-
statsItem.addEventListener("click", (e) => {
|
|
5968
|
+
const statsRow = this._settingsMenu.querySelector("[data-stats]");
|
|
5969
|
+
if (statsRow) {
|
|
5970
|
+
statsRow.addEventListener("click", (e) => {
|
|
5806
5971
|
e.stopPropagation();
|
|
5807
5972
|
this._toggleStats();
|
|
5808
5973
|
this._closeSettings();
|
|
@@ -5881,16 +6046,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5881
6046
|
_showControls() {
|
|
5882
6047
|
this.showControls();
|
|
5883
6048
|
}
|
|
5884
|
-
_scheduleHide(durationMs
|
|
6049
|
+
_scheduleHide(durationMs) {
|
|
6050
|
+
const ms = durationMs ?? this._getControlsTimeout();
|
|
5885
6051
|
if (this._controlsTimer) clearTimeout(this._controlsTimer);
|
|
5886
6052
|
if (this._state !== "playing" && this._state !== "buffering") return;
|
|
5887
6053
|
if (this._settingsOpen) return;
|
|
6054
|
+
if (ms <= 0) return;
|
|
5888
6055
|
this._controlsTimer = setTimeout(() => {
|
|
5889
6056
|
if (this._state === "playing") {
|
|
5890
6057
|
this.setAttribute("data-controls-hidden", "");
|
|
5891
6058
|
this._toolbarTop.setAttribute("data-visible", "false");
|
|
5892
6059
|
}
|
|
5893
|
-
},
|
|
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;
|
|
5894
6069
|
}
|
|
5895
6070
|
// Strategy is visible in Stats for Nerds, no badge in controls bar.
|
|
5896
6071
|
// ── Click / tap handling (YouTube delayed-tap pattern) ──────────────────
|
|
@@ -5902,6 +6077,9 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5902
6077
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
5903
6078
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
5904
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;
|
|
5905
6083
|
/** True if the event's composed path passes through consumer-slotted
|
|
5906
6084
|
* content (toolbar or content-overlay). Slotted content lives in the
|
|
5907
6085
|
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
@@ -5915,6 +6093,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5915
6093
|
_onContainerClick(e) {
|
|
5916
6094
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5917
6095
|
if (this._isSlottedContentEvent(e)) return;
|
|
6096
|
+
if (this._settingsOpen) {
|
|
6097
|
+
this._closeSettings();
|
|
6098
|
+
return;
|
|
6099
|
+
}
|
|
5918
6100
|
if (this._lastPointerTypeWasTouch) {
|
|
5919
6101
|
this._lastPointerTypeWasTouch = false;
|
|
5920
6102
|
return;
|
|
@@ -5931,6 +6113,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5931
6113
|
_onContainerDblClick(e) {
|
|
5932
6114
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
5933
6115
|
if (this._isSlottedContentEvent(e)) return;
|
|
6116
|
+
if (this._touchDoubleTapConsumed) return;
|
|
5934
6117
|
if (this._tapTimer) {
|
|
5935
6118
|
clearTimeout(this._tapTimer);
|
|
5936
6119
|
this._tapTimer = null;
|
|
@@ -5953,6 +6136,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5953
6136
|
this._lastPointerTypeWasTouch = true;
|
|
5954
6137
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5955
6138
|
if (this._isSlottedContentEvent(e)) return;
|
|
6139
|
+
if (this._settingsOpen) {
|
|
6140
|
+
this._closeSettings();
|
|
6141
|
+
return;
|
|
6142
|
+
}
|
|
5956
6143
|
const now = Date.now();
|
|
5957
6144
|
if (now - this._lastTapTime < 300) {
|
|
5958
6145
|
if (this._tapTimer) {
|
|
@@ -5968,6 +6155,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5968
6155
|
} else {
|
|
5969
6156
|
this._toggleFullscreen();
|
|
5970
6157
|
}
|
|
6158
|
+
this._touchDoubleTapConsumed = true;
|
|
6159
|
+
setTimeout(() => {
|
|
6160
|
+
this._touchDoubleTapConsumed = false;
|
|
6161
|
+
}, 100);
|
|
5971
6162
|
this._lastTapTime = 0;
|
|
5972
6163
|
return;
|
|
5973
6164
|
}
|
|
@@ -6001,6 +6192,13 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6001
6192
|
this._video.currentTime = Math.max(0, this._video.currentTime + delta);
|
|
6002
6193
|
}
|
|
6003
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
|
+
}
|
|
6004
6202
|
_onKeydown(e) {
|
|
6005
6203
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
6006
6204
|
switch (e.key) {
|
|
@@ -6045,6 +6243,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6045
6243
|
this._video.playbackRate = Math.max(0.25, this._video.playbackRate - 0.25);
|
|
6046
6244
|
this._buildSettingsMenu();
|
|
6047
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;
|
|
6048
6259
|
case "Escape":
|
|
6049
6260
|
if (this._settingsOpen) {
|
|
6050
6261
|
e.preventDefault();
|
|
@@ -6202,6 +6413,15 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6202
6413
|
async setAudioTrack(id) {
|
|
6203
6414
|
return this._video.setAudioTrack(id);
|
|
6204
6415
|
}
|
|
6416
|
+
addSettingsSection(config) {
|
|
6417
|
+
this._customSections = this._customSections.filter((s) => s.id !== config.id);
|
|
6418
|
+
this._customSections.push(config);
|
|
6419
|
+
this._buildSettingsMenu();
|
|
6420
|
+
}
|
|
6421
|
+
removeSettingsSection(id) {
|
|
6422
|
+
this._customSections = this._customSections.filter((s) => s.id !== id);
|
|
6423
|
+
this._buildSettingsMenu();
|
|
6424
|
+
}
|
|
6205
6425
|
async setSubtitleTrack(id) {
|
|
6206
6426
|
return this._video.setSubtitleTrack(id);
|
|
6207
6427
|
}
|