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.cjs
CHANGED
|
@@ -239,7 +239,7 @@ async function probe(source, transport) {
|
|
|
239
239
|
const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
|
|
240
240
|
if (hasUnknownCodec) {
|
|
241
241
|
try {
|
|
242
|
-
const { probeWithLibav } = await import('./avi-
|
|
242
|
+
const { probeWithLibav } = await import('./avi-2ILLBNPQ.cjs');
|
|
243
243
|
return await probeWithLibav(normalized, sniffed);
|
|
244
244
|
} catch {
|
|
245
245
|
return result;
|
|
@@ -252,7 +252,7 @@ async function probe(source, transport) {
|
|
|
252
252
|
mediabunnyErr.message
|
|
253
253
|
);
|
|
254
254
|
try {
|
|
255
|
-
const { probeWithLibav } = await import('./avi-
|
|
255
|
+
const { probeWithLibav } = await import('./avi-2ILLBNPQ.cjs');
|
|
256
256
|
return await probeWithLibav(normalized, sniffed);
|
|
257
257
|
} catch (libavErr) {
|
|
258
258
|
const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
|
|
@@ -266,7 +266,7 @@ async function probe(source, transport) {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
try {
|
|
269
|
-
const { probeWithLibav } = await import('./avi-
|
|
269
|
+
const { probeWithLibav } = await import('./avi-2ILLBNPQ.cjs');
|
|
270
270
|
return await probeWithLibav(normalized, sniffed);
|
|
271
271
|
} catch (err) {
|
|
272
272
|
const inner = err instanceof Error ? err.message : String(err);
|
|
@@ -371,7 +371,13 @@ var FALLBACK_VIDEO_CODECS = /* @__PURE__ */ new Set([
|
|
|
371
371
|
"rv40",
|
|
372
372
|
"mpeg2",
|
|
373
373
|
"mpeg1",
|
|
374
|
-
"theora"
|
|
374
|
+
"theora",
|
|
375
|
+
"dv",
|
|
376
|
+
"hq_hqa",
|
|
377
|
+
"rawvideo",
|
|
378
|
+
"qtrle",
|
|
379
|
+
"png",
|
|
380
|
+
"vp6f"
|
|
375
381
|
]);
|
|
376
382
|
var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
|
|
377
383
|
"wmav2",
|
|
@@ -1207,6 +1213,12 @@ async function createRemuxSession(context, video) {
|
|
|
1207
1213
|
}
|
|
1208
1214
|
const wasPlaying = !video.paused;
|
|
1209
1215
|
await pipeline.seek(time, wasPlaying || wantPlay);
|
|
1216
|
+
queueMicrotask(() => {
|
|
1217
|
+
try {
|
|
1218
|
+
video.dispatchEvent(new Event("seeked"));
|
|
1219
|
+
} catch {
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1210
1222
|
},
|
|
1211
1223
|
async setAudioTrack(id) {
|
|
1212
1224
|
if (!context.audioTracks.some((t) => t.id === id)) {
|
|
@@ -1595,6 +1607,10 @@ var AudioOutput = class {
|
|
|
1595
1607
|
_volume = 1;
|
|
1596
1608
|
/** User-set muted flag. When true, gain is forced to 0. */
|
|
1597
1609
|
_muted = false;
|
|
1610
|
+
/** Playback rate. Scales the media clock and each AudioBufferSourceNode's
|
|
1611
|
+
* playbackRate so audio pitches up/down accordingly (same as native
|
|
1612
|
+
* <video>.playbackRate). Default 1. */
|
|
1613
|
+
_rate = 1;
|
|
1598
1614
|
constructor() {
|
|
1599
1615
|
this.ctx = new AudioContext();
|
|
1600
1616
|
this.gain = this.ctx.createGain();
|
|
@@ -1616,6 +1632,20 @@ var AudioOutput = class {
|
|
|
1616
1632
|
getMuted() {
|
|
1617
1633
|
return this._muted;
|
|
1618
1634
|
}
|
|
1635
|
+
/** Set playback rate. Scales the media clock and pitches audio output
|
|
1636
|
+
* (same as native <video>.playbackRate — speed without pitch correction).
|
|
1637
|
+
* Rebases the anchor so the clock transition is seamless. */
|
|
1638
|
+
setPlaybackRate(rate) {
|
|
1639
|
+
if (rate === this._rate) return;
|
|
1640
|
+
const t = this.now();
|
|
1641
|
+
this.mediaTimeOfAnchor = t;
|
|
1642
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1643
|
+
this.wallAnchorMs = performance.now();
|
|
1644
|
+
this._rate = rate;
|
|
1645
|
+
}
|
|
1646
|
+
getPlaybackRate() {
|
|
1647
|
+
return this._rate;
|
|
1648
|
+
}
|
|
1619
1649
|
applyGain() {
|
|
1620
1650
|
const target = this._muted ? 0 : this._volume;
|
|
1621
1651
|
try {
|
|
@@ -1636,12 +1666,12 @@ var AudioOutput = class {
|
|
|
1636
1666
|
now() {
|
|
1637
1667
|
if (this.noAudio) {
|
|
1638
1668
|
if (this.state === "playing") {
|
|
1639
|
-
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3;
|
|
1669
|
+
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3 * this._rate;
|
|
1640
1670
|
}
|
|
1641
1671
|
return this.mediaTimeOfAnchor;
|
|
1642
1672
|
}
|
|
1643
1673
|
if (this.state === "playing") {
|
|
1644
|
-
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor);
|
|
1674
|
+
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
|
|
1645
1675
|
}
|
|
1646
1676
|
return this.mediaTimeOfAnchor;
|
|
1647
1677
|
}
|
|
@@ -1693,7 +1723,8 @@ var AudioOutput = class {
|
|
|
1693
1723
|
const node = this.ctx.createBufferSource();
|
|
1694
1724
|
node.buffer = buffer;
|
|
1695
1725
|
node.connect(this.gain);
|
|
1696
|
-
|
|
1726
|
+
if (this._rate !== 1) node.playbackRate.value = this._rate;
|
|
1727
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
1697
1728
|
if (ctxStart < this.ctx.currentTime) {
|
|
1698
1729
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1699
1730
|
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
@@ -1843,6 +1874,17 @@ function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
|
|
|
1843
1874
|
pkt.time_base_num = 1;
|
|
1844
1875
|
pkt.time_base_den = 1e6;
|
|
1845
1876
|
}
|
|
1877
|
+
function packetPtsSec(pkt, timeBase) {
|
|
1878
|
+
const lo = pkt.pts ?? 0;
|
|
1879
|
+
const hi = pkt.ptshi ?? 0;
|
|
1880
|
+
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
1881
|
+
if (isInvalid) return null;
|
|
1882
|
+
const tb = timeBase ?? [1, 1e6];
|
|
1883
|
+
if (!tb[0] || !tb[1]) return null;
|
|
1884
|
+
const pts64 = hi * 4294967296 + lo;
|
|
1885
|
+
const sec = pts64 * tb[0] / tb[1];
|
|
1886
|
+
return Number.isFinite(sec) ? sec : null;
|
|
1887
|
+
}
|
|
1846
1888
|
var AV_SAMPLE_FMT_U8 = 0;
|
|
1847
1889
|
var AV_SAMPLE_FMT_S16 = 1;
|
|
1848
1890
|
var AV_SAMPLE_FMT_S32 = 2;
|
|
@@ -2103,6 +2145,7 @@ async function startHybridDecoder(opts) {
|
|
|
2103
2145
|
let videoFramesDecoded = 0;
|
|
2104
2146
|
let audioFramesDecoded = 0;
|
|
2105
2147
|
let videoChunksFed = 0;
|
|
2148
|
+
let bufferedUntilSec = 0;
|
|
2106
2149
|
let syntheticVideoUs = 0;
|
|
2107
2150
|
let syntheticAudioUs = 0;
|
|
2108
2151
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
@@ -2123,6 +2166,18 @@ async function startHybridDecoder(opts) {
|
|
|
2123
2166
|
if (myToken !== pumpToken || destroyed) return;
|
|
2124
2167
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2125
2168
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2169
|
+
if (videoPackets && videoTimeBase) {
|
|
2170
|
+
for (const pkt of videoPackets) {
|
|
2171
|
+
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
2172
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
if (audioPackets && audioTimeBase) {
|
|
2176
|
+
for (const pkt of audioPackets) {
|
|
2177
|
+
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
2178
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2126
2181
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2127
2182
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2128
2183
|
}
|
|
@@ -2379,6 +2434,9 @@ async function startHybridDecoder(opts) {
|
|
|
2379
2434
|
(err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
|
|
2380
2435
|
);
|
|
2381
2436
|
},
|
|
2437
|
+
bufferedUntilSec() {
|
|
2438
|
+
return bufferedUntilSec;
|
|
2439
|
+
},
|
|
2382
2440
|
stats() {
|
|
2383
2441
|
return {
|
|
2384
2442
|
decoderType: "webcodecs-hybrid",
|
|
@@ -2494,6 +2552,14 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2494
2552
|
get: () => ctx.duration ?? NaN
|
|
2495
2553
|
});
|
|
2496
2554
|
}
|
|
2555
|
+
Object.defineProperty(target, "playbackRate", {
|
|
2556
|
+
configurable: true,
|
|
2557
|
+
get: () => audio.getPlaybackRate(),
|
|
2558
|
+
set: (v) => {
|
|
2559
|
+
audio.setPlaybackRate(v);
|
|
2560
|
+
target.dispatchEvent(new Event("ratechange"));
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2497
2563
|
Object.defineProperty(target, "readyState", {
|
|
2498
2564
|
configurable: true,
|
|
2499
2565
|
get: () => {
|
|
@@ -2506,6 +2572,13 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2506
2572
|
configurable: true,
|
|
2507
2573
|
get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
|
|
2508
2574
|
});
|
|
2575
|
+
Object.defineProperty(target, "buffered", {
|
|
2576
|
+
configurable: true,
|
|
2577
|
+
get: () => {
|
|
2578
|
+
const end = handles.bufferedUntilSec();
|
|
2579
|
+
return makeTimeRanges(end > 0 ? [[0, end]] : []);
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2509
2582
|
async function waitForBuffer() {
|
|
2510
2583
|
const start = performance.now();
|
|
2511
2584
|
while (true) {
|
|
@@ -2519,6 +2592,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2519
2592
|
}
|
|
2520
2593
|
async function doSeek(timeSec) {
|
|
2521
2594
|
const wasPlaying = audio.isPlaying();
|
|
2595
|
+
target.dispatchEvent(new Event("seeking"));
|
|
2522
2596
|
await audio.pause().catch(() => {
|
|
2523
2597
|
});
|
|
2524
2598
|
await handles.seek(timeSec).catch(
|
|
@@ -2530,7 +2604,14 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2530
2604
|
await waitForBuffer();
|
|
2531
2605
|
await audio.start();
|
|
2532
2606
|
}
|
|
2607
|
+
target.dispatchEvent(new Event("seeked"));
|
|
2533
2608
|
}
|
|
2609
|
+
queueMicrotask(() => {
|
|
2610
|
+
try {
|
|
2611
|
+
target.dispatchEvent(new Event("loadedmetadata"));
|
|
2612
|
+
} catch {
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2534
2615
|
let fatalErrorHandler = null;
|
|
2535
2616
|
handles.onFatalError((reason) => fatalErrorHandler?.(reason));
|
|
2536
2617
|
return {
|
|
@@ -2589,6 +2670,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2589
2670
|
delete target.muted;
|
|
2590
2671
|
delete target.readyState;
|
|
2591
2672
|
delete target.seekable;
|
|
2673
|
+
delete target.playbackRate;
|
|
2592
2674
|
} catch {
|
|
2593
2675
|
}
|
|
2594
2676
|
},
|
|
@@ -2715,6 +2797,7 @@ async function startDecoder(opts) {
|
|
|
2715
2797
|
let pumpRunning = null;
|
|
2716
2798
|
let packetsRead = 0;
|
|
2717
2799
|
let videoFramesDecoded = 0;
|
|
2800
|
+
let bufferedUntilSec = 0;
|
|
2718
2801
|
let audioFramesDecoded = 0;
|
|
2719
2802
|
let watchdogFirstFrameMs = 0;
|
|
2720
2803
|
let watchdogSlowSinceMs = 0;
|
|
@@ -2740,6 +2823,18 @@ async function startDecoder(opts) {
|
|
|
2740
2823
|
if (myToken !== pumpToken || destroyed) return;
|
|
2741
2824
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2742
2825
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2826
|
+
if (videoPackets && videoTimeBase) {
|
|
2827
|
+
for (const pkt of videoPackets) {
|
|
2828
|
+
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
2829
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
if (audioPackets && audioTimeBase) {
|
|
2833
|
+
for (const pkt of audioPackets) {
|
|
2834
|
+
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
2835
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2743
2838
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2744
2839
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2745
2840
|
}
|
|
@@ -3021,6 +3116,9 @@ async function startDecoder(opts) {
|
|
|
3021
3116
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
3022
3117
|
);
|
|
3023
3118
|
},
|
|
3119
|
+
bufferedUntilSec() {
|
|
3120
|
+
return bufferedUntilSec;
|
|
3121
|
+
},
|
|
3024
3122
|
stats() {
|
|
3025
3123
|
return {
|
|
3026
3124
|
decoderType: "libav-wasm",
|
|
@@ -3108,6 +3206,14 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3108
3206
|
get: () => ctx.duration ?? NaN
|
|
3109
3207
|
});
|
|
3110
3208
|
}
|
|
3209
|
+
Object.defineProperty(target, "playbackRate", {
|
|
3210
|
+
configurable: true,
|
|
3211
|
+
get: () => audio.getPlaybackRate(),
|
|
3212
|
+
set: (v) => {
|
|
3213
|
+
audio.setPlaybackRate(v);
|
|
3214
|
+
target.dispatchEvent(new Event("ratechange"));
|
|
3215
|
+
}
|
|
3216
|
+
});
|
|
3111
3217
|
Object.defineProperty(target, "readyState", {
|
|
3112
3218
|
configurable: true,
|
|
3113
3219
|
get: () => {
|
|
@@ -3120,6 +3226,13 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3120
3226
|
configurable: true,
|
|
3121
3227
|
get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
|
|
3122
3228
|
});
|
|
3229
|
+
Object.defineProperty(target, "buffered", {
|
|
3230
|
+
configurable: true,
|
|
3231
|
+
get: () => {
|
|
3232
|
+
const end = handles.bufferedUntilSec();
|
|
3233
|
+
return makeTimeRanges(end > 0 ? [[0, end]] : []);
|
|
3234
|
+
}
|
|
3235
|
+
});
|
|
3123
3236
|
async function waitForBuffer() {
|
|
3124
3237
|
const start = performance.now();
|
|
3125
3238
|
let firstFrameAtMs = 0;
|
|
@@ -3159,6 +3272,7 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3159
3272
|
}
|
|
3160
3273
|
async function doSeek(timeSec) {
|
|
3161
3274
|
const wasPlaying = audio.isPlaying();
|
|
3275
|
+
target.dispatchEvent(new Event("seeking"));
|
|
3162
3276
|
await audio.pause().catch(() => {
|
|
3163
3277
|
});
|
|
3164
3278
|
await handles.seek(timeSec).catch(
|
|
@@ -3170,7 +3284,14 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3170
3284
|
await waitForBuffer();
|
|
3171
3285
|
await audio.start();
|
|
3172
3286
|
}
|
|
3287
|
+
target.dispatchEvent(new Event("seeked"));
|
|
3173
3288
|
}
|
|
3289
|
+
queueMicrotask(() => {
|
|
3290
|
+
try {
|
|
3291
|
+
target.dispatchEvent(new Event("loadedmetadata"));
|
|
3292
|
+
} catch {
|
|
3293
|
+
}
|
|
3294
|
+
});
|
|
3174
3295
|
return {
|
|
3175
3296
|
strategy: "fallback",
|
|
3176
3297
|
async play() {
|
|
@@ -3224,6 +3345,7 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3224
3345
|
delete target.muted;
|
|
3225
3346
|
delete target.readyState;
|
|
3226
3347
|
delete target.seekable;
|
|
3348
|
+
delete target.playbackRate;
|
|
3227
3349
|
} catch {
|
|
3228
3350
|
}
|
|
3229
3351
|
},
|
|
@@ -4250,9 +4372,10 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4250
4372
|
else this.removeAttribute("autoplay");
|
|
4251
4373
|
}
|
|
4252
4374
|
get muted() {
|
|
4253
|
-
return this.
|
|
4375
|
+
return this._videoEl.muted;
|
|
4254
4376
|
}
|
|
4255
4377
|
set muted(value) {
|
|
4378
|
+
this._videoEl.muted = value;
|
|
4256
4379
|
if (value) this.setAttribute("muted", "");
|
|
4257
4380
|
else this.removeAttribute("muted");
|
|
4258
4381
|
}
|
|
@@ -4317,11 +4440,16 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4317
4440
|
}
|
|
4318
4441
|
/**
|
|
4319
4442
|
* Buffered time ranges for the active source. Mirrors the standard
|
|
4320
|
-
* `<video>.buffered` `TimeRanges` API.
|
|
4321
|
-
*
|
|
4322
|
-
*
|
|
4323
|
-
*
|
|
4324
|
-
*
|
|
4443
|
+
* `<video>.buffered` `TimeRanges` API.
|
|
4444
|
+
*
|
|
4445
|
+
* - **Native / remux:** pass-through to the real `<video>.buffered`
|
|
4446
|
+
* (reflects the browser's SourceBuffer / progressive-download state).
|
|
4447
|
+
* - **Hybrid / fallback:** a single `[0, frontier]` range synthesized
|
|
4448
|
+
* from the demuxer's read progress — "how far libav has ever pumped
|
|
4449
|
+
* packets through." Monotonic; does not shrink on seek. This is an
|
|
4450
|
+
* approximation, not MSE-fidelity: decoded frames on canvas strategies
|
|
4451
|
+
* are consumed in flight, so we can't report per-range availability
|
|
4452
|
+
* the way MSE does. Enough for a seek-bar buffered indicator.
|
|
4325
4453
|
*/
|
|
4326
4454
|
get buffered() {
|
|
4327
4455
|
return this._videoEl.buffered;
|
|
@@ -4625,11 +4753,17 @@ var PLAYER_STYLES = (
|
|
|
4625
4753
|
|
|
4626
4754
|
/* \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 */
|
|
4627
4755
|
|
|
4756
|
+
:host {
|
|
4757
|
+
-webkit-tap-highlight-color: transparent;
|
|
4758
|
+
outline: none;
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4628
4761
|
.avp {
|
|
4629
4762
|
position: relative;
|
|
4630
4763
|
width: 100%;
|
|
4631
4764
|
height: 100%;
|
|
4632
|
-
|
|
4765
|
+
-webkit-tap-highlight-color: transparent;
|
|
4766
|
+
user-select: none;
|
|
4633
4767
|
}
|
|
4634
4768
|
|
|
4635
4769
|
.avp avbridge-video {
|
|
@@ -4828,7 +4962,14 @@ var PLAYER_STYLES = (
|
|
|
4828
4962
|
pointer-events: auto;
|
|
4829
4963
|
}
|
|
4830
4964
|
|
|
4831
|
-
|
|
4965
|
+
/* Left slot fills remaining space so slotted text/content can grow.
|
|
4966
|
+
min-width: 0 prevents flex children from overflowing the toolbar. */
|
|
4967
|
+
.avp-toolbar-top-left {
|
|
4968
|
+
flex: 1;
|
|
4969
|
+
min-width: 0;
|
|
4970
|
+
}
|
|
4971
|
+
|
|
4972
|
+
.avp-toolbar-top-right { margin-left: auto; flex-shrink: 0; }
|
|
4832
4973
|
|
|
4833
4974
|
/* Hide the gradient band when no consumer has slotted anything \u2014 we
|
|
4834
4975
|
toggle data-toolbar-empty from JS via slotchange. */
|
|
@@ -4841,6 +4982,30 @@ var PLAYER_STYLES = (
|
|
|
4841
4982
|
pointer-events: none;
|
|
4842
4983
|
}
|
|
4843
4984
|
|
|
4985
|
+
/* \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 */
|
|
4986
|
+
/* Consumer-provided rich content (tweet cards, media info, annotations).
|
|
4987
|
+
Sits above the video, below the play-button overlay and controls in
|
|
4988
|
+
z-order. Auto-hides with the chrome. The wrapper is pointer-events:none
|
|
4989
|
+
so taps fall through to the video; consumers opt in on their content
|
|
4990
|
+
with pointer-events:auto. */
|
|
4991
|
+
|
|
4992
|
+
.avp-content-overlay {
|
|
4993
|
+
position: absolute;
|
|
4994
|
+
inset: 0;
|
|
4995
|
+
z-index: 1;
|
|
4996
|
+
pointer-events: none;
|
|
4997
|
+
opacity: 1;
|
|
4998
|
+
transition: opacity 0.25s;
|
|
4999
|
+
}
|
|
5000
|
+
|
|
5001
|
+
.avp-content-overlay ::slotted(*) {
|
|
5002
|
+
pointer-events: auto;
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
:host([data-controls-hidden]) .avp-content-overlay {
|
|
5006
|
+
opacity: 0;
|
|
5007
|
+
}
|
|
5008
|
+
|
|
4844
5009
|
/* \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 */
|
|
4845
5010
|
|
|
4846
5011
|
.avp-seek {
|
|
@@ -4927,6 +5092,15 @@ var PLAYER_STYLES = (
|
|
|
4927
5092
|
|
|
4928
5093
|
.avp-seek:hover .avp-seek-tooltip { display: block; }
|
|
4929
5094
|
|
|
5095
|
+
/* Show tooltip during active drag (touch or mouse). The JS side sets
|
|
5096
|
+
data-seeking on .avp-seek while the user is scrubbing. */
|
|
5097
|
+
.avp-seek[data-seeking] .avp-seek-tooltip { display: block; }
|
|
5098
|
+
|
|
5099
|
+
/* Enlarge thumb while scrubbing. */
|
|
5100
|
+
.avp-seek[data-seeking] .avp-seek-thumb {
|
|
5101
|
+
transform: translate(-50%, -50%) scale(1.4);
|
|
5102
|
+
}
|
|
5103
|
+
|
|
4930
5104
|
/* \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 */
|
|
4931
5105
|
|
|
4932
5106
|
.avp-bottom {
|
|
@@ -5037,60 +5211,114 @@ var PLAYER_STYLES = (
|
|
|
5037
5211
|
|
|
5038
5212
|
.avp-spacer { flex: 1; }
|
|
5039
5213
|
|
|
5040
|
-
/* \u2500\u2500 Settings
|
|
5214
|
+
/* \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 */
|
|
5041
5215
|
|
|
5216
|
+
/* Scrim \u2014 semi-transparent overlay behind the sheet, above the video.
|
|
5217
|
+
Tapping it dismisses the sheet. */
|
|
5218
|
+
.avp-settings-scrim {
|
|
5219
|
+
position: absolute;
|
|
5220
|
+
inset: 0;
|
|
5221
|
+
z-index: 9;
|
|
5222
|
+
background: rgba(0, 0, 0, 0.4);
|
|
5223
|
+
opacity: 0;
|
|
5224
|
+
pointer-events: none;
|
|
5225
|
+
transition: opacity 0.2s;
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
.avp-settings-scrim.open {
|
|
5229
|
+
opacity: 1;
|
|
5230
|
+
pointer-events: auto;
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5233
|
+
/* Sheet container \u2014 slides up from the bottom. Height is content-driven
|
|
5234
|
+
up to a JS-measured max (set on open via style.maxHeight). */
|
|
5042
5235
|
.avp-settings {
|
|
5043
5236
|
position: absolute;
|
|
5044
|
-
bottom:
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
border-radius: 8px;
|
|
5048
|
-
min-width: 220px;
|
|
5049
|
-
max-height: 300px;
|
|
5050
|
-
overflow-y: auto;
|
|
5051
|
-
display: none;
|
|
5237
|
+
bottom: 0;
|
|
5238
|
+
left: 0;
|
|
5239
|
+
right: 0;
|
|
5052
5240
|
z-index: 10;
|
|
5053
|
-
|
|
5241
|
+
background: rgba(28, 28, 28, 0.97);
|
|
5242
|
+
border-radius: 12px 12px 0 0;
|
|
5243
|
+
overflow-y: auto;
|
|
5244
|
+
overscroll-behavior: contain;
|
|
5245
|
+
transform: translateY(100%);
|
|
5246
|
+
transition: transform 0.2s ease-out;
|
|
5247
|
+
max-height: 70%;
|
|
5248
|
+
padding-bottom: 52px;
|
|
5249
|
+
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
|
|
5054
5250
|
}
|
|
5055
5251
|
|
|
5056
|
-
.avp-settings.open {
|
|
5252
|
+
.avp-settings.open {
|
|
5253
|
+
transform: translateY(0);
|
|
5254
|
+
}
|
|
5057
5255
|
|
|
5058
|
-
.
|
|
5059
|
-
|
|
5060
|
-
|
|
5256
|
+
/* Drag handle indicator at top of sheet. */
|
|
5257
|
+
.avp-settings-handle {
|
|
5258
|
+
width: 36px;
|
|
5259
|
+
height: 4px;
|
|
5260
|
+
border-radius: 2px;
|
|
5261
|
+
background: rgba(255, 255, 255, 0.3);
|
|
5262
|
+
margin: 8px auto 4px;
|
|
5061
5263
|
}
|
|
5062
5264
|
|
|
5063
|
-
|
|
5265
|
+
/* \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 */
|
|
5064
5266
|
|
|
5065
|
-
.avp-settings-
|
|
5066
|
-
|
|
5067
|
-
font-size: 11px;
|
|
5068
|
-
text-transform: uppercase;
|
|
5069
|
-
letter-spacing: 0.5px;
|
|
5070
|
-
opacity: 0.5;
|
|
5267
|
+
.avp-settings-section {
|
|
5268
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
5071
5269
|
}
|
|
5072
5270
|
|
|
5073
|
-
.avp-settings-
|
|
5271
|
+
.avp-settings-section:last-child { border-bottom: none; }
|
|
5272
|
+
|
|
5273
|
+
/* Section header \u2014 clickable row showing label + current value. */
|
|
5274
|
+
.avp-settings-header {
|
|
5275
|
+
position: relative;
|
|
5074
5276
|
display: flex;
|
|
5075
5277
|
align-items: center;
|
|
5076
|
-
|
|
5077
|
-
|
|
5278
|
+
justify-content: space-between;
|
|
5279
|
+
padding: 12px 16px;
|
|
5078
5280
|
cursor: pointer;
|
|
5281
|
+
font-size: 14px;
|
|
5079
5282
|
transition: background 0.1s;
|
|
5080
5283
|
}
|
|
5081
5284
|
|
|
5082
|
-
.avp-settings-
|
|
5285
|
+
.avp-settings-header:hover { background: rgba(255, 255, 255, 0.06); }
|
|
5083
5286
|
|
|
5084
|
-
.avp-settings-
|
|
5085
|
-
|
|
5287
|
+
.avp-settings-header-label {
|
|
5288
|
+
display: flex;
|
|
5289
|
+
align-items: center;
|
|
5290
|
+
gap: 8px;
|
|
5291
|
+
font-weight: 500;
|
|
5292
|
+
}
|
|
5293
|
+
|
|
5294
|
+
.avp-settings-header-value {
|
|
5295
|
+
margin-left: auto;
|
|
5296
|
+
opacity: 0.6;
|
|
5297
|
+
font-size: 13px;
|
|
5298
|
+
text-align: right;
|
|
5086
5299
|
}
|
|
5087
5300
|
|
|
5088
|
-
.
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5301
|
+
/* Invisible native <select> layered over the value portion of the row.
|
|
5302
|
+
Covers from the value text to the right edge so tapping the value
|
|
5303
|
+
opens the OS picker. The label side remains inert. */
|
|
5304
|
+
.avp-settings-select {
|
|
5305
|
+
position: absolute;
|
|
5306
|
+
top: 0;
|
|
5307
|
+
right: 0;
|
|
5308
|
+
bottom: 0;
|
|
5309
|
+
width: 50%;
|
|
5310
|
+
opacity: 0;
|
|
5311
|
+
cursor: pointer;
|
|
5312
|
+
font-size: 16px;
|
|
5313
|
+
direction: rtl;
|
|
5092
5314
|
}
|
|
5093
5315
|
|
|
5316
|
+
/* Toggle-style rows (Stats for Nerds) \u2014 no select, just clickable. */
|
|
5317
|
+
.avp-settings-toggle {
|
|
5318
|
+
cursor: pointer;
|
|
5319
|
+
}
|
|
5320
|
+
.avp-settings-toggle:hover { background: rgba(255, 255, 255, 0.06); }
|
|
5321
|
+
|
|
5094
5322
|
/* \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 */
|
|
5095
5323
|
|
|
5096
5324
|
.avp-stats {
|
|
@@ -5118,9 +5346,24 @@ var PLAYER_STYLES = (
|
|
|
5118
5346
|
@media (pointer: coarse) {
|
|
5119
5347
|
.avp-btn svg { width: 28px; height: 28px; }
|
|
5120
5348
|
.avp-btn { padding: 8px; }
|
|
5349
|
+
|
|
5350
|
+
/* Taller touch target on mobile (44px, matching YouTube Mobile)
|
|
5351
|
+
while keeping the visual track thin. Negative margin collapses
|
|
5352
|
+
the extra space so the controls layout doesn't shift. */
|
|
5353
|
+
.avp-seek { height: 44px; margin-top: -12px; margin-bottom: -12px; }
|
|
5121
5354
|
.avp-seek-track { height: 4px; }
|
|
5122
5355
|
.avp-seek:hover .avp-seek-track { height: 4px; }
|
|
5123
|
-
.avp-seek-thumb {
|
|
5356
|
+
.avp-seek-thumb {
|
|
5357
|
+
transform: translate(-50%, -50%) scale(1);
|
|
5358
|
+
width: 16px;
|
|
5359
|
+
height: 16px;
|
|
5360
|
+
}
|
|
5361
|
+
.avp-seek[data-seeking] .avp-seek-thumb {
|
|
5362
|
+
transform: translate(-50%, -50%) scale(1.5);
|
|
5363
|
+
}
|
|
5364
|
+
/* Move tooltip above the taller touch zone. */
|
|
5365
|
+
.avp-seek-tooltip { bottom: 32px; }
|
|
5366
|
+
|
|
5124
5367
|
.avp-volume:hover .avp-volume-slider { width: 0; }
|
|
5125
5368
|
.avp-overlay-btn { width: 56px; height: 56px; }
|
|
5126
5369
|
.avp-overlay-btn svg { width: 30px; height: 30px; }
|
|
@@ -5213,6 +5456,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5213
5456
|
_volumeInput;
|
|
5214
5457
|
_settingsBtn;
|
|
5215
5458
|
_settingsMenu;
|
|
5459
|
+
_settingsScrim;
|
|
5460
|
+
_customSections = [];
|
|
5216
5461
|
_fullscreenBtn;
|
|
5217
5462
|
// Strategy badge removed — visible in Stats for Nerds instead.
|
|
5218
5463
|
// Spinner is rendered but driven entirely by CSS :host([data-state]) selectors.
|
|
@@ -5254,6 +5499,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5254
5499
|
this._volumeInput = shadow.querySelector(".avp-volume-input");
|
|
5255
5500
|
this._settingsBtn = shadow.querySelector(".avp-settings-btn");
|
|
5256
5501
|
this._settingsMenu = shadow.querySelector(".avp-settings");
|
|
5502
|
+
this._settingsScrim = shadow.querySelector(".avp-settings-scrim");
|
|
5257
5503
|
this._fullscreenBtn = shadow.querySelector(".avp-fullscreen");
|
|
5258
5504
|
this._speedIndicator = shadow.querySelector(".avp-speed-indicator");
|
|
5259
5505
|
this._statsEl = shadow.querySelector(".avp-stats");
|
|
@@ -5278,6 +5524,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5278
5524
|
<div part="toolbar-top-left" class="avp-toolbar-top-left"><slot name="top-left"></slot></div>
|
|
5279
5525
|
<div part="toolbar-top-right" class="avp-toolbar-top-right"><slot name="top-right"></slot></div>
|
|
5280
5526
|
</div>
|
|
5527
|
+
<div part="content-overlay" class="avp-content-overlay"><slot name="content-overlay"></slot></div>
|
|
5281
5528
|
<div part="overlay" class="avp-overlay">
|
|
5282
5529
|
<button class="avp-overlay-btn" aria-label="Play">${ICON_PLAY}</button>
|
|
5283
5530
|
<div class="avp-spinner"></div>
|
|
@@ -5309,7 +5556,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5309
5556
|
<button class="avp-btn avp-settings-btn" part="settings-button" aria-label="Settings">${ICON_SETTINGS}</button>
|
|
5310
5557
|
<button class="avp-btn avp-fullscreen" part="fullscreen-button" aria-label="Fullscreen">${ICON_FULLSCREEN}</button>
|
|
5311
5558
|
</div>
|
|
5312
|
-
<div class="avp-settings
|
|
5559
|
+
<div class="avp-settings-scrim"></div>
|
|
5560
|
+
<div class="avp-settings" part="settings-menu"><div class="avp-settings-handle"></div></div>
|
|
5313
5561
|
</div>
|
|
5314
5562
|
</div>`;
|
|
5315
5563
|
}
|
|
@@ -5381,6 +5629,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5381
5629
|
e.stopPropagation();
|
|
5382
5630
|
this._toggleSettings();
|
|
5383
5631
|
});
|
|
5632
|
+
on(this._settingsScrim, "click", () => this._closeSettings());
|
|
5384
5633
|
on(this._fullscreenBtn, "click", (e) => {
|
|
5385
5634
|
e.stopPropagation();
|
|
5386
5635
|
this._toggleFullscreen();
|
|
@@ -5389,11 +5638,6 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5389
5638
|
const container = this.shadowRoot.querySelector(".avp");
|
|
5390
5639
|
on(container, "click", (e) => this._onContainerClick(e));
|
|
5391
5640
|
on(container, "dblclick", (e) => this._onContainerDblClick(e));
|
|
5392
|
-
on(container, "click", (e) => {
|
|
5393
|
-
if (this._settingsOpen && !e.target.closest?.(".avp-settings-btn, .avp-settings")) {
|
|
5394
|
-
this._closeSettings();
|
|
5395
|
-
}
|
|
5396
|
-
});
|
|
5397
5641
|
on(document, "click", (e) => {
|
|
5398
5642
|
if (this._settingsOpen && !this.contains(e.target)) {
|
|
5399
5643
|
this._closeSettings();
|
|
@@ -5493,19 +5737,23 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5493
5737
|
this._userSeeking = true;
|
|
5494
5738
|
const seekBar = this.shadowRoot.querySelector(".avp-seek");
|
|
5495
5739
|
seekBar.setPointerCapture(e.pointerId);
|
|
5740
|
+
seekBar.setAttribute("data-seeking", "");
|
|
5496
5741
|
const initial = this._timeFromSeekPointer(e.clientX);
|
|
5497
5742
|
this._seekInput.value = String(initial);
|
|
5498
5743
|
this._onSeekInput();
|
|
5744
|
+
this._updateSeekTooltip(e.clientX);
|
|
5499
5745
|
const onMove = (ev) => {
|
|
5500
5746
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5501
5747
|
this._seekInput.value = String(t);
|
|
5502
5748
|
this._onSeekInput();
|
|
5749
|
+
this._updateSeekTooltip(ev.clientX);
|
|
5503
5750
|
};
|
|
5504
5751
|
const onUp = (ev) => {
|
|
5505
5752
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5506
5753
|
this._seekInput.value = String(t);
|
|
5507
5754
|
this._onSeekCommit();
|
|
5508
5755
|
this._seekInput.focus();
|
|
5756
|
+
seekBar.removeAttribute("data-seeking");
|
|
5509
5757
|
seekBar.removeEventListener("pointermove", onMove);
|
|
5510
5758
|
seekBar.removeEventListener("pointerup", onUp);
|
|
5511
5759
|
seekBar.removeEventListener("pointercancel", onUp);
|
|
@@ -5519,8 +5767,11 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5519
5767
|
seekBar.addEventListener("pointercancel", onUp);
|
|
5520
5768
|
}
|
|
5521
5769
|
_onSeekHover(e) {
|
|
5770
|
+
this._updateSeekTooltip(e.clientX);
|
|
5771
|
+
}
|
|
5772
|
+
_updateSeekTooltip(clientX) {
|
|
5522
5773
|
const rect = this._seekInput.getBoundingClientRect();
|
|
5523
|
-
const frac = Math.max(0, Math.min(1, (
|
|
5774
|
+
const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
5524
5775
|
const t = frac * (this._video.duration || 0);
|
|
5525
5776
|
this._seekTooltip.textContent = formatTime(t);
|
|
5526
5777
|
this._seekTooltip.style.left = `${frac * 100}%`;
|
|
@@ -5564,83 +5815,111 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5564
5815
|
_toggleSettings() {
|
|
5565
5816
|
this._settingsOpen = !this._settingsOpen;
|
|
5566
5817
|
this._settingsMenu.classList.toggle("open", this._settingsOpen);
|
|
5567
|
-
|
|
5818
|
+
this._settingsScrim.classList.toggle("open", this._settingsOpen);
|
|
5819
|
+
if (this._settingsOpen) {
|
|
5820
|
+
this._fitSettingsToPlayer();
|
|
5821
|
+
this._showControls();
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
_fitSettingsToPlayer() {
|
|
5825
|
+
const container = this.shadowRoot?.querySelector(".avp");
|
|
5826
|
+
if (!container) return;
|
|
5827
|
+
const rect = container.getBoundingClientRect();
|
|
5828
|
+
const maxH = Math.max(120, Math.floor(rect.height * 0.7));
|
|
5829
|
+
this._settingsMenu.style.maxHeight = `${maxH}px`;
|
|
5568
5830
|
}
|
|
5569
5831
|
_closeSettings() {
|
|
5570
5832
|
this._settingsOpen = false;
|
|
5571
5833
|
this._settingsMenu.classList.remove("open");
|
|
5834
|
+
this._settingsScrim.classList.remove("open");
|
|
5572
5835
|
}
|
|
5573
5836
|
_buildSettingsMenu() {
|
|
5574
5837
|
const sections = [];
|
|
5575
|
-
|
|
5576
|
-
const currentFit = this._video.fit ?? "contain";
|
|
5577
|
-
let fitItems = "";
|
|
5578
|
-
for (const mode of FIT_MODES) {
|
|
5579
|
-
const active = mode === currentFit;
|
|
5580
|
-
const label = mode[0].toUpperCase() + mode.slice(1);
|
|
5581
|
-
fitItems += `<div class="avp-settings-item${active ? " active" : ""}" data-fit="${mode}">${label}</div>`;
|
|
5582
|
-
}
|
|
5583
|
-
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Fit</div>${fitItems}</div>`);
|
|
5584
|
-
}
|
|
5838
|
+
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>`;
|
|
5585
5839
|
const currentRate = this._video.playbackRate ?? 1;
|
|
5586
|
-
|
|
5840
|
+
const speedValue = Math.abs(currentRate - 1) < 0.01 ? "Normal" : `${currentRate}x`;
|
|
5841
|
+
let speedOpts = "";
|
|
5587
5842
|
for (const spd of PLAYBACK_SPEEDS) {
|
|
5588
|
-
const
|
|
5843
|
+
const sel = Math.abs(spd - currentRate) < 0.01 ? " selected" : "";
|
|
5589
5844
|
const label = spd === 1 ? "Normal" : `${spd}x`;
|
|
5590
|
-
|
|
5845
|
+
speedOpts += `<option value="${spd}"${sel}>${label}</option>`;
|
|
5846
|
+
}
|
|
5847
|
+
sections.push(selectRow("Speed", speedValue, speedOpts, `data-action="speed"`));
|
|
5848
|
+
const audios = this._video.audioTracks ?? [];
|
|
5849
|
+
if (audios.length > 1) {
|
|
5850
|
+
let audioOpts = "";
|
|
5851
|
+
for (const t of audios) {
|
|
5852
|
+
audioOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
|
|
5853
|
+
}
|
|
5854
|
+
sections.push(selectRow("Audio", audios[0]?.language ?? "Track 1", audioOpts, `data-action="audio"`));
|
|
5591
5855
|
}
|
|
5592
|
-
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Speed</div>${speedItems}</div>`);
|
|
5593
5856
|
const subs = this._video.subtitleTracks ?? [];
|
|
5594
5857
|
if (subs.length > 0) {
|
|
5595
|
-
let
|
|
5858
|
+
let subOpts = `<option value="-1" selected>Off</option>`;
|
|
5596
5859
|
for (const t of subs) {
|
|
5597
|
-
|
|
5860
|
+
subOpts += `<option value="${t.id}">${t.language ?? `Track ${t.id}`}</option>`;
|
|
5598
5861
|
}
|
|
5599
|
-
sections.push(
|
|
5862
|
+
sections.push(selectRow("Subtitles", "Off", subOpts, `data-action="subtitle"`));
|
|
5600
5863
|
}
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5864
|
+
if (this.hasAttribute("show-fit")) {
|
|
5865
|
+
const currentFit = this._video.fit ?? "contain";
|
|
5866
|
+
const fitValue = currentFit[0].toUpperCase() + currentFit.slice(1);
|
|
5867
|
+
let fitOpts = "";
|
|
5868
|
+
for (const mode of FIT_MODES) {
|
|
5869
|
+
const sel = mode === currentFit ? " selected" : "";
|
|
5870
|
+
const label = mode[0].toUpperCase() + mode.slice(1);
|
|
5871
|
+
fitOpts += `<option value="${mode}"${sel}>${label}</option>`;
|
|
5606
5872
|
}
|
|
5607
|
-
sections.push(
|
|
5873
|
+
sections.push(selectRow("Fit", fitValue, fitOpts, `data-action="fit"`));
|
|
5608
5874
|
}
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
item.
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
});
|
|
5875
|
+
for (const cfg of this._customSections) {
|
|
5876
|
+
const activeItem = cfg.items.find((i) => i.active);
|
|
5877
|
+
let customOpts = "";
|
|
5878
|
+
for (const item of cfg.items) {
|
|
5879
|
+
const sel = item.active ? " selected" : "";
|
|
5880
|
+
customOpts += `<option value="${item.id}"${sel}>${item.label}</option>`;
|
|
5881
|
+
}
|
|
5882
|
+
sections.push(selectRow(cfg.label, activeItem?.label ?? "", customOpts, `data-action="custom" data-custom-id="${cfg.id}"`));
|
|
5618
5883
|
}
|
|
5619
|
-
|
|
5620
|
-
|
|
5884
|
+
sections.push(
|
|
5885
|
+
`<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>`
|
|
5886
|
+
);
|
|
5887
|
+
const handle = this._settingsMenu.querySelector(".avp-settings-handle");
|
|
5888
|
+
this._settingsMenu.innerHTML = "";
|
|
5889
|
+
if (handle) this._settingsMenu.appendChild(handle);
|
|
5890
|
+
else this._settingsMenu.insertAdjacentHTML("afterbegin", `<div class="avp-settings-handle"></div>`);
|
|
5891
|
+
this._settingsMenu.insertAdjacentHTML("beforeend", sections.join(""));
|
|
5892
|
+
for (const sel of this._settingsMenu.querySelectorAll(".avp-settings-select")) {
|
|
5893
|
+
sel.addEventListener("change", (e) => {
|
|
5621
5894
|
e.stopPropagation();
|
|
5622
|
-
|
|
5895
|
+
const action = sel.dataset.action;
|
|
5896
|
+
const val = sel.value;
|
|
5897
|
+
switch (action) {
|
|
5898
|
+
case "speed":
|
|
5899
|
+
this._video.playbackRate = Number(val);
|
|
5900
|
+
break;
|
|
5901
|
+
case "audio":
|
|
5902
|
+
void this._video.setAudioTrack(Number(val));
|
|
5903
|
+
break;
|
|
5904
|
+
case "subtitle":
|
|
5905
|
+
void this._video.setSubtitleTrack(Number(val) >= 0 ? Number(val) : null);
|
|
5906
|
+
break;
|
|
5907
|
+
case "fit":
|
|
5908
|
+
this.setAttribute("fit", val);
|
|
5909
|
+
break;
|
|
5910
|
+
case "custom": {
|
|
5911
|
+
const cfgId = sel.dataset.customId;
|
|
5912
|
+
const cfg = this._customSections.find((s) => s.id === cfgId);
|
|
5913
|
+
cfg?.onSelect(val);
|
|
5914
|
+
break;
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5623
5917
|
this._buildSettingsMenu();
|
|
5624
5918
|
});
|
|
5625
5919
|
}
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
const id = Number(item.dataset.subtitle);
|
|
5630
|
-
void this._video.setSubtitleTrack(id >= 0 ? id : null);
|
|
5631
|
-
this._closeSettings();
|
|
5632
|
-
});
|
|
5633
|
-
}
|
|
5634
|
-
for (const item of this._settingsMenu.querySelectorAll("[data-audio]")) {
|
|
5635
|
-
item.addEventListener("click", (e) => {
|
|
5636
|
-
e.stopPropagation();
|
|
5637
|
-
void this._video.setAudioTrack(Number(item.dataset.audio));
|
|
5638
|
-
this._closeSettings();
|
|
5639
|
-
});
|
|
5640
|
-
}
|
|
5641
|
-
const statsItem = this._settingsMenu.querySelector("[data-stats]");
|
|
5642
|
-
if (statsItem) {
|
|
5643
|
-
statsItem.addEventListener("click", (e) => {
|
|
5920
|
+
const statsRow = this._settingsMenu.querySelector("[data-stats]");
|
|
5921
|
+
if (statsRow) {
|
|
5922
|
+
statsRow.addEventListener("click", (e) => {
|
|
5644
5923
|
e.stopPropagation();
|
|
5645
5924
|
this._toggleStats();
|
|
5646
5925
|
this._closeSettings();
|
|
@@ -5740,19 +6019,23 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5740
6019
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
5741
6020
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
5742
6021
|
_lastPointerTypeWasTouch = false;
|
|
5743
|
-
/** True if the event's composed path passes through consumer-slotted
|
|
5744
|
-
* content. Slotted content lives in the
|
|
5745
|
-
* on the event target won't
|
|
5746
|
-
* does. */
|
|
5747
|
-
|
|
6022
|
+
/** True if the event's composed path passes through consumer-slotted
|
|
6023
|
+
* content (toolbar or content-overlay). Slotted content lives in the
|
|
6024
|
+
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
6025
|
+
* find the shadow-DOM wrapper — `composedPath()` does. */
|
|
6026
|
+
_isSlottedContentEvent(e) {
|
|
5748
6027
|
for (const node of e.composedPath()) {
|
|
5749
|
-
if (node instanceof HTMLElement && node.classList.contains("avp-toolbar-top")) return true;
|
|
6028
|
+
if (node instanceof HTMLElement && (node.classList.contains("avp-toolbar-top") || node.classList.contains("avp-content-overlay"))) return true;
|
|
5750
6029
|
}
|
|
5751
6030
|
return false;
|
|
5752
6031
|
}
|
|
5753
6032
|
_onContainerClick(e) {
|
|
5754
6033
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5755
|
-
if (this.
|
|
6034
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
6035
|
+
if (this._settingsOpen) {
|
|
6036
|
+
this._closeSettings();
|
|
6037
|
+
return;
|
|
6038
|
+
}
|
|
5756
6039
|
if (this._lastPointerTypeWasTouch) {
|
|
5757
6040
|
this._lastPointerTypeWasTouch = false;
|
|
5758
6041
|
return;
|
|
@@ -5768,7 +6051,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5768
6051
|
}
|
|
5769
6052
|
_onContainerDblClick(e) {
|
|
5770
6053
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
5771
|
-
if (this.
|
|
6054
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
5772
6055
|
if (this._tapTimer) {
|
|
5773
6056
|
clearTimeout(this._tapTimer);
|
|
5774
6057
|
this._tapTimer = null;
|
|
@@ -5790,7 +6073,11 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5790
6073
|
if (e.pointerType !== "touch") return;
|
|
5791
6074
|
this._lastPointerTypeWasTouch = true;
|
|
5792
6075
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5793
|
-
if (this.
|
|
6076
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
6077
|
+
if (this._settingsOpen) {
|
|
6078
|
+
this._closeSettings();
|
|
6079
|
+
return;
|
|
6080
|
+
}
|
|
5794
6081
|
const now = Date.now();
|
|
5795
6082
|
if (now - this._lastTapTime < 300) {
|
|
5796
6083
|
if (this._tapTimer) {
|
|
@@ -6040,6 +6327,15 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6040
6327
|
async setAudioTrack(id) {
|
|
6041
6328
|
return this._video.setAudioTrack(id);
|
|
6042
6329
|
}
|
|
6330
|
+
addSettingsSection(config) {
|
|
6331
|
+
this._customSections = this._customSections.filter((s) => s.id !== config.id);
|
|
6332
|
+
this._customSections.push(config);
|
|
6333
|
+
this._buildSettingsMenu();
|
|
6334
|
+
}
|
|
6335
|
+
removeSettingsSection(id) {
|
|
6336
|
+
this._customSections = this._customSections.filter((s) => s.id !== id);
|
|
6337
|
+
this._buildSettingsMenu();
|
|
6338
|
+
}
|
|
6043
6339
|
async setSubtitleTrack(id) {
|
|
6044
6340
|
return this._video.setSubtitleTrack(id);
|
|
6045
6341
|
}
|