avbridge 2.10.0 → 2.12.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 +69 -0
- package/dist/{avi-B5CQYB7L.cjs → avi-EQE6AR75.cjs} +4 -4
- package/dist/{avi-2ILLBNPQ.cjs.map → avi-EQE6AR75.cjs.map} +1 -1
- package/dist/{avi-RWWPN2PR.js → avi-NNHH4AAA.js} +3 -3
- package/dist/{avi-JXU4GQL2.js.map → avi-NNHH4AAA.js.map} +1 -1
- package/dist/{avi-JXU4GQL2.js → avi-S7EY54YA.js} +3 -3
- package/dist/{avi-RWWPN2PR.js.map → avi-S7EY54YA.js.map} +1 -1
- package/dist/{avi-2ILLBNPQ.cjs → avi-Y3N325WZ.cjs} +4 -4
- package/dist/{avi-B5CQYB7L.cjs.map → avi-Y3N325WZ.cjs.map} +1 -1
- package/dist/{chunk-GYIJU44C.js → chunk-2LNXMGT6.js} +5 -5
- package/dist/{chunk-GYIJU44C.js.map → chunk-2LNXMGT6.js.map} +1 -1
- package/dist/{chunk-DCSOQH2N.js → chunk-3AI5WFFN.js} +40 -16
- package/dist/chunk-3AI5WFFN.js.map +1 -0
- package/dist/{chunk-2NSOOMXW.js → chunk-3YKWU4FM.js} +3 -3
- package/dist/{chunk-2NSOOMXW.js.map → chunk-3YKWU4FM.js.map} +1 -1
- package/dist/{chunk-CL6UEUQF.js → chunk-5Y5BTB5D.js} +5 -5
- package/dist/{chunk-CL6UEUQF.js.map → chunk-5Y5BTB5D.js.map} +1 -1
- package/dist/{chunk-NQULEIA3.cjs → chunk-7EF4VTUS.cjs} +36 -28
- package/dist/chunk-7EF4VTUS.cjs.map +1 -0
- package/dist/{chunk-5KVLE6YI.js → chunk-EDDWAN2L.js} +3 -2
- package/dist/chunk-EDDWAN2L.js.map +1 -0
- package/dist/{chunk-OTFS7DC4.cjs → chunk-GJBNLPGI.cjs} +14 -14
- package/dist/{chunk-OTFS7DC4.cjs.map → chunk-GJBNLPGI.cjs.map} +1 -1
- package/dist/{chunk-BYGZN4Z5.cjs → chunk-HBHSUGNI.cjs} +5 -5
- package/dist/{chunk-BYGZN4Z5.cjs.map → chunk-HBHSUGNI.cjs.map} +1 -1
- package/dist/{chunk-L7A3ECI2.cjs → chunk-HZUVMXBN.cjs} +4 -4
- package/dist/{chunk-L7A3ECI2.cjs.map → chunk-HZUVMXBN.cjs.map} +1 -1
- package/dist/{chunk-S4WAZC2T.cjs → chunk-WRKO6Q42.cjs} +3 -2
- package/dist/chunk-WRKO6Q42.cjs.map +1 -0
- package/dist/{chunk-Z33SBWL5.cjs → chunk-YPZFGJV3.cjs} +40 -16
- package/dist/chunk-YPZFGJV3.cjs.map +1 -0
- package/dist/{chunk-3GKM5DFM.js → chunk-Z26PXRUY.js} +18 -10
- package/dist/chunk-Z26PXRUY.js.map +1 -0
- package/dist/element-browser.js +65 -19
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +21 -8
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +20 -7
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +23 -23
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +10 -10
- package/dist/{libav-demux-3N5Y3VQA.cjs → libav-demux-575OYCT2.cjs} +9 -9
- package/dist/{libav-demux-3N5Y3VQA.cjs.map → libav-demux-575OYCT2.cjs.map} +1 -1
- package/dist/{libav-demux-JXD4OTLM.js → libav-demux-SXZDLC7W.js} +4 -4
- package/dist/{libav-demux-JXD4OTLM.js.map → libav-demux-SXZDLC7W.js.map} +1 -1
- package/dist/libav-http-reader-2S5HAHW4.js +3 -0
- package/dist/{libav-http-reader-WXG3Z7AI.js.map → libav-http-reader-2S5HAHW4.js.map} +1 -1
- package/dist/libav-http-reader-Q356EO2K.cjs +16 -0
- package/dist/{libav-http-reader-AZLE7YFS.cjs.map → libav-http-reader-Q356EO2K.cjs.map} +1 -1
- package/dist/{player-DDdNVFDv.d.cts → player-bQ6n4hVp.d.cts} +15 -0
- package/dist/{player-DDdNVFDv.d.ts → player-bQ6n4hVp.d.ts} +15 -0
- package/dist/player.cjs +166 -33
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +36 -0
- package/dist/player.d.ts +36 -0
- package/dist/player.js +162 -29
- package/dist/player.js.map +1 -1
- package/dist/remux-7TA4FKTY.js +10 -0
- package/dist/{remux-56V7LDAD.js.map → remux-7TA4FKTY.js.map} +1 -1
- package/dist/remux-VPKCLHHM.cjs +35 -0
- package/dist/{remux-KUS5GIL6.cjs.map → remux-VPKCLHHM.cjs.map} +1 -1
- package/dist/subtitles-5H24MEBJ.js +4 -0
- package/dist/{subtitles-4T74JRGT.js.map → subtitles-5H24MEBJ.js.map} +1 -1
- package/dist/subtitles-HMVGWTU2.cjs +29 -0
- package/dist/{subtitles-QUH4LPI4.cjs.map → subtitles-HMVGWTU2.cjs.map} +1 -1
- package/package.json +1 -1
- package/src/element/avbridge-player.ts +128 -18
- package/src/element/avbridge-subtitles.ts +273 -0
- package/src/element/avbridge-video.ts +21 -1
- package/src/element/player-styles.ts +13 -1
- package/src/player.ts +3 -3
- package/src/strategies/fallback/audio-output.ts +10 -0
- package/src/subtitles/index.ts +2 -0
- package/src/types.ts +15 -0
- package/src/util/libav-http-reader.ts +58 -19
- package/dist/chunk-3GKM5DFM.js.map +0 -1
- package/dist/chunk-5KVLE6YI.js.map +0 -1
- package/dist/chunk-DCSOQH2N.js.map +0 -1
- package/dist/chunk-NQULEIA3.cjs.map +0 -1
- package/dist/chunk-S4WAZC2T.cjs.map +0 -1
- package/dist/chunk-Z33SBWL5.cjs.map +0 -1
- package/dist/libav-http-reader-AZLE7YFS.cjs +0 -16
- package/dist/libav-http-reader-WXG3Z7AI.js +0 -3
- package/dist/remux-56V7LDAD.js +0 -10
- package/dist/remux-KUS5GIL6.cjs +0 -35
- package/dist/subtitles-4T74JRGT.js +0 -4
- package/dist/subtitles-QUH4LPI4.cjs +0 -29
|
@@ -219,6 +219,14 @@ interface CreatePlayerOptions {
|
|
|
219
219
|
* for interceptors, logging, or environments without a global fetch.
|
|
220
220
|
*/
|
|
221
221
|
fetchFn?: FetchFn;
|
|
222
|
+
/**
|
|
223
|
+
* Byte budget for the libav HTTP reader's LRU cache of fetched ranges.
|
|
224
|
+
* Defaults to 8 MB. Set to `0` to disable caching. Raise this when the
|
|
225
|
+
* app plays seek-heavy legacy-container media from URLs — hot regions
|
|
226
|
+
* (header/moov, tail index, current window) stay resident instead of
|
|
227
|
+
* being re-fetched on every bounce.
|
|
228
|
+
*/
|
|
229
|
+
cacheBytes?: number;
|
|
222
230
|
}
|
|
223
231
|
/** Signature-compatible with `globalThis.fetch`. */
|
|
224
232
|
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
@@ -226,6 +234,13 @@ type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Respons
|
|
|
226
234
|
interface TransportConfig {
|
|
227
235
|
requestInit?: RequestInit;
|
|
228
236
|
fetchFn?: FetchFn;
|
|
237
|
+
/**
|
|
238
|
+
* Byte budget for the libav HTTP reader's LRU cache of fetched ranges.
|
|
239
|
+
* Defaults to 8 MB. Set to `0` to disable caching entirely. Higher
|
|
240
|
+
* values help seek-heavy network playback keep hot regions
|
|
241
|
+
* (header/moov, tail index, current read) resident.
|
|
242
|
+
*/
|
|
243
|
+
cacheBytes?: number;
|
|
229
244
|
}
|
|
230
245
|
/** Events emitted by {@link UnifiedPlayer}. Strongly typed. */
|
|
231
246
|
interface PlayerEventMap {
|
|
@@ -219,6 +219,14 @@ interface CreatePlayerOptions {
|
|
|
219
219
|
* for interceptors, logging, or environments without a global fetch.
|
|
220
220
|
*/
|
|
221
221
|
fetchFn?: FetchFn;
|
|
222
|
+
/**
|
|
223
|
+
* Byte budget for the libav HTTP reader's LRU cache of fetched ranges.
|
|
224
|
+
* Defaults to 8 MB. Set to `0` to disable caching. Raise this when the
|
|
225
|
+
* app plays seek-heavy legacy-container media from URLs — hot regions
|
|
226
|
+
* (header/moov, tail index, current window) stay resident instead of
|
|
227
|
+
* being re-fetched on every bounce.
|
|
228
|
+
*/
|
|
229
|
+
cacheBytes?: number;
|
|
222
230
|
}
|
|
223
231
|
/** Signature-compatible with `globalThis.fetch`. */
|
|
224
232
|
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
@@ -226,6 +234,13 @@ type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Respons
|
|
|
226
234
|
interface TransportConfig {
|
|
227
235
|
requestInit?: RequestInit;
|
|
228
236
|
fetchFn?: FetchFn;
|
|
237
|
+
/**
|
|
238
|
+
* Byte budget for the libav HTTP reader's LRU cache of fetched ranges.
|
|
239
|
+
* Defaults to 8 MB. Set to `0` to disable caching entirely. Higher
|
|
240
|
+
* values help seek-heavy network playback keep hot regions
|
|
241
|
+
* (header/moov, tail index, current read) resident.
|
|
242
|
+
*/
|
|
243
|
+
cacheBytes?: number;
|
|
229
244
|
}
|
|
230
245
|
/** Events emitted by {@link UnifiedPlayer}. Strongly typed. */
|
|
231
246
|
interface PlayerEventMap {
|
package/dist/player.cjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
var chunk2XW2O3YI_cjs = require('./chunk-2XW2O3YI.cjs');
|
|
4
4
|
var chunkNNVOHKXJ_cjs = require('./chunk-NNVOHKXJ.cjs');
|
|
5
|
-
require('./chunk-
|
|
6
|
-
var
|
|
5
|
+
require('./chunk-YPZFGJV3.cjs');
|
|
6
|
+
var chunkWRKO6Q42_cjs = require('./chunk-WRKO6Q42.cjs');
|
|
7
7
|
require('./chunk-QDJLQR53.cjs');
|
|
8
8
|
|
|
9
9
|
// src/events.ts
|
|
@@ -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-Y3N325WZ.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-Y3N325WZ.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-Y3N325WZ.cjs');
|
|
270
270
|
return await probeWithLibav(normalized, sniffed);
|
|
271
271
|
} catch (err) {
|
|
272
272
|
const inner = err instanceof Error ? err.message : String(err);
|
|
@@ -1286,7 +1286,7 @@ var VideoRenderer = class {
|
|
|
1286
1286
|
}
|
|
1287
1287
|
target.style.visibility = "hidden";
|
|
1288
1288
|
const overlayParent = parent instanceof HTMLElement ? parent : document.body;
|
|
1289
|
-
this.subtitleOverlay = new
|
|
1289
|
+
this.subtitleOverlay = new chunkWRKO6Q42_cjs.SubtitleOverlay(overlayParent);
|
|
1290
1290
|
this.watchTextTracks(target);
|
|
1291
1291
|
const ctx = this.canvas.getContext("2d");
|
|
1292
1292
|
if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
|
|
@@ -1751,6 +1751,10 @@ var AudioOutput = class {
|
|
|
1751
1751
|
if (this.ctx.state === "suspended") {
|
|
1752
1752
|
await this.ctx.resume();
|
|
1753
1753
|
}
|
|
1754
|
+
try {
|
|
1755
|
+
this.gain.connect(this.ctx.destination);
|
|
1756
|
+
} catch {
|
|
1757
|
+
}
|
|
1754
1758
|
if (this.state === "paused") {
|
|
1755
1759
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1756
1760
|
this.state = "playing";
|
|
@@ -1777,6 +1781,10 @@ var AudioOutput = class {
|
|
|
1777
1781
|
this.mediaTimeOfAnchor = this.now();
|
|
1778
1782
|
this.state = "paused";
|
|
1779
1783
|
if (this.noAudio) return;
|
|
1784
|
+
try {
|
|
1785
|
+
this.gain.disconnect();
|
|
1786
|
+
} catch {
|
|
1787
|
+
}
|
|
1780
1788
|
if (this.ctx.state === "running") {
|
|
1781
1789
|
await this.ctx.suspend();
|
|
1782
1790
|
}
|
|
@@ -2015,7 +2023,7 @@ async function startHybridDecoder(opts) {
|
|
|
2015
2023
|
const variant = pickLibavVariant(opts.context);
|
|
2016
2024
|
const libav = await chunkNNVOHKXJ_cjs.loadLibav(variant);
|
|
2017
2025
|
const bridge = await loadBridge();
|
|
2018
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
2026
|
+
const { prepareLibavInput } = await import('./libav-http-reader-Q356EO2K.cjs');
|
|
2019
2027
|
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
2020
2028
|
const readPkt = await libav.av_packet_alloc();
|
|
2021
2029
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
@@ -2685,7 +2693,7 @@ async function startDecoder(opts) {
|
|
|
2685
2693
|
const variant = "avbridge";
|
|
2686
2694
|
const libav = await chunkNNVOHKXJ_cjs.loadLibav(variant);
|
|
2687
2695
|
const bridge = await loadBridge2();
|
|
2688
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
2696
|
+
const { prepareLibavInput } = await import('./libav-http-reader-Q356EO2K.cjs');
|
|
2689
2697
|
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
2690
2698
|
const readPkt = await libav.av_packet_alloc();
|
|
2691
2699
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
@@ -3414,9 +3422,9 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3414
3422
|
constructor(options, registry) {
|
|
3415
3423
|
this.options = options;
|
|
3416
3424
|
this.registry = registry;
|
|
3417
|
-
const { requestInit, fetchFn } = options;
|
|
3418
|
-
if (requestInit || fetchFn) {
|
|
3419
|
-
this.transport = { requestInit, fetchFn };
|
|
3425
|
+
const { requestInit, fetchFn, cacheBytes } = options;
|
|
3426
|
+
if (requestInit || fetchFn || cacheBytes !== void 0) {
|
|
3427
|
+
this.transport = { requestInit, fetchFn, cacheBytes };
|
|
3420
3428
|
}
|
|
3421
3429
|
}
|
|
3422
3430
|
options;
|
|
@@ -3456,7 +3464,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3456
3464
|
switchingPromise = Promise.resolve();
|
|
3457
3465
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
3458
3466
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
3459
|
-
subtitleResources = new
|
|
3467
|
+
subtitleResources = new chunkWRKO6Q42_cjs.SubtitleResourceBag();
|
|
3460
3468
|
// Transport config extracted from CreatePlayerOptions. Threaded to probe,
|
|
3461
3469
|
// subtitle fetches, and strategy session creators. Not stored on MediaContext
|
|
3462
3470
|
// because it's runtime config, not media analysis.
|
|
@@ -3502,7 +3510,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3502
3510
|
}
|
|
3503
3511
|
}
|
|
3504
3512
|
if (this.options.directory && this.options.source instanceof File) {
|
|
3505
|
-
const found = await
|
|
3513
|
+
const found = await chunkWRKO6Q42_cjs.discoverSidecars(this.options.source, this.options.directory);
|
|
3506
3514
|
for (const s of found) {
|
|
3507
3515
|
this.subtitleResources.track(s.url);
|
|
3508
3516
|
ctx.subtitleTracks.push({
|
|
@@ -3525,7 +3533,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3525
3533
|
reason: decision.reason
|
|
3526
3534
|
});
|
|
3527
3535
|
await this.startSession(decision.strategy, decision.reason);
|
|
3528
|
-
await
|
|
3536
|
+
await chunkWRKO6Q42_cjs.attachSubtitleTracks(
|
|
3529
3537
|
this.options.target,
|
|
3530
3538
|
ctx.subtitleTracks,
|
|
3531
3539
|
this.subtitleResources,
|
|
@@ -4567,7 +4575,7 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4567
4575
|
* strategies pick up the new track via their textTracks watcher.
|
|
4568
4576
|
*/
|
|
4569
4577
|
async addSubtitle(subtitle) {
|
|
4570
|
-
const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-
|
|
4578
|
+
const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-HMVGWTU2.cjs');
|
|
4571
4579
|
const format = subtitle.format ?? (subtitle.url.endsWith(".srt") ? "srt" : "vtt");
|
|
4572
4580
|
const track = {
|
|
4573
4581
|
id: this._subtitleTracks.length,
|
|
@@ -4576,14 +4584,27 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4576
4584
|
sidecarUrl: subtitle.url
|
|
4577
4585
|
};
|
|
4578
4586
|
this._subtitleTracks.push(track);
|
|
4587
|
+
console.log(`[avbridge:subs] addSubtitle id=${track.id} format=${format} lang=${subtitle.language ?? "?"}`);
|
|
4579
4588
|
await attachSubtitleTracks2(
|
|
4580
4589
|
this._videoEl,
|
|
4581
4590
|
this._subtitleTracks,
|
|
4582
4591
|
void 0,
|
|
4583
4592
|
(err, t) => {
|
|
4584
|
-
console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
|
|
4593
|
+
console.warn(`[avbridge:subs] subtitle ${t.id} failed: ${err.message}`);
|
|
4585
4594
|
}
|
|
4586
4595
|
);
|
|
4596
|
+
const textTracks = this._videoEl.textTracks;
|
|
4597
|
+
for (let i = 0; i < textTracks.length; i++) {
|
|
4598
|
+
if (textTracks[i].label === (subtitle.language ?? `Subtitle ${track.id}`)) {
|
|
4599
|
+
textTracks[i].mode = "showing";
|
|
4600
|
+
console.log(`[avbridge:subs] enabled textTrack[${i}] mode=showing`);
|
|
4601
|
+
break;
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
this._dispatch("trackschange", {
|
|
4605
|
+
audioTracks: this._audioTracks,
|
|
4606
|
+
subtitleTracks: this.subtitleTracks
|
|
4607
|
+
});
|
|
4587
4608
|
}
|
|
4588
4609
|
/**
|
|
4589
4610
|
* Disable the automatic `screen.orientation.lock()` that runs on
|
|
@@ -5014,6 +5035,12 @@ var PLAYER_STYLES = (
|
|
|
5014
5035
|
display: flex;
|
|
5015
5036
|
align-items: center;
|
|
5016
5037
|
cursor: pointer;
|
|
5038
|
+
/* Claim all touch gestures on the seek bar. Without this, Android
|
|
5039
|
+
* browsers (Chrome, Samsung Internet) treat horizontal drags as
|
|
5040
|
+
* scroll candidates and cancel pointermove once the gesture
|
|
5041
|
+
* resolves, breaking scrub. touch-action must be set in CSS \u2014
|
|
5042
|
+
* preventDefault() on pointerdown is too late. */
|
|
5043
|
+
touch-action: none;
|
|
5017
5044
|
}
|
|
5018
5045
|
|
|
5019
5046
|
.avp-seek-track {
|
|
@@ -5031,7 +5058,13 @@ var PLAYER_STYLES = (
|
|
|
5031
5058
|
|
|
5032
5059
|
.avp-seek-buffered {
|
|
5033
5060
|
position: absolute;
|
|
5034
|
-
|
|
5061
|
+
inset: 0;
|
|
5062
|
+
pointer-events: none;
|
|
5063
|
+
}
|
|
5064
|
+
|
|
5065
|
+
.avp-seek-buffered-range {
|
|
5066
|
+
position: absolute;
|
|
5067
|
+
top: 0;
|
|
5035
5068
|
height: 100%;
|
|
5036
5069
|
background: rgba(255, 255, 255, 0.35);
|
|
5037
5070
|
border-radius: inherit;
|
|
@@ -5397,7 +5430,7 @@ function formatTime(sec) {
|
|
|
5397
5430
|
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
|
|
5398
5431
|
}
|
|
5399
5432
|
var PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
|
5400
|
-
var
|
|
5433
|
+
var DEFAULT_CONTROLS_HIDE_MS = 3e3;
|
|
5401
5434
|
var FORWARDED_EVENTS = [
|
|
5402
5435
|
"ready",
|
|
5403
5436
|
"error",
|
|
@@ -5440,7 +5473,7 @@ var PROXY_ATTRIBUTES = [
|
|
|
5440
5473
|
];
|
|
5441
5474
|
var PLAYER_ATTRIBUTES = ["show-fit"];
|
|
5442
5475
|
var FIT_MODES = ["contain", "cover", "fill"];
|
|
5443
|
-
var AvbridgePlayerElement = class extends HTMLElement {
|
|
5476
|
+
var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
5444
5477
|
static observedAttributes = [...PROXY_ATTRIBUTES, ...PLAYER_ATTRIBUTES];
|
|
5445
5478
|
// ── Internal DOM refs ──────────────────────────────────────────────────
|
|
5446
5479
|
_video;
|
|
@@ -5468,6 +5501,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5468
5501
|
_state = "idle";
|
|
5469
5502
|
_controlsTimer = null;
|
|
5470
5503
|
_settingsOpen = false;
|
|
5504
|
+
_activeAudioTrackId = null;
|
|
5505
|
+
_activeSubtitleTrackId = null;
|
|
5471
5506
|
_userSeeking = false;
|
|
5472
5507
|
_holdTimer = null;
|
|
5473
5508
|
_holdSpeedActive = false;
|
|
@@ -5589,6 +5624,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5589
5624
|
on(this._video, "ended", () => this._setState("ended"));
|
|
5590
5625
|
on(this._video, "error", () => this._setState("error"));
|
|
5591
5626
|
on(this._video, "timeupdate", () => this._updateTime());
|
|
5627
|
+
on(this._video, "progress", () => this._updateBuffered());
|
|
5592
5628
|
on(this._video, "volumechange", () => this._updateVolume());
|
|
5593
5629
|
on(this._video, "trackschange", () => this._buildSettingsMenu());
|
|
5594
5630
|
on(this._video, "durationchange", () => {
|
|
@@ -5731,6 +5767,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5731
5767
|
const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
5732
5768
|
return frac * (this._video.duration || 0);
|
|
5733
5769
|
}
|
|
5770
|
+
/** Seekbar width below which drag-to-scrub seeks in real-time (vs
|
|
5771
|
+
* preview-only). On narrow bars precise positioning is hard, so
|
|
5772
|
+
* immediate video feedback is more useful than a time tooltip. */
|
|
5773
|
+
static SCRUB_WIDTH_THRESHOLD = 400;
|
|
5734
5774
|
_onSeekPointerDown(e) {
|
|
5735
5775
|
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
5736
5776
|
e.preventDefault();
|
|
@@ -5738,15 +5778,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5738
5778
|
const seekBar = this.shadowRoot.querySelector(".avp-seek");
|
|
5739
5779
|
seekBar.setPointerCapture(e.pointerId);
|
|
5740
5780
|
seekBar.setAttribute("data-seeking", "");
|
|
5781
|
+
const scrubMode = seekBar.getBoundingClientRect().width < _AvbridgePlayerElement.SCRUB_WIDTH_THRESHOLD;
|
|
5782
|
+
let lastScrubCommit = 0;
|
|
5741
5783
|
const initial = this._timeFromSeekPointer(e.clientX);
|
|
5742
5784
|
this._seekInput.value = String(initial);
|
|
5743
5785
|
this._onSeekInput();
|
|
5744
5786
|
this._updateSeekTooltip(e.clientX);
|
|
5787
|
+
if (scrubMode) this._onSeekCommit();
|
|
5745
5788
|
const onMove = (ev) => {
|
|
5746
5789
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5747
5790
|
this._seekInput.value = String(t);
|
|
5748
5791
|
this._onSeekInput();
|
|
5749
5792
|
this._updateSeekTooltip(ev.clientX);
|
|
5793
|
+
if (scrubMode) {
|
|
5794
|
+
const now = performance.now();
|
|
5795
|
+
if (now - lastScrubCommit > 250) {
|
|
5796
|
+
lastScrubCommit = now;
|
|
5797
|
+
this._onSeekCommit();
|
|
5798
|
+
this._userSeeking = true;
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5750
5801
|
};
|
|
5751
5802
|
const onUp = (ev) => {
|
|
5752
5803
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
@@ -5790,13 +5841,45 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5790
5841
|
this._seekInput.value = String(t);
|
|
5791
5842
|
this._updateSeekVisuals(t);
|
|
5792
5843
|
this._timeDisplay.textContent = `${formatTime(t)} / ${formatTime(d)}`;
|
|
5844
|
+
this._updateBuffered();
|
|
5845
|
+
}
|
|
5846
|
+
/**
|
|
5847
|
+
* Render every buffered range as its own segment so gaps (common on MSE
|
|
5848
|
+
* after seeks) are visible. Not gated by `_userSeeking` — ranges should
|
|
5849
|
+
* keep updating while the user scrubs, and runs cheaply on `progress`.
|
|
5850
|
+
*/
|
|
5851
|
+
_updateBuffered() {
|
|
5852
|
+
const d = this._video.duration;
|
|
5853
|
+
if (!(d > 0)) return;
|
|
5854
|
+
let buf;
|
|
5793
5855
|
try {
|
|
5794
|
-
|
|
5795
|
-
if (buf && buf.length > 0 && d > 0) {
|
|
5796
|
-
const end = buf.end(buf.length - 1);
|
|
5797
|
-
this._seekBuffered.style.width = `${end / d * 100}%`;
|
|
5798
|
-
}
|
|
5856
|
+
buf = this._video.buffered;
|
|
5799
5857
|
} catch {
|
|
5858
|
+
return;
|
|
5859
|
+
}
|
|
5860
|
+
const count = buf ? buf.length : 0;
|
|
5861
|
+
const host = this._seekBuffered;
|
|
5862
|
+
while (host.childElementCount > count) host.lastElementChild.remove();
|
|
5863
|
+
while (host.childElementCount < count) {
|
|
5864
|
+
const seg = document.createElement("div");
|
|
5865
|
+
seg.className = "avp-seek-buffered-range";
|
|
5866
|
+
host.appendChild(seg);
|
|
5867
|
+
}
|
|
5868
|
+
for (let i = 0; i < count; i++) {
|
|
5869
|
+
let start;
|
|
5870
|
+
let end;
|
|
5871
|
+
try {
|
|
5872
|
+
start = buf.start(i);
|
|
5873
|
+
end = buf.end(i);
|
|
5874
|
+
} catch {
|
|
5875
|
+
continue;
|
|
5876
|
+
}
|
|
5877
|
+
const s = Math.max(0, start);
|
|
5878
|
+
const e = Math.min(d, end);
|
|
5879
|
+
if (e <= s) continue;
|
|
5880
|
+
const seg = host.children[i];
|
|
5881
|
+
seg.style.left = `${s / d * 100}%`;
|
|
5882
|
+
seg.style.width = `${(e - s) / d * 100}%`;
|
|
5800
5883
|
}
|
|
5801
5884
|
}
|
|
5802
5885
|
// ── Controls: volume ───────────────────────────────────────────────────
|
|
@@ -5847,19 +5930,27 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5847
5930
|
sections.push(selectRow("Speed", speedValue, speedOpts, `data-action="speed"`));
|
|
5848
5931
|
const audios = this._video.audioTracks ?? [];
|
|
5849
5932
|
if (audios.length > 1) {
|
|
5933
|
+
const activeAudioId = this._activeAudioTrackId ?? audios[0]?.id;
|
|
5934
|
+
const activeAudio = audios.find((t) => t.id === activeAudioId) ?? audios[0];
|
|
5935
|
+
const audioValue = activeAudio?.language ?? `Track ${activeAudio?.id ?? 1}`;
|
|
5850
5936
|
let audioOpts = "";
|
|
5851
5937
|
for (const t of audios) {
|
|
5852
|
-
|
|
5938
|
+
const sel = t.id === activeAudioId ? " selected" : "";
|
|
5939
|
+
audioOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
|
|
5853
5940
|
}
|
|
5854
|
-
sections.push(selectRow("Audio",
|
|
5941
|
+
sections.push(selectRow("Audio", audioValue, audioOpts, `data-action="audio"`));
|
|
5855
5942
|
}
|
|
5856
5943
|
const subs = this._video.subtitleTracks ?? [];
|
|
5857
5944
|
if (subs.length > 0) {
|
|
5858
|
-
|
|
5945
|
+
const activeSubId = this._activeSubtitleTrackId;
|
|
5946
|
+
const activeSub = activeSubId != null ? subs.find((t) => t.id === activeSubId) : null;
|
|
5947
|
+
const subValue = activeSub ? activeSub.language ?? `Track ${activeSub.id}` : "Off";
|
|
5948
|
+
let subOpts = `<option value="-1"${activeSubId == null ? " selected" : ""}>Off</option>`;
|
|
5859
5949
|
for (const t of subs) {
|
|
5860
|
-
|
|
5950
|
+
const sel = t.id === activeSubId ? " selected" : "";
|
|
5951
|
+
subOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
|
|
5861
5952
|
}
|
|
5862
|
-
sections.push(selectRow("Subtitles",
|
|
5953
|
+
sections.push(selectRow("Subtitles", subValue, subOpts, `data-action="subtitle"`));
|
|
5863
5954
|
}
|
|
5864
5955
|
if (this.hasAttribute("show-fit")) {
|
|
5865
5956
|
const currentFit = this._video.fit ?? "contain";
|
|
@@ -5899,11 +5990,15 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5899
5990
|
this._video.playbackRate = Number(val);
|
|
5900
5991
|
break;
|
|
5901
5992
|
case "audio":
|
|
5993
|
+
this._activeAudioTrackId = Number(val);
|
|
5902
5994
|
void this._video.setAudioTrack(Number(val));
|
|
5903
5995
|
break;
|
|
5904
|
-
case "subtitle":
|
|
5905
|
-
|
|
5996
|
+
case "subtitle": {
|
|
5997
|
+
const subId = Number(val);
|
|
5998
|
+
this._activeSubtitleTrackId = subId >= 0 ? subId : null;
|
|
5999
|
+
void this._video.setSubtitleTrack(subId >= 0 ? subId : null);
|
|
5906
6000
|
break;
|
|
6001
|
+
}
|
|
5907
6002
|
case "fit":
|
|
5908
6003
|
this.setAttribute("fit", val);
|
|
5909
6004
|
break;
|
|
@@ -5998,16 +6093,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5998
6093
|
_showControls() {
|
|
5999
6094
|
this.showControls();
|
|
6000
6095
|
}
|
|
6001
|
-
_scheduleHide(durationMs
|
|
6096
|
+
_scheduleHide(durationMs) {
|
|
6097
|
+
const ms = durationMs ?? this._getControlsTimeout();
|
|
6002
6098
|
if (this._controlsTimer) clearTimeout(this._controlsTimer);
|
|
6003
6099
|
if (this._state !== "playing" && this._state !== "buffering") return;
|
|
6004
6100
|
if (this._settingsOpen) return;
|
|
6101
|
+
if (ms <= 0) return;
|
|
6005
6102
|
this._controlsTimer = setTimeout(() => {
|
|
6006
6103
|
if (this._state === "playing") {
|
|
6007
6104
|
this.setAttribute("data-controls-hidden", "");
|
|
6008
6105
|
this._toolbarTop.setAttribute("data-visible", "false");
|
|
6009
6106
|
}
|
|
6010
|
-
},
|
|
6107
|
+
}, ms);
|
|
6108
|
+
}
|
|
6109
|
+
/** Read the controls-timeout attribute. 0 or negative = never hide.
|
|
6110
|
+
* Unset = default 3000ms. */
|
|
6111
|
+
_getControlsTimeout() {
|
|
6112
|
+
const attr = this.getAttribute("controls-timeout");
|
|
6113
|
+
if (attr == null) return DEFAULT_CONTROLS_HIDE_MS;
|
|
6114
|
+
const n = Number(attr);
|
|
6115
|
+
return Number.isFinite(n) ? n : DEFAULT_CONTROLS_HIDE_MS;
|
|
6011
6116
|
}
|
|
6012
6117
|
// Strategy is visible in Stats for Nerds, no badge in controls bar.
|
|
6013
6118
|
// ── Click / tap handling (YouTube delayed-tap pattern) ──────────────────
|
|
@@ -6019,6 +6124,9 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6019
6124
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
6020
6125
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
6021
6126
|
_lastPointerTypeWasTouch = false;
|
|
6127
|
+
/** True for ~50ms after a touch double-tap was handled, so the
|
|
6128
|
+
* synthetic dblclick from the browser doesn't also fire fullscreen. */
|
|
6129
|
+
_touchDoubleTapConsumed = false;
|
|
6022
6130
|
/** True if the event's composed path passes through consumer-slotted
|
|
6023
6131
|
* content (toolbar or content-overlay). Slotted content lives in the
|
|
6024
6132
|
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
@@ -6052,6 +6160,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6052
6160
|
_onContainerDblClick(e) {
|
|
6053
6161
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
6054
6162
|
if (this._isSlottedContentEvent(e)) return;
|
|
6163
|
+
if (this._touchDoubleTapConsumed) return;
|
|
6055
6164
|
if (this._tapTimer) {
|
|
6056
6165
|
clearTimeout(this._tapTimer);
|
|
6057
6166
|
this._tapTimer = null;
|
|
@@ -6093,6 +6202,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6093
6202
|
} else {
|
|
6094
6203
|
this._toggleFullscreen();
|
|
6095
6204
|
}
|
|
6205
|
+
this._touchDoubleTapConsumed = true;
|
|
6206
|
+
setTimeout(() => {
|
|
6207
|
+
this._touchDoubleTapConsumed = false;
|
|
6208
|
+
}, 100);
|
|
6096
6209
|
this._lastTapTime = 0;
|
|
6097
6210
|
return;
|
|
6098
6211
|
}
|
|
@@ -6126,6 +6239,13 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6126
6239
|
this._video.currentTime = Math.max(0, this._video.currentTime + delta);
|
|
6127
6240
|
}
|
|
6128
6241
|
// ── Keyboard shortcuts ─────────────────────────────────────────────────
|
|
6242
|
+
/** Duration of one frame in seconds, derived from diagnostics fps or
|
|
6243
|
+
* a 30fps default. Used for frame-step shortcuts (`,` / `.`). */
|
|
6244
|
+
_frameDuration() {
|
|
6245
|
+
const diag = this._video.getDiagnostics();
|
|
6246
|
+
const fps = diag?.fps && diag.fps > 0 ? diag.fps : 30;
|
|
6247
|
+
return 1 / fps;
|
|
6248
|
+
}
|
|
6129
6249
|
_onKeydown(e) {
|
|
6130
6250
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
6131
6251
|
switch (e.key) {
|
|
@@ -6170,6 +6290,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6170
6290
|
this._video.playbackRate = Math.max(0.25, this._video.playbackRate - 0.25);
|
|
6171
6291
|
this._buildSettingsMenu();
|
|
6172
6292
|
break;
|
|
6293
|
+
case ",":
|
|
6294
|
+
e.preventDefault();
|
|
6295
|
+
if (!this._video.paused) this._video.pause();
|
|
6296
|
+
this._video.currentTime = Math.max(0, this._video.currentTime - this._frameDuration());
|
|
6297
|
+
break;
|
|
6298
|
+
case ".":
|
|
6299
|
+
e.preventDefault();
|
|
6300
|
+
if (!this._video.paused) this._video.pause();
|
|
6301
|
+
this._video.currentTime = Math.min(
|
|
6302
|
+
this._video.duration || 0,
|
|
6303
|
+
this._video.currentTime + this._frameDuration()
|
|
6304
|
+
);
|
|
6305
|
+
break;
|
|
6173
6306
|
case "Escape":
|
|
6174
6307
|
if (this._settingsOpen) {
|
|
6175
6308
|
e.preventDefault();
|