avbridge 2.2.0 → 2.3.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 (121) hide show
  1. package/CHANGELOG.md +125 -1
  2. package/NOTICE.md +2 -2
  3. package/README.md +100 -74
  4. package/THIRD_PARTY_LICENSES.md +2 -2
  5. package/dist/avi-2JPBSHGA.js +183 -0
  6. package/dist/avi-2JPBSHGA.js.map +1 -0
  7. package/dist/avi-F6WZJK5T.cjs +185 -0
  8. package/dist/avi-F6WZJK5T.cjs.map +1 -0
  9. package/dist/{avi-GCGM7OJI.js → avi-NJXAXUXK.js} +9 -3
  10. package/dist/avi-NJXAXUXK.js.map +1 -0
  11. package/dist/{avi-6SJLWIWW.cjs → avi-W6L3BTWU.cjs} +10 -4
  12. package/dist/avi-W6L3BTWU.cjs.map +1 -0
  13. package/dist/{chunk-ILKDNBSE.js → chunk-2PGRFCWB.js} +59 -10
  14. package/dist/chunk-2PGRFCWB.js.map +1 -0
  15. package/dist/chunk-5YAWWKA3.js +18 -0
  16. package/dist/chunk-5YAWWKA3.js.map +1 -0
  17. package/dist/chunk-6UUT4BEA.cjs +219 -0
  18. package/dist/chunk-6UUT4BEA.cjs.map +1 -0
  19. package/dist/{chunk-OE66B34H.cjs → chunk-7RGG6ME7.cjs} +562 -94
  20. package/dist/chunk-7RGG6ME7.cjs.map +1 -0
  21. package/dist/{chunk-WD2ZNQA7.js → chunk-DCSOQH2N.js} +7 -4
  22. package/dist/chunk-DCSOQH2N.js.map +1 -0
  23. package/dist/chunk-F3LQJKXK.cjs +20 -0
  24. package/dist/chunk-F3LQJKXK.cjs.map +1 -0
  25. package/dist/chunk-IAYKFGFG.js +200 -0
  26. package/dist/chunk-IAYKFGFG.js.map +1 -0
  27. package/dist/chunk-NNVOHKXJ.cjs +204 -0
  28. package/dist/chunk-NNVOHKXJ.cjs.map +1 -0
  29. package/dist/{chunk-C5VA5U5O.js → chunk-NV7ILLWH.js} +556 -92
  30. package/dist/chunk-NV7ILLWH.js.map +1 -0
  31. package/dist/{chunk-HZLQNKFN.cjs → chunk-QQXBPW72.js} +54 -15
  32. package/dist/chunk-QQXBPW72.js.map +1 -0
  33. package/dist/chunk-XKPSTC34.cjs +210 -0
  34. package/dist/chunk-XKPSTC34.cjs.map +1 -0
  35. package/dist/{chunk-L4NPOJ36.cjs → chunk-Z33SBWL5.cjs} +7 -4
  36. package/dist/chunk-Z33SBWL5.cjs.map +1 -0
  37. package/dist/element-browser.js +631 -103
  38. package/dist/element-browser.js.map +1 -1
  39. package/dist/element.cjs +4 -4
  40. package/dist/element.d.cts +1 -1
  41. package/dist/element.d.ts +1 -1
  42. package/dist/element.js +3 -3
  43. package/dist/index.cjs +174 -26
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +48 -4
  46. package/dist/index.d.ts +48 -4
  47. package/dist/index.js +93 -12
  48. package/dist/index.js.map +1 -1
  49. package/dist/libav-http-reader-AZLE7YFS.cjs +16 -0
  50. package/dist/{libav-http-reader-FPYDBMYK.cjs.map → libav-http-reader-AZLE7YFS.cjs.map} +1 -1
  51. package/dist/libav-http-reader-WXG3Z7AI.js +3 -0
  52. package/dist/{libav-http-reader-NQJVY273.js.map → libav-http-reader-WXG3Z7AI.js.map} +1 -1
  53. package/dist/{player-DUyvltvy.d.cts → player-B6WB74RD.d.cts} +63 -3
  54. package/dist/{player-DUyvltvy.d.ts → player-B6WB74RD.d.ts} +63 -3
  55. package/dist/player.cjs +5500 -0
  56. package/dist/player.cjs.map +1 -0
  57. package/dist/player.d.cts +649 -0
  58. package/dist/player.d.ts +649 -0
  59. package/dist/player.js +5498 -0
  60. package/dist/player.js.map +1 -0
  61. package/dist/source-73CAH6HW.cjs +28 -0
  62. package/dist/{source-CN43EI7Z.cjs.map → source-73CAH6HW.cjs.map} +1 -1
  63. package/dist/source-F656KYYV.js +3 -0
  64. package/dist/{source-FFZ7TW2B.js.map → source-F656KYYV.js.map} +1 -1
  65. package/dist/source-QJR3OHTW.js +3 -0
  66. package/dist/source-QJR3OHTW.js.map +1 -0
  67. package/dist/source-VB74JQ7Z.cjs +28 -0
  68. package/dist/source-VB74JQ7Z.cjs.map +1 -0
  69. package/dist/variant-routing-434STYAB.js +3 -0
  70. package/dist/{variant-routing-JOBWXYKD.js.map → variant-routing-434STYAB.js.map} +1 -1
  71. package/dist/variant-routing-HONNAA6R.cjs +12 -0
  72. package/dist/{variant-routing-GOHB2RZN.cjs.map → variant-routing-HONNAA6R.cjs.map} +1 -1
  73. package/package.json +9 -1
  74. package/src/classify/rules.ts +27 -5
  75. package/src/convert/remux.ts +8 -0
  76. package/src/convert/transcode.ts +41 -8
  77. package/src/element/avbridge-player.ts +845 -0
  78. package/src/element/player-icons.ts +25 -0
  79. package/src/element/player-styles.ts +472 -0
  80. package/src/errors.ts +47 -0
  81. package/src/index.ts +23 -0
  82. package/src/player-element.ts +18 -0
  83. package/src/player.ts +127 -27
  84. package/src/plugins/builtin.ts +2 -2
  85. package/src/probe/avi.ts +4 -0
  86. package/src/probe/index.ts +40 -10
  87. package/src/strategies/fallback/audio-output.ts +31 -0
  88. package/src/strategies/fallback/decoder.ts +83 -2
  89. package/src/strategies/fallback/index.ts +34 -1
  90. package/src/strategies/fallback/variant-routing.ts +7 -13
  91. package/src/strategies/fallback/video-renderer.ts +129 -33
  92. package/src/strategies/hybrid/decoder.ts +131 -20
  93. package/src/strategies/hybrid/index.ts +36 -2
  94. package/src/strategies/remux/index.ts +13 -1
  95. package/src/strategies/remux/mse.ts +12 -2
  96. package/src/strategies/remux/pipeline.ts +6 -0
  97. package/src/subtitles/index.ts +7 -3
  98. package/src/types.ts +53 -1
  99. package/src/util/libav-http-reader.ts +5 -1
  100. package/src/util/source.ts +28 -8
  101. package/src/util/transport.ts +26 -0
  102. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
  103. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
  104. package/dist/avi-6SJLWIWW.cjs.map +0 -1
  105. package/dist/avi-GCGM7OJI.js.map +0 -1
  106. package/dist/chunk-C5VA5U5O.js.map +0 -1
  107. package/dist/chunk-HZLQNKFN.cjs.map +0 -1
  108. package/dist/chunk-ILKDNBSE.js.map +0 -1
  109. package/dist/chunk-J5MCMN3S.js +0 -27
  110. package/dist/chunk-J5MCMN3S.js.map +0 -1
  111. package/dist/chunk-L4NPOJ36.cjs.map +0 -1
  112. package/dist/chunk-NZU7W256.cjs +0 -29
  113. package/dist/chunk-NZU7W256.cjs.map +0 -1
  114. package/dist/chunk-OE66B34H.cjs.map +0 -1
  115. package/dist/chunk-WD2ZNQA7.js.map +0 -1
  116. package/dist/libav-http-reader-FPYDBMYK.cjs +0 -16
  117. package/dist/libav-http-reader-NQJVY273.js +0 -3
  118. package/dist/source-CN43EI7Z.cjs +0 -28
  119. package/dist/source-FFZ7TW2B.js +0 -3
  120. package/dist/variant-routing-GOHB2RZN.cjs +0 -12
  121. package/dist/variant-routing-JOBWXYKD.js +0 -3
