avbridge 2.8.4 → 2.9.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 +133 -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-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-YX4AGLNF.cjs → chunk-EY6DZEDT.cjs} +89 -15
- package/dist/chunk-EY6DZEDT.cjs.map +1 -0
- 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-Q2VUO52Z.cjs → chunk-OTFS7DC4.cjs} +12 -12
- package/dist/{chunk-Q2VUO52Z.cjs.map → chunk-OTFS7DC4.cjs.map} +1 -1
- package/dist/{chunk-KBWQRGHS.js → chunk-SN4WZE24.js} +79 -5
- package/dist/chunk-SN4WZE24.js.map +1 -0
- package/dist/element-browser.js +104 -7
- 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-DEcidWk6.d.cts} +1 -1
- package/dist/{player-BptSJPfn.d.ts → player-DEcidWk6.d.ts} +1 -1
- package/dist/player.cjs +187 -23
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +17 -11
- package/dist/player.d.ts +17 -11
- package/dist/player.js +187 -23
- 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 +22 -11
- package/src/element/avbridge-video.ts +22 -6
- package/src/element/player-styles.ts +68 -3
- package/src/probe/avi.ts +2 -0
- package/src/strategies/fallback/decoder.ts +30 -0
- package/src/strategies/fallback/index.ts +30 -0
- package/src/strategies/hybrid/decoder.ts +35 -0
- package/src/strategies/hybrid/index.ts +17 -0
- package/src/strategies/remux/index.ts +8 -0
- package/src/types.ts +6 -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)) {
|
|
@@ -1843,6 +1855,17 @@ function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
|
|
|
1843
1855
|
pkt.time_base_num = 1;
|
|
1844
1856
|
pkt.time_base_den = 1e6;
|
|
1845
1857
|
}
|
|
1858
|
+
function packetPtsSec(pkt, timeBase) {
|
|
1859
|
+
const lo = pkt.pts ?? 0;
|
|
1860
|
+
const hi = pkt.ptshi ?? 0;
|
|
1861
|
+
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
1862
|
+
if (isInvalid) return null;
|
|
1863
|
+
const tb = timeBase ?? [1, 1e6];
|
|
1864
|
+
if (!tb[0] || !tb[1]) return null;
|
|
1865
|
+
const pts64 = hi * 4294967296 + lo;
|
|
1866
|
+
const sec = pts64 * tb[0] / tb[1];
|
|
1867
|
+
return Number.isFinite(sec) ? sec : null;
|
|
1868
|
+
}
|
|
1846
1869
|
var AV_SAMPLE_FMT_U8 = 0;
|
|
1847
1870
|
var AV_SAMPLE_FMT_S16 = 1;
|
|
1848
1871
|
var AV_SAMPLE_FMT_S32 = 2;
|
|
@@ -2103,6 +2126,7 @@ async function startHybridDecoder(opts) {
|
|
|
2103
2126
|
let videoFramesDecoded = 0;
|
|
2104
2127
|
let audioFramesDecoded = 0;
|
|
2105
2128
|
let videoChunksFed = 0;
|
|
2129
|
+
let bufferedUntilSec = 0;
|
|
2106
2130
|
let syntheticVideoUs = 0;
|
|
2107
2131
|
let syntheticAudioUs = 0;
|
|
2108
2132
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
@@ -2123,6 +2147,18 @@ async function startHybridDecoder(opts) {
|
|
|
2123
2147
|
if (myToken !== pumpToken || destroyed) return;
|
|
2124
2148
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2125
2149
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2150
|
+
if (videoPackets && videoTimeBase) {
|
|
2151
|
+
for (const pkt of videoPackets) {
|
|
2152
|
+
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
2153
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
if (audioPackets && audioTimeBase) {
|
|
2157
|
+
for (const pkt of audioPackets) {
|
|
2158
|
+
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
2159
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2126
2162
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2127
2163
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2128
2164
|
}
|
|
@@ -2379,6 +2415,9 @@ async function startHybridDecoder(opts) {
|
|
|
2379
2415
|
(err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
|
|
2380
2416
|
);
|
|
2381
2417
|
},
|
|
2418
|
+
bufferedUntilSec() {
|
|
2419
|
+
return bufferedUntilSec;
|
|
2420
|
+
},
|
|
2382
2421
|
stats() {
|
|
2383
2422
|
return {
|
|
2384
2423
|
decoderType: "webcodecs-hybrid",
|
|
@@ -2506,6 +2545,13 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2506
2545
|
configurable: true,
|
|
2507
2546
|
get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
|
|
2508
2547
|
});
|
|
2548
|
+
Object.defineProperty(target, "buffered", {
|
|
2549
|
+
configurable: true,
|
|
2550
|
+
get: () => {
|
|
2551
|
+
const end = handles.bufferedUntilSec();
|
|
2552
|
+
return makeTimeRanges(end > 0 ? [[0, end]] : []);
|
|
2553
|
+
}
|
|
2554
|
+
});
|
|
2509
2555
|
async function waitForBuffer() {
|
|
2510
2556
|
const start = performance.now();
|
|
2511
2557
|
while (true) {
|
|
@@ -2519,6 +2565,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2519
2565
|
}
|
|
2520
2566
|
async function doSeek(timeSec) {
|
|
2521
2567
|
const wasPlaying = audio.isPlaying();
|
|
2568
|
+
target.dispatchEvent(new Event("seeking"));
|
|
2522
2569
|
await audio.pause().catch(() => {
|
|
2523
2570
|
});
|
|
2524
2571
|
await handles.seek(timeSec).catch(
|
|
@@ -2530,7 +2577,14 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2530
2577
|
await waitForBuffer();
|
|
2531
2578
|
await audio.start();
|
|
2532
2579
|
}
|
|
2580
|
+
target.dispatchEvent(new Event("seeked"));
|
|
2533
2581
|
}
|
|
2582
|
+
queueMicrotask(() => {
|
|
2583
|
+
try {
|
|
2584
|
+
target.dispatchEvent(new Event("loadedmetadata"));
|
|
2585
|
+
} catch {
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2534
2588
|
let fatalErrorHandler = null;
|
|
2535
2589
|
handles.onFatalError((reason) => fatalErrorHandler?.(reason));
|
|
2536
2590
|
return {
|
|
@@ -2715,6 +2769,7 @@ async function startDecoder(opts) {
|
|
|
2715
2769
|
let pumpRunning = null;
|
|
2716
2770
|
let packetsRead = 0;
|
|
2717
2771
|
let videoFramesDecoded = 0;
|
|
2772
|
+
let bufferedUntilSec = 0;
|
|
2718
2773
|
let audioFramesDecoded = 0;
|
|
2719
2774
|
let watchdogFirstFrameMs = 0;
|
|
2720
2775
|
let watchdogSlowSinceMs = 0;
|
|
@@ -2740,6 +2795,18 @@ async function startDecoder(opts) {
|
|
|
2740
2795
|
if (myToken !== pumpToken || destroyed) return;
|
|
2741
2796
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2742
2797
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2798
|
+
if (videoPackets && videoTimeBase) {
|
|
2799
|
+
for (const pkt of videoPackets) {
|
|
2800
|
+
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
2801
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
if (audioPackets && audioTimeBase) {
|
|
2805
|
+
for (const pkt of audioPackets) {
|
|
2806
|
+
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
2807
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2743
2810
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2744
2811
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2745
2812
|
}
|
|
@@ -3021,6 +3088,9 @@ async function startDecoder(opts) {
|
|
|
3021
3088
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
3022
3089
|
);
|
|
3023
3090
|
},
|
|
3091
|
+
bufferedUntilSec() {
|
|
3092
|
+
return bufferedUntilSec;
|
|
3093
|
+
},
|
|
3024
3094
|
stats() {
|
|
3025
3095
|
return {
|
|
3026
3096
|
decoderType: "libav-wasm",
|
|
@@ -3120,6 +3190,13 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3120
3190
|
configurable: true,
|
|
3121
3191
|
get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
|
|
3122
3192
|
});
|
|
3193
|
+
Object.defineProperty(target, "buffered", {
|
|
3194
|
+
configurable: true,
|
|
3195
|
+
get: () => {
|
|
3196
|
+
const end = handles.bufferedUntilSec();
|
|
3197
|
+
return makeTimeRanges(end > 0 ? [[0, end]] : []);
|
|
3198
|
+
}
|
|
3199
|
+
});
|
|
3123
3200
|
async function waitForBuffer() {
|
|
3124
3201
|
const start = performance.now();
|
|
3125
3202
|
let firstFrameAtMs = 0;
|
|
@@ -3159,6 +3236,7 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3159
3236
|
}
|
|
3160
3237
|
async function doSeek(timeSec) {
|
|
3161
3238
|
const wasPlaying = audio.isPlaying();
|
|
3239
|
+
target.dispatchEvent(new Event("seeking"));
|
|
3162
3240
|
await audio.pause().catch(() => {
|
|
3163
3241
|
});
|
|
3164
3242
|
await handles.seek(timeSec).catch(
|
|
@@ -3170,7 +3248,14 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3170
3248
|
await waitForBuffer();
|
|
3171
3249
|
await audio.start();
|
|
3172
3250
|
}
|
|
3251
|
+
target.dispatchEvent(new Event("seeked"));
|
|
3173
3252
|
}
|
|
3253
|
+
queueMicrotask(() => {
|
|
3254
|
+
try {
|
|
3255
|
+
target.dispatchEvent(new Event("loadedmetadata"));
|
|
3256
|
+
} catch {
|
|
3257
|
+
}
|
|
3258
|
+
});
|
|
3174
3259
|
return {
|
|
3175
3260
|
strategy: "fallback",
|
|
3176
3261
|
async play() {
|
|
@@ -4250,9 +4335,10 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4250
4335
|
else this.removeAttribute("autoplay");
|
|
4251
4336
|
}
|
|
4252
4337
|
get muted() {
|
|
4253
|
-
return this.
|
|
4338
|
+
return this._videoEl.muted;
|
|
4254
4339
|
}
|
|
4255
4340
|
set muted(value) {
|
|
4341
|
+
this._videoEl.muted = value;
|
|
4256
4342
|
if (value) this.setAttribute("muted", "");
|
|
4257
4343
|
else this.removeAttribute("muted");
|
|
4258
4344
|
}
|
|
@@ -4317,11 +4403,16 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4317
4403
|
}
|
|
4318
4404
|
/**
|
|
4319
4405
|
* Buffered time ranges for the active source. Mirrors the standard
|
|
4320
|
-
* `<video>.buffered` `TimeRanges` API.
|
|
4321
|
-
*
|
|
4322
|
-
*
|
|
4323
|
-
*
|
|
4324
|
-
*
|
|
4406
|
+
* `<video>.buffered` `TimeRanges` API.
|
|
4407
|
+
*
|
|
4408
|
+
* - **Native / remux:** pass-through to the real `<video>.buffered`
|
|
4409
|
+
* (reflects the browser's SourceBuffer / progressive-download state).
|
|
4410
|
+
* - **Hybrid / fallback:** a single `[0, frontier]` range synthesized
|
|
4411
|
+
* from the demuxer's read progress — "how far libav has ever pumped
|
|
4412
|
+
* packets through." Monotonic; does not shrink on seek. This is an
|
|
4413
|
+
* approximation, not MSE-fidelity: decoded frames on canvas strategies
|
|
4414
|
+
* are consumed in flight, so we can't report per-range availability
|
|
4415
|
+
* the way MSE does. Enough for a seek-bar buffered indicator.
|
|
4325
4416
|
*/
|
|
4326
4417
|
get buffered() {
|
|
4327
4418
|
return this._videoEl.buffered;
|
|
@@ -4625,11 +4716,18 @@ var PLAYER_STYLES = (
|
|
|
4625
4716
|
|
|
4626
4717
|
/* \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
4718
|
|
|
4719
|
+
:host {
|
|
4720
|
+
-webkit-tap-highlight-color: transparent;
|
|
4721
|
+
outline: none;
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4628
4724
|
.avp {
|
|
4629
4725
|
position: relative;
|
|
4630
4726
|
width: 100%;
|
|
4631
4727
|
height: 100%;
|
|
4632
4728
|
cursor: pointer;
|
|
4729
|
+
-webkit-tap-highlight-color: transparent;
|
|
4730
|
+
user-select: none;
|
|
4633
4731
|
}
|
|
4634
4732
|
|
|
4635
4733
|
.avp avbridge-video {
|
|
@@ -4828,7 +4926,14 @@ var PLAYER_STYLES = (
|
|
|
4828
4926
|
pointer-events: auto;
|
|
4829
4927
|
}
|
|
4830
4928
|
|
|
4831
|
-
|
|
4929
|
+
/* Left slot fills remaining space so slotted text/content can grow.
|
|
4930
|
+
min-width: 0 prevents flex children from overflowing the toolbar. */
|
|
4931
|
+
.avp-toolbar-top-left {
|
|
4932
|
+
flex: 1;
|
|
4933
|
+
min-width: 0;
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4936
|
+
.avp-toolbar-top-right { margin-left: auto; flex-shrink: 0; }
|
|
4832
4937
|
|
|
4833
4938
|
/* Hide the gradient band when no consumer has slotted anything \u2014 we
|
|
4834
4939
|
toggle data-toolbar-empty from JS via slotchange. */
|
|
@@ -4841,6 +4946,30 @@ var PLAYER_STYLES = (
|
|
|
4841
4946
|
pointer-events: none;
|
|
4842
4947
|
}
|
|
4843
4948
|
|
|
4949
|
+
/* \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 */
|
|
4950
|
+
/* Consumer-provided rich content (tweet cards, media info, annotations).
|
|
4951
|
+
Sits above the video, below the play-button overlay and controls in
|
|
4952
|
+
z-order. Auto-hides with the chrome. The wrapper is pointer-events:none
|
|
4953
|
+
so taps fall through to the video; consumers opt in on their content
|
|
4954
|
+
with pointer-events:auto. */
|
|
4955
|
+
|
|
4956
|
+
.avp-content-overlay {
|
|
4957
|
+
position: absolute;
|
|
4958
|
+
inset: 0;
|
|
4959
|
+
z-index: 1;
|
|
4960
|
+
pointer-events: none;
|
|
4961
|
+
opacity: 1;
|
|
4962
|
+
transition: opacity 0.25s;
|
|
4963
|
+
}
|
|
4964
|
+
|
|
4965
|
+
.avp-content-overlay ::slotted(*) {
|
|
4966
|
+
pointer-events: auto;
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4969
|
+
:host([data-controls-hidden]) .avp-content-overlay {
|
|
4970
|
+
opacity: 0;
|
|
4971
|
+
}
|
|
4972
|
+
|
|
4844
4973
|
/* \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
4974
|
|
|
4846
4975
|
.avp-seek {
|
|
@@ -4927,6 +5056,15 @@ var PLAYER_STYLES = (
|
|
|
4927
5056
|
|
|
4928
5057
|
.avp-seek:hover .avp-seek-tooltip { display: block; }
|
|
4929
5058
|
|
|
5059
|
+
/* Show tooltip during active drag (touch or mouse). The JS side sets
|
|
5060
|
+
data-seeking on .avp-seek while the user is scrubbing. */
|
|
5061
|
+
.avp-seek[data-seeking] .avp-seek-tooltip { display: block; }
|
|
5062
|
+
|
|
5063
|
+
/* Enlarge thumb while scrubbing. */
|
|
5064
|
+
.avp-seek[data-seeking] .avp-seek-thumb {
|
|
5065
|
+
transform: translate(-50%, -50%) scale(1.4);
|
|
5066
|
+
}
|
|
5067
|
+
|
|
4930
5068
|
/* \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
5069
|
|
|
4932
5070
|
.avp-bottom {
|
|
@@ -5046,7 +5184,10 @@ var PLAYER_STYLES = (
|
|
|
5046
5184
|
background: rgba(28, 28, 28, 0.95);
|
|
5047
5185
|
border-radius: 8px;
|
|
5048
5186
|
min-width: 220px;
|
|
5049
|
-
|
|
5187
|
+
/* Fit within the player: leave room for the controls bar (52px bottom)
|
|
5188
|
+
and a small top margin (8px). On tall players this caps at 300px;
|
|
5189
|
+
on short players it shrinks to whatever fits. */
|
|
5190
|
+
max-height: min(300px, calc(100% - 52px - 8px));
|
|
5050
5191
|
overflow-y: auto;
|
|
5051
5192
|
display: none;
|
|
5052
5193
|
z-index: 10;
|
|
@@ -5118,9 +5259,24 @@ var PLAYER_STYLES = (
|
|
|
5118
5259
|
@media (pointer: coarse) {
|
|
5119
5260
|
.avp-btn svg { width: 28px; height: 28px; }
|
|
5120
5261
|
.avp-btn { padding: 8px; }
|
|
5262
|
+
|
|
5263
|
+
/* Taller touch target on mobile (44px, matching YouTube Mobile)
|
|
5264
|
+
while keeping the visual track thin. Negative margin collapses
|
|
5265
|
+
the extra space so the controls layout doesn't shift. */
|
|
5266
|
+
.avp-seek { height: 44px; margin-top: -12px; margin-bottom: -12px; }
|
|
5121
5267
|
.avp-seek-track { height: 4px; }
|
|
5122
5268
|
.avp-seek:hover .avp-seek-track { height: 4px; }
|
|
5123
|
-
.avp-seek-thumb {
|
|
5269
|
+
.avp-seek-thumb {
|
|
5270
|
+
transform: translate(-50%, -50%) scale(1);
|
|
5271
|
+
width: 16px;
|
|
5272
|
+
height: 16px;
|
|
5273
|
+
}
|
|
5274
|
+
.avp-seek[data-seeking] .avp-seek-thumb {
|
|
5275
|
+
transform: translate(-50%, -50%) scale(1.5);
|
|
5276
|
+
}
|
|
5277
|
+
/* Move tooltip above the taller touch zone. */
|
|
5278
|
+
.avp-seek-tooltip { bottom: 32px; }
|
|
5279
|
+
|
|
5124
5280
|
.avp-volume:hover .avp-volume-slider { width: 0; }
|
|
5125
5281
|
.avp-overlay-btn { width: 56px; height: 56px; }
|
|
5126
5282
|
.avp-overlay-btn svg { width: 30px; height: 30px; }
|
|
@@ -5278,6 +5434,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5278
5434
|
<div part="toolbar-top-left" class="avp-toolbar-top-left"><slot name="top-left"></slot></div>
|
|
5279
5435
|
<div part="toolbar-top-right" class="avp-toolbar-top-right"><slot name="top-right"></slot></div>
|
|
5280
5436
|
</div>
|
|
5437
|
+
<div part="content-overlay" class="avp-content-overlay"><slot name="content-overlay"></slot></div>
|
|
5281
5438
|
<div part="overlay" class="avp-overlay">
|
|
5282
5439
|
<button class="avp-overlay-btn" aria-label="Play">${ICON_PLAY}</button>
|
|
5283
5440
|
<div class="avp-spinner"></div>
|
|
@@ -5493,19 +5650,23 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5493
5650
|
this._userSeeking = true;
|
|
5494
5651
|
const seekBar = this.shadowRoot.querySelector(".avp-seek");
|
|
5495
5652
|
seekBar.setPointerCapture(e.pointerId);
|
|
5653
|
+
seekBar.setAttribute("data-seeking", "");
|
|
5496
5654
|
const initial = this._timeFromSeekPointer(e.clientX);
|
|
5497
5655
|
this._seekInput.value = String(initial);
|
|
5498
5656
|
this._onSeekInput();
|
|
5657
|
+
this._updateSeekTooltip(e.clientX);
|
|
5499
5658
|
const onMove = (ev) => {
|
|
5500
5659
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5501
5660
|
this._seekInput.value = String(t);
|
|
5502
5661
|
this._onSeekInput();
|
|
5662
|
+
this._updateSeekTooltip(ev.clientX);
|
|
5503
5663
|
};
|
|
5504
5664
|
const onUp = (ev) => {
|
|
5505
5665
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5506
5666
|
this._seekInput.value = String(t);
|
|
5507
5667
|
this._onSeekCommit();
|
|
5508
5668
|
this._seekInput.focus();
|
|
5669
|
+
seekBar.removeAttribute("data-seeking");
|
|
5509
5670
|
seekBar.removeEventListener("pointermove", onMove);
|
|
5510
5671
|
seekBar.removeEventListener("pointerup", onUp);
|
|
5511
5672
|
seekBar.removeEventListener("pointercancel", onUp);
|
|
@@ -5519,8 +5680,11 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5519
5680
|
seekBar.addEventListener("pointercancel", onUp);
|
|
5520
5681
|
}
|
|
5521
5682
|
_onSeekHover(e) {
|
|
5683
|
+
this._updateSeekTooltip(e.clientX);
|
|
5684
|
+
}
|
|
5685
|
+
_updateSeekTooltip(clientX) {
|
|
5522
5686
|
const rect = this._seekInput.getBoundingClientRect();
|
|
5523
|
-
const frac = Math.max(0, Math.min(1, (
|
|
5687
|
+
const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
5524
5688
|
const t = frac * (this._video.duration || 0);
|
|
5525
5689
|
this._seekTooltip.textContent = formatTime(t);
|
|
5526
5690
|
this._seekTooltip.style.left = `${frac * 100}%`;
|
|
@@ -5740,19 +5904,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5740
5904
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
5741
5905
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
5742
5906
|
_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
|
-
|
|
5907
|
+
/** True if the event's composed path passes through consumer-slotted
|
|
5908
|
+
* content (toolbar or content-overlay). Slotted content lives in the
|
|
5909
|
+
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
5910
|
+
* find the shadow-DOM wrapper — `composedPath()` does. */
|
|
5911
|
+
_isSlottedContentEvent(e) {
|
|
5748
5912
|
for (const node of e.composedPath()) {
|
|
5749
|
-
if (node instanceof HTMLElement && node.classList.contains("avp-toolbar-top")) return true;
|
|
5913
|
+
if (node instanceof HTMLElement && (node.classList.contains("avp-toolbar-top") || node.classList.contains("avp-content-overlay"))) return true;
|
|
5750
5914
|
}
|
|
5751
5915
|
return false;
|
|
5752
5916
|
}
|
|
5753
5917
|
_onContainerClick(e) {
|
|
5754
5918
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5755
|
-
if (this.
|
|
5919
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
5756
5920
|
if (this._lastPointerTypeWasTouch) {
|
|
5757
5921
|
this._lastPointerTypeWasTouch = false;
|
|
5758
5922
|
return;
|
|
@@ -5768,7 +5932,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5768
5932
|
}
|
|
5769
5933
|
_onContainerDblClick(e) {
|
|
5770
5934
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
5771
|
-
if (this.
|
|
5935
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
5772
5936
|
if (this._tapTimer) {
|
|
5773
5937
|
clearTimeout(this._tapTimer);
|
|
5774
5938
|
this._tapTimer = null;
|
|
@@ -5790,7 +5954,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5790
5954
|
if (e.pointerType !== "touch") return;
|
|
5791
5955
|
this._lastPointerTypeWasTouch = true;
|
|
5792
5956
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5793
|
-
if (this.
|
|
5957
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
5794
5958
|
const now = Date.now();
|
|
5795
5959
|
if (now - this._lastTapTime < 300) {
|
|
5796
5960
|
if (this._tapTimer) {
|