avbridge 2.11.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/{avi-B5CQYB7L.cjs → avi-EQE6AR75.cjs} +4 -4
  3. package/dist/{avi-2ILLBNPQ.cjs.map → avi-EQE6AR75.cjs.map} +1 -1
  4. package/dist/{avi-RWWPN2PR.js → avi-NNHH4AAA.js} +3 -3
  5. package/dist/{avi-JXU4GQL2.js.map → avi-NNHH4AAA.js.map} +1 -1
  6. package/dist/{avi-JXU4GQL2.js → avi-S7EY54YA.js} +3 -3
  7. package/dist/{avi-RWWPN2PR.js.map → avi-S7EY54YA.js.map} +1 -1
  8. package/dist/{avi-2ILLBNPQ.cjs → avi-Y3N325WZ.cjs} +4 -4
  9. package/dist/{avi-B5CQYB7L.cjs.map → avi-Y3N325WZ.cjs.map} +1 -1
  10. package/dist/{chunk-GYIJU44C.js → chunk-2LNXMGT6.js} +5 -5
  11. package/dist/{chunk-GYIJU44C.js.map → chunk-2LNXMGT6.js.map} +1 -1
  12. package/dist/{chunk-DCSOQH2N.js → chunk-3AI5WFFN.js} +40 -16
  13. package/dist/chunk-3AI5WFFN.js.map +1 -0
  14. package/dist/{chunk-2NSOOMXW.js → chunk-3YKWU4FM.js} +3 -3
  15. package/dist/{chunk-2NSOOMXW.js.map → chunk-3YKWU4FM.js.map} +1 -1
  16. package/dist/{chunk-CL6UEUQF.js → chunk-5Y5BTB5D.js} +5 -5
  17. package/dist/{chunk-CL6UEUQF.js.map → chunk-5Y5BTB5D.js.map} +1 -1
  18. package/dist/{chunk-37UOSAVI.cjs → chunk-7EF4VTUS.cjs} +23 -23
  19. package/dist/{chunk-IHNHHEA2.js.map → chunk-7EF4VTUS.cjs.map} +1 -1
  20. package/dist/{chunk-OTFS7DC4.cjs → chunk-GJBNLPGI.cjs} +14 -14
  21. package/dist/{chunk-OTFS7DC4.cjs.map → chunk-GJBNLPGI.cjs.map} +1 -1
  22. package/dist/{chunk-BYGZN4Z5.cjs → chunk-HBHSUGNI.cjs} +5 -5
  23. package/dist/{chunk-BYGZN4Z5.cjs.map → chunk-HBHSUGNI.cjs.map} +1 -1
  24. package/dist/{chunk-L7A3ECI2.cjs → chunk-HZUVMXBN.cjs} +4 -4
  25. package/dist/{chunk-L7A3ECI2.cjs.map → chunk-HZUVMXBN.cjs.map} +1 -1
  26. package/dist/{chunk-Z33SBWL5.cjs → chunk-YPZFGJV3.cjs} +40 -16
  27. package/dist/chunk-YPZFGJV3.cjs.map +1 -0
  28. package/dist/{chunk-IHNHHEA2.js → chunk-Z26PXRUY.js} +9 -9
  29. package/dist/chunk-Z26PXRUY.js.map +1 -0
  30. package/dist/element-browser.js +42 -18
  31. package/dist/element-browser.js.map +1 -1
  32. package/dist/element.cjs +5 -5
  33. package/dist/element.d.cts +1 -1
  34. package/dist/element.d.ts +1 -1
  35. package/dist/element.js +4 -4
  36. package/dist/index.cjs +21 -21
  37. package/dist/index.d.cts +2 -2
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.js +9 -9
  40. package/dist/{libav-demux-3N5Y3VQA.cjs → libav-demux-575OYCT2.cjs} +9 -9
  41. package/dist/{libav-demux-3N5Y3VQA.cjs.map → libav-demux-575OYCT2.cjs.map} +1 -1
  42. package/dist/{libav-demux-JXD4OTLM.js → libav-demux-SXZDLC7W.js} +4 -4
  43. package/dist/{libav-demux-JXD4OTLM.js.map → libav-demux-SXZDLC7W.js.map} +1 -1
  44. package/dist/libav-http-reader-2S5HAHW4.js +3 -0
  45. package/dist/{libav-http-reader-WXG3Z7AI.js.map → libav-http-reader-2S5HAHW4.js.map} +1 -1
  46. package/dist/libav-http-reader-Q356EO2K.cjs +16 -0
  47. package/dist/{libav-http-reader-AZLE7YFS.cjs.map → libav-http-reader-Q356EO2K.cjs.map} +1 -1
  48. package/dist/{player-DDdNVFDv.d.cts → player-bQ6n4hVp.d.cts} +15 -0
  49. package/dist/{player-DDdNVFDv.d.ts → player-bQ6n4hVp.d.ts} +15 -0
  50. package/dist/player.cjs +60 -15
  51. package/dist/player.cjs.map +1 -1
  52. package/dist/player.d.cts +21 -0
  53. package/dist/player.d.ts +21 -0
  54. package/dist/player.js +60 -15
  55. package/dist/player.js.map +1 -1
  56. package/dist/remux-7TA4FKTY.js +10 -0
  57. package/dist/{remux-56V7LDAD.js.map → remux-7TA4FKTY.js.map} +1 -1
  58. package/dist/remux-VPKCLHHM.cjs +35 -0
  59. package/dist/{remux-KUS5GIL6.cjs.map → remux-VPKCLHHM.cjs.map} +1 -1
  60. package/package.json +1 -1
  61. package/src/element/avbridge-player.ts +36 -8
  62. package/src/element/player-styles.ts +13 -1
  63. package/src/player.ts +3 -3
  64. package/src/types.ts +15 -0
  65. package/src/util/libav-http-reader.ts +58 -19
  66. package/dist/chunk-37UOSAVI.cjs.map +0 -1
  67. package/dist/chunk-DCSOQH2N.js.map +0 -1
  68. package/dist/chunk-Z33SBWL5.cjs.map +0 -1
  69. package/dist/libav-http-reader-AZLE7YFS.cjs +0 -16
  70. package/dist/libav-http-reader-WXG3Z7AI.js +0 -3
  71. package/dist/remux-56V7LDAD.js +0 -10
  72. package/dist/remux-KUS5GIL6.cjs +0 -35
