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
|
@@ -856,15 +856,35 @@ export class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
856
856
|
sidecarUrl: subtitle.url,
|
|
857
857
|
};
|
|
858
858
|
this._subtitleTracks.push(track);
|
|
859
|
+
// eslint-disable-next-line no-console
|
|
860
|
+
console.log(`[avbridge:subs] addSubtitle id=${track.id} format=${format} lang=${subtitle.language ?? "?"}`);
|
|
859
861
|
await attachSubtitleTracks(
|
|
860
862
|
this._videoEl,
|
|
861
863
|
this._subtitleTracks,
|
|
862
864
|
undefined,
|
|
863
865
|
(err, t) => {
|
|
864
866
|
// eslint-disable-next-line no-console
|
|
865
|
-
console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
|
|
867
|
+
console.warn(`[avbridge:subs] subtitle ${t.id} failed: ${err.message}`);
|
|
866
868
|
},
|
|
867
869
|
);
|
|
870
|
+
// Enable the newly-added track so it renders immediately. On native
|
|
871
|
+
// strategy the <video>'s textTrack must be mode="showing"; on canvas
|
|
872
|
+
// strategies the renderer's watchTextTracks picks it up from the
|
|
873
|
+
// hidden-mode textTracks.
|
|
874
|
+
const textTracks = this._videoEl.textTracks;
|
|
875
|
+
for (let i = 0; i < textTracks.length; i++) {
|
|
876
|
+
if (textTracks[i].label === (subtitle.language ?? `Subtitle ${track.id}`)) {
|
|
877
|
+
textTracks[i].mode = "showing";
|
|
878
|
+
// eslint-disable-next-line no-console
|
|
879
|
+
console.log(`[avbridge:subs] enabled textTrack[${i}] mode=showing`);
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// Notify the settings sheet so it rebuilds with the new track.
|
|
884
|
+
this._dispatch("trackschange", {
|
|
885
|
+
audioTracks: this._audioTracks,
|
|
886
|
+
subtitleTracks: this.subtitleTracks,
|
|
887
|
+
});
|
|
868
888
|
}
|
|
869
889
|
|
|
870
890
|
/**
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -281,6 +281,10 @@ export class AudioOutput implements ClockSource {
|
|
|
281
281
|
await this.ctx.resume();
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// Reconnect the gain node — pause() disconnects it to cut off
|
|
285
|
+
// in-flight audio instantly. Safe to call even if already connected.
|
|
286
|
+
try { this.gain.connect(this.ctx.destination); } catch { /* ignore */ }
|
|
287
|
+
|
|
284
288
|
if (this.state === "paused") {
|
|
285
289
|
// Resume: media time should continue from where we paused. ctx.currentTime
|
|
286
290
|
// is preserved across suspend/resume, so re-anchoring it to "now" with
|
|
@@ -317,6 +321,12 @@ export class AudioOutput implements ClockSource {
|
|
|
317
321
|
this.mediaTimeOfAnchor = this.now();
|
|
318
322
|
this.state = "paused";
|
|
319
323
|
if (this.noAudio) return;
|
|
324
|
+
// Disconnect the gain node immediately so any in-flight scheduled
|
|
325
|
+
// buffers are silenced instantly. ctx.suspend() is async and
|
|
326
|
+
// already-started AudioBufferSourceNodes keep playing until the
|
|
327
|
+
// context actually suspends — without the disconnect, audio bleeds
|
|
328
|
+
// through for ~200ms after pause().
|
|
329
|
+
try { this.gain.disconnect(); } catch { /* ignore */ }
|
|
320
330
|
if (this.ctx.state === "running") {
|
|
321
331
|
await this.ctx.suspend();
|
|
322
332
|
}
|
package/src/subtitles/index.ts
CHANGED
|
@@ -110,6 +110,8 @@ export async function attachSubtitleTracks(
|
|
|
110
110
|
|
|
111
111
|
for (const t of tracks) {
|
|
112
112
|
if (!t.sidecarUrl) continue;
|
|
113
|
+
// eslint-disable-next-line no-console
|
|
114
|
+
console.log(`[avbridge:subs] attaching track id=${t.id} format=${t.format} url=${t.sidecarUrl.slice(0, 60)}`);
|
|
113
115
|
try {
|
|
114
116
|
let url = t.sidecarUrl;
|
|
115
117
|
if (t.format === "srt") {
|
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
|
};
|