avbridge 2.9.0 → 2.10.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 +31 -0
- package/dist/{chunk-SN4WZE24.js → chunk-3GKM5DFM.js} +42 -5
- package/dist/chunk-3GKM5DFM.js.map +1 -0
- package/dist/{chunk-EY6DZEDT.cjs → chunk-NQULEIA3.cjs} +42 -5
- package/dist/chunk-NQULEIA3.cjs.map +1 -0
- package/dist/element-browser.js +40 -3
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +2 -2
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +1 -1
- package/dist/index.cjs +8 -8
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- 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 +230 -98
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +27 -0
- package/dist/player.d.ts +27 -0
- package/dist/player.js +230 -98
- package/dist/player.js.map +1 -1
- package/package.json +1 -1
- package/src/element/avbridge-player.ts +150 -75
- package/src/element/player-styles.ts +85 -35
- package/src/index.ts +1 -0
- package/src/strategies/fallback/audio-output.ts +29 -4
- package/src/strategies/fallback/index.ts +12 -0
- package/src/strategies/hybrid/index.ts +9 -0
- package/src/types.ts +25 -0
- package/dist/chunk-EY6DZEDT.cjs.map +0 -1
- package/dist/chunk-SN4WZE24.js.map +0 -1
package/dist/player.d.cts
CHANGED
|
@@ -287,6 +287,28 @@ interface AvbridgeVideoElementEventMap {
|
|
|
287
287
|
fit: "contain" | "cover" | "fill";
|
|
288
288
|
}>;
|
|
289
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Configuration for a custom settings section added to `<avbridge-player>`
|
|
292
|
+
* via {@link addSettingsSection}. Sections render in the bottom-sheet
|
|
293
|
+
* settings panel alongside built-in sections (Speed, Audio, Subtitles,
|
|
294
|
+
* Fit, Stats for Nerds). The player owns rendering — consumers describe
|
|
295
|
+
* data; avbridge renders it in a consistent visual style.
|
|
296
|
+
*/
|
|
297
|
+
interface SettingsSectionConfig {
|
|
298
|
+
/** Unique id for this section. Used to update/remove later. */
|
|
299
|
+
id: string;
|
|
300
|
+
/** Display label (e.g. "Quality", "Translate"). */
|
|
301
|
+
label: string;
|
|
302
|
+
/** Items to show when the section is expanded. */
|
|
303
|
+
items: Array<{
|
|
304
|
+
id: string;
|
|
305
|
+
label: string;
|
|
306
|
+
/** Mark the currently-selected item. */
|
|
307
|
+
active?: boolean;
|
|
308
|
+
}>;
|
|
309
|
+
/** Called when the user picks an item. */
|
|
310
|
+
onSelect(itemId: string): void;
|
|
311
|
+
}
|
|
290
312
|
|
|
291
313
|
/**
|
|
292
314
|
* `<avbridge-player>` — YouTube-style controls element.
|
|
@@ -314,6 +336,8 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
314
336
|
private _volumeInput;
|
|
315
337
|
private _settingsBtn;
|
|
316
338
|
private _settingsMenu;
|
|
339
|
+
private _settingsScrim;
|
|
340
|
+
private _customSections;
|
|
317
341
|
private _fullscreenBtn;
|
|
318
342
|
private _speedIndicator;
|
|
319
343
|
private _rippleLeft;
|
|
@@ -353,6 +377,7 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
353
377
|
private _toggleMute;
|
|
354
378
|
private _updateVolume;
|
|
355
379
|
private _toggleSettings;
|
|
380
|
+
private _fitSettingsToPlayer;
|
|
356
381
|
private _closeSettings;
|
|
357
382
|
private _buildSettingsMenu;
|
|
358
383
|
private _toggleStats;
|
|
@@ -437,6 +462,8 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
437
462
|
load(): Promise<void>;
|
|
438
463
|
destroy(): Promise<void>;
|
|
439
464
|
setAudioTrack(id: number): Promise<void>;
|
|
465
|
+
addSettingsSection(config: SettingsSectionConfig): void;
|
|
466
|
+
removeSettingsSection(id: string): void;
|
|
440
467
|
setSubtitleTrack(id: number | null): Promise<void>;
|
|
441
468
|
getDiagnostics(): unknown;
|
|
442
469
|
canPlayType(mime: string): string;
|
package/dist/player.d.ts
CHANGED
|
@@ -287,6 +287,28 @@ interface AvbridgeVideoElementEventMap {
|
|
|
287
287
|
fit: "contain" | "cover" | "fill";
|
|
288
288
|
}>;
|
|
289
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Configuration for a custom settings section added to `<avbridge-player>`
|
|
292
|
+
* via {@link addSettingsSection}. Sections render in the bottom-sheet
|
|
293
|
+
* settings panel alongside built-in sections (Speed, Audio, Subtitles,
|
|
294
|
+
* Fit, Stats for Nerds). The player owns rendering — consumers describe
|
|
295
|
+
* data; avbridge renders it in a consistent visual style.
|
|
296
|
+
*/
|
|
297
|
+
interface SettingsSectionConfig {
|
|
298
|
+
/** Unique id for this section. Used to update/remove later. */
|
|
299
|
+
id: string;
|
|
300
|
+
/** Display label (e.g. "Quality", "Translate"). */
|
|
301
|
+
label: string;
|
|
302
|
+
/** Items to show when the section is expanded. */
|
|
303
|
+
items: Array<{
|
|
304
|
+
id: string;
|
|
305
|
+
label: string;
|
|
306
|
+
/** Mark the currently-selected item. */
|
|
307
|
+
active?: boolean;
|
|
308
|
+
}>;
|
|
309
|
+
/** Called when the user picks an item. */
|
|
310
|
+
onSelect(itemId: string): void;
|
|
311
|
+
}
|
|
290
312
|
|
|
291
313
|
/**
|
|
292
314
|
* `<avbridge-player>` — YouTube-style controls element.
|
|
@@ -314,6 +336,8 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
314
336
|
private _volumeInput;
|
|
315
337
|
private _settingsBtn;
|
|
316
338
|
private _settingsMenu;
|
|
339
|
+
private _settingsScrim;
|
|
340
|
+
private _customSections;
|
|
317
341
|
private _fullscreenBtn;
|
|
318
342
|
private _speedIndicator;
|
|
319
343
|
private _rippleLeft;
|
|
@@ -353,6 +377,7 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
353
377
|
private _toggleMute;
|
|
354
378
|
private _updateVolume;
|
|
355
379
|
private _toggleSettings;
|
|
380
|
+
private _fitSettingsToPlayer;
|
|
356
381
|
private _closeSettings;
|
|
357
382
|
private _buildSettingsMenu;
|
|
358
383
|
private _toggleStats;
|
|
@@ -437,6 +462,8 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
437
462
|
load(): Promise<void>;
|
|
438
463
|
destroy(): Promise<void>;
|
|
439
464
|
setAudioTrack(id: number): Promise<void>;
|
|
465
|
+
addSettingsSection(config: SettingsSectionConfig): void;
|
|
466
|
+
removeSettingsSection(id: string): void;
|
|
440
467
|
setSubtitleTrack(id: number | null): Promise<void>;
|
|
441
468
|
getDiagnostics(): unknown;
|
|
442
469
|
canPlayType(mime: string): string;
|
package/dist/player.js
CHANGED
|
@@ -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;
|
|
@@ -2531,6 +2550,14 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2531
2550
|
get: () => ctx.duration ?? NaN
|
|
2532
2551
|
});
|
|
2533
2552
|
}
|
|
2553
|
+
Object.defineProperty(target, "playbackRate", {
|
|
2554
|
+
configurable: true,
|
|
2555
|
+
get: () => audio.getPlaybackRate(),
|
|
2556
|
+
set: (v) => {
|
|
2557
|
+
audio.setPlaybackRate(v);
|
|
2558
|
+
target.dispatchEvent(new Event("ratechange"));
|
|
2559
|
+
}
|
|
2560
|
+
});
|
|
2534
2561
|
Object.defineProperty(target, "readyState", {
|
|
2535
2562
|
configurable: true,
|
|
2536
2563
|
get: () => {
|
|
@@ -2641,6 +2668,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2641
2668
|
delete target.muted;
|
|
2642
2669
|
delete target.readyState;
|
|
2643
2670
|
delete target.seekable;
|
|
2671
|
+
delete target.playbackRate;
|
|
2644
2672
|
} catch {
|
|
2645
2673
|
}
|
|
2646
2674
|
},
|
|
@@ -3176,6 +3204,14 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3176
3204
|
get: () => ctx.duration ?? NaN
|
|
3177
3205
|
});
|
|
3178
3206
|
}
|
|
3207
|
+
Object.defineProperty(target, "playbackRate", {
|
|
3208
|
+
configurable: true,
|
|
3209
|
+
get: () => audio.getPlaybackRate(),
|
|
3210
|
+
set: (v) => {
|
|
3211
|
+
audio.setPlaybackRate(v);
|
|
3212
|
+
target.dispatchEvent(new Event("ratechange"));
|
|
3213
|
+
}
|
|
3214
|
+
});
|
|
3179
3215
|
Object.defineProperty(target, "readyState", {
|
|
3180
3216
|
configurable: true,
|
|
3181
3217
|
get: () => {
|
|
@@ -3307,6 +3343,7 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3307
3343
|
delete target.muted;
|
|
3308
3344
|
delete target.readyState;
|
|
3309
3345
|
delete target.seekable;
|
|
3346
|
+
delete target.playbackRate;
|
|
3310
3347
|
} catch {
|
|
3311
3348
|
}
|
|
3312
3349
|
},
|
|
@@ -4723,7 +4760,6 @@ var PLAYER_STYLES = (
|
|
|
4723
4760
|
position: relative;
|
|
4724
4761
|
width: 100%;
|
|
4725
4762
|
height: 100%;
|
|
4726
|
-
cursor: pointer;
|
|
4727
4763
|
-webkit-tap-highlight-color: transparent;
|
|
4728
4764
|
user-select: none;
|
|
4729
4765
|
}
|
|
@@ -5173,63 +5209,114 @@ var PLAYER_STYLES = (
|
|
|
5173
5209
|
|
|
5174
5210
|
.avp-spacer { flex: 1; }
|
|
5175
5211
|
|
|
5176
|
-
/* \u2500\u2500 Settings
|
|
5212
|
+
/* \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 */
|
|
5177
5213
|
|
|
5214
|
+
/* Scrim \u2014 semi-transparent overlay behind the sheet, above the video.
|
|
5215
|
+
Tapping it dismisses the sheet. */
|
|
5216
|
+
.avp-settings-scrim {
|
|
5217
|
+
position: absolute;
|
|
5218
|
+
inset: 0;
|
|
5219
|
+
z-index: 9;
|
|
5220
|
+
background: rgba(0, 0, 0, 0.4);
|
|
5221
|
+
opacity: 0;
|
|
5222
|
+
pointer-events: none;
|
|
5223
|
+
transition: opacity 0.2s;
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
.avp-settings-scrim.open {
|
|
5227
|
+
opacity: 1;
|
|
5228
|
+
pointer-events: auto;
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
/* Sheet container \u2014 slides up from the bottom. Height is content-driven
|
|
5232
|
+
up to a JS-measured max (set on open via style.maxHeight). */
|
|
5178
5233
|
.avp-settings {
|
|
5179
5234
|
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;
|
|
5235
|
+
bottom: 0;
|
|
5236
|
+
left: 0;
|
|
5237
|
+
right: 0;
|
|
5191
5238
|
z-index: 10;
|
|
5192
|
-
|
|
5239
|
+
background: rgba(28, 28, 28, 0.97);
|
|
5240
|
+
border-radius: 12px 12px 0 0;
|
|
5241
|
+
overflow-y: auto;
|
|
5242
|
+
overscroll-behavior: contain;
|
|
5243
|
+
transform: translateY(100%);
|
|
5244
|
+
transition: transform 0.2s ease-out;
|
|
5245
|
+
max-height: 70%;
|
|
5246
|
+
padding-bottom: 52px;
|
|
5247
|
+
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
|
|
5193
5248
|
}
|
|
5194
5249
|
|
|
5195
|
-
.avp-settings.open {
|
|
5250
|
+
.avp-settings.open {
|
|
5251
|
+
transform: translateY(0);
|
|
5252
|
+
}
|
|
5196
5253
|
|
|
5197
|
-
.
|
|
5198
|
-
|
|
5199
|
-
|
|
5254
|
+
/* Drag handle indicator at top of sheet. */
|
|
5255
|
+
.avp-settings-handle {
|
|
5256
|
+
width: 36px;
|
|
5257
|
+
height: 4px;
|
|
5258
|
+
border-radius: 2px;
|
|
5259
|
+
background: rgba(255, 255, 255, 0.3);
|
|
5260
|
+
margin: 8px auto 4px;
|
|
5200
5261
|
}
|
|
5201
5262
|
|
|
5202
|
-
|
|
5263
|
+
/* \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
5264
|
|
|
5204
|
-
.avp-settings-
|
|
5205
|
-
|
|
5206
|
-
font-size: 11px;
|
|
5207
|
-
text-transform: uppercase;
|
|
5208
|
-
letter-spacing: 0.5px;
|
|
5209
|
-
opacity: 0.5;
|
|
5265
|
+
.avp-settings-section {
|
|
5266
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
5210
5267
|
}
|
|
5211
5268
|
|
|
5212
|
-
.avp-settings-
|
|
5269
|
+
.avp-settings-section:last-child { border-bottom: none; }
|
|
5270
|
+
|
|
5271
|
+
/* Section header \u2014 clickable row showing label + current value. */
|
|
5272
|
+
.avp-settings-header {
|
|
5273
|
+
position: relative;
|
|
5213
5274
|
display: flex;
|
|
5214
5275
|
align-items: center;
|
|
5215
|
-
|
|
5216
|
-
|
|
5276
|
+
justify-content: space-between;
|
|
5277
|
+
padding: 12px 16px;
|
|
5217
5278
|
cursor: pointer;
|
|
5279
|
+
font-size: 14px;
|
|
5218
5280
|
transition: background 0.1s;
|
|
5219
5281
|
}
|
|
5220
5282
|
|
|
5221
|
-
.avp-settings-
|
|
5283
|
+
.avp-settings-header:hover { background: rgba(255, 255, 255, 0.06); }
|
|
5222
5284
|
|
|
5223
|
-
.avp-settings-
|
|
5224
|
-
|
|
5285
|
+
.avp-settings-header-label {
|
|
5286
|
+
display: flex;
|
|
5287
|
+
align-items: center;
|
|
5288
|
+
gap: 8px;
|
|
5289
|
+
font-weight: 500;
|
|
5225
5290
|
}
|
|
5226
5291
|
|
|
5227
|
-
.avp-settings-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
font-
|
|
5292
|
+
.avp-settings-header-value {
|
|
5293
|
+
margin-left: auto;
|
|
5294
|
+
opacity: 0.6;
|
|
5295
|
+
font-size: 13px;
|
|
5296
|
+
text-align: right;
|
|
5231
5297
|
}
|
|
5232
5298
|
|
|
5299
|
+
/* Invisible native <select> layered over the value portion of the row.
|
|
5300
|
+
Covers from the value text to the right edge so tapping the value
|
|
5301
|
+
opens the OS picker. The label side remains inert. */
|
|
5302
|
+
.avp-settings-select {
|
|
5303
|
+
position: absolute;
|
|
5304
|
+
top: 0;
|
|
5305
|
+
right: 0;
|
|
5306
|
+
bottom: 0;
|
|
5307
|
+
width: 50%;
|
|
5308
|
+
opacity: 0;
|
|
5309
|
+
cursor: pointer;
|
|
5310
|
+
font-size: 16px;
|
|
5311
|
+
direction: rtl;
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
/* Toggle-style rows (Stats for Nerds) \u2014 no select, just clickable. */
|
|
5315
|
+
.avp-settings-toggle {
|
|
5316
|
+
cursor: pointer;
|
|
5317
|
+
}
|
|
5318
|
+
.avp-settings-toggle:hover { background: rgba(255, 255, 255, 0.06); }
|
|
5319
|
+
|
|
5233
5320
|
/* \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
5321
|
|
|
5235
5322
|
.avp-stats {
|
|
@@ -5367,6 +5454,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5367
5454
|
_volumeInput;
|
|
5368
5455
|
_settingsBtn;
|
|
5369
5456
|
_settingsMenu;
|
|
5457
|
+
_settingsScrim;
|
|
5458
|
+
_customSections = [];
|
|
5370
5459
|
_fullscreenBtn;
|
|
5371
5460
|
// Strategy badge removed — visible in Stats for Nerds instead.
|
|
5372
5461
|
// Spinner is rendered but driven entirely by CSS :host([data-state]) selectors.
|
|
@@ -5408,6 +5497,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5408
5497
|
this._volumeInput = shadow.querySelector(".avp-volume-input");
|
|
5409
5498
|
this._settingsBtn = shadow.querySelector(".avp-settings-btn");
|
|
5410
5499
|
this._settingsMenu = shadow.querySelector(".avp-settings");
|
|
5500
|
+
this._settingsScrim = shadow.querySelector(".avp-settings-scrim");
|
|
5411
5501
|
this._fullscreenBtn = shadow.querySelector(".avp-fullscreen");
|
|
5412
5502
|
this._speedIndicator = shadow.querySelector(".avp-speed-indicator");
|
|
5413
5503
|
this._statsEl = shadow.querySelector(".avp-stats");
|
|
@@ -5464,7 +5554,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5464
5554
|
<button class="avp-btn avp-settings-btn" part="settings-button" aria-label="Settings">${ICON_SETTINGS}</button>
|
|
5465
5555
|
<button class="avp-btn avp-fullscreen" part="fullscreen-button" aria-label="Fullscreen">${ICON_FULLSCREEN}</button>
|
|
5466
5556
|
</div>
|
|
5467
|
-
<div class="avp-settings
|
|
5557
|
+
<div class="avp-settings-scrim"></div>
|
|
5558
|
+
<div class="avp-settings" part="settings-menu"><div class="avp-settings-handle"></div></div>
|
|
5468
5559
|
</div>
|
|
5469
5560
|
</div>`;
|
|
5470
5561
|
}
|
|
@@ -5536,6 +5627,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5536
5627
|
e.stopPropagation();
|
|
5537
5628
|
this._toggleSettings();
|
|
5538
5629
|
});
|
|
5630
|
+
on(this._settingsScrim, "click", () => this._closeSettings());
|
|
5539
5631
|
on(this._fullscreenBtn, "click", (e) => {
|
|
5540
5632
|
e.stopPropagation();
|
|
5541
5633
|
this._toggleFullscreen();
|
|
@@ -5544,11 +5636,6 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5544
5636
|
const container = this.shadowRoot.querySelector(".avp");
|
|
5545
5637
|
on(container, "click", (e) => this._onContainerClick(e));
|
|
5546
5638
|
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
5639
|
on(document, "click", (e) => {
|
|
5553
5640
|
if (this._settingsOpen && !this.contains(e.target)) {
|
|
5554
5641
|
this._closeSettings();
|
|
@@ -5726,83 +5813,111 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5726
5813
|
_toggleSettings() {
|
|
5727
5814
|
this._settingsOpen = !this._settingsOpen;
|
|
5728
5815
|
this._settingsMenu.classList.toggle("open", this._settingsOpen);
|
|
5729
|
-
|
|
5816
|
+
this._settingsScrim.classList.toggle("open", this._settingsOpen);
|
|
5817
|
+
if (this._settingsOpen) {
|
|
5818
|
+
this._fitSettingsToPlayer();
|
|
5819
|
+
this._showControls();
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
_fitSettingsToPlayer() {
|
|
5823
|
+
const container = this.shadowRoot?.querySelector(".avp");
|
|
5824
|
+
if (!container) return;
|
|
5825
|
+
const rect = container.getBoundingClientRect();
|
|
5826
|
+
const maxH = Math.max(120, Math.floor(rect.height * 0.7));
|
|
5827
|
+
this._settingsMenu.style.maxHeight = `${maxH}px`;
|
|
5730
5828
|
}
|
|
5731
5829
|
_closeSettings() {
|
|
5732
5830
|
this._settingsOpen = false;
|
|
5733
5831
|
this._settingsMenu.classList.remove("open");
|
|
5832
|
+
this._settingsScrim.classList.remove("open");
|
|
5734
5833
|
}
|
|
5735
5834
|
_buildSettingsMenu() {
|
|
5736
5835
|
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
|
-
}
|
|
5836
|
+
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
5837
|
const currentRate = this._video.playbackRate ?? 1;
|
|
5748
|
-
|
|
5838
|
+
const speedValue = Math.abs(currentRate - 1) < 0.01 ? "Normal" : `${currentRate}x`;
|
|
5839
|
+
let speedOpts = "";
|
|
5749
5840
|
for (const spd of PLAYBACK_SPEEDS) {
|
|
5750
|
-
const
|
|
5841
|
+
const sel = Math.abs(spd - currentRate) < 0.01 ? " selected" : "";
|
|
5751
5842
|
const label = spd === 1 ? "Normal" : `${spd}x`;
|
|
5752
|
-
|
|
5843
|
+
speedOpts += `<option value="${spd}"${sel}>${label}</option>`;
|
|
5844
|
+
}
|
|
5845
|
+
sections.push(selectRow("Speed", speedValue, speedOpts, `data-action="speed"`));
|
|
5846
|
+
const audios = this._video.audioTracks ?? [];
|
|
5847
|
+
if (audios.length > 1) {
|
|
5848
|
+
let audioOpts = "";
|
|
5849
|
+
for (const t of audios) {
|
|
5850
|
+
audioOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
|
|
5851
|
+
}
|
|
5852
|
+
sections.push(selectRow("Audio", audios[0]?.language ?? "Track 1", audioOpts, `data-action="audio"`));
|
|
5753
5853
|
}
|
|
5754
|
-
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Speed</div>${speedItems}</div>`);
|
|
5755
5854
|
const subs = this._video.subtitleTracks ?? [];
|
|
5756
5855
|
if (subs.length > 0) {
|
|
5757
|
-
let
|
|
5856
|
+
let subOpts = `<option value="-1" selected>Off</option>`;
|
|
5758
5857
|
for (const t of subs) {
|
|
5759
|
-
|
|
5858
|
+
subOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
|
|
5760
5859
|
}
|
|
5761
|
-
sections.push(
|
|
5860
|
+
sections.push(selectRow("Subtitles", "Off", subOpts, `data-action="subtitle"`));
|
|
5762
5861
|
}
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5862
|
+
if (this.hasAttribute("show-fit")) {
|
|
5863
|
+
const currentFit = this._video.fit ?? "contain";
|
|
5864
|
+
const fitValue = currentFit[0].toUpperCase() + currentFit.slice(1);
|
|
5865
|
+
let fitOpts = "";
|
|
5866
|
+
for (const mode of FIT_MODES) {
|
|
5867
|
+
const sel = mode === currentFit ? " selected" : "";
|
|
5868
|
+
const label = mode[0].toUpperCase() + mode.slice(1);
|
|
5869
|
+
fitOpts += `<option value="${mode}"${sel}>${label}</option>`;
|
|
5768
5870
|
}
|
|
5769
|
-
sections.push(
|
|
5871
|
+
sections.push(selectRow("Fit", fitValue, fitOpts, `data-action="fit"`));
|
|
5770
5872
|
}
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
item.
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
});
|
|
5873
|
+
for (const cfg of this._customSections) {
|
|
5874
|
+
const activeItem = cfg.items.find((i) => i.active);
|
|
5875
|
+
let customOpts = "";
|
|
5876
|
+
for (const item of cfg.items) {
|
|
5877
|
+
const sel = item.active ? " selected" : "";
|
|
5878
|
+
customOpts += `<option value="${item.id}"${sel}>${item.label}</option>`;
|
|
5879
|
+
}
|
|
5880
|
+
sections.push(selectRow(cfg.label, activeItem?.label ?? "", customOpts, `data-action="custom" data-custom-id="${cfg.id}"`));
|
|
5780
5881
|
}
|
|
5781
|
-
|
|
5782
|
-
|
|
5882
|
+
sections.push(
|
|
5883
|
+
`<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>`
|
|
5884
|
+
);
|
|
5885
|
+
const handle = this._settingsMenu.querySelector(".avp-settings-handle");
|
|
5886
|
+
this._settingsMenu.innerHTML = "";
|
|
5887
|
+
if (handle) this._settingsMenu.appendChild(handle);
|
|
5888
|
+
else this._settingsMenu.insertAdjacentHTML("afterbegin", `<div class="avp-settings-handle"></div>`);
|
|
5889
|
+
this._settingsMenu.insertAdjacentHTML("beforeend", sections.join(""));
|
|
5890
|
+
for (const sel of this._settingsMenu.querySelectorAll(".avp-settings-select")) {
|
|
5891
|
+
sel.addEventListener("change", (e) => {
|
|
5783
5892
|
e.stopPropagation();
|
|
5784
|
-
|
|
5893
|
+
const action = sel.dataset.action;
|
|
5894
|
+
const val = sel.value;
|
|
5895
|
+
switch (action) {
|
|
5896
|
+
case "speed":
|
|
5897
|
+
this._video.playbackRate = Number(val);
|
|
5898
|
+
break;
|
|
5899
|
+
case "audio":
|
|
5900
|
+
void this._video.setAudioTrack(Number(val));
|
|
5901
|
+
break;
|
|
5902
|
+
case "subtitle":
|
|
5903
|
+
void this._video.setSubtitleTrack(Number(val) >= 0 ? Number(val) : null);
|
|
5904
|
+
break;
|
|
5905
|
+
case "fit":
|
|
5906
|
+
this.setAttribute("fit", val);
|
|
5907
|
+
break;
|
|
5908
|
+
case "custom": {
|
|
5909
|
+
const cfgId = sel.dataset.customId;
|
|
5910
|
+
const cfg = this._customSections.find((s) => s.id === cfgId);
|
|
5911
|
+
cfg?.onSelect(val);
|
|
5912
|
+
break;
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5785
5915
|
this._buildSettingsMenu();
|
|
5786
5916
|
});
|
|
5787
5917
|
}
|
|
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) => {
|
|
5918
|
+
const statsRow = this._settingsMenu.querySelector("[data-stats]");
|
|
5919
|
+
if (statsRow) {
|
|
5920
|
+
statsRow.addEventListener("click", (e) => {
|
|
5806
5921
|
e.stopPropagation();
|
|
5807
5922
|
this._toggleStats();
|
|
5808
5923
|
this._closeSettings();
|
|
@@ -5915,6 +6030,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5915
6030
|
_onContainerClick(e) {
|
|
5916
6031
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5917
6032
|
if (this._isSlottedContentEvent(e)) return;
|
|
6033
|
+
if (this._settingsOpen) {
|
|
6034
|
+
this._closeSettings();
|
|
6035
|
+
return;
|
|
6036
|
+
}
|
|
5918
6037
|
if (this._lastPointerTypeWasTouch) {
|
|
5919
6038
|
this._lastPointerTypeWasTouch = false;
|
|
5920
6039
|
return;
|
|
@@ -5953,6 +6072,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5953
6072
|
this._lastPointerTypeWasTouch = true;
|
|
5954
6073
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5955
6074
|
if (this._isSlottedContentEvent(e)) return;
|
|
6075
|
+
if (this._settingsOpen) {
|
|
6076
|
+
this._closeSettings();
|
|
6077
|
+
return;
|
|
6078
|
+
}
|
|
5956
6079
|
const now = Date.now();
|
|
5957
6080
|
if (now - this._lastTapTime < 300) {
|
|
5958
6081
|
if (this._tapTimer) {
|
|
@@ -6202,6 +6325,15 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6202
6325
|
async setAudioTrack(id) {
|
|
6203
6326
|
return this._video.setAudioTrack(id);
|
|
6204
6327
|
}
|
|
6328
|
+
addSettingsSection(config) {
|
|
6329
|
+
this._customSections = this._customSections.filter((s) => s.id !== config.id);
|
|
6330
|
+
this._customSections.push(config);
|
|
6331
|
+
this._buildSettingsMenu();
|
|
6332
|
+
}
|
|
6333
|
+
removeSettingsSection(id) {
|
|
6334
|
+
this._customSections = this._customSections.filter((s) => s.id !== id);
|
|
6335
|
+
this._buildSettingsMenu();
|
|
6336
|
+
}
|
|
6205
6337
|
async setSubtitleTrack(id) {
|
|
6206
6338
|
return this._video.setSubtitleTrack(id);
|
|
6207
6339
|
}
|