@@ -0,0 +1,10 @@
1
+ export { createOutputFormat, generateFilename, mimeForFormat, remux, validateRemuxEligibility } from './chunk-5Y5BTB5D.js';
2
+ import './chunk-2LNXMGT6.js';
3
+ import './chunk-CPJLFFCC.js';
4
+ import './chunk-LUFA47FP.js';
5
+ import './chunk-3YKWU4FM.js';
6
+ import './chunk-3AI5WFFN.js';
7
+ import './chunk-5DMTJVIU.js';
8
+ import './chunk-5YAWWKA3.js';
9
+ //# sourceMappingURL=remux-7TA4FKTY.js.map
10
+ //# sourceMappingURL=remux-7TA4FKTY.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"remux-56V7LDAD.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"remux-7TA4FKTY.js"}
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ var chunkGJBNLPGI_cjs = require('./chunk-GJBNLPGI.cjs');
4
+ require('./chunk-HBHSUGNI.cjs');
5
+ require('./chunk-2IJ66NTD.cjs');
6
+ require('./chunk-QDJLQR53.cjs');
7
+ require('./chunk-HZUVMXBN.cjs');
8
+ require('./chunk-YPZFGJV3.cjs');
9
+ require('./chunk-G4APZMCP.cjs');
10
+ require('./chunk-F3LQJKXK.cjs');
11
+
12
+
13
+
14
+ Object.defineProperty(exports, "createOutputFormat", {
15
+ enumerable: true,
16
+ get: function () { return chunkGJBNLPGI_cjs.createOutputFormat; }
17
+ });
18
+ Object.defineProperty(exports, "generateFilename", {
19
+ enumerable: true,
20
+ get: function () { return chunkGJBNLPGI_cjs.generateFilename; }
21
+ });
22
+ Object.defineProperty(exports, "mimeForFormat", {
23
+ enumerable: true,
24
+ get: function () { return chunkGJBNLPGI_cjs.mimeForFormat; }
25
+ });
26
+ Object.defineProperty(exports, "remux", {
27
+ enumerable: true,
28
+ get: function () { return chunkGJBNLPGI_cjs.remux; }
29
+ });
30
+ Object.defineProperty(exports, "validateRemuxEligibility", {
31
+ enumerable: true,
32
+ get: function () { return chunkGJBNLPGI_cjs.validateRemuxEligibility; }
33
+ });
34
+ //# sourceMappingURL=remux-VPKCLHHM.cjs.map
35
+ //# sourceMappingURL=remux-VPKCLHHM.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"remux-KUS5GIL6.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"remux-VPKCLHHM.cjs"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "avbridge",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Play and convert arbitrary video files in the browser. Native, remux, hybrid, fallback, and transcode — one API.",
5
5
  "license": "MIT",
