avbridge 2.8.4 → 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 +164 -0
- package/README.md +74 -1
- package/dist/{avi-F6WZJK5T.cjs → avi-2ILLBNPQ.cjs} +8 -2
- package/dist/avi-2ILLBNPQ.cjs.map +1 -0
- package/dist/{avi-W6L3BTWU.cjs → avi-B5CQYB7L.cjs} +8 -2
- package/dist/avi-B5CQYB7L.cjs.map +1 -0
- package/dist/{avi-2JPBSHGA.js → avi-JXU4GQL2.js} +8 -2
- package/dist/avi-JXU4GQL2.js.map +1 -0
- package/dist/{avi-NJXAXUXK.js → avi-RWWPN2PR.js} +8 -2
- package/dist/avi-RWWPN2PR.js.map +1 -0
- package/dist/{chunk-X2K3GIWE.js → chunk-2NSOOMXW.js} +14 -3
- package/dist/chunk-2NSOOMXW.js.map +1 -0
- package/dist/{chunk-KBWQRGHS.js → chunk-3GKM5DFM.js} +119 -8
- package/dist/chunk-3GKM5DFM.js.map +1 -0
- package/dist/{chunk-ZCUXHW55.cjs → chunk-BYGZN4Z5.cjs} +5 -5
- package/dist/{chunk-ZCUXHW55.cjs.map → chunk-BYGZN4Z5.cjs.map} +1 -1
- package/dist/{chunk-SMH6IOP2.js → chunk-CL6UEUQF.js} +4 -4
- package/dist/{chunk-SMH6IOP2.js.map → chunk-CL6UEUQF.js.map} +1 -1
- package/dist/{chunk-SR3MPV4D.js → chunk-GYIJU44C.js} +5 -5
- package/dist/{chunk-SR3MPV4D.js.map → chunk-GYIJU44C.js.map} +1 -1
- package/dist/{chunk-CPZ7PXAM.cjs → chunk-L7A3ECI2.cjs} +14 -2
- package/dist/chunk-L7A3ECI2.cjs.map +1 -0
- package/dist/{chunk-YX4AGLNF.cjs → chunk-NQULEIA3.cjs} +129 -18
- package/dist/chunk-NQULEIA3.cjs.map +1 -0
- package/dist/{chunk-Q2VUO52Z.cjs → chunk-OTFS7DC4.cjs} +12 -12
- package/dist/{chunk-Q2VUO52Z.cjs.map → chunk-OTFS7DC4.cjs.map} +1 -1
- package/dist/element-browser.js +144 -10
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +16 -10
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +11 -6
- package/dist/element.d.ts +11 -6
- package/dist/element.js +15 -9
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +20 -20
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -8
- package/dist/libav-demux-3N5Y3VQA.cjs +31 -0
- package/dist/{libav-demux-H2GS46GH.cjs.map → libav-demux-3N5Y3VQA.cjs.map} +1 -1
- package/dist/libav-demux-JXD4OTLM.js +6 -0
- package/dist/{libav-demux-OWZ4T2YW.js.map → libav-demux-JXD4OTLM.js.map} +1 -1
- package/dist/{player-BptSJPfn.d.cts → player-DDdNVFDv.d.cts} +24 -2
- package/dist/{player-BptSJPfn.d.ts → player-DDdNVFDv.d.ts} +24 -2
- package/dist/player.cjs +413 -117
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +44 -11
- package/dist/player.d.ts +44 -11
- package/dist/player.js +413 -117
- package/dist/player.js.map +1 -1
- package/dist/{remux-WBYIZBBX.js → remux-56V7LDAD.js} +5 -5
- package/dist/{remux-WBYIZBBX.js.map → remux-56V7LDAD.js.map} +1 -1
- package/dist/{remux-OBSMIENG.cjs → remux-KUS5GIL6.cjs} +10 -10
- package/dist/{remux-OBSMIENG.cjs.map → remux-KUS5GIL6.cjs.map} +1 -1
- package/package.json +1 -1
- package/src/classify/rules.ts +2 -0
- package/src/element/avbridge-player.ts +172 -86
- package/src/element/avbridge-video.ts +22 -6
- package/src/element/player-styles.ts +149 -34
- package/src/index.ts +1 -0
- package/src/probe/avi.ts +2 -0
- package/src/strategies/fallback/audio-output.ts +29 -4
- package/src/strategies/fallback/decoder.ts +30 -0
- package/src/strategies/fallback/index.ts +42 -0
- package/src/strategies/hybrid/decoder.ts +35 -0
- package/src/strategies/hybrid/index.ts +26 -0
- package/src/strategies/remux/index.ts +8 -0
- package/src/types.ts +31 -0
- package/src/util/libav-demux.ts +26 -0
- package/dist/avi-2JPBSHGA.js.map +0 -1
- package/dist/avi-F6WZJK5T.cjs.map +0 -1
- package/dist/avi-NJXAXUXK.js.map +0 -1
- package/dist/avi-W6L3BTWU.cjs.map +0 -1
- package/dist/chunk-CPZ7PXAM.cjs.map +0 -1
- package/dist/chunk-KBWQRGHS.js.map +0 -1
- package/dist/chunk-X2K3GIWE.js.map +0 -1
- package/dist/chunk-YX4AGLNF.cjs.map +0 -1
- package/dist/libav-demux-H2GS46GH.cjs +0 -27
- package/dist/libav-demux-OWZ4T2YW.js +0 -6
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { createOutputFormat, generateFilename, mimeForFormat, remux, validateRemuxEligibility } from './chunk-
|
|
2
|
-
import './chunk-
|
|
1
|
+
export { createOutputFormat, generateFilename, mimeForFormat, remux, validateRemuxEligibility } from './chunk-CL6UEUQF.js';
|
|
2
|
+
import './chunk-GYIJU44C.js';
|
|
3
3
|
import './chunk-CPJLFFCC.js';
|
|
4
4
|
import './chunk-LUFA47FP.js';
|
|
5
|
-
import './chunk-
|
|
5
|
+
import './chunk-2NSOOMXW.js';
|
|
6
6
|
import './chunk-DCSOQH2N.js';
|
|
7
7
|
import './chunk-5DMTJVIU.js';
|
|
8
8
|
import './chunk-5YAWWKA3.js';
|
|
9
|
-
//# sourceMappingURL=remux-
|
|
10
|
-
//# sourceMappingURL=remux-
|
|
9
|
+
//# sourceMappingURL=remux-56V7LDAD.js.map
|
|
10
|
+
//# sourceMappingURL=remux-56V7LDAD.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"remux-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"remux-56V7LDAD.js"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
require('./chunk-
|
|
3
|
+
var chunkOTFS7DC4_cjs = require('./chunk-OTFS7DC4.cjs');
|
|
4
|
+
require('./chunk-BYGZN4Z5.cjs');
|
|
5
5
|
require('./chunk-2IJ66NTD.cjs');
|
|
6
6
|
require('./chunk-QDJLQR53.cjs');
|
|
7
|
-
require('./chunk-
|
|
7
|
+
require('./chunk-L7A3ECI2.cjs');
|
|
8
8
|
require('./chunk-Z33SBWL5.cjs');
|
|
9
9
|
require('./chunk-G4APZMCP.cjs');
|
|
10
10
|
require('./chunk-F3LQJKXK.cjs');
|
|
@@ -13,23 +13,23 @@ require('./chunk-F3LQJKXK.cjs');
|
|
|
13
13
|
|
|
14
14
|
Object.defineProperty(exports, "createOutputFormat", {
|
|
15
15
|
enumerable: true,
|
|
16
|
-
get: function () { return
|
|
16
|
+
get: function () { return chunkOTFS7DC4_cjs.createOutputFormat; }
|
|
17
17
|
});
|
|
18
18
|
Object.defineProperty(exports, "generateFilename", {
|
|
19
19
|
enumerable: true,
|
|
20
|
-
get: function () { return
|
|
20
|
+
get: function () { return chunkOTFS7DC4_cjs.generateFilename; }
|
|
21
21
|
});
|
|
22
22
|
Object.defineProperty(exports, "mimeForFormat", {
|
|
23
23
|
enumerable: true,
|
|
24
|
-
get: function () { return
|
|
24
|
+
get: function () { return chunkOTFS7DC4_cjs.mimeForFormat; }
|
|
25
25
|
});
|
|
26
26
|
Object.defineProperty(exports, "remux", {
|
|
27
27
|
enumerable: true,
|
|
28
|
-
get: function () { return
|
|
28
|
+
get: function () { return chunkOTFS7DC4_cjs.remux; }
|
|
29
29
|
});
|
|
30
30
|
Object.defineProperty(exports, "validateRemuxEligibility", {
|
|
31
31
|
enumerable: true,
|
|
32
|
-
get: function () { return
|
|
32
|
+
get: function () { return chunkOTFS7DC4_cjs.validateRemuxEligibility; }
|
|
33
33
|
});
|
|
34
|
-
//# sourceMappingURL=remux-
|
|
35
|
-
//# sourceMappingURL=remux-
|
|
34
|
+
//# sourceMappingURL=remux-KUS5GIL6.cjs.map
|
|
35
|
+
//# sourceMappingURL=remux-KUS5GIL6.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"remux-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"remux-KUS5GIL6.cjs"}
|
package/package.json
CHANGED
package/src/classify/rules.ts
CHANGED
|
@@ -31,6 +31,8 @@ export const FALLBACK_VIDEO_CODECS = new Set<VideoCodec>([
|
|
|
31
31
|
"wmv3", "vc1", "mpeg4",
|
|
32
32
|
"rv10", "rv20", "rv30", "rv40",
|
|
33
33
|
"mpeg2", "mpeg1", "theora",
|
|
34
|
+
"dv", "hq_hqa",
|
|
35
|
+
"rawvideo", "qtrle", "png", "vp6f",
|
|
34
36
|
]);
|
|
35
37
|
export const FALLBACK_AUDIO_CODECS = new Set<AudioCodec>([
|
|
36
38
|
"wmav2", "wmapro", "ac3", "eac3",
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
ICON_FULLSCREEN, ICON_FULLSCREEN_EXIT,
|
|
24
24
|
ICON_REPLAY_10, ICON_FORWARD_10,
|
|
25
25
|
} from "./player-icons.js";
|
|
26
|
-
import type { AvbridgeVideoElementEventMap } from "../types.js";
|
|
26
|
+
import type { AvbridgeVideoElementEventMap, SettingsSectionConfig } from "../types.js";
|
|
27
27
|
|
|
28
28
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
29
29
|
|
|
@@ -87,6 +87,8 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
87
87
|
private _volumeInput!: HTMLInputElement;
|
|
88
88
|
private _settingsBtn!: HTMLButtonElement;
|
|
89
89
|
private _settingsMenu!: HTMLDivElement;
|
|
90
|
+
private _settingsScrim!: HTMLDivElement;
|
|
91
|
+
private _customSections: import("../types.js").SettingsSectionConfig[] = [];
|
|
90
92
|
private _fullscreenBtn!: HTMLButtonElement;
|
|
91
93
|
// Strategy badge removed — visible in Stats for Nerds instead.
|
|
92
94
|
// Spinner is rendered but driven entirely by CSS :host([data-state]) selectors.
|
|
@@ -133,6 +135,7 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
133
135
|
this._volumeInput = shadow.querySelector(".avp-volume-input") as HTMLInputElement;
|
|
134
136
|
this._settingsBtn = shadow.querySelector(".avp-settings-btn") as HTMLButtonElement;
|
|
135
137
|
this._settingsMenu = shadow.querySelector(".avp-settings") as HTMLDivElement;
|
|
138
|
+
this._settingsScrim = shadow.querySelector(".avp-settings-scrim") as HTMLDivElement;
|
|
136
139
|
this._fullscreenBtn = shadow.querySelector(".avp-fullscreen") as HTMLButtonElement;
|
|
137
140
|
// Badge removed from controls bar — strategy visible in Stats for Nerds.
|
|
138
141
|
// Spinner is rendered in shadow DOM, driven by CSS :host([data-state]).
|
|
@@ -167,6 +170,7 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
167
170
|
<div part="toolbar-top-left" class="avp-toolbar-top-left"><slot name="top-left"></slot></div>
|
|
168
171
|
<div part="toolbar-top-right" class="avp-toolbar-top-right"><slot name="top-right"></slot></div>
|
|
169
172
|
</div>
|
|
173
|
+
<div part="content-overlay" class="avp-content-overlay"><slot name="content-overlay"></slot></div>
|
|
170
174
|
<div part="overlay" class="avp-overlay">
|
|
171
175
|
<button class="avp-overlay-btn" aria-label="Play">${ICON_PLAY}</button>
|
|
172
176
|
<div class="avp-spinner"></div>
|
|
@@ -198,7 +202,8 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
198
202
|
<button class="avp-btn avp-settings-btn" part="settings-button" aria-label="Settings">${ICON_SETTINGS}</button>
|
|
199
203
|
<button class="avp-btn avp-fullscreen" part="fullscreen-button" aria-label="Fullscreen">${ICON_FULLSCREEN}</button>
|
|
200
204
|
</div>
|
|
201
|
-
<div class="avp-settings
|
|
205
|
+
<div class="avp-settings-scrim"></div>
|
|
206
|
+
<div class="avp-settings" part="settings-menu"><div class="avp-settings-handle"></div></div>
|
|
202
207
|
</div>
|
|
203
208
|
</div>`;
|
|
204
209
|
}
|
|
@@ -284,6 +289,7 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
284
289
|
|
|
285
290
|
// Settings
|
|
286
291
|
on(this._settingsBtn, "click", (e) => { e.stopPropagation(); this._toggleSettings(); });
|
|
292
|
+
on(this._settingsScrim, "click", () => this._closeSettings());
|
|
287
293
|
|
|
288
294
|
// Fullscreen
|
|
289
295
|
on(this._fullscreenBtn, "click", (e) => { e.stopPropagation(); this._toggleFullscreen(); });
|
|
@@ -297,14 +303,9 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
297
303
|
on(container, "click", (e) => this._onContainerClick(e as MouseEvent));
|
|
298
304
|
on(container, "dblclick", (e) => this._onContainerDblClick(e as MouseEvent));
|
|
299
305
|
|
|
300
|
-
// Dismiss settings
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
!(e.target as HTMLElement).closest?.(".avp-settings-btn, .avp-settings")) {
|
|
304
|
-
this._closeSettings();
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
// Also dismiss if user clicks outside the player element entirely
|
|
306
|
+
// Dismiss settings sheet when clicking outside it. The scrim handles
|
|
307
|
+
// most of this (its own click handler calls _closeSettings), but we
|
|
308
|
+
// also catch clicks outside the player element entirely.
|
|
308
309
|
on(document, "click", (e) => {
|
|
309
310
|
if (this._settingsOpen && !this.contains(e.target as Node)) {
|
|
310
311
|
this._closeSettings();
|
|
@@ -441,21 +442,25 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
441
442
|
this._userSeeking = true;
|
|
442
443
|
const seekBar = this.shadowRoot!.querySelector(".avp-seek") as HTMLElement;
|
|
443
444
|
seekBar.setPointerCapture(e.pointerId);
|
|
445
|
+
seekBar.setAttribute("data-seeking", "");
|
|
444
446
|
|
|
445
447
|
const initial = this._timeFromSeekPointer(e.clientX);
|
|
446
448
|
this._seekInput.value = String(initial);
|
|
447
449
|
this._onSeekInput();
|
|
450
|
+
this._updateSeekTooltip(e.clientX);
|
|
448
451
|
|
|
449
452
|
const onMove = (ev: PointerEvent) => {
|
|
450
453
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
451
454
|
this._seekInput.value = String(t);
|
|
452
455
|
this._onSeekInput();
|
|
456
|
+
this._updateSeekTooltip(ev.clientX);
|
|
453
457
|
};
|
|
454
458
|
const onUp = (ev: PointerEvent) => {
|
|
455
459
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
456
460
|
this._seekInput.value = String(t);
|
|
457
461
|
this._onSeekCommit();
|
|
458
|
-
this._seekInput.focus();
|
|
462
|
+
this._seekInput.focus();
|
|
463
|
+
seekBar.removeAttribute("data-seeking");
|
|
459
464
|
seekBar.removeEventListener("pointermove", onMove);
|
|
460
465
|
seekBar.removeEventListener("pointerup", onUp);
|
|
461
466
|
seekBar.removeEventListener("pointercancel", onUp);
|
|
@@ -467,8 +472,12 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
467
472
|
}
|
|
468
473
|
|
|
469
474
|
private _onSeekHover(e: PointerEvent): void {
|
|
475
|
+
this._updateSeekTooltip(e.clientX);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private _updateSeekTooltip(clientX: number): void {
|
|
470
479
|
const rect = this._seekInput.getBoundingClientRect();
|
|
471
|
-
const frac = Math.max(0, Math.min(1, (
|
|
480
|
+
const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
472
481
|
const t = frac * (this._video.duration || 0);
|
|
473
482
|
this._seekTooltip.textContent = formatTime(t);
|
|
474
483
|
this._seekTooltip.style.left = `${frac * 100}%`;
|
|
@@ -526,100 +535,151 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
526
535
|
private _toggleSettings(): void {
|
|
527
536
|
this._settingsOpen = !this._settingsOpen;
|
|
528
537
|
this._settingsMenu.classList.toggle("open", this._settingsOpen);
|
|
529
|
-
|
|
538
|
+
this._settingsScrim.classList.toggle("open", this._settingsOpen);
|
|
539
|
+
if (this._settingsOpen) {
|
|
540
|
+
this._fitSettingsToPlayer();
|
|
541
|
+
this._showControls();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private _fitSettingsToPlayer(): void {
|
|
546
|
+
const container = this.shadowRoot?.querySelector(".avp") as HTMLElement | null;
|
|
547
|
+
if (!container) return;
|
|
548
|
+
const rect = container.getBoundingClientRect();
|
|
549
|
+
// Bottom sheet can use up to 70% of the player height, leaving room
|
|
550
|
+
// to see the video behind the scrim. Floor at 120px so it's always
|
|
551
|
+
// usable.
|
|
552
|
+
const maxH = Math.max(120, Math.floor(rect.height * 0.7));
|
|
553
|
+
this._settingsMenu.style.maxHeight = `${maxH}px`;
|
|
530
554
|
}
|
|
531
555
|
|
|
532
556
|
private _closeSettings(): void {
|
|
533
557
|
this._settingsOpen = false;
|
|
534
558
|
this._settingsMenu.classList.remove("open");
|
|
559
|
+
this._settingsScrim.classList.remove("open");
|
|
535
560
|
}
|
|
536
561
|
|
|
537
562
|
private _buildSettingsMenu(): void {
|
|
538
563
|
const sections: string[] = [];
|
|
539
564
|
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
// Playback speed
|
|
565
|
+
// Helper: a row with an invisible native <select> layered on top of
|
|
566
|
+
// a styled label+value. Tapping opens the OS picker (escapes the
|
|
567
|
+
// shadow DOM — intentional for small players). The visible label
|
|
568
|
+
// updates on change.
|
|
569
|
+
const selectRow = (label: string, currentValue: string, options: string, selectAttrs: string): string =>
|
|
570
|
+
`<div class="avp-settings-section">` +
|
|
571
|
+
`<div class="avp-settings-header">` +
|
|
572
|
+
`<span class="avp-settings-header-label">${label}</span>` +
|
|
573
|
+
`<span class="avp-settings-header-value">${currentValue}</span>` +
|
|
574
|
+
`<select class="avp-settings-select" ${selectAttrs}>${options}</select>` +
|
|
575
|
+
`</div>` +
|
|
576
|
+
`</div>`;
|
|
577
|
+
|
|
578
|
+
// Speed
|
|
555
579
|
const currentRate = this._video.playbackRate ?? 1;
|
|
556
|
-
|
|
580
|
+
const speedValue = Math.abs(currentRate - 1) < 0.01 ? "Normal" : `${currentRate}x`;
|
|
581
|
+
let speedOpts = "";
|
|
557
582
|
for (const spd of PLAYBACK_SPEEDS) {
|
|
558
|
-
const
|
|
583
|
+
const sel = Math.abs(spd - currentRate) < 0.01 ? " selected" : "";
|
|
559
584
|
const label = spd === 1 ? "Normal" : `${spd}x`;
|
|
560
|
-
|
|
585
|
+
speedOpts += `<option value="${spd}"${sel}>${label}</option>`;
|
|
586
|
+
}
|
|
587
|
+
sections.push(selectRow("Speed", speedValue, speedOpts, `data-action="speed"`));
|
|
588
|
+
|
|
589
|
+
// Audio tracks
|
|
590
|
+
const audios = this._video.audioTracks ?? [];
|
|
591
|
+
if (audios.length > 1) {
|
|
592
|
+
let audioOpts = "";
|
|
593
|
+
for (const t of audios) {
|
|
594
|
+
audioOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
|
|
595
|
+
}
|
|
596
|
+
sections.push(selectRow("Audio", audios[0]?.language ?? "Track 1", audioOpts, `data-action="audio"`));
|
|
561
597
|
}
|
|
562
|
-
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Speed</div>${speedItems}</div>`);
|
|
563
598
|
|
|
564
599
|
// Subtitle tracks
|
|
565
600
|
const subs = this._video.subtitleTracks ?? [];
|
|
566
601
|
if (subs.length > 0) {
|
|
567
|
-
let
|
|
602
|
+
let subOpts = `<option value="-1" selected>Off</option>`;
|
|
568
603
|
for (const t of subs) {
|
|
569
|
-
|
|
604
|
+
subOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
|
|
570
605
|
}
|
|
571
|
-
sections.push(
|
|
606
|
+
sections.push(selectRow("Subtitles", "Off", subOpts, `data-action="subtitle"`));
|
|
572
607
|
}
|
|
573
608
|
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
609
|
+
// Fit mode — opt-in via the `show-fit` attribute
|
|
610
|
+
if (this.hasAttribute("show-fit")) {
|
|
611
|
+
const currentFit = (this._video.fit ?? "contain") as FitMode;
|
|
612
|
+
const fitValue = currentFit[0].toUpperCase() + currentFit.slice(1);
|
|
613
|
+
let fitOpts = "";
|
|
614
|
+
for (const mode of FIT_MODES) {
|
|
615
|
+
const sel = mode === currentFit ? " selected" : "";
|
|
616
|
+
const label = mode[0].toUpperCase() + mode.slice(1);
|
|
617
|
+
fitOpts += `<option value="${mode}"${sel}>${label}</option>`;
|
|
580
618
|
}
|
|
581
|
-
sections.push(
|
|
619
|
+
sections.push(selectRow("Fit", fitValue, fitOpts, `data-action="fit"`));
|
|
582
620
|
}
|
|
583
621
|
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const mode = (item as HTMLElement).dataset.fit as FitMode;
|
|
594
|
-
this.setAttribute("fit", mode);
|
|
595
|
-
this._buildSettingsMenu();
|
|
596
|
-
});
|
|
622
|
+
// Consumer-added sections
|
|
623
|
+
for (const cfg of this._customSections) {
|
|
624
|
+
const activeItem = cfg.items.find((i) => i.active);
|
|
625
|
+
let customOpts = "";
|
|
626
|
+
for (const item of cfg.items) {
|
|
627
|
+
const sel = item.active ? " selected" : "";
|
|
628
|
+
customOpts += `<option value="${item.id}"${sel}>${item.label}</option>`;
|
|
629
|
+
}
|
|
630
|
+
sections.push(selectRow(cfg.label, activeItem?.label ?? "", customOpts, `data-action="custom" data-custom-id="${cfg.id}"`));
|
|
597
631
|
}
|
|
598
|
-
|
|
599
|
-
|
|
632
|
+
|
|
633
|
+
// Stats for nerds — toggle row (no select)
|
|
634
|
+
sections.push(
|
|
635
|
+
`<div class="avp-settings-section">` +
|
|
636
|
+
`<div class="avp-settings-header avp-settings-toggle" data-stats>` +
|
|
637
|
+
`<span class="avp-settings-header-label">Stats for Nerds</span>` +
|
|
638
|
+
`</div>` +
|
|
639
|
+
`</div>`,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// Rebuild sheet content (preserve the handle).
|
|
643
|
+
const handle = this._settingsMenu.querySelector(".avp-settings-handle");
|
|
644
|
+
this._settingsMenu.innerHTML = "";
|
|
645
|
+
if (handle) this._settingsMenu.appendChild(handle);
|
|
646
|
+
else this._settingsMenu.insertAdjacentHTML("afterbegin", `<div class="avp-settings-handle"></div>`);
|
|
647
|
+
this._settingsMenu.insertAdjacentHTML("beforeend", sections.join(""));
|
|
648
|
+
|
|
649
|
+
// ── <select> change handlers ──
|
|
650
|
+
for (const sel of this._settingsMenu.querySelectorAll<HTMLSelectElement>(".avp-settings-select")) {
|
|
651
|
+
sel.addEventListener("change", (e) => {
|
|
600
652
|
e.stopPropagation();
|
|
601
|
-
|
|
653
|
+
const action = sel.dataset.action;
|
|
654
|
+
const val = sel.value;
|
|
655
|
+
switch (action) {
|
|
656
|
+
case "speed":
|
|
657
|
+
this._video.playbackRate = Number(val);
|
|
658
|
+
break;
|
|
659
|
+
case "audio":
|
|
660
|
+
void this._video.setAudioTrack(Number(val));
|
|
661
|
+
break;
|
|
662
|
+
case "subtitle":
|
|
663
|
+
void this._video.setSubtitleTrack(Number(val) >= 0 ? Number(val) : null);
|
|
664
|
+
break;
|
|
665
|
+
case "fit":
|
|
666
|
+
this.setAttribute("fit", val);
|
|
667
|
+
break;
|
|
668
|
+
case "custom": {
|
|
669
|
+
const cfgId = sel.dataset.customId!;
|
|
670
|
+
const cfg = this._customSections.find((s) => s.id === cfgId);
|
|
671
|
+
cfg?.onSelect(val);
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
602
675
|
this._buildSettingsMenu();
|
|
603
676
|
});
|
|
604
677
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
this._closeSettings();
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
for (const item of this._settingsMenu.querySelectorAll("[data-audio]")) {
|
|
614
|
-
item.addEventListener("click", (e) => {
|
|
615
|
-
e.stopPropagation();
|
|
616
|
-
void this._video.setAudioTrack(Number((item as HTMLElement).dataset.audio));
|
|
617
|
-
this._closeSettings();
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
const statsItem = this._settingsMenu.querySelector("[data-stats]");
|
|
621
|
-
if (statsItem) {
|
|
622
|
-
statsItem.addEventListener("click", (e) => {
|
|
678
|
+
|
|
679
|
+
// Stats toggle (no select — just a clickable row)
|
|
680
|
+
const statsRow = this._settingsMenu.querySelector("[data-stats]");
|
|
681
|
+
if (statsRow) {
|
|
682
|
+
statsRow.addEventListener("click", (e) => {
|
|
623
683
|
e.stopPropagation();
|
|
624
684
|
this._toggleStats();
|
|
625
685
|
this._closeSettings();
|
|
@@ -727,21 +787,30 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
727
787
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
728
788
|
private _lastPointerTypeWasTouch = false;
|
|
729
789
|
|
|
730
|
-
/** True if the event's composed path passes through consumer-slotted
|
|
731
|
-
* content. Slotted content lives in the
|
|
732
|
-
* on the event target won't
|
|
733
|
-
* does. */
|
|
734
|
-
private
|
|
790
|
+
/** True if the event's composed path passes through consumer-slotted
|
|
791
|
+
* content (toolbar or content-overlay). Slotted content lives in the
|
|
792
|
+
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
793
|
+
* find the shadow-DOM wrapper — `composedPath()` does. */
|
|
794
|
+
private _isSlottedContentEvent(e: Event): boolean {
|
|
735
795
|
for (const node of e.composedPath()) {
|
|
736
|
-
if (node instanceof HTMLElement &&
|
|
796
|
+
if (node instanceof HTMLElement &&
|
|
797
|
+
(node.classList.contains("avp-toolbar-top") ||
|
|
798
|
+
node.classList.contains("avp-content-overlay"))) return true;
|
|
737
799
|
}
|
|
738
800
|
return false;
|
|
739
801
|
}
|
|
740
802
|
|
|
741
803
|
private _onContainerClick(e: MouseEvent): void {
|
|
742
|
-
// Ignore clicks on controls
|
|
804
|
+
// Ignore clicks on controls and slotted content
|
|
743
805
|
if ((e.target as HTMLElement).closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
744
|
-
if (this.
|
|
806
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
807
|
+
|
|
808
|
+
// If the bottom sheet is open, any click outside it dismisses
|
|
809
|
+
// instead of toggling play/pause.
|
|
810
|
+
if (this._settingsOpen) {
|
|
811
|
+
this._closeSettings();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
745
814
|
|
|
746
815
|
// Touch taps are handled by _onPointerUp (show/hide controls + double-tap).
|
|
747
816
|
// The browser fires a synthetic click after touchend — skip it.
|
|
@@ -760,7 +829,7 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
760
829
|
|
|
761
830
|
private _onContainerDblClick(e: MouseEvent): void {
|
|
762
831
|
if ((e.target as HTMLElement).closest?.(".avp-controls, .avp-settings")) return;
|
|
763
|
-
if (this.
|
|
832
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
764
833
|
// Cancel the pending single-click play/pause
|
|
765
834
|
if (this._tapTimer) { clearTimeout(this._tapTimer); this._tapTimer = null; }
|
|
766
835
|
this._toggleFullscreen();
|
|
@@ -786,7 +855,13 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
786
855
|
|
|
787
856
|
// Ignore touches on controls — buttons have their own handlers
|
|
788
857
|
if ((e.target as HTMLElement).closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
789
|
-
if (this.
|
|
858
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
859
|
+
|
|
860
|
+
// If the bottom sheet is open, dismiss it on any touch outside.
|
|
861
|
+
if (this._settingsOpen) {
|
|
862
|
+
this._closeSettings();
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
790
865
|
|
|
791
866
|
// Double-tap detection
|
|
792
867
|
const now = Date.now();
|
|
@@ -980,6 +1055,17 @@ export class AvbridgePlayerElement extends HTMLElement {
|
|
|
980
1055
|
return this._video.destroy();
|
|
981
1056
|
}
|
|
982
1057
|
async setAudioTrack(id: number): Promise<void> { return this._video.setAudioTrack(id); }
|
|
1058
|
+
|
|
1059
|
+
addSettingsSection(config: SettingsSectionConfig): void {
|
|
1060
|
+
this._customSections = this._customSections.filter((s) => s.id !== config.id);
|
|
1061
|
+
this._customSections.push(config);
|
|
1062
|
+
this._buildSettingsMenu();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
removeSettingsSection(id: string): void {
|
|
1066
|
+
this._customSections = this._customSections.filter((s) => s.id !== id);
|
|
1067
|
+
this._buildSettingsMenu();
|
|
1068
|
+
}
|
|
983
1069
|
async setSubtitleTrack(id: number | null): Promise<void> { return this._video.setSubtitleTrack(id); }
|
|
984
1070
|
getDiagnostics(): unknown { return this._video.getDiagnostics(); }
|
|
985
1071
|
canPlayType(mime: string): string { return this._video.canPlayType(mime); }
|
|
@@ -598,10 +598,21 @@ export class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
get muted(): boolean {
|
|
601
|
-
|
|
601
|
+
// Read through to the inner <video>'s IDL property — on canvas
|
|
602
|
+
// strategies the property is patched via Object.defineProperty to
|
|
603
|
+
// mirror AudioOutput state, and consumers need the truthful value.
|
|
604
|
+
return this._videoEl.muted;
|
|
602
605
|
}
|
|
603
606
|
|
|
604
607
|
set muted(value: boolean) {
|
|
608
|
+
// Drive the IDL property (fires volumechange per HTML spec) rather
|
|
609
|
+
// than toggling the attribute (which on most browsers is parse-time
|
|
610
|
+
// only and does NOT fire volumechange when toggled runtime). On
|
|
611
|
+
// canvas strategies, the property is patched via Object.defineProperty
|
|
612
|
+
// which also dispatches volumechange; one code path, both worlds.
|
|
613
|
+
this._videoEl.muted = value;
|
|
614
|
+
// Keep the attribute in sync so CSS selectors like [muted] and
|
|
615
|
+
// re-queries via getAttribute reflect current state.
|
|
605
616
|
if (value) this.setAttribute("muted", "");
|
|
606
617
|
else this.removeAttribute("muted");
|
|
607
618
|
}
|
|
@@ -683,11 +694,16 @@ export class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
683
694
|
|
|
684
695
|
/**
|
|
685
696
|
* Buffered time ranges for the active source. Mirrors the standard
|
|
686
|
-
* `<video>.buffered` `TimeRanges` API.
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
697
|
+
* `<video>.buffered` `TimeRanges` API.
|
|
698
|
+
*
|
|
699
|
+
* - **Native / remux:** pass-through to the real `<video>.buffered`
|
|
700
|
+
* (reflects the browser's SourceBuffer / progressive-download state).
|
|
701
|
+
* - **Hybrid / fallback:** a single `[0, frontier]` range synthesized
|
|
702
|
+
* from the demuxer's read progress — "how far libav has ever pumped
|
|
703
|
+
* packets through." Monotonic; does not shrink on seek. This is an
|
|
704
|
+
* approximation, not MSE-fidelity: decoded frames on canvas strategies
|
|
705
|
+
* are consumed in flight, so we can't report per-range availability
|
|
706
|
+
* the way MSE does. Enough for a seek-bar buffered indicator.
|
|
691
707
|
*/
|
|
692
708
|
get buffered(): TimeRanges {
|
|
693
709
|
return this._videoEl.buffered;
|