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
package/dist/player.js
CHANGED
|
@@ -237,7 +237,7 @@ async function probe(source, transport) {
|
|
|
237
237
|
const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
|
|
238
238
|
if (hasUnknownCodec) {
|
|
239
239
|
try {
|
|
240
|
-
const { probeWithLibav } = await import('./avi-
|
|
240
|
+
const { probeWithLibav } = await import('./avi-JXU4GQL2.js');
|
|
241
241
|
return await probeWithLibav(normalized, sniffed);
|
|
242
242
|
} catch {
|
|
243
243
|
return result;
|
|
@@ -250,7 +250,7 @@ async function probe(source, transport) {
|
|
|
250
250
|
mediabunnyErr.message
|
|
251
251
|
);
|
|
252
252
|
try {
|
|
253
|
-
const { probeWithLibav } = await import('./avi-
|
|
253
|
+
const { probeWithLibav } = await import('./avi-JXU4GQL2.js');
|
|
254
254
|
return await probeWithLibav(normalized, sniffed);
|
|
255
255
|
} catch (libavErr) {
|
|
256
256
|
const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
|
|
@@ -264,7 +264,7 @@ async function probe(source, transport) {
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
try {
|
|
267
|
-
const { probeWithLibav } = await import('./avi-
|
|
267
|
+
const { probeWithLibav } = await import('./avi-JXU4GQL2.js');
|
|
268
268
|
return await probeWithLibav(normalized, sniffed);
|
|
269
269
|
} catch (err) {
|
|
270
270
|
const inner = err instanceof Error ? err.message : String(err);
|
|
@@ -369,7 +369,13 @@ var FALLBACK_VIDEO_CODECS = /* @__PURE__ */ new Set([
|
|
|
369
369
|
"rv40",
|
|
370
370
|
"mpeg2",
|
|
371
371
|
"mpeg1",
|
|
372
|
-
"theora"
|
|
372
|
+
"theora",
|
|
373
|
+
"dv",
|
|
374
|
+
"hq_hqa",
|
|
375
|
+
"rawvideo",
|
|
376
|
+
"qtrle",
|
|
377
|
+
"png",
|
|
378
|
+
"vp6f"
|
|
373
379
|
]);
|
|
374
380
|
var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
|
|
375
381
|
"wmav2",
|
|
@@ -1205,6 +1211,12 @@ async function createRemuxSession(context, video) {
|
|
|
1205
1211
|
}
|
|
1206
1212
|
const wasPlaying = !video.paused;
|
|
1207
1213
|
await pipeline.seek(time, wasPlaying || wantPlay);
|
|
1214
|
+
queueMicrotask(() => {
|
|
1215
|
+
try {
|
|
1216
|
+
video.dispatchEvent(new Event("seeked"));
|
|
1217
|
+
} catch {
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1208
1220
|
},
|
|
1209
1221
|
async setAudioTrack(id) {
|
|
1210
1222
|
if (!context.audioTracks.some((t) => t.id === id)) {
|
|
@@ -1593,6 +1605,10 @@ var AudioOutput = class {
|
|
|
1593
1605
|
_volume = 1;
|
|
1594
1606
|
/** User-set muted flag. When true, gain is forced to 0. */
|
|
1595
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;
|
|
1596
1612
|
constructor() {
|
|
1597
1613
|
this.ctx = new AudioContext();
|
|
1598
1614
|
this.gain = this.ctx.createGain();
|
|
@@ -1614,6 +1630,20 @@ var AudioOutput = class {
|
|
|
1614
1630
|
getMuted() {
|
|
1615
1631
|
return this._muted;
|
|
1616
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
|
+
}
|
|
1617
1647
|
applyGain() {
|
|
1618
1648
|
const target = this._muted ? 0 : this._volume;
|
|
1619
1649
|
try {
|
|
@@ -1634,12 +1664,12 @@ var AudioOutput = class {
|
|
|
1634
1664
|
now() {
|
|
1635
1665
|
if (this.noAudio) {
|
|
1636
1666
|
if (this.state === "playing") {
|
|
1637
|
-
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3;
|
|
1667
|
+
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3 * this._rate;
|
|
1638
1668
|
}
|
|
1639
1669
|
return this.mediaTimeOfAnchor;
|
|
1640
1670
|
}
|
|
1641
1671
|
if (this.state === "playing") {
|
|
1642
|
-
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor);
|
|
1672
|
+
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
|
|
1643
1673
|
}
|
|
1644
1674
|
return this.mediaTimeOfAnchor;
|
|
1645
1675
|
}
|
|
@@ -1691,7 +1721,8 @@ var AudioOutput = class {
|
|
|
1691
1721
|
const node = this.ctx.createBufferSource();
|
|
1692
1722
|
node.buffer = buffer;
|
|
1693
1723
|
node.connect(this.gain);
|
|
1694
|
-
|
|
1724
|
+
if (this._rate !== 1) node.playbackRate.value = this._rate;
|
|
1725
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
1695
1726
|
if (ctxStart < this.ctx.currentTime) {
|
|
1696
1727
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1697
1728
|
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
@@ -1841,6 +1872,17 @@ function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
|
|
|
1841
1872
|
pkt.time_base_num = 1;
|
|
1842
1873
|
pkt.time_base_den = 1e6;
|
|
1843
1874
|
}
|
|
1875
|
+
function packetPtsSec(pkt, timeBase) {
|
|
1876
|
+
const lo = pkt.pts ?? 0;
|
|
1877
|
+
const hi = pkt.ptshi ?? 0;
|
|
1878
|
+
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
1879
|
+
if (isInvalid) return null;
|
|
1880
|
+
const tb = timeBase ?? [1, 1e6];
|
|
1881
|
+
if (!tb[0] || !tb[1]) return null;
|
|
1882
|
+
const pts64 = hi * 4294967296 + lo;
|
|
1883
|
+
const sec = pts64 * tb[0] / tb[1];
|
|
1884
|
+
return Number.isFinite(sec) ? sec : null;
|
|
1885
|
+
}
|
|
1844
1886
|
var AV_SAMPLE_FMT_U8 = 0;
|
|
1845
1887
|
var AV_SAMPLE_FMT_S16 = 1;
|
|
1846
1888
|
var AV_SAMPLE_FMT_S32 = 2;
|
|
@@ -2101,6 +2143,7 @@ async function startHybridDecoder(opts) {
|
|
|
2101
2143
|
let videoFramesDecoded = 0;
|
|
2102
2144
|
let audioFramesDecoded = 0;
|
|
2103
2145
|
let videoChunksFed = 0;
|
|
2146
|
+
let bufferedUntilSec = 0;
|
|
2104
2147
|
let syntheticVideoUs = 0;
|
|
2105
2148
|
let syntheticAudioUs = 0;
|
|
2106
2149
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
@@ -2121,6 +2164,18 @@ async function startHybridDecoder(opts) {
|
|
|
2121
2164
|
if (myToken !== pumpToken || destroyed) return;
|
|
2122
2165
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2123
2166
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2167
|
+
if (videoPackets && videoTimeBase) {
|
|
2168
|
+
for (const pkt of videoPackets) {
|
|
2169
|
+
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
2170
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
if (audioPackets && audioTimeBase) {
|
|
2174
|
+
for (const pkt of audioPackets) {
|
|
2175
|
+
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
2176
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2124
2179
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2125
2180
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2126
2181
|
}
|
|
@@ -2377,6 +2432,9 @@ async function startHybridDecoder(opts) {
|
|
|
2377
2432
|
(err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
|
|
2378
2433
|
);
|
|
2379
2434
|
},
|
|
2435
|
+
bufferedUntilSec() {
|
|
2436
|
+
return bufferedUntilSec;
|
|
2437
|
+
},
|
|
2380
2438
|
stats() {
|
|
2381
2439
|
return {
|
|
2382
2440
|
decoderType: "webcodecs-hybrid",
|
|
@@ -2492,6 +2550,14 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2492
2550
|
get: () => ctx.duration ?? NaN
|
|
2493
2551
|
});
|
|
2494
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
|
+
});
|
|
2495
2561
|
Object.defineProperty(target, "readyState", {
|
|
2496
2562
|
configurable: true,
|
|
2497
2563
|
get: () => {
|
|
@@ -2504,6 +2570,13 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2504
2570
|
configurable: true,
|
|
2505
2571
|
get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
|
|
2506
2572
|
});
|
|
2573
|
+
Object.defineProperty(target, "buffered", {
|
|
2574
|
+
configurable: true,
|
|
2575
|
+
get: () => {
|
|
2576
|
+
const end = handles.bufferedUntilSec();
|
|
2577
|
+
return makeTimeRanges(end > 0 ? [[0, end]] : []);
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2507
2580
|
async function waitForBuffer() {
|
|
2508
2581
|
const start = performance.now();
|
|
2509
2582
|
while (true) {
|
|
@@ -2517,6 +2590,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2517
2590
|
}
|
|
2518
2591
|
async function doSeek(timeSec) {
|
|
2519
2592
|
const wasPlaying = audio.isPlaying();
|
|
2593
|
+
target.dispatchEvent(new Event("seeking"));
|
|
2520
2594
|
await audio.pause().catch(() => {
|
|
2521
2595
|
});
|
|
2522
2596
|
await handles.seek(timeSec).catch(
|
|
@@ -2528,7 +2602,14 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2528
2602
|
await waitForBuffer();
|
|
2529
2603
|
await audio.start();
|
|
2530
2604
|
}
|
|
2605
|
+
target.dispatchEvent(new Event("seeked"));
|
|
2531
2606
|
}
|
|
2607
|
+
queueMicrotask(() => {
|
|
2608
|
+
try {
|
|
2609
|
+
target.dispatchEvent(new Event("loadedmetadata"));
|
|
2610
|
+
} catch {
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2532
2613
|
let fatalErrorHandler = null;
|
|
2533
2614
|
handles.onFatalError((reason) => fatalErrorHandler?.(reason));
|
|
2534
2615
|
return {
|
|
@@ -2587,6 +2668,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2587
2668
|
delete target.muted;
|
|
2588
2669
|
delete target.readyState;
|
|
2589
2670
|
delete target.seekable;
|
|
2671
|
+
delete target.playbackRate;
|
|
2590
2672
|
} catch {
|
|
2591
2673
|
}
|
|
2592
2674
|
},
|
|
@@ -2713,6 +2795,7 @@ async function startDecoder(opts) {
|
|
|
2713
2795
|
let pumpRunning = null;
|
|
2714
2796
|
let packetsRead = 0;
|
|
2715
2797
|
let videoFramesDecoded = 0;
|
|
2798
|
+
let bufferedUntilSec = 0;
|
|
2716
2799
|
let audioFramesDecoded = 0;
|
|
2717
2800
|
let watchdogFirstFrameMs = 0;
|
|
2718
2801
|
let watchdogSlowSinceMs = 0;
|
|
@@ -2738,6 +2821,18 @@ async function startDecoder(opts) {
|
|
|
2738
2821
|
if (myToken !== pumpToken || destroyed) return;
|
|
2739
2822
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2740
2823
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2824
|
+
if (videoPackets && videoTimeBase) {
|
|
2825
|
+
for (const pkt of videoPackets) {
|
|
2826
|
+
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
2827
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
if (audioPackets && audioTimeBase) {
|
|
2831
|
+
for (const pkt of audioPackets) {
|
|
2832
|
+
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
2833
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2741
2836
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2742
2837
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2743
2838
|
}
|
|
@@ -3019,6 +3114,9 @@ async function startDecoder(opts) {
|
|
|
3019
3114
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
3020
3115
|
);
|
|
3021
3116
|
},
|
|
3117
|
+
bufferedUntilSec() {
|
|
3118
|
+
return bufferedUntilSec;
|
|
3119
|
+
},
|
|
3022
3120
|
stats() {
|
|
3023
3121
|
return {
|
|
3024
3122
|
decoderType: "libav-wasm",
|
|
@@ -3106,6 +3204,14 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3106
3204
|
get: () => ctx.duration ?? NaN
|
|
3107
3205
|
});
|
|
3108
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
|
+
});
|
|
3109
3215
|
Object.defineProperty(target, "readyState", {
|
|
3110
3216
|
configurable: true,
|
|
3111
3217
|
get: () => {
|
|
@@ -3118,6 +3224,13 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3118
3224
|
configurable: true,
|
|
3119
3225
|
get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
|
|
3120
3226
|
});
|
|
3227
|
+
Object.defineProperty(target, "buffered", {
|
|
3228
|
+
configurable: true,
|
|
3229
|
+
get: () => {
|
|
3230
|
+
const end = handles.bufferedUntilSec();
|
|
3231
|
+
return makeTimeRanges(end > 0 ? [[0, end]] : []);
|
|
3232
|
+
}
|
|
3233
|
+
});
|
|
3121
3234
|
async function waitForBuffer() {
|
|
3122
3235
|
const start = performance.now();
|
|
3123
3236
|
let firstFrameAtMs = 0;
|
|
@@ -3157,6 +3270,7 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3157
3270
|
}
|
|
3158
3271
|
async function doSeek(timeSec) {
|
|
3159
3272
|
const wasPlaying = audio.isPlaying();
|
|
3273
|
+
target.dispatchEvent(new Event("seeking"));
|
|
3160
3274
|
await audio.pause().catch(() => {
|
|
3161
3275
|
});
|
|
3162
3276
|
await handles.seek(timeSec).catch(
|
|
@@ -3168,7 +3282,14 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3168
3282
|
await waitForBuffer();
|
|
3169
3283
|
await audio.start();
|
|
3170
3284
|
}
|
|
3285
|
+
target.dispatchEvent(new Event("seeked"));
|
|
3171
3286
|
}
|
|
3287
|
+
queueMicrotask(() => {
|
|
3288
|
+
try {
|
|
3289
|
+
target.dispatchEvent(new Event("loadedmetadata"));
|
|
3290
|
+
} catch {
|
|
3291
|
+
}
|
|
3292
|
+
});
|
|
3172
3293
|
return {
|
|
3173
3294
|
strategy: "fallback",
|
|
3174
3295
|
async play() {
|
|
@@ -3222,6 +3343,7 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3222
3343
|
delete target.muted;
|
|
3223
3344
|
delete target.readyState;
|
|
3224
3345
|
delete target.seekable;
|
|
3346
|
+
delete target.playbackRate;
|
|
3225
3347
|
} catch {
|
|
3226
3348
|
}
|
|
3227
3349
|
},
|
|
@@ -4248,9 +4370,10 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4248
4370
|
else this.removeAttribute("autoplay");
|
|
4249
4371
|
}
|
|
4250
4372
|
get muted() {
|
|
4251
|
-
return this.
|
|
4373
|
+
return this._videoEl.muted;
|
|
4252
4374
|
}
|
|
4253
4375
|
set muted(value) {
|
|
4376
|
+
this._videoEl.muted = value;
|
|
4254
4377
|
if (value) this.setAttribute("muted", "");
|
|
4255
4378
|
else this.removeAttribute("muted");
|
|
4256
4379
|
}
|
|
@@ -4315,11 +4438,16 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4315
4438
|
}
|
|
4316
4439
|
/**
|
|
4317
4440
|
* Buffered time ranges for the active source. Mirrors the standard
|
|
4318
|
-
* `<video>.buffered` `TimeRanges` API.
|
|
4319
|
-
*
|
|
4320
|
-
*
|
|
4321
|
-
*
|
|
4322
|
-
*
|
|
4441
|
+
* `<video>.buffered` `TimeRanges` API.
|
|
4442
|
+
*
|
|
4443
|
+
* - **Native / remux:** pass-through to the real `<video>.buffered`
|
|
4444
|
+
* (reflects the browser's SourceBuffer / progressive-download state).
|
|
4445
|
+
* - **Hybrid / fallback:** a single `[0, frontier]` range synthesized
|
|
4446
|
+
* from the demuxer's read progress — "how far libav has ever pumped
|
|
4447
|
+
* packets through." Monotonic; does not shrink on seek. This is an
|
|
4448
|
+
* approximation, not MSE-fidelity: decoded frames on canvas strategies
|
|
4449
|
+
* are consumed in flight, so we can't report per-range availability
|
|
4450
|
+
* the way MSE does. Enough for a seek-bar buffered indicator.
|
|
4323
4451
|
*/
|
|
4324
4452
|
get buffered() {
|
|
4325
4453
|
return this._videoEl.buffered;
|
|
@@ -4623,11 +4751,17 @@ var PLAYER_STYLES = (
|
|
|
4623
4751
|
|
|
4624
4752
|
/* \u2500\u2500 Container \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\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
4625
4753
|
|
|
4754
|
+
:host {
|
|
4755
|
+
-webkit-tap-highlight-color: transparent;
|
|
4756
|
+
outline: none;
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4626
4759
|
.avp {
|
|
4627
4760
|
position: relative;
|
|
4628
4761
|
width: 100%;
|
|
4629
4762
|
height: 100%;
|
|
4630
|
-
|
|
4763
|
+
-webkit-tap-highlight-color: transparent;
|
|
4764
|
+
user-select: none;
|
|
4631
4765
|
}
|
|
4632
4766
|
|
|
4633
4767
|
.avp avbridge-video {
|
|
@@ -4826,7 +4960,14 @@ var PLAYER_STYLES = (
|
|
|
4826
4960
|
pointer-events: auto;
|
|
4827
4961
|
}
|
|
4828
4962
|
|
|
4829
|
-
|
|
4963
|
+
/* Left slot fills remaining space so slotted text/content can grow.
|
|
4964
|
+
min-width: 0 prevents flex children from overflowing the toolbar. */
|
|
4965
|
+
.avp-toolbar-top-left {
|
|
4966
|
+
flex: 1;
|
|
4967
|
+
min-width: 0;
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
.avp-toolbar-top-right { margin-left: auto; flex-shrink: 0; }
|
|
4830
4971
|
|
|
4831
4972
|
/* Hide the gradient band when no consumer has slotted anything \u2014 we
|
|
4832
4973
|
toggle data-toolbar-empty from JS via slotchange. */
|
|
@@ -4839,6 +4980,30 @@ var PLAYER_STYLES = (
|
|
|
4839
4980
|
pointer-events: none;
|
|
4840
4981
|
}
|
|
4841
4982
|
|
|
4983
|
+
/* \u2500\u2500 Content overlay \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 */
|
|
4984
|
+
/* Consumer-provided rich content (tweet cards, media info, annotations).
|
|
4985
|
+
Sits above the video, below the play-button overlay and controls in
|
|
4986
|
+
z-order. Auto-hides with the chrome. The wrapper is pointer-events:none
|
|
4987
|
+
so taps fall through to the video; consumers opt in on their content
|
|
4988
|
+
with pointer-events:auto. */
|
|
4989
|
+
|
|
4990
|
+
.avp-content-overlay {
|
|
4991
|
+
position: absolute;
|
|
4992
|
+
inset: 0;
|
|
4993
|
+
z-index: 1;
|
|
4994
|
+
pointer-events: none;
|
|
4995
|
+
opacity: 1;
|
|
4996
|
+
transition: opacity 0.25s;
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4999
|
+
.avp-content-overlay ::slotted(*) {
|
|
5000
|
+
pointer-events: auto;
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
:host([data-controls-hidden]) .avp-content-overlay {
|
|
5004
|
+
opacity: 0;
|
|
5005
|
+
}
|
|
5006
|
+
|
|
4842
5007
|
/* \u2500\u2500 Seek bar \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
4843
5008
|
|
|
4844
5009
|
.avp-seek {
|
|
@@ -4925,6 +5090,15 @@ var PLAYER_STYLES = (
|
|
|
4925
5090
|
|
|
4926
5091
|
.avp-seek:hover .avp-seek-tooltip { display: block; }
|
|
4927
5092
|
|
|
5093
|
+
/* Show tooltip during active drag (touch or mouse). The JS side sets
|
|
5094
|
+
data-seeking on .avp-seek while the user is scrubbing. */
|
|
5095
|
+
.avp-seek[data-seeking] .avp-seek-tooltip { display: block; }
|
|
5096
|
+
|
|
5097
|
+
/* Enlarge thumb while scrubbing. */
|
|
5098
|
+
.avp-seek[data-seeking] .avp-seek-thumb {
|
|
5099
|
+
transform: translate(-50%, -50%) scale(1.4);
|
|
5100
|
+
}
|
|
5101
|
+
|
|
4928
5102
|
/* \u2500\u2500 Bottom row \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\u2500\u2500\u2500\u2500\u2500 */
|
|
4929
5103
|
|
|
4930
5104
|
.avp-bottom {
|
|
@@ -5035,60 +5209,114 @@ var PLAYER_STYLES = (
|
|
|
5035
5209
|
|
|
5036
5210
|
.avp-spacer { flex: 1; }
|
|
5037
5211
|
|
|
5038
|
-
/* \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 */
|
|
5039
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). */
|
|
5040
5233
|
.avp-settings {
|
|
5041
5234
|
position: absolute;
|
|
5042
|
-
bottom:
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
border-radius: 8px;
|
|
5046
|
-
min-width: 220px;
|
|
5047
|
-
max-height: 300px;
|
|
5048
|
-
overflow-y: auto;
|
|
5049
|
-
display: none;
|
|
5235
|
+
bottom: 0;
|
|
5236
|
+
left: 0;
|
|
5237
|
+
right: 0;
|
|
5050
5238
|
z-index: 10;
|
|
5051
|
-
|
|
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);
|
|
5052
5248
|
}
|
|
5053
5249
|
|
|
5054
|
-
.avp-settings.open {
|
|
5250
|
+
.avp-settings.open {
|
|
5251
|
+
transform: translateY(0);
|
|
5252
|
+
}
|
|
5055
5253
|
|
|
5056
|
-
.
|
|
5057
|
-
|
|
5058
|
-
|
|
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;
|
|
5059
5261
|
}
|
|
5060
5262
|
|
|
5061
|
-
|
|
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 */
|
|
5062
5264
|
|
|
5063
|
-
.avp-settings-
|
|
5064
|
-
|
|
5065
|
-
font-size: 11px;
|
|
5066
|
-
text-transform: uppercase;
|
|
5067
|
-
letter-spacing: 0.5px;
|
|
5068
|
-
opacity: 0.5;
|
|
5265
|
+
.avp-settings-section {
|
|
5266
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
5069
5267
|
}
|
|
5070
5268
|
|
|
5071
|
-
.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;
|
|
5072
5274
|
display: flex;
|
|
5073
5275
|
align-items: center;
|
|
5074
|
-
|
|
5075
|
-
|
|
5276
|
+
justify-content: space-between;
|
|
5277
|
+
padding: 12px 16px;
|
|
5076
5278
|
cursor: pointer;
|
|
5279
|
+
font-size: 14px;
|
|
5077
5280
|
transition: background 0.1s;
|
|
5078
5281
|
}
|
|
5079
5282
|
|
|
5080
|
-
.avp-settings-
|
|
5283
|
+
.avp-settings-header:hover { background: rgba(255, 255, 255, 0.06); }
|
|
5081
5284
|
|
|
5082
|
-
.avp-settings-
|
|
5083
|
-
|
|
5285
|
+
.avp-settings-header-label {
|
|
5286
|
+
display: flex;
|
|
5287
|
+
align-items: center;
|
|
5288
|
+
gap: 8px;
|
|
5289
|
+
font-weight: 500;
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
.avp-settings-header-value {
|
|
5293
|
+
margin-left: auto;
|
|
5294
|
+
opacity: 0.6;
|
|
5295
|
+
font-size: 13px;
|
|
5296
|
+
text-align: right;
|
|
5084
5297
|
}
|
|
5085
5298
|
|
|
5086
|
-
.
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
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;
|
|
5090
5312
|
}
|
|
5091
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
|
+
|
|
5092
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 */
|
|
5093
5321
|
|
|
5094
5322
|
.avp-stats {
|
|
@@ -5116,9 +5344,24 @@ var PLAYER_STYLES = (
|
|
|
5116
5344
|
@media (pointer: coarse) {
|
|
5117
5345
|
.avp-btn svg { width: 28px; height: 28px; }
|
|
5118
5346
|
.avp-btn { padding: 8px; }
|
|
5347
|
+
|
|
5348
|
+
/* Taller touch target on mobile (44px, matching YouTube Mobile)
|
|
5349
|
+
while keeping the visual track thin. Negative margin collapses
|
|
5350
|
+
the extra space so the controls layout doesn't shift. */
|
|
5351
|
+
.avp-seek { height: 44px; margin-top: -12px; margin-bottom: -12px; }
|
|
5119
5352
|
.avp-seek-track { height: 4px; }
|
|
5120
5353
|
.avp-seek:hover .avp-seek-track { height: 4px; }
|
|
5121
|
-
.avp-seek-thumb {
|
|
5354
|
+
.avp-seek-thumb {
|
|
5355
|
+
transform: translate(-50%, -50%) scale(1);
|
|
5356
|
+
width: 16px;
|
|
5357
|
+
height: 16px;
|
|
5358
|
+
}
|
|
5359
|
+
.avp-seek[data-seeking] .avp-seek-thumb {
|
|
5360
|
+
transform: translate(-50%, -50%) scale(1.5);
|
|
5361
|
+
}
|
|
5362
|
+
/* Move tooltip above the taller touch zone. */
|
|
5363
|
+
.avp-seek-tooltip { bottom: 32px; }
|
|
5364
|
+
|
|
5122
5365
|
.avp-volume:hover .avp-volume-slider { width: 0; }
|
|
5123
5366
|
.avp-overlay-btn { width: 56px; height: 56px; }
|
|
5124
5367
|
.avp-overlay-btn svg { width: 30px; height: 30px; }
|
|
@@ -5211,6 +5454,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5211
5454
|
_volumeInput;
|
|
5212
5455
|
_settingsBtn;
|
|
5213
5456
|
_settingsMenu;
|
|
5457
|
+
_settingsScrim;
|
|
5458
|
+
_customSections = [];
|
|
5214
5459
|
_fullscreenBtn;
|
|
5215
5460
|
// Strategy badge removed — visible in Stats for Nerds instead.
|
|
5216
5461
|
// Spinner is rendered but driven entirely by CSS :host([data-state]) selectors.
|
|
@@ -5252,6 +5497,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5252
5497
|
this._volumeInput = shadow.querySelector(".avp-volume-input");
|
|
5253
5498
|
this._settingsBtn = shadow.querySelector(".avp-settings-btn");
|
|
5254
5499
|
this._settingsMenu = shadow.querySelector(".avp-settings");
|
|
5500
|
+
this._settingsScrim = shadow.querySelector(".avp-settings-scrim");
|
|
5255
5501
|
this._fullscreenBtn = shadow.querySelector(".avp-fullscreen");
|
|
5256
5502
|
this._speedIndicator = shadow.querySelector(".avp-speed-indicator");
|
|
5257
5503
|
this._statsEl = shadow.querySelector(".avp-stats");
|
|
@@ -5276,6 +5522,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5276
5522
|
<div part="toolbar-top-left" class="avp-toolbar-top-left"><slot name="top-left"></slot></div>
|
|
5277
5523
|
<div part="toolbar-top-right" class="avp-toolbar-top-right"><slot name="top-right"></slot></div>
|
|
5278
5524
|
</div>
|
|
5525
|
+
<div part="content-overlay" class="avp-content-overlay"><slot name="content-overlay"></slot></div>
|
|
5279
5526
|
<div part="overlay" class="avp-overlay">
|
|
5280
5527
|
<button class="avp-overlay-btn" aria-label="Play">${ICON_PLAY}</button>
|
|
5281
5528
|
<div class="avp-spinner"></div>
|
|
@@ -5307,7 +5554,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5307
5554
|
<button class="avp-btn avp-settings-btn" part="settings-button" aria-label="Settings">${ICON_SETTINGS}</button>
|
|
5308
5555
|
<button class="avp-btn avp-fullscreen" part="fullscreen-button" aria-label="Fullscreen">${ICON_FULLSCREEN}</button>
|
|
5309
5556
|
</div>
|
|
5310
|
-
<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>
|
|
5311
5559
|
</div>
|
|
5312
5560
|
</div>`;
|
|
5313
5561
|
}
|
|
@@ -5379,6 +5627,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5379
5627
|
e.stopPropagation();
|
|
5380
5628
|
this._toggleSettings();
|
|
5381
5629
|
});
|
|
5630
|
+
on(this._settingsScrim, "click", () => this._closeSettings());
|
|
5382
5631
|
on(this._fullscreenBtn, "click", (e) => {
|
|
5383
5632
|
e.stopPropagation();
|
|
5384
5633
|
this._toggleFullscreen();
|
|
@@ -5387,11 +5636,6 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5387
5636
|
const container = this.shadowRoot.querySelector(".avp");
|
|
5388
5637
|
on(container, "click", (e) => this._onContainerClick(e));
|
|
5389
5638
|
on(container, "dblclick", (e) => this._onContainerDblClick(e));
|
|
5390
|
-
on(container, "click", (e) => {
|
|
5391
|
-
if (this._settingsOpen && !e.target.closest?.(".avp-settings-btn, .avp-settings")) {
|
|
5392
|
-
this._closeSettings();
|
|
5393
|
-
}
|
|
5394
|
-
});
|
|
5395
5639
|
on(document, "click", (e) => {
|
|
5396
5640
|
if (this._settingsOpen && !this.contains(e.target)) {
|
|
5397
5641
|
this._closeSettings();
|
|
@@ -5491,19 +5735,23 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5491
5735
|
this._userSeeking = true;
|
|
5492
5736
|
const seekBar = this.shadowRoot.querySelector(".avp-seek");
|
|
5493
5737
|
seekBar.setPointerCapture(e.pointerId);
|
|
5738
|
+
seekBar.setAttribute("data-seeking", "");
|
|
5494
5739
|
const initial = this._timeFromSeekPointer(e.clientX);
|
|
5495
5740
|
this._seekInput.value = String(initial);
|
|
5496
5741
|
this._onSeekInput();
|
|
5742
|
+
this._updateSeekTooltip(e.clientX);
|
|
5497
5743
|
const onMove = (ev) => {
|
|
5498
5744
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5499
5745
|
this._seekInput.value = String(t);
|
|
5500
5746
|
this._onSeekInput();
|
|
5747
|
+
this._updateSeekTooltip(ev.clientX);
|
|
5501
5748
|
};
|
|
5502
5749
|
const onUp = (ev) => {
|
|
5503
5750
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5504
5751
|
this._seekInput.value = String(t);
|
|
5505
5752
|
this._onSeekCommit();
|
|
5506
5753
|
this._seekInput.focus();
|
|
5754
|
+
seekBar.removeAttribute("data-seeking");
|
|
5507
5755
|
seekBar.removeEventListener("pointermove", onMove);
|
|
5508
5756
|
seekBar.removeEventListener("pointerup", onUp);
|
|
5509
5757
|
seekBar.removeEventListener("pointercancel", onUp);
|
|
@@ -5517,8 +5765,11 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5517
5765
|
seekBar.addEventListener("pointercancel", onUp);
|
|
5518
5766
|
}
|
|
5519
5767
|
_onSeekHover(e) {
|
|
5768
|
+
this._updateSeekTooltip(e.clientX);
|
|
5769
|
+
}
|
|
5770
|
+
_updateSeekTooltip(clientX) {
|
|
5520
5771
|
const rect = this._seekInput.getBoundingClientRect();
|
|
5521
|
-
const frac = Math.max(0, Math.min(1, (
|
|
5772
|
+
const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
5522
5773
|
const t = frac * (this._video.duration || 0);
|
|
5523
5774
|
this._seekTooltip.textContent = formatTime(t);
|
|
5524
5775
|
this._seekTooltip.style.left = `${frac * 100}%`;
|
|
@@ -5562,83 +5813,111 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5562
5813
|
_toggleSettings() {
|
|
5563
5814
|
this._settingsOpen = !this._settingsOpen;
|
|
5564
5815
|
this._settingsMenu.classList.toggle("open", this._settingsOpen);
|
|
5565
|
-
|
|
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`;
|
|
5566
5828
|
}
|
|
5567
5829
|
_closeSettings() {
|
|
5568
5830
|
this._settingsOpen = false;
|
|
5569
5831
|
this._settingsMenu.classList.remove("open");
|
|
5832
|
+
this._settingsScrim.classList.remove("open");
|
|
5570
5833
|
}
|
|
5571
5834
|
_buildSettingsMenu() {
|
|
5572
5835
|
const sections = [];
|
|
5573
|
-
|
|
5574
|
-
const currentFit = this._video.fit ?? "contain";
|
|
5575
|
-
let fitItems = "";
|
|
5576
|
-
for (const mode of FIT_MODES) {
|
|
5577
|
-
const active = mode === currentFit;
|
|
5578
|
-
const label = mode[0].toUpperCase() + mode.slice(1);
|
|
5579
|
-
fitItems += `<div class="avp-settings-item${active ? " active" : ""}" data-fit="${mode}">${label}</div>`;
|
|
5580
|
-
}
|
|
5581
|
-
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Fit</div>${fitItems}</div>`);
|
|
5582
|
-
}
|
|
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>`;
|
|
5583
5837
|
const currentRate = this._video.playbackRate ?? 1;
|
|
5584
|
-
|
|
5838
|
+
const speedValue = Math.abs(currentRate - 1) < 0.01 ? "Normal" : `${currentRate}x`;
|
|
5839
|
+
let speedOpts = "";
|
|
5585
5840
|
for (const spd of PLAYBACK_SPEEDS) {
|
|
5586
|
-
const
|
|
5841
|
+
const sel = Math.abs(spd - currentRate) < 0.01 ? " selected" : "";
|
|
5587
5842
|
const label = spd === 1 ? "Normal" : `${spd}x`;
|
|
5588
|
-
|
|
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"`));
|
|
5589
5853
|
}
|
|
5590
|
-
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Speed</div>${speedItems}</div>`);
|
|
5591
5854
|
const subs = this._video.subtitleTracks ?? [];
|
|
5592
5855
|
if (subs.length > 0) {
|
|
5593
|
-
let
|
|
5856
|
+
let subOpts = `<option value="-1" selected>Off</option>`;
|
|
5594
5857
|
for (const t of subs) {
|
|
5595
|
-
|
|
5858
|
+
subOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
|
|
5596
5859
|
}
|
|
5597
|
-
sections.push(
|
|
5860
|
+
sections.push(selectRow("Subtitles", "Off", subOpts, `data-action="subtitle"`));
|
|
5598
5861
|
}
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
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>`;
|
|
5604
5870
|
}
|
|
5605
|
-
sections.push(
|
|
5871
|
+
sections.push(selectRow("Fit", fitValue, fitOpts, `data-action="fit"`));
|
|
5606
5872
|
}
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
item.
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
});
|
|
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}"`));
|
|
5616
5881
|
}
|
|
5617
|
-
|
|
5618
|
-
|
|
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) => {
|
|
5619
5892
|
e.stopPropagation();
|
|
5620
|
-
|
|
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
|
+
}
|
|
5621
5915
|
this._buildSettingsMenu();
|
|
5622
5916
|
});
|
|
5623
5917
|
}
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
const id = Number(item.dataset.subtitle);
|
|
5628
|
-
void this._video.setSubtitleTrack(id >= 0 ? id : null);
|
|
5629
|
-
this._closeSettings();
|
|
5630
|
-
});
|
|
5631
|
-
}
|
|
5632
|
-
for (const item of this._settingsMenu.querySelectorAll("[data-audio]")) {
|
|
5633
|
-
item.addEventListener("click", (e) => {
|
|
5634
|
-
e.stopPropagation();
|
|
5635
|
-
void this._video.setAudioTrack(Number(item.dataset.audio));
|
|
5636
|
-
this._closeSettings();
|
|
5637
|
-
});
|
|
5638
|
-
}
|
|
5639
|
-
const statsItem = this._settingsMenu.querySelector("[data-stats]");
|
|
5640
|
-
if (statsItem) {
|
|
5641
|
-
statsItem.addEventListener("click", (e) => {
|
|
5918
|
+
const statsRow = this._settingsMenu.querySelector("[data-stats]");
|
|
5919
|
+
if (statsRow) {
|
|
5920
|
+
statsRow.addEventListener("click", (e) => {
|
|
5642
5921
|
e.stopPropagation();
|
|
5643
5922
|
this._toggleStats();
|
|
5644
5923
|
this._closeSettings();
|
|
@@ -5738,19 +6017,23 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5738
6017
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
5739
6018
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
5740
6019
|
_lastPointerTypeWasTouch = false;
|
|
5741
|
-
/** True if the event's composed path passes through consumer-slotted
|
|
5742
|
-
* content. Slotted content lives in the
|
|
5743
|
-
* on the event target won't
|
|
5744
|
-
* does. */
|
|
5745
|
-
|
|
6020
|
+
/** True if the event's composed path passes through consumer-slotted
|
|
6021
|
+
* content (toolbar or content-overlay). Slotted content lives in the
|
|
6022
|
+
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
6023
|
+
* find the shadow-DOM wrapper — `composedPath()` does. */
|
|
6024
|
+
_isSlottedContentEvent(e) {
|
|
5746
6025
|
for (const node of e.composedPath()) {
|
|
5747
|
-
if (node instanceof HTMLElement && node.classList.contains("avp-toolbar-top")) return true;
|
|
6026
|
+
if (node instanceof HTMLElement && (node.classList.contains("avp-toolbar-top") || node.classList.contains("avp-content-overlay"))) return true;
|
|
5748
6027
|
}
|
|
5749
6028
|
return false;
|
|
5750
6029
|
}
|
|
5751
6030
|
_onContainerClick(e) {
|
|
5752
6031
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5753
|
-
if (this.
|
|
6032
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
6033
|
+
if (this._settingsOpen) {
|
|
6034
|
+
this._closeSettings();
|
|
6035
|
+
return;
|
|
6036
|
+
}
|
|
5754
6037
|
if (this._lastPointerTypeWasTouch) {
|
|
5755
6038
|
this._lastPointerTypeWasTouch = false;
|
|
5756
6039
|
return;
|
|
@@ -5766,7 +6049,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5766
6049
|
}
|
|
5767
6050
|
_onContainerDblClick(e) {
|
|
5768
6051
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
5769
|
-
if (this.
|
|
6052
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
5770
6053
|
if (this._tapTimer) {
|
|
5771
6054
|
clearTimeout(this._tapTimer);
|
|
5772
6055
|
this._tapTimer = null;
|
|
@@ -5788,7 +6071,11 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5788
6071
|
if (e.pointerType !== "touch") return;
|
|
5789
6072
|
this._lastPointerTypeWasTouch = true;
|
|
5790
6073
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5791
|
-
if (this.
|
|
6074
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
6075
|
+
if (this._settingsOpen) {
|
|
6076
|
+
this._closeSettings();
|
|
6077
|
+
return;
|
|
6078
|
+
}
|
|
5792
6079
|
const now = Date.now();
|
|
5793
6080
|
if (now - this._lastTapTime < 300) {
|
|
5794
6081
|
if (this._tapTimer) {
|
|
@@ -6038,6 +6325,15 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6038
6325
|
async setAudioTrack(id) {
|
|
6039
6326
|
return this._video.setAudioTrack(id);
|
|
6040
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
|
+
}
|
|
6041
6337
|
async setSubtitleTrack(id) {
|
|
6042
6338
|
return this._video.setSubtitleTrack(id);
|
|
6043
6339
|
}
|