avbridge 2.11.0 → 2.12.1
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 +111 -0
- package/dist/{avi-B5CQYB7L.cjs → avi-32UABODO.cjs} +14 -6
- package/dist/avi-32UABODO.cjs.map +1 -0
- package/dist/{avi-2ILLBNPQ.cjs → avi-5BPR6QUX.cjs} +14 -6
- package/dist/avi-5BPR6QUX.cjs.map +1 -0
- package/dist/{avi-RWWPN2PR.js → avi-BLIH7KKV.js} +13 -5
- package/dist/avi-BLIH7KKV.js.map +1 -0
- package/dist/{avi-JXU4GQL2.js → avi-GX2H34IQ.js} +13 -5
- package/dist/avi-GX2H34IQ.js.map +1 -0
- 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-GYIJU44C.js → chunk-5CX7BVVV.js} +5 -5
- package/dist/{chunk-GYIJU44C.js.map → chunk-5CX7BVVV.js.map} +1 -1
- package/dist/{chunk-CL6UEUQF.js → chunk-B76QWPFM.js} +5 -5
- package/dist/{chunk-CL6UEUQF.js.map → chunk-B76QWPFM.js.map} +1 -1
- package/dist/{chunk-IHNHHEA2.js → chunk-BN7BRTLY.js} +143 -32
- package/dist/chunk-BN7BRTLY.js.map +1 -0
- package/dist/{chunk-OTFS7DC4.cjs → chunk-E5MAM2P4.cjs} +14 -14
- package/dist/{chunk-OTFS7DC4.cjs.map → chunk-E5MAM2P4.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-37UOSAVI.cjs → chunk-UM6WCSGL.cjs} +157 -46
- package/dist/chunk-UM6WCSGL.cjs.map +1 -0
- package/dist/{chunk-BYGZN4Z5.cjs → chunk-VLI3Y6IJ.cjs} +5 -5
- package/dist/{chunk-BYGZN4Z5.cjs.map → chunk-VLI3Y6IJ.cjs.map} +1 -1
- package/dist/{chunk-Z33SBWL5.cjs → chunk-YPZFGJV3.cjs} +40 -16
- package/dist/chunk-YPZFGJV3.cjs.map +1 -0
- package/dist/element-browser.js +186 -43
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +5 -5
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +4 -4
- package/dist/index.cjs +21 -21
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -9
- 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 +264 -53
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +22 -0
- package/dist/player.d.ts +22 -0
- package/dist/player.js +264 -53
- package/dist/player.js.map +1 -1
- package/dist/remux-NSBJFMLG.cjs +35 -0
- package/dist/{remux-KUS5GIL6.cjs.map → remux-NSBJFMLG.cjs.map} +1 -1
- package/dist/remux-PHUHO3VV.js +10 -0
- package/dist/{remux-56V7LDAD.js.map → remux-PHUHO3VV.js.map} +1 -1
- package/package.json +1 -1
- package/src/element/avbridge-player.ts +123 -23
- package/src/element/player-styles.ts +13 -1
- package/src/player.ts +3 -3
- package/src/probe/avi.ts +34 -2
- package/src/strategies/fallback/decoder.ts +148 -19
- package/src/strategies/fallback/video-renderer.ts +41 -3
- package/src/strategies/hybrid/decoder.ts +34 -9
- package/src/types.ts +15 -0
- package/src/util/libav-http-reader.ts +58 -19
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
- package/vendor/libav/avbridge/libav-avbridge.mjs +1 -1
- package/dist/avi-2ILLBNPQ.cjs.map +0 -1
- package/dist/avi-B5CQYB7L.cjs.map +0 -1
- package/dist/avi-JXU4GQL2.js.map +0 -1
- package/dist/avi-RWWPN2PR.js.map +0 -1
- package/dist/chunk-37UOSAVI.cjs.map +0 -1
- package/dist/chunk-DCSOQH2N.js.map +0 -1
- package/dist/chunk-IHNHHEA2.js.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
|
@@ -141,11 +141,22 @@ export class VideoRenderer {
|
|
|
141
141
|
this.rafHandle = requestAnimationFrame(this.tick);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* True once at least one frame has been enqueued *since the last flush*.
|
|
146
|
+
* Used by `readyState` — initial cold-start reports HAVE_NOTHING until
|
|
147
|
+
* any frame has arrived, and after a seek we want the same semantics
|
|
148
|
+
* (HAVE_NOTHING until post-seek frames arrive), so the cumulative
|
|
149
|
+
* `framesPainted > 0` that used to live here was wrong: it kept the
|
|
150
|
+
* state "true forever" after the first frame ever, so post-seek
|
|
151
|
+
* `waitForBuffer()` would exit immediately with an empty queue and
|
|
152
|
+
* leave video frozen while audio kept going.
|
|
153
|
+
*/
|
|
145
154
|
hasFrames(): boolean {
|
|
146
|
-
return this.queue.length > 0 || this.
|
|
155
|
+
return this.queue.length > 0 || this.hasEverEnqueuedSinceFlush;
|
|
147
156
|
}
|
|
148
157
|
|
|
158
|
+
private hasEverEnqueuedSinceFlush = false;
|
|
159
|
+
|
|
149
160
|
/** Current depth of the frame queue. Used by the decoder for backpressure. */
|
|
150
161
|
queueDepth(): number {
|
|
151
162
|
return this.queue.length;
|
|
@@ -166,6 +177,7 @@ export class VideoRenderer {
|
|
|
166
177
|
return;
|
|
167
178
|
}
|
|
168
179
|
this.queue.push(frame);
|
|
180
|
+
this.hasEverEnqueuedSinceFlush = true;
|
|
169
181
|
if (this.queue.length === 1 && this.framesPainted === 0) {
|
|
170
182
|
this.resolveFirstFrame();
|
|
171
183
|
}
|
|
@@ -342,7 +354,16 @@ export class VideoRenderer {
|
|
|
342
354
|
}
|
|
343
355
|
|
|
344
356
|
// Only drop frames that are more than 2 frame-durations behind.
|
|
345
|
-
|
|
357
|
+
// Diagnostic escape hatch: `globalThis.AVBRIDGE_RELAX_DROP = true`
|
|
358
|
+
// pushes the threshold so far back that frames are effectively
|
|
359
|
+
// never dropped as late. The display will run behind the audio
|
|
360
|
+
// clock but won't stutter from drop bursts. Useful for isolating
|
|
361
|
+
// "is the problem decode throughput or drop policy?".
|
|
362
|
+
const _relaxDrop =
|
|
363
|
+
(globalThis as { AVBRIDGE_RELAX_DROP?: boolean }).AVBRIDGE_RELAX_DROP === true;
|
|
364
|
+
const dropThresholdUs = _relaxDrop
|
|
365
|
+
? audioNowUs - 60 * 1_000_000 /* 60 s */
|
|
366
|
+
: audioNowUs - frameDurationUs * 2;
|
|
346
367
|
let dropped = 0;
|
|
347
368
|
while (bestIdx > 0) {
|
|
348
369
|
const ts = this.queue[0].timestamp ?? 0;
|
|
@@ -419,6 +440,7 @@ export class VideoRenderer {
|
|
|
419
440
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
420
441
|
this.prerolled = false;
|
|
421
442
|
this.ptsCalibrated = false; // recalibrate at new seek position
|
|
443
|
+
this.hasEverEnqueuedSinceFlush = false; // so waitForBuffer() waits for post-flush frames
|
|
422
444
|
if (isDebug() && count > 0) {
|
|
423
445
|
// eslint-disable-next-line no-console
|
|
424
446
|
console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
|
|
@@ -426,11 +448,27 @@ export class VideoRenderer {
|
|
|
426
448
|
}
|
|
427
449
|
|
|
428
450
|
stats(): Record<string, unknown> {
|
|
451
|
+
// Queue span — the gap between the oldest and newest queued frame's
|
|
452
|
+
// PTS, in ms. If this collapses while audio keeps advancing, the
|
|
453
|
+
// producer has stalled. If it stays wide with stale head, the
|
|
454
|
+
// producer is bursting faster than realtime but the renderer can't
|
|
455
|
+
// catch up.
|
|
456
|
+
let queueSpanMs = 0;
|
|
457
|
+
let queueHeadMs = 0;
|
|
458
|
+
let queueTailMs = 0;
|
|
459
|
+
if (this.queue.length > 0) {
|
|
460
|
+
queueHeadMs = Math.round((this.queue[0].timestamp ?? 0) / 1000);
|
|
461
|
+
queueTailMs = Math.round((this.queue[this.queue.length - 1].timestamp ?? 0) / 1000);
|
|
462
|
+
queueSpanMs = Math.max(0, queueTailMs - queueHeadMs);
|
|
463
|
+
}
|
|
429
464
|
return {
|
|
430
465
|
framesPainted: this.framesPainted,
|
|
431
466
|
framesDroppedLate: this.framesDroppedLate,
|
|
432
467
|
framesDroppedOverflow: this.framesDroppedOverflow,
|
|
433
468
|
queueDepth: this.queue.length,
|
|
469
|
+
queueHeadMs,
|
|
470
|
+
queueTailMs,
|
|
471
|
+
queueSpanMs,
|
|
434
472
|
};
|
|
435
473
|
}
|
|
436
474
|
|
|
@@ -165,6 +165,7 @@ export async function startHybridDecoder(opts: StartHybridDecoderOptions): Promi
|
|
|
165
165
|
// ── Bitstream filter for MPEG-4 Part 2 packed B-frames ───────────────
|
|
166
166
|
let bsfCtx: number | null = null;
|
|
167
167
|
let bsfPkt: number | null = null;
|
|
168
|
+
let bsfRequiredButMissing = false;
|
|
168
169
|
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
169
170
|
try {
|
|
170
171
|
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
@@ -175,15 +176,23 @@ export async function startHybridDecoder(opts: StartHybridDecoderOptions): Promi
|
|
|
175
176
|
bsfPkt = await libav.av_packet_alloc();
|
|
176
177
|
dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
|
|
177
178
|
} else {
|
|
178
|
-
|
|
179
|
-
console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
|
|
179
|
+
bsfRequiredButMissing = true;
|
|
180
180
|
bsfCtx = null;
|
|
181
181
|
}
|
|
182
182
|
} catch (err) {
|
|
183
|
-
|
|
184
|
-
console.warn("[avbridge] hybrid: failed to init BSF:", (err as Error).message);
|
|
183
|
+
bsfRequiredButMissing = true;
|
|
185
184
|
bsfCtx = null;
|
|
186
185
|
bsfPkt = null;
|
|
186
|
+
dbg.warn("bsf", `hybrid: mpeg4_unpack_bframes BSF init failed: ${(err as Error).message}`);
|
|
187
|
+
}
|
|
188
|
+
if (bsfRequiredButMissing) {
|
|
189
|
+
// eslint-disable-next-line no-console
|
|
190
|
+
console.error(
|
|
191
|
+
"[avbridge] MPEG-4 Part 2 (DivX/Xvid) detected but mpeg4_unpack_bframes " +
|
|
192
|
+
"BSF is unavailable in this libav variant. Files with packed B-frames " +
|
|
193
|
+
"will play with incorrect frame ordering. Rebuild the libav variant " +
|
|
194
|
+
"with the `avbsf` fragment included.",
|
|
195
|
+
);
|
|
187
196
|
}
|
|
188
197
|
}
|
|
189
198
|
|
|
@@ -193,7 +202,13 @@ export async function startHybridDecoder(opts: StartHybridDecoderOptions): Promi
|
|
|
193
202
|
for (const pkt of packets) {
|
|
194
203
|
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
195
204
|
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
196
|
-
if (sendErr < 0) {
|
|
205
|
+
if (sendErr < 0) {
|
|
206
|
+
// BSF rejected — DON'T pass the original through. Its buffer may
|
|
207
|
+
// have been transferred into the worker by ff_copyin_packet, so
|
|
208
|
+
// re-posting it would throw DataCloneError on a detached
|
|
209
|
+
// ArrayBuffer. See fallback/decoder.ts for the full explanation.
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
197
212
|
while (true) {
|
|
198
213
|
const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
199
214
|
if (recvErr < 0) break;
|
|
@@ -206,10 +221,18 @@ export async function startHybridDecoder(opts: StartHybridDecoderOptions): Promi
|
|
|
206
221
|
async function flushBSF(): Promise<void> {
|
|
207
222
|
if (!bsfCtx || !bsfPkt) return;
|
|
208
223
|
try {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
224
|
+
// Use av_bsf_flush to reset the BSF without putting it in EOF mode.
|
|
225
|
+
// See the matching comment in src/strategies/fallback/decoder.ts —
|
|
226
|
+
// sending NULL as the flush signal puts the BSF into EOF state so
|
|
227
|
+
// subsequent sends fail, which corrupts the post-seek pipeline with
|
|
228
|
+
// detached-buffer DataCloneErrors.
|
|
229
|
+
if (libav.av_bsf_flush) {
|
|
230
|
+
await libav.av_bsf_flush(bsfCtx);
|
|
231
|
+
} else {
|
|
232
|
+
while (true) {
|
|
233
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
234
|
+
if (err < 0) break;
|
|
235
|
+
}
|
|
213
236
|
}
|
|
214
237
|
} catch { /* ignore */ }
|
|
215
238
|
}
|
|
@@ -562,6 +585,7 @@ export async function startHybridDecoder(opts: StartHybridDecoderOptions): Promi
|
|
|
562
585
|
videoChunksFed,
|
|
563
586
|
audioFramesDecoded,
|
|
564
587
|
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
588
|
+
bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
|
|
565
589
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
566
590
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
567
591
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -687,6 +711,7 @@ interface LibavRuntime {
|
|
|
687
711
|
av_bsf_init(ctx: number): Promise<number>;
|
|
688
712
|
av_bsf_send_packet(ctx: number, pkt: number): Promise<number>;
|
|
689
713
|
av_bsf_receive_packet(ctx: number, pkt: number): Promise<number>;
|
|
714
|
+
av_bsf_flush?(ctx: number): Promise<void>;
|
|
690
715
|
av_bsf_free(ctx: number): Promise<void>;
|
|
691
716
|
ff_copyin_packet(pktPtr: number, packet: LibavPacket): Promise<void>;
|
|
692
717
|
ff_copyout_packet(pkt: number): Promise<LibavPacket>;
|
package/src/types.ts
CHANGED
|
@@ -287,6 +287,14 @@ export interface CreatePlayerOptions {
|
|
|
287
287
|
* for interceptors, logging, or environments without a global fetch.
|
|
288
288
|
*/
|
|
289
289
|
fetchFn?: FetchFn;
|
|
290
|
+
/**
|
|
291
|
+
* Byte budget for the libav HTTP reader's LRU cache of fetched ranges.
|
|
292
|
+
* Defaults to 8 MB. Set to `0` to disable caching. Raise this when the
|
|
293
|
+
* app plays seek-heavy legacy-container media from URLs — hot regions
|
|
294
|
+
* (header/moov, tail index, current window) stay resident instead of
|
|
295
|
+
* being re-fetched on every bounce.
|
|
296
|
+
*/
|
|
297
|
+
cacheBytes?: number;
|
|
290
298
|
}
|
|
291
299
|
|
|
292
300
|
/** Signature-compatible with `globalThis.fetch`. */
|
|
@@ -296,6 +304,13 @@ export type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<
|
|
|
296
304
|
export interface TransportConfig {
|
|
297
305
|
requestInit?: RequestInit;
|
|
298
306
|
fetchFn?: FetchFn;
|
|
307
|
+
/**
|
|
308
|
+
* Byte budget for the libav HTTP reader's LRU cache of fetched ranges.
|
|
309
|
+
* Defaults to 8 MB. Set to `0` to disable caching entirely. Higher
|
|
310
|
+
* values help seek-heavy network playback keep hot regions
|
|
311
|
+
* (header/moov, tail index, current read) resident.
|
|
312
|
+
*/
|
|
313
|
+
cacheBytes?: number;
|
|
299
314
|
}
|
|
300
315
|
|
|
301
316
|
/** Events emitted by {@link UnifiedPlayer}. Strongly typed. */
|
|
@@ -32,8 +32,13 @@
|
|
|
32
32
|
* clamped to `[256 KB, 1 MB]`. Small reads get amortized; pathological
|
|
33
33
|
* large requests don't OOM us.
|
|
34
34
|
*
|
|
35
|
-
* - **
|
|
36
|
-
*
|
|
35
|
+
* - **LRU block cache.** Fetched blocks are kept in a Map keyed by start
|
|
36
|
+
* position, bounded by a byte budget (default 8 MB, configurable via
|
|
37
|
+
* `cacheBytes`). Map insertion order doubles as recency; re-accessing
|
|
38
|
+
* a block promotes it via delete+set. Eviction walks oldest-first
|
|
39
|
+
* until total bytes fit the budget. Typical seek pattern has three
|
|
40
|
+
* hot regions — header/moov at the front, index at the tail, current
|
|
41
|
+
* read position — all of which fit comfortably under the default.
|
|
37
42
|
*
|
|
38
43
|
* - **Safe detach.** `detach()` clears `libav.onblockread`, sets a
|
|
39
44
|
* destroyed flag, and ignores any in-flight fetch resolutions so we
|
|
@@ -42,6 +47,7 @@
|
|
|
42
47
|
|
|
43
48
|
const MIN_READ = 256 * 1024;
|
|
44
49
|
const MAX_READ = 1 * 1024 * 1024;
|
|
50
|
+
const DEFAULT_CACHE_BYTES = 8 * 1024 * 1024;
|
|
45
51
|
|
|
46
52
|
interface LibavLike {
|
|
47
53
|
mkblockreaderdev(name: string, size: number): Promise<void>;
|
|
@@ -69,6 +75,12 @@ export interface AttachLibavHttpReaderOptions {
|
|
|
69
75
|
requestInit?: RequestInit;
|
|
70
76
|
/** Override fetch (for testing). Defaults to globalThis.fetch. */
|
|
71
77
|
fetchFn?: typeof fetch;
|
|
78
|
+
/**
|
|
79
|
+
* Byte budget for the LRU block cache. Defaults to 8 MB. Set to `0`
|
|
80
|
+
* to disable caching. Raise this (e.g. 32 MB) for apps that play
|
|
81
|
+
* seek-heavy legacy-container media over the network.
|
|
82
|
+
*/
|
|
83
|
+
cacheBytes?: number;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
/**
|
|
@@ -108,6 +120,7 @@ export async function prepareLibavInput(
|
|
|
108
120
|
const handle = await attachLibavHttpReader(libav, filename, source.url, {
|
|
109
121
|
requestInit: transport?.requestInit,
|
|
110
122
|
fetchFn: transport?.fetchFn,
|
|
123
|
+
cacheBytes: transport?.cacheBytes,
|
|
111
124
|
});
|
|
112
125
|
return {
|
|
113
126
|
filename,
|
|
@@ -192,9 +205,12 @@ export async function attachLibavHttpReader(
|
|
|
192
205
|
// ── State ───────────────────────────────────────────────────────────────
|
|
193
206
|
|
|
194
207
|
let detached = false;
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
|
|
208
|
+
// LRU cache of fetched blocks, keyed by start position. Map insertion
|
|
209
|
+
// order = recency. Bounded by `cacheBudget` bytes; evicts oldest-first
|
|
210
|
+
// on overflow. Set budget to 0 to disable caching.
|
|
211
|
+
const cache = new Map<number, Uint8Array>();
|
|
212
|
+
let cacheBytes = 0;
|
|
213
|
+
const cacheBudget = Math.max(0, options.cacheBytes ?? DEFAULT_CACHE_BYTES);
|
|
198
214
|
// The currently in-flight fetch, if any. Used both for serialization
|
|
199
215
|
// (we await this before starting another) and for in-flight dedup.
|
|
200
216
|
let inflight: Promise<void> | null = null;
|
|
@@ -206,17 +222,39 @@ export async function attachLibavHttpReader(
|
|
|
206
222
|
return doubled;
|
|
207
223
|
}
|
|
208
224
|
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Look up a cached block that fully covers `[pos, pos+length)`. On hit,
|
|
227
|
+
* promote the block to most-recent and return the slice. On miss, null.
|
|
228
|
+
*/
|
|
229
|
+
function cacheLookup(pos: number, length: number): Uint8Array | null {
|
|
230
|
+
for (const [blockPos, bytes] of cache) {
|
|
231
|
+
if (pos >= blockPos && pos + length <= blockPos + bytes.byteLength) {
|
|
232
|
+
cache.delete(blockPos);
|
|
233
|
+
cache.set(blockPos, bytes);
|
|
234
|
+
const offset = pos - blockPos;
|
|
235
|
+
return bytes.subarray(offset, offset + length);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
213
239
|
}
|
|
214
240
|
|
|
215
|
-
/**
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
241
|
+
/** Insert a fetched block; evict least-recently-used until under budget. */
|
|
242
|
+
function cacheInsert(pos: number, bytes: Uint8Array): void {
|
|
243
|
+
const existing = cache.get(pos);
|
|
244
|
+
if (existing) {
|
|
245
|
+
cacheBytes -= existing.byteLength;
|
|
246
|
+
cache.delete(pos);
|
|
247
|
+
}
|
|
248
|
+
cache.set(pos, bytes);
|
|
249
|
+
cacheBytes += bytes.byteLength;
|
|
250
|
+
while (cacheBytes > cacheBudget && cache.size > 0) {
|
|
251
|
+
const oldestKey = cache.keys().next().value as number | undefined;
|
|
252
|
+
if (oldestKey === undefined) break;
|
|
253
|
+
const oldest = cache.get(oldestKey);
|
|
254
|
+
if (!oldest) break;
|
|
255
|
+
cache.delete(oldestKey);
|
|
256
|
+
cacheBytes -= oldest.byteLength;
|
|
257
|
+
}
|
|
220
258
|
}
|
|
221
259
|
|
|
222
260
|
/** Fetch one Range and update the cache. */
|
|
@@ -235,7 +273,7 @@ export async function attachLibavHttpReader(
|
|
|
235
273
|
);
|
|
236
274
|
}
|
|
237
275
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
238
|
-
|
|
276
|
+
cacheInsert(pos, buf);
|
|
239
277
|
return buf;
|
|
240
278
|
}
|
|
241
279
|
|
|
@@ -252,9 +290,9 @@ export async function attachLibavHttpReader(
|
|
|
252
290
|
if (detached) return;
|
|
253
291
|
|
|
254
292
|
// Cache hit — reply directly without a network round-trip.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
try { await libav.ff_block_reader_dev_send(name, pos,
|
|
293
|
+
const hit = cacheLookup(pos, length);
|
|
294
|
+
if (hit) {
|
|
295
|
+
try { await libav.ff_block_reader_dev_send(name, pos, hit); } catch { /* ignore — libav may have torn down */ }
|
|
258
296
|
return;
|
|
259
297
|
}
|
|
260
298
|
|
|
@@ -312,7 +350,8 @@ export async function attachLibavHttpReader(
|
|
|
312
350
|
try { await inflight; } catch { /* ignore */ }
|
|
313
351
|
}
|
|
314
352
|
// Drop the cache and unlink the virtual file.
|
|
315
|
-
|
|
353
|
+
cache.clear();
|
|
354
|
+
cacheBytes = 0;
|
|
316
355
|
try { await libav.unlinkreadaheadfile(filename); } catch { /* ignore */ }
|
|
317
356
|
},
|
|
318
357
|
};
|