@decentnetwork/lan 0.1.183 → 0.1.184
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.
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -41,6 +41,13 @@ export interface HlsPrefetcherOptions {
|
|
|
41
41
|
concurrency?: number;
|
|
42
42
|
/** Skip caching bodies larger than this (default 32 MB). */
|
|
43
43
|
maxEntryBytes?: number;
|
|
44
|
+
/** Split each segment into this many parallel byte-range stripes across the
|
|
45
|
+
* exit pool (default 6). 1 disables striping. Lets ONE segment ride the SUM
|
|
46
|
+
* of the exits' bandwidth instead of a single throttled connection. */
|
|
47
|
+
stripes?: number;
|
|
48
|
+
/** Stripe chunk size — also the first probe chunk (default 192 KB). Segments
|
|
49
|
+
* smaller than this aren't striped. */
|
|
50
|
+
stripeChunkBytes?: number;
|
|
44
51
|
}
|
|
45
52
|
export declare class HlsPrefetcher {
|
|
46
53
|
#private;
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
// master playlist (#EXT-X-STREAM-INF) lists variants, not segments.
|
|
18
18
|
//
|
|
19
19
|
const SEGMENT_EXTS = [".ts", ".m4s", ".m4a", ".m4v", ".aac", ".mp3", ".mp4", ".vtt"];
|
|
20
|
+
/** Parse the total size out of a `Content-Range: bytes 0-N/TOTAL` header. */
|
|
21
|
+
function contentRangeTotal(cr) {
|
|
22
|
+
const s = Array.isArray(cr) ? cr[0] : cr;
|
|
23
|
+
const m = s ? /\/(\d+)\s*$/.exec(s) : null;
|
|
24
|
+
return m ? Number(m[1]) : 0;
|
|
25
|
+
}
|
|
20
26
|
export function looksLikePlaylist(pathname) {
|
|
21
27
|
const p = pathname.toLowerCase();
|
|
22
28
|
return p.endsWith(".m3u8") || p.endsWith(".m3u");
|
|
@@ -91,6 +97,8 @@ export class HlsPrefetcher {
|
|
|
91
97
|
#ahead;
|
|
92
98
|
#concurrency;
|
|
93
99
|
#maxEntryBytes;
|
|
100
|
+
#stripes;
|
|
101
|
+
#stripeChunkBytes;
|
|
94
102
|
#hits = 0;
|
|
95
103
|
#misses = 0;
|
|
96
104
|
constructor(fetcher, opts = {}) {
|
|
@@ -100,6 +108,54 @@ export class HlsPrefetcher {
|
|
|
100
108
|
this.#ahead = opts.prefetchAhead ?? 4;
|
|
101
109
|
this.#concurrency = opts.concurrency ?? 4;
|
|
102
110
|
this.#maxEntryBytes = opts.maxEntryBytes ?? 32 * 1024 * 1024;
|
|
111
|
+
this.#stripes = Math.max(1, opts.stripes ?? 6);
|
|
112
|
+
this.#stripeChunkBytes = opts.stripeChunkBytes ?? 192 * 1024;
|
|
113
|
+
}
|
|
114
|
+
/** Fetch `u` as PARALLEL byte-range stripes. Each range is a separate
|
|
115
|
+
* `#fetcher` call, which the router load-balances onto a DIFFERENT exit — so
|
|
116
|
+
* one segment downloads at the SUM of the pool's bandwidth, beating the GFW
|
|
117
|
+
* per-connection throttle (the reason a single stream stalls even with many
|
|
118
|
+
* exits). Falls back to one whole-body fetch if the origin ignores Range or
|
|
119
|
+
* any stripe fails (correctness over speed). */
|
|
120
|
+
async #stripeFetch(u) {
|
|
121
|
+
const whole = () => this.#fetcher(u, { host: u.host }, "prefetch");
|
|
122
|
+
if (this.#stripes <= 1)
|
|
123
|
+
return whole();
|
|
124
|
+
const guess = this.#stripeChunkBytes;
|
|
125
|
+
let first;
|
|
126
|
+
try {
|
|
127
|
+
first = await this.#fetcher(u, { host: u.host, range: `bytes=0-${guess - 1}` }, "stripe");
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return whole();
|
|
131
|
+
}
|
|
132
|
+
if (first.status === 200)
|
|
133
|
+
return first; // origin ignored Range → whole body already
|
|
134
|
+
if (first.status !== 206)
|
|
135
|
+
return whole();
|
|
136
|
+
const total = contentRangeTotal(first.headers["content-range"]);
|
|
137
|
+
const outHeaders = { ...first.headers };
|
|
138
|
+
delete outHeaders["content-range"];
|
|
139
|
+
if (!total || total <= first.body.length)
|
|
140
|
+
return { status: 200, headers: outHeaders, body: first.body };
|
|
141
|
+
const remaining = total - first.body.length;
|
|
142
|
+
const n = Math.max(1, Math.min(this.#stripes - 1, Math.ceil(remaining / guess)));
|
|
143
|
+
const chunk = Math.ceil(remaining / n);
|
|
144
|
+
const parts = await Promise.all(Array.from({ length: n }, (_, i) => {
|
|
145
|
+
const start = first.body.length + i * chunk;
|
|
146
|
+
const end = Math.min(start + chunk - 1, total - 1);
|
|
147
|
+
if (start > end)
|
|
148
|
+
return Promise.resolve(Buffer.alloc(0));
|
|
149
|
+
return this.#fetcher(u, { host: u.host, range: `bytes=${start}-${end}` }, "stripe")
|
|
150
|
+
.then((r) => ((r.status === 206 || r.status === 200) && r.body.length === end - start + 1 ? r.body : null))
|
|
151
|
+
.catch(() => null);
|
|
152
|
+
}));
|
|
153
|
+
if (parts.some((p) => p === null))
|
|
154
|
+
return whole(); // a stripe failed → refetch whole for integrity
|
|
155
|
+
const body = Buffer.concat([first.body, ...parts]);
|
|
156
|
+
if (body.length !== total)
|
|
157
|
+
return whole();
|
|
158
|
+
return { status: 200, headers: outHeaders, body };
|
|
103
159
|
}
|
|
104
160
|
get stats() {
|
|
105
161
|
return { hits: this.#hits, misses: this.#misses, entries: this.#cache.size, bytes: this.#cacheBytes };
|
|
@@ -158,7 +214,7 @@ export class HlsPrefetcher {
|
|
|
158
214
|
const p = (async () => {
|
|
159
215
|
try {
|
|
160
216
|
const u = new URL(url);
|
|
161
|
-
const res = await this.#
|
|
217
|
+
const res = await this.#stripeFetch(u);
|
|
162
218
|
if (res.status !== 200 || res.body.length === 0 || res.body.length > this.#maxEntryBytes)
|
|
163
219
|
return null;
|
|
164
220
|
const ct = res.headers["content-type"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.184",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|