@@ -3,6 +3,8 @@
3
3
  * `SourceBuffer` with an append queue that respects `updateend` backpressure.
4
4
  */
5
5
 
6
+ import { AvbridgeError, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from "../../errors.js";
7
+
6
8
  export interface MseSinkOptions {
7
9
  mime: string;
8
10
  video: HTMLVideoElement;
@@ -23,10 +25,18 @@ export class MseSink {
23
25
 
24
26
  constructor(private readonly options: MseSinkOptions) {
25
27
  if (typeof MediaSource === "undefined") {
26
- throw new Error("MSE not supported in this environment");
28
+ throw new AvbridgeError(
29
+ ERR_MSE_NOT_SUPPORTED,
30
+ "MediaSource Extensions (MSE) are not supported in this environment.",
31
+ "MSE is required for the remux strategy. Use a browser that supports MSE, or try the fallback strategy.",
32
+ );
27
33
  }
28
34
  if (!MediaSource.isTypeSupported(options.mime)) {
29
- throw new Error(`MSE does not support MIME "${options.mime}" — cannot remux`);
35
+ throw new AvbridgeError(
36
+ ERR_MSE_CODEC_NOT_SUPPORTED,
37
+ `This browser's MSE does not support "${options.mime}".`,
38
+ "The codec combination can't be played via remux in this browser. The player will try the next strategy automatically.",
39
+ );
30
40
  }
31
41
 
32
42
  this.mediaSource = new MediaSource();
@@ -24,6 +24,8 @@ import {
24
24
  export interface RemuxPipeline {
25
25
  start(fromTime?: number, autoPlay?: boolean): Promise<void>;
26
26
  seek(time: number, autoPlay?: boolean): Promise<void>;
27
+ /** Update the autoplay intent mid-flight — used when play() arrives after seek() but before the MseSink has been constructed. */
28
+ setAutoPlay(autoPlay: boolean): void;
27
29
  destroy(): Promise<void>;
28
30
  stats(): Record<string, unknown>;
29
31
  }
@@ -248,6 +250,10 @@ export async function createRemuxPipeline(
248
250
  console.error("[avbridge] remux pipeline reseek failed:", err);
249
251
  });
250
252
  },
253
+ setAutoPlay(autoPlay) {
254
+ pendingAutoPlay = autoPlay;
255
+ if (sink) sink.setPlayOnSeek(autoPlay);
256
+ },
251
257
  async destroy() {
252
258
  destroyed = true;
253
259
  pumpToken++;
@@ -1,4 +1,5 @@
1
- import type { SubtitleTrackInfo } from "../types.js";
1
+ import type { SubtitleTrackInfo, TransportConfig } from "../types.js";
2
+ import { fetchWith } from "../util/transport.js";
2
3
  import { srtToVtt } from "./srt.js";
3
4
  import { isVtt } from "./vtt.js";
4
5
 
@@ -98,7 +99,10 @@ export async function attachSubtitleTracks(
98
99
  tracks: SubtitleTrackInfo[],
99
100
  bag?: SubtitleResourceBag,
100
101
  onError?: (err: Error, track: SubtitleTrackInfo) => void,
102
+ transport?: TransportConfig,
101
103
  ): Promise<void> {
104
+ const doFetch = fetchWith(transport);
105
+
102
106
  // Clear existing dynamically-attached tracks.
103
107
  for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
104
108
  t.remove();
@@ -109,14 +113,14 @@ export async function attachSubtitleTracks(
109
113
  try {
110
114
  let url = t.sidecarUrl;
111
115
  if (t.format === "srt") {
112
- const res = await fetch(t.sidecarUrl);
116
+ const res = await doFetch(t.sidecarUrl, transport?.requestInit);
113
117
  const text = await res.text();
114
118
  const vtt = srtToVtt(text);
115
119
  const blob = new Blob([vtt], { type: "text/vtt" });
116
120
  url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
117
121
  } else if (t.format === "vtt") {
118
122
  // Validate quickly so a malformed file fails loudly here.
119
- const res = await fetch(t.sidecarUrl);
123
+ const res = await doFetch(t.sidecarUrl, transport?.requestInit);
120
124
  const text = await res.text();
121
125
  if (!isVtt(text)) {
122
126
  // eslint-disable-next-line no-console
package/src/types.ts CHANGED
@@ -69,6 +69,8 @@ export type AudioCodec =
69
69
  | "ra_288" // RealAudio 2.0 (28.8 kbps)
70
70
  | "sipr" // RealAudio Sipr (voice codec)
71
71
  | "atrac3" // Sony ATRAC3 (sometimes seen in .rm)
72
+ | "dts" // DTS (common in Blu-ray MKV rips)
73
+ | "truehd" // Dolby TrueHD (Blu-ray lossless)
72
74
  | (string & {});
73
75
 
74
76
  export interface VideoTrackInfo {
@@ -219,7 +221,7 @@ export interface Plugin {
219
221
  name: string;
220
222
  canHandle(context: MediaContext): boolean;
221
223
  /** Returns a session if it claims the context, otherwise throws. */
222
- execute(context: MediaContext, target: HTMLVideoElement): Promise<PlaybackSession>;
224
+ execute(context: MediaContext, target: HTMLVideoElement, transport?: TransportConfig): Promise<PlaybackSession>;
223
225
  }
224
226
 
225
227
  /** Player creation options. */
@@ -256,6 +258,38 @@ export interface CreatePlayerOptions {
256
258
  * strategy in the fallback chain on failure or stall.
257
259
  */
258
260
  autoEscalate?: boolean;
261
+ /**
262
+ * Behavior when the browser tab becomes hidden.
263
+ * - `"pause"` (default): auto-pause on hide, auto-resume on visible
264
+ * if the user had been playing. Matches YouTube, Netflix, and
265
+ * native media players. Prevents degraded playback from Chrome's
266
+ * background throttling of requestAnimationFrame and setTimeout.
267
+ * - `"continue"`: keep playing. Playback will degrade anyway due to
268
+ * browser throttling, but useful for consumers who want full
269
+ * control of visibility handling themselves.
270
+ */
271
+ backgroundBehavior?: "pause" | "continue";
272
+ /**
273
+ * Extra {@link RequestInit} merged into every HTTP request the player
274
+ * makes (probe Range requests, subtitle fetches, libav HTTP reader).
275
+ * Headers are merged, not overwritten — so you can add `Authorization`
276
+ * without losing the player's `Range` header.
277
+ */
278
+ requestInit?: RequestInit;
279
+ /**
280
+ * Custom fetch implementation. Defaults to `globalThis.fetch`. Useful
281
+ * for interceptors, logging, or environments without a global fetch.
282
+ */
283
+ fetchFn?: FetchFn;
284
+ }
285
+
286
+ /** Signature-compatible with `globalThis.fetch`. */
287
+ export type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
288
+
289
+ /** Internal transport config bundle. Not part of the public API. */
290
+ export interface TransportConfig {
291
+ requestInit?: RequestInit;
292
+ fetchFn?: FetchFn;
259
293
  }
260
294
 
261
295
  /** Events emitted by {@link UnifiedPlayer}. Strongly typed. */
@@ -293,6 +327,24 @@ export interface ConvertOptions {
293
327
  onProgress?: (info: ProgressInfo) => void;
294
328
  /** When true, reject on any uncertain codec/container combo. Default: `false` (best-effort). */
295
329
  strict?: boolean;
330
+ /**
331
+ * Write output progressively to a `WritableStream` instead of accumulating
332
+ * in memory. Use with the File System Access API (`showSaveFilePicker()`) to
333
+ * transcode files larger than available memory.
334
+ *
335
+ * When set, the returned `ConvertResult.blob` will be an empty Blob (the
336
+ * real data went to the stream). The caller is responsible for closing the
337
+ * stream after the returned promise resolves.
338
+ *
339
+ * @example
340
+ * ```ts
341
+ * const handle = await showSaveFilePicker({ suggestedName: "output.mp4" });
342
+ * const writable = await handle.createWritable();
343
+ * const result = await transcode(file, { outputStream: writable });
344
+ * await writable.close();
345
+ * ```
346
+ */
347
+ outputStream?: WritableStream;
296
348
  }
297
349
 
298
350
  /** Progress information passed to {@link ConvertOptions.onProgress}. */
@@ -102,9 +102,13 @@ export async function prepareLibavInput(
102
102
  libav: LibavLikeWithBlob,
103
103
  filename: string,
104
104
  source: import("./source.js").NormalizedSource,
105
+ transport?: import("../types.js").TransportConfig,
105
106
  ): Promise<LibavInputHandle> {
106
107
  if (source.kind === "url") {
107
- const handle = await attachLibavHttpReader(libav, filename, source.url);
108
+ const handle = await attachLibavHttpReader(libav, filename, source.url, {
109
+ requestInit: transport?.requestInit,
110
+ fetchFn: transport?.fetchFn,
111
+ });
108
112
  return {
109
113
  filename,
110
114
  transport: "http-range",
@@ -1,4 +1,6 @@
1
- import type { ContainerKind, MediaInput } from "../types.js";
1
+ import type { ContainerKind, MediaInput, TransportConfig } from "../types.js";
2
+ import { mergeFetchInit, fetchWith } from "./transport.js";
3
+ import { AvbridgeError, ERR_PROBE_FETCH_FAILED } from "../errors.js";
2
4
 
3
5
  /**
4
6
  * Bytes needed by the sniffer to identify every container we recognize.
@@ -59,7 +61,10 @@ export function isInMemorySource(source: NormalizedSource): source is Extract<No
59
61
  *
60
62
  * For non-URL inputs, the bytes are already in memory and we just wrap them.
61
63
  */
62
- export async function normalizeSource(source: MediaInput): Promise<NormalizedSource> {
64
+ export async function normalizeSource(
65
+ source: MediaInput,
66
+ transport?: TransportConfig,
67
+ ): Promise<NormalizedSource> {
63
68
  if (source instanceof File) {
64
69
  return {
65
70
  kind: "blob",
@@ -82,7 +87,7 @@ export async function normalizeSource(source: MediaInput): Promise<NormalizedSou
82
87
  }
83
88
  if (typeof source === "string" || source instanceof URL) {
84
89
  const url = source instanceof URL ? source.toString() : source;
85
- return await fetchUrlForSniff(url, source);
90
+ return await fetchUrlForSniff(url, source, transport);
86
91
  }
87
92
  throw new TypeError("unsupported source type");
88
93
  }
@@ -93,20 +98,35 @@ export async function normalizeSource(source: MediaInput): Promise<NormalizedSou
93
98
  * we only read the first 32 KB and abort the rest of the response so we
94
99
  * don't accidentally buffer a large file.
95
100
  */
96
- async function fetchUrlForSniff(url: string, originalSource: MediaInput): Promise<NormalizedSource> {
101
+ async function fetchUrlForSniff(
102
+ url: string,
103
+ originalSource: MediaInput,
104
+ transport?: TransportConfig,
105
+ ): Promise<NormalizedSource> {
97
106
  const name = url.split("/").pop()?.split("?")[0] ?? undefined;
107
+ const doFetch = fetchWith(transport);
98
108
 
99
109
  // First attempt: Range request for the sniff window.
100
110
  let res: Response;
101
111
  try {
102
- res = await fetch(url, {
112
+ res = await doFetch(url, mergeFetchInit(transport?.requestInit, {
103
113
  headers: { Range: `bytes=0-${URL_SNIFF_RANGE_BYTES - 1}` },
104
- });
114
+ })!);
105
115
  } catch (err) {
106
- throw new Error(`failed to fetch source ${url}: ${(err as Error).message}`);
116
+ throw new AvbridgeError(
117
+ ERR_PROBE_FETCH_FAILED,
118
+ `Failed to fetch source ${url}: ${(err as Error).message}`,
119
+ "Check that the URL is reachable and CORS is configured. If the source requires authentication, pass requestInit with credentials/headers.",
120
+ );
107
121
  }
108
122
  if (!res.ok && res.status !== 206) {
109
- throw new Error(`failed to fetch source ${url}: ${res.status} ${res.statusText}`);
123
+ throw new AvbridgeError(
124
+ ERR_PROBE_FETCH_FAILED,
125
+ `Failed to fetch source ${url}: ${res.status} ${res.statusText}`,
126
+ res.status === 403 || res.status === 401
127
+ ? "The server rejected the request. Pass requestInit with the required Authorization header or credentials."
128
+ : "Check that the URL is correct and the server is reachable.",
129
+ );
110
130
  }
111
131
 
112
132
  // Determine the total file size from Content-Range (preferred) or Content-Length.
@@ -0,0 +1,26 @@
1
+ import type { FetchFn, TransportConfig } from "../types.js";
2
+
3
+ /**
4
+ * Merge two RequestInit objects. Headers are merged (not overwritten) so
5
+ * the caller's auth headers coexist with the player's Range headers.
6
+ * Other fields (credentials, mode, signal, etc.) in `extra` override `base`.
7
+ */
8
+ export function mergeFetchInit(
9
+ base: RequestInit | undefined,
10
+ extra: RequestInit | undefined,
11
+ ): RequestInit | undefined {
12
+ if (!base && !extra) return undefined;
13
+ return {
14
+ ...base,
15
+ ...extra,
16
+ headers: {
17
+ ...(base?.headers as Record<string, string> | undefined ?? {}),
18
+ ...(extra?.headers as Record<string, string> | undefined ?? {}),
19
+ },
20
+ };
21
+ }
22
+
23
+ /** Return the fetch function from a TransportConfig, falling back to globalThis.fetch. */
24
+ export function fetchWith(transport?: TransportConfig): FetchFn {
25
+ return transport?.fetchFn ?? globalThis.fetch;
26
+ }