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.d.cts
CHANGED
|
@@ -15,7 +15,7 @@ type MediaInput = File | Blob | string | URL | ArrayBuffer | Uint8Array;
|
|
|
15
15
|
/** Container format families we know about. */
|
|
16
16
|
type ContainerKind = "mp4" | "mov" | "mkv" | "webm" | "avi" | "asf" | "flv" | "rm" | "ogg" | "wav" | "mp3" | "flac" | "adts" | "mpegts" | "unknown";
|
|
17
17
|
/** Video codec families. Strings, not enums, so plugins can extend. */
|
|
18
|
-
type VideoCodec = "h264" | "h265" | "vp8" | "vp9" | "av1" | "mpeg4" | "wmv3" | "vc1" | "rv10" | "rv20" | "rv30" | "rv40" | "mpeg2" | "mpeg1" | "theora" | (string & {});
|
|
18
|
+
type VideoCodec = "h264" | "h265" | "vp8" | "vp9" | "av1" | "mpeg4" | "wmv3" | "vc1" | "rv10" | "rv20" | "rv30" | "rv40" | "mpeg2" | "mpeg1" | "theora" | "dv" | "hq_hqa" | "rawvideo" | "qtrle" | "png" | "vp6f" | (string & {});
|
|
19
19
|
/** Audio codec families. */
|
|
20
20
|
type AudioCodec = "aac" | "mp3" | "opus" | "vorbis" | "flac" | "pcm" | "ac3" | "eac3" | "wmav2" | "wmapro" | "alac" | "cook" | "ra_144" | "ra_288" | "sipr" | "atrac3" | "dts" | "truehd" | (string & {});
|
|
21
21
|
interface VideoTrackInfo {
|
|
@@ -347,6 +347,7 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
347
347
|
private _timeFromSeekPointer;
|
|
348
348
|
private _onSeekPointerDown;
|
|
349
349
|
private _onSeekHover;
|
|
350
|
+
private _updateSeekTooltip;
|
|
350
351
|
private _updateSeekVisuals;
|
|
351
352
|
private _updateTime;
|
|
352
353
|
private _toggleMute;
|
|
@@ -375,11 +376,11 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
375
376
|
private _scheduleHide;
|
|
376
377
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
377
378
|
private _lastPointerTypeWasTouch;
|
|
378
|
-
/** True if the event's composed path passes through consumer-slotted
|
|
379
|
-
* content. Slotted content lives in the
|
|
380
|
-
* on the event target won't
|
|
381
|
-
* does. */
|
|
382
|
-
private
|
|
379
|
+
/** True if the event's composed path passes through consumer-slotted
|
|
380
|
+
* content (toolbar or content-overlay). Slotted content lives in the
|
|
381
|
+
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
382
|
+
* find the shadow-DOM wrapper — `composedPath()` does. */
|
|
383
|
+
private _isSlottedContentEvent;
|
|
383
384
|
private _onContainerClick;
|
|
384
385
|
private _onContainerDblClick;
|
|
385
386
|
private _onPointerDown;
|
|
@@ -694,11 +695,16 @@ declare class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
694
695
|
get readyState(): number;
|
|
695
696
|
/**
|
|
696
697
|
* Buffered time ranges for the active source. Mirrors the standard
|
|
697
|
-
* `<video>.buffered` `TimeRanges` API.
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
698
|
+
* `<video>.buffered` `TimeRanges` API.
|
|
699
|
+
*
|
|
700
|
+
* - **Native / remux:** pass-through to the real `<video>.buffered`
|
|
701
|
+
* (reflects the browser's SourceBuffer / progressive-download state).
|
|
702
|
+
* - **Hybrid / fallback:** a single `[0, frontier]` range synthesized
|
|
703
|
+
* from the demuxer's read progress — "how far libav has ever pumped
|
|
704
|
+
* packets through." Monotonic; does not shrink on seek. This is an
|
|
705
|
+
* approximation, not MSE-fidelity: decoded frames on canvas strategies
|
|
706
|
+
* are consumed in flight, so we can't report per-range availability
|
|
707
|
+
* the way MSE does. Enough for a seek-bar buffered indicator.
|
|
702
708
|
*/
|
|
703
709
|
get buffered(): TimeRanges;
|
|
704
710
|
get poster(): string;
|
package/dist/player.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ type MediaInput = File | Blob | string | URL | ArrayBuffer | Uint8Array;
|
|
|
15
15
|
/** Container format families we know about. */
|
|
16
16
|
type ContainerKind = "mp4" | "mov" | "mkv" | "webm" | "avi" | "asf" | "flv" | "rm" | "ogg" | "wav" | "mp3" | "flac" | "adts" | "mpegts" | "unknown";
|
|
17
17
|
/** Video codec families. Strings, not enums, so plugins can extend. */
|
|
18
|
-
type VideoCodec = "h264" | "h265" | "vp8" | "vp9" | "av1" | "mpeg4" | "wmv3" | "vc1" | "rv10" | "rv20" | "rv30" | "rv40" | "mpeg2" | "mpeg1" | "theora" | (string & {});
|
|
18
|
+
type VideoCodec = "h264" | "h265" | "vp8" | "vp9" | "av1" | "mpeg4" | "wmv3" | "vc1" | "rv10" | "rv20" | "rv30" | "rv40" | "mpeg2" | "mpeg1" | "theora" | "dv" | "hq_hqa" | "rawvideo" | "qtrle" | "png" | "vp6f" | (string & {});
|
|
19
19
|
/** Audio codec families. */
|
|
20
20
|
type AudioCodec = "aac" | "mp3" | "opus" | "vorbis" | "flac" | "pcm" | "ac3" | "eac3" | "wmav2" | "wmapro" | "alac" | "cook" | "ra_144" | "ra_288" | "sipr" | "atrac3" | "dts" | "truehd" | (string & {});
|
|
21
21
|
interface VideoTrackInfo {
|
|
@@ -347,6 +347,7 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
347
347
|
private _timeFromSeekPointer;
|
|
348
348
|
private _onSeekPointerDown;
|
|
349
349
|
private _onSeekHover;
|
|
350
|
+
private _updateSeekTooltip;
|
|
350
351
|
private _updateSeekVisuals;
|
|
351
352
|
private _updateTime;
|
|
352
353
|
private _toggleMute;
|
|
@@ -375,11 +376,11 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
375
376
|
private _scheduleHide;
|
|
376
377
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
377
378
|
private _lastPointerTypeWasTouch;
|
|
378
|
-
/** True if the event's composed path passes through consumer-slotted
|
|
379
|
-
* content. Slotted content lives in the
|
|
380
|
-
* on the event target won't
|
|
381
|
-
* does. */
|
|
382
|
-
private
|
|
379
|
+
/** True if the event's composed path passes through consumer-slotted
|
|
380
|
+
* content (toolbar or content-overlay). Slotted content lives in the
|
|
381
|
+
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
382
|
+
* find the shadow-DOM wrapper — `composedPath()` does. */
|
|
383
|
+
private _isSlottedContentEvent;
|
|
383
384
|
private _onContainerClick;
|
|
384
385
|
private _onContainerDblClick;
|
|
385
386
|
private _onPointerDown;
|
|
@@ -694,11 +695,16 @@ declare class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
694
695
|
get readyState(): number;
|
|
695
696
|
/**
|
|
696
697
|
* Buffered time ranges for the active source. Mirrors the standard
|
|
697
|
-
* `<video>.buffered` `TimeRanges` API.
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
698
|
+
* `<video>.buffered` `TimeRanges` API.
|
|
699
|
+
*
|
|
700
|
+
* - **Native / remux:** pass-through to the real `<video>.buffered`
|
|
701
|
+
* (reflects the browser's SourceBuffer / progressive-download state).
|
|
702
|
+
* - **Hybrid / fallback:** a single `[0, frontier]` range synthesized
|
|
703
|
+
* from the demuxer's read progress — "how far libav has ever pumped
|
|
704
|
+
* packets through." Monotonic; does not shrink on seek. This is an
|
|
705
|
+
* approximation, not MSE-fidelity: decoded frames on canvas strategies
|
|
706
|
+
* are consumed in flight, so we can't report per-range availability
|
|
707
|
+
* the way MSE does. Enough for a seek-bar buffered indicator.
|
|
702
708
|
*/
|
|
703
709
|
get buffered(): TimeRanges;
|
|
704
710
|
get poster(): string;
|
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)) {
|
|
@@ -1841,6 +1853,17 @@ function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
|
|
|
1841
1853
|
pkt.time_base_num = 1;
|
|
1842
1854
|
pkt.time_base_den = 1e6;
|
|
1843
1855
|
}
|
|
1856
|
+
function packetPtsSec(pkt, timeBase) {
|
|
1857
|
+
const lo = pkt.pts ?? 0;
|
|
1858
|
+
const hi = pkt.ptshi ?? 0;
|
|
1859
|
+
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
1860
|
+
if (isInvalid) return null;
|
|
1861
|
+
const tb = timeBase ?? [1, 1e6];
|
|
1862
|
+
if (!tb[0] || !tb[1]) return null;
|
|
1863
|
+
const pts64 = hi * 4294967296 + lo;
|
|
1864
|
+
const sec = pts64 * tb[0] / tb[1];
|
|
1865
|
+
return Number.isFinite(sec) ? sec : null;
|
|
1866
|
+
}
|
|
1844
1867
|
var AV_SAMPLE_FMT_U8 = 0;
|
|
1845
1868
|
var AV_SAMPLE_FMT_S16 = 1;
|
|
1846
1869
|
var AV_SAMPLE_FMT_S32 = 2;
|
|
@@ -2101,6 +2124,7 @@ async function startHybridDecoder(opts) {
|
|
|
2101
2124
|
let videoFramesDecoded = 0;
|
|
2102
2125
|
let audioFramesDecoded = 0;
|
|
2103
2126
|
let videoChunksFed = 0;
|
|
2127
|
+
let bufferedUntilSec = 0;
|
|
2104
2128
|
let syntheticVideoUs = 0;
|
|
2105
2129
|
let syntheticAudioUs = 0;
|
|
2106
2130
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
@@ -2121,6 +2145,18 @@ async function startHybridDecoder(opts) {
|
|
|
2121
2145
|
if (myToken !== pumpToken || destroyed) return;
|
|
2122
2146
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2123
2147
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2148
|
+
if (videoPackets && videoTimeBase) {
|
|
2149
|
+
for (const pkt of videoPackets) {
|
|
2150
|
+
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
2151
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (audioPackets && audioTimeBase) {
|
|
2155
|
+
for (const pkt of audioPackets) {
|
|
2156
|
+
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
2157
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2124
2160
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2125
2161
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2126
2162
|
}
|
|
@@ -2377,6 +2413,9 @@ async function startHybridDecoder(opts) {
|
|
|
2377
2413
|
(err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
|
|
2378
2414
|
);
|
|
2379
2415
|
},
|
|
2416
|
+
bufferedUntilSec() {
|
|
2417
|
+
return bufferedUntilSec;
|
|
2418
|
+
},
|
|
2380
2419
|
stats() {
|
|
2381
2420
|
return {
|
|
2382
2421
|
decoderType: "webcodecs-hybrid",
|
|
@@ -2504,6 +2543,13 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2504
2543
|
configurable: true,
|
|
2505
2544
|
get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
|
|
2506
2545
|
});
|
|
2546
|
+
Object.defineProperty(target, "buffered", {
|
|
2547
|
+
configurable: true,
|
|
2548
|
+
get: () => {
|
|
2549
|
+
const end = handles.bufferedUntilSec();
|
|
2550
|
+
return makeTimeRanges(end > 0 ? [[0, end]] : []);
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2507
2553
|
async function waitForBuffer() {
|
|
2508
2554
|
const start = performance.now();
|
|
2509
2555
|
while (true) {
|
|
@@ -2517,6 +2563,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2517
2563
|
}
|
|
2518
2564
|
async function doSeek(timeSec) {
|
|
2519
2565
|
const wasPlaying = audio.isPlaying();
|
|
2566
|
+
target.dispatchEvent(new Event("seeking"));
|
|
2520
2567
|
await audio.pause().catch(() => {
|
|
2521
2568
|
});
|
|
2522
2569
|
await handles.seek(timeSec).catch(
|
|
@@ -2528,7 +2575,14 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2528
2575
|
await waitForBuffer();
|
|
2529
2576
|
await audio.start();
|
|
2530
2577
|
}
|
|
2578
|
+
target.dispatchEvent(new Event("seeked"));
|
|
2531
2579
|
}
|
|
2580
|
+
queueMicrotask(() => {
|
|
2581
|
+
try {
|
|
2582
|
+
target.dispatchEvent(new Event("loadedmetadata"));
|
|
2583
|
+
} catch {
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2532
2586
|
let fatalErrorHandler = null;
|
|
2533
2587
|
handles.onFatalError((reason) => fatalErrorHandler?.(reason));
|
|
2534
2588
|
return {
|
|
@@ -2713,6 +2767,7 @@ async function startDecoder(opts) {
|
|
|
2713
2767
|
let pumpRunning = null;
|
|
2714
2768
|
let packetsRead = 0;
|
|
2715
2769
|
let videoFramesDecoded = 0;
|
|
2770
|
+
let bufferedUntilSec = 0;
|
|
2716
2771
|
let audioFramesDecoded = 0;
|
|
2717
2772
|
let watchdogFirstFrameMs = 0;
|
|
2718
2773
|
let watchdogSlowSinceMs = 0;
|
|
@@ -2738,6 +2793,18 @@ async function startDecoder(opts) {
|
|
|
2738
2793
|
if (myToken !== pumpToken || destroyed) return;
|
|
2739
2794
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2740
2795
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2796
|
+
if (videoPackets && videoTimeBase) {
|
|
2797
|
+
for (const pkt of videoPackets) {
|
|
2798
|
+
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
2799
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
if (audioPackets && audioTimeBase) {
|
|
2803
|
+
for (const pkt of audioPackets) {
|
|
2804
|
+
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
2805
|
+
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2741
2808
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2742
2809
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2743
2810
|
}
|
|
@@ -3019,6 +3086,9 @@ async function startDecoder(opts) {
|
|
|
3019
3086
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
3020
3087
|
);
|
|
3021
3088
|
},
|
|
3089
|
+
bufferedUntilSec() {
|
|
3090
|
+
return bufferedUntilSec;
|
|
3091
|
+
},
|
|
3022
3092
|
stats() {
|
|
3023
3093
|
return {
|
|
3024
3094
|
decoderType: "libav-wasm",
|
|
@@ -3118,6 +3188,13 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3118
3188
|
configurable: true,
|
|
3119
3189
|
get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
|
|
3120
3190
|
});
|
|
3191
|
+
Object.defineProperty(target, "buffered", {
|
|
3192
|
+
configurable: true,
|
|
3193
|
+
get: () => {
|
|
3194
|
+
const end = handles.bufferedUntilSec();
|
|
3195
|
+
return makeTimeRanges(end > 0 ? [[0, end]] : []);
|
|
3196
|
+
}
|
|
3197
|
+
});
|
|
3121
3198
|
async function waitForBuffer() {
|
|
3122
3199
|
const start = performance.now();
|
|
3123
3200
|
let firstFrameAtMs = 0;
|
|
@@ -3157,6 +3234,7 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3157
3234
|
}
|
|
3158
3235
|
async function doSeek(timeSec) {
|
|
3159
3236
|
const wasPlaying = audio.isPlaying();
|
|
3237
|
+
target.dispatchEvent(new Event("seeking"));
|
|
3160
3238
|
await audio.pause().catch(() => {
|
|
3161
3239
|
});
|
|
3162
3240
|
await handles.seek(timeSec).catch(
|
|
@@ -3168,7 +3246,14 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
3168
3246
|
await waitForBuffer();
|
|
3169
3247
|
await audio.start();
|
|
3170
3248
|
}
|
|
3249
|
+
target.dispatchEvent(new Event("seeked"));
|
|
3171
3250
|
}
|
|
3251
|
+
queueMicrotask(() => {
|
|
3252
|
+
try {
|
|
3253
|
+
target.dispatchEvent(new Event("loadedmetadata"));
|
|
3254
|
+
} catch {
|
|
3255
|
+
}
|
|
3256
|
+
});
|
|
3172
3257
|
return {
|
|
3173
3258
|
strategy: "fallback",
|
|
3174
3259
|
async play() {
|
|
@@ -4248,9 +4333,10 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4248
4333
|
else this.removeAttribute("autoplay");
|
|
4249
4334
|
}
|
|
4250
4335
|
get muted() {
|
|
4251
|
-
return this.
|
|
4336
|
+
return this._videoEl.muted;
|
|
4252
4337
|
}
|
|
4253
4338
|
set muted(value) {
|
|
4339
|
+
this._videoEl.muted = value;
|
|
4254
4340
|
if (value) this.setAttribute("muted", "");
|
|
4255
4341
|
else this.removeAttribute("muted");
|
|
4256
4342
|
}
|
|
@@ -4315,11 +4401,16 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4315
4401
|
}
|
|
4316
4402
|
/**
|
|
4317
4403
|
* Buffered time ranges for the active source. Mirrors the standard
|
|
4318
|
-
* `<video>.buffered` `TimeRanges` API.
|
|
4319
|
-
*
|
|
4320
|
-
*
|
|
4321
|
-
*
|
|
4322
|
-
*
|
|
4404
|
+
* `<video>.buffered` `TimeRanges` API.
|
|
4405
|
+
*
|
|
4406
|
+
* - **Native / remux:** pass-through to the real `<video>.buffered`
|
|
4407
|
+
* (reflects the browser's SourceBuffer / progressive-download state).
|
|
4408
|
+
* - **Hybrid / fallback:** a single `[0, frontier]` range synthesized
|
|
4409
|
+
* from the demuxer's read progress — "how far libav has ever pumped
|
|
4410
|
+
* packets through." Monotonic; does not shrink on seek. This is an
|
|
4411
|
+
* approximation, not MSE-fidelity: decoded frames on canvas strategies
|
|
4412
|
+
* are consumed in flight, so we can't report per-range availability
|
|
4413
|
+
* the way MSE does. Enough for a seek-bar buffered indicator.
|
|
4323
4414
|
*/
|
|
4324
4415
|
get buffered() {
|
|
4325
4416
|
return this._videoEl.buffered;
|
|
@@ -4623,11 +4714,18 @@ var PLAYER_STYLES = (
|
|
|
4623
4714
|
|
|
4624
4715
|
/* \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
4716
|
|
|
4717
|
+
:host {
|
|
4718
|
+
-webkit-tap-highlight-color: transparent;
|
|
4719
|
+
outline: none;
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4626
4722
|
.avp {
|
|
4627
4723
|
position: relative;
|
|
4628
4724
|
width: 100%;
|
|
4629
4725
|
height: 100%;
|
|
4630
4726
|
cursor: pointer;
|
|
4727
|
+
-webkit-tap-highlight-color: transparent;
|
|
4728
|
+
user-select: none;
|
|
4631
4729
|
}
|
|
4632
4730
|
|
|
4633
4731
|
.avp avbridge-video {
|
|
@@ -4826,7 +4924,14 @@ var PLAYER_STYLES = (
|
|
|
4826
4924
|
pointer-events: auto;
|
|
4827
4925
|
}
|
|
4828
4926
|
|
|
4829
|
-
|
|
4927
|
+
/* Left slot fills remaining space so slotted text/content can grow.
|
|
4928
|
+
min-width: 0 prevents flex children from overflowing the toolbar. */
|
|
4929
|
+
.avp-toolbar-top-left {
|
|
4930
|
+
flex: 1;
|
|
4931
|
+
min-width: 0;
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4934
|
+
.avp-toolbar-top-right { margin-left: auto; flex-shrink: 0; }
|
|
4830
4935
|
|
|
4831
4936
|
/* Hide the gradient band when no consumer has slotted anything \u2014 we
|
|
4832
4937
|
toggle data-toolbar-empty from JS via slotchange. */
|
|
@@ -4839,6 +4944,30 @@ var PLAYER_STYLES = (
|
|
|
4839
4944
|
pointer-events: none;
|
|
4840
4945
|
}
|
|
4841
4946
|
|
|
4947
|
+
/* \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 */
|
|
4948
|
+
/* Consumer-provided rich content (tweet cards, media info, annotations).
|
|
4949
|
+
Sits above the video, below the play-button overlay and controls in
|
|
4950
|
+
z-order. Auto-hides with the chrome. The wrapper is pointer-events:none
|
|
4951
|
+
so taps fall through to the video; consumers opt in on their content
|
|
4952
|
+
with pointer-events:auto. */
|
|
4953
|
+
|
|
4954
|
+
.avp-content-overlay {
|
|
4955
|
+
position: absolute;
|
|
4956
|
+
inset: 0;
|
|
4957
|
+
z-index: 1;
|
|
4958
|
+
pointer-events: none;
|
|
4959
|
+
opacity: 1;
|
|
4960
|
+
transition: opacity 0.25s;
|
|
4961
|
+
}
|
|
4962
|
+
|
|
4963
|
+
.avp-content-overlay ::slotted(*) {
|
|
4964
|
+
pointer-events: auto;
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
:host([data-controls-hidden]) .avp-content-overlay {
|
|
4968
|
+
opacity: 0;
|
|
4969
|
+
}
|
|
4970
|
+
|
|
4842
4971
|
/* \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
4972
|
|
|
4844
4973
|
.avp-seek {
|
|
@@ -4925,6 +5054,15 @@ var PLAYER_STYLES = (
|
|
|
4925
5054
|
|
|
4926
5055
|
.avp-seek:hover .avp-seek-tooltip { display: block; }
|
|
4927
5056
|
|
|
5057
|
+
/* Show tooltip during active drag (touch or mouse). The JS side sets
|
|
5058
|
+
data-seeking on .avp-seek while the user is scrubbing. */
|
|
5059
|
+
.avp-seek[data-seeking] .avp-seek-tooltip { display: block; }
|
|
5060
|
+
|
|
5061
|
+
/* Enlarge thumb while scrubbing. */
|
|
5062
|
+
.avp-seek[data-seeking] .avp-seek-thumb {
|
|
5063
|
+
transform: translate(-50%, -50%) scale(1.4);
|
|
5064
|
+
}
|
|
5065
|
+
|
|
4928
5066
|
/* \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
5067
|
|
|
4930
5068
|
.avp-bottom {
|
|
@@ -5044,7 +5182,10 @@ var PLAYER_STYLES = (
|
|
|
5044
5182
|
background: rgba(28, 28, 28, 0.95);
|
|
5045
5183
|
border-radius: 8px;
|
|
5046
5184
|
min-width: 220px;
|
|
5047
|
-
|
|
5185
|
+
/* Fit within the player: leave room for the controls bar (52px bottom)
|
|
5186
|
+
and a small top margin (8px). On tall players this caps at 300px;
|
|
5187
|
+
on short players it shrinks to whatever fits. */
|
|
5188
|
+
max-height: min(300px, calc(100% - 52px - 8px));
|
|
5048
5189
|
overflow-y: auto;
|
|
5049
5190
|
display: none;
|
|
5050
5191
|
z-index: 10;
|
|
@@ -5116,9 +5257,24 @@ var PLAYER_STYLES = (
|
|
|
5116
5257
|
@media (pointer: coarse) {
|
|
5117
5258
|
.avp-btn svg { width: 28px; height: 28px; }
|
|
5118
5259
|
.avp-btn { padding: 8px; }
|
|
5260
|
+
|
|
5261
|
+
/* Taller touch target on mobile (44px, matching YouTube Mobile)
|
|
5262
|
+
while keeping the visual track thin. Negative margin collapses
|
|
5263
|
+
the extra space so the controls layout doesn't shift. */
|
|
5264
|
+
.avp-seek { height: 44px; margin-top: -12px; margin-bottom: -12px; }
|
|
5119
5265
|
.avp-seek-track { height: 4px; }
|
|
5120
5266
|
.avp-seek:hover .avp-seek-track { height: 4px; }
|
|
5121
|
-
.avp-seek-thumb {
|
|
5267
|
+
.avp-seek-thumb {
|
|
5268
|
+
transform: translate(-50%, -50%) scale(1);
|
|
5269
|
+
width: 16px;
|
|
5270
|
+
height: 16px;
|
|
5271
|
+
}
|
|
5272
|
+
.avp-seek[data-seeking] .avp-seek-thumb {
|
|
5273
|
+
transform: translate(-50%, -50%) scale(1.5);
|
|
5274
|
+
}
|
|
5275
|
+
/* Move tooltip above the taller touch zone. */
|
|
5276
|
+
.avp-seek-tooltip { bottom: 32px; }
|
|
5277
|
+
|
|
5122
5278
|
.avp-volume:hover .avp-volume-slider { width: 0; }
|
|
5123
5279
|
.avp-overlay-btn { width: 56px; height: 56px; }
|
|
5124
5280
|
.avp-overlay-btn svg { width: 30px; height: 30px; }
|
|
@@ -5276,6 +5432,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5276
5432
|
<div part="toolbar-top-left" class="avp-toolbar-top-left"><slot name="top-left"></slot></div>
|
|
5277
5433
|
<div part="toolbar-top-right" class="avp-toolbar-top-right"><slot name="top-right"></slot></div>
|
|
5278
5434
|
</div>
|
|
5435
|
+
<div part="content-overlay" class="avp-content-overlay"><slot name="content-overlay"></slot></div>
|
|
5279
5436
|
<div part="overlay" class="avp-overlay">
|
|
5280
5437
|
<button class="avp-overlay-btn" aria-label="Play">${ICON_PLAY}</button>
|
|
5281
5438
|
<div class="avp-spinner"></div>
|
|
@@ -5491,19 +5648,23 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5491
5648
|
this._userSeeking = true;
|
|
5492
5649
|
const seekBar = this.shadowRoot.querySelector(".avp-seek");
|
|
5493
5650
|
seekBar.setPointerCapture(e.pointerId);
|
|
5651
|
+
seekBar.setAttribute("data-seeking", "");
|
|
5494
5652
|
const initial = this._timeFromSeekPointer(e.clientX);
|
|
5495
5653
|
this._seekInput.value = String(initial);
|
|
5496
5654
|
this._onSeekInput();
|
|
5655
|
+
this._updateSeekTooltip(e.clientX);
|
|
5497
5656
|
const onMove = (ev) => {
|
|
5498
5657
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5499
5658
|
this._seekInput.value = String(t);
|
|
5500
5659
|
this._onSeekInput();
|
|
5660
|
+
this._updateSeekTooltip(ev.clientX);
|
|
5501
5661
|
};
|
|
5502
5662
|
const onUp = (ev) => {
|
|
5503
5663
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5504
5664
|
this._seekInput.value = String(t);
|
|
5505
5665
|
this._onSeekCommit();
|
|
5506
5666
|
this._seekInput.focus();
|
|
5667
|
+
seekBar.removeAttribute("data-seeking");
|
|
5507
5668
|
seekBar.removeEventListener("pointermove", onMove);
|
|
5508
5669
|
seekBar.removeEventListener("pointerup", onUp);
|
|
5509
5670
|
seekBar.removeEventListener("pointercancel", onUp);
|
|
@@ -5517,8 +5678,11 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5517
5678
|
seekBar.addEventListener("pointercancel", onUp);
|
|
5518
5679
|
}
|
|
5519
5680
|
_onSeekHover(e) {
|
|
5681
|
+
this._updateSeekTooltip(e.clientX);
|
|
5682
|
+
}
|
|
5683
|
+
_updateSeekTooltip(clientX) {
|
|
5520
5684
|
const rect = this._seekInput.getBoundingClientRect();
|
|
5521
|
-
const frac = Math.max(0, Math.min(1, (
|
|
5685
|
+
const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
5522
5686
|
const t = frac * (this._video.duration || 0);
|
|
5523
5687
|
this._seekTooltip.textContent = formatTime(t);
|
|
5524
5688
|
this._seekTooltip.style.left = `${frac * 100}%`;
|
|
@@ -5738,19 +5902,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5738
5902
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
5739
5903
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
5740
5904
|
_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
|
-
|
|
5905
|
+
/** True if the event's composed path passes through consumer-slotted
|
|
5906
|
+
* content (toolbar or content-overlay). Slotted content lives in the
|
|
5907
|
+
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
5908
|
+
* find the shadow-DOM wrapper — `composedPath()` does. */
|
|
5909
|
+
_isSlottedContentEvent(e) {
|
|
5746
5910
|
for (const node of e.composedPath()) {
|
|
5747
|
-
if (node instanceof HTMLElement && node.classList.contains("avp-toolbar-top")) return true;
|
|
5911
|
+
if (node instanceof HTMLElement && (node.classList.contains("avp-toolbar-top") || node.classList.contains("avp-content-overlay"))) return true;
|
|
5748
5912
|
}
|
|
5749
5913
|
return false;
|
|
5750
5914
|
}
|
|
5751
5915
|
_onContainerClick(e) {
|
|
5752
5916
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5753
|
-
if (this.
|
|
5917
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
5754
5918
|
if (this._lastPointerTypeWasTouch) {
|
|
5755
5919
|
this._lastPointerTypeWasTouch = false;
|
|
5756
5920
|
return;
|
|
@@ -5766,7 +5930,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5766
5930
|
}
|
|
5767
5931
|
_onContainerDblClick(e) {
|
|
5768
5932
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
5769
|
-
if (this.
|
|
5933
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
5770
5934
|
if (this._tapTimer) {
|
|
5771
5935
|
clearTimeout(this._tapTimer);
|
|
5772
5936
|
this._tapTimer = null;
|
|
@@ -5788,7 +5952,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5788
5952
|
if (e.pointerType !== "touch") return;
|
|
5789
5953
|
this._lastPointerTypeWasTouch = true;
|
|
5790
5954
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5791
|
-
if (this.
|
|
5955
|
+
if (this._isSlottedContentEvent(e)) return;
|
|
5792
5956
|
const now = Date.now();
|
|
5793
5957
|
if (now - this._lastTapTime < 300) {
|
|
5794
5958
|
if (this._tapTimer) {
|