6
6
  "author": "Keishi Hattori",
@@ -247,6 +247,11 @@ export class AvbridgePlayerElement extends HTMLElement {
247
247
  on(this._video, "ended", () => this._setState("ended"));
248
248
  on(this._video, "error", () => this._setState("error"));
249
249
  on(this._video, "timeupdate", () => this._updateTime());
250
+ // `progress` fires as the inner element's buffered ranges grow — keep the
251
+ // seek bar's buffered indicator fresh even when paused or filling ahead
252
+ // without timeupdate advancing. `<avbridge-video>` dispatches this on
253
+ // all strategies (including the synthesized ranges for canvas strategies).
254
+ on(this._video, "progress", () => this._updateBuffered());
250
255
  on(this._video, "volumechange", () => this._updateVolume());
251
256
  // Strategy changes are visible in Stats for Nerds.
252
257
  on(this._video, "trackschange", () => this._buildSettingsMenu());
@@ -524,15 +529,38 @@ export class AvbridgePlayerElement extends HTMLElement {
524
529
  this._seekInput.value = String(t);
525
530
  this._updateSeekVisuals(t);
526
531
  this._timeDisplay.textContent = `${formatTime(t)} / ${formatTime(d)}`;
532
+ this._updateBuffered();
533
+ }
527
534
 
528
- // Buffered ranges
529
- try {
530
- const buf = this._video.buffered;
531
- if (buf && buf.length > 0 && d > 0) {
532
- const end = buf.end(buf.length - 1);
533
- this._seekBuffered.style.width = `${(end / d) * 100}%`;
534
- }
535
- } catch { /* ignore */ }
535
+ /**
536
+ * Render every buffered range as its own segment so gaps (common on MSE
537
+ * after seeks) are visible. Not gated by `_userSeeking` — ranges should
538
+ * keep updating while the user scrubs, and runs cheaply on `progress`.
539
+ */
540
+ private _updateBuffered(): void {
541
+ const d = this._video.duration;
542
+ if (!(d > 0)) return;
543
+ let buf: TimeRanges;
544
+ try { buf = this._video.buffered; } catch { return; }
545
+ const count = buf ? buf.length : 0;
546
+ const host = this._seekBuffered;
547
+ // Reconcile child count. Segment divs are styled via .avp-seek-buffered-range.
548
+ while (host.childElementCount > count) host.lastElementChild!.remove();
549
+ while (host.childElementCount < count) {
550
+ const seg = document.createElement("div");
551
+ seg.className = "avp-seek-buffered-range";
552
+ host.appendChild(seg);
553
+ }
554
+ for (let i = 0; i < count; i++) {
555
+ let start: number; let end: number;
556
+ try { start = buf.start(i); end = buf.end(i); } catch { continue; }
557
+ const s = Math.max(0, start);
558
+ const e = Math.min(d, end);
559
+ if (e <= s) continue;
560
+ const seg = host.children[i] as HTMLElement;
561
+ seg.style.left = `${(s / d) * 100}%`;
562
+ seg.style.width = `${((e - s) / d) * 100}%`;
563
+ }
536
564
  }
537
565
 
538
566
  // ── Controls: volume ───────────────────────────────────────────────────
@@ -286,6 +286,12 @@ export const PLAYER_STYLES = /* css */ `
286
286
  display: flex;
287
287
  align-items: center;
288
288
  cursor: pointer;
289
+ /* Claim all touch gestures on the seek bar. Without this, Android
290
+ * browsers (Chrome, Samsung Internet) treat horizontal drags as
291
+ * scroll candidates and cancel pointermove once the gesture
292
+ * resolves, breaking scrub. touch-action must be set in CSS —
293
+ * preventDefault() on pointerdown is too late. */
294
+ touch-action: none;
289
295
  }
290
296
 
291
297
  .avp-seek-track {
@@ -303,7 +309,13 @@ export const PLAYER_STYLES = /* css */ `
303
309
 
304
310
  .avp-seek-buffered {
305
311
  position: absolute;
306
- left: 0;
312
+ inset: 0;
313
+ pointer-events: none;
314
+ }
315
+
316
+ .avp-seek-buffered-range {
317
+ position: absolute;
318
+ top: 0;
307
319
  height: 100%;
308
320
  background: rgba(255, 255, 255, 0.35);
309
321
  border-radius: inherit;
package/src/player.ts CHANGED
@@ -132,9 +132,9 @@ export class UnifiedPlayer {
132
132
  private readonly options: CreatePlayerOptions,
133
133
  private readonly registry: PluginRegistry,
134
134
  ) {
135
- const { requestInit, fetchFn } = options;
136
- if (requestInit || fetchFn) {
137
- this.transport = { requestInit, fetchFn };
135
+ const { requestInit, fetchFn, cacheBytes } = options;
136
+ if (requestInit || fetchFn || cacheBytes !== undefined) {
137
+ this.transport = { requestInit, fetchFn, cacheBytes };
138
138
  }
139
139
  }
140
140
 
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
- * - **Last-block cache.** Only the most-recent fetched block is kept.
36
- * Re-fetches via Range are cheap; an LRU cache is post-1.0.
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
- // Most-recently fetched block. Cached so re-reads of the same region
196
- // (e.g. demuxer re-walks the header) don't issue another HTTP request.
197
- let cached: { pos: number; bytes: Uint8Array } | null = null;
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
- /** True if the cached block fully covers `[pos, pos+length)`. */
210
- function cacheCovers(pos: number, length: number): boolean {
211
- if (!cached) return false;
212
- return pos >= cached.pos && pos + length <= cached.pos + cached.bytes.byteLength;
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
- /** Slice the requested window out of the cached block. */
216
- function sliceFromCache(pos: number, length: number): Uint8Array {
217
- if (!cached) throw new Error("sliceFromCache called with no cache");
218
- const offset = pos - cached.pos;
219
- return cached.bytes.subarray(offset, offset + length);
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
- cached = { pos, bytes: buf };
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
- if (cacheCovers(pos, length)) {
256
- const data = sliceFromCache(pos, length);
257
- try { await libav.ff_block_reader_dev_send(name, pos, data); } catch { /* ignore — libav may have torn down */ }
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
- cached = null;
353
+ cache.clear();
354
+ cacheBytes = 0;
316
355
  try { await libav.unlinkreadaheadfile(filename); } catch { /* ignore */ }
317
356
  },
318
357
  };