{"version":3,"sources":["../src/util/libav-http-reader.ts"],"names":[],"mappings":"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","file":"chunk-YPZFGJV3.cjs","sourcesContent":["/**\n * libav.js HTTP block reader.\n *\n * Wraps `libav.mkblockreaderdev` + `libav.onblockread` +\n * `libav.ff_block_reader_dev_send` so that libav can demux a remote file\n * via HTTP Range requests instead of needing the entire file in memory.\n *\n * Used by the AVI/ASF/FLV probe path and the libav-backed playback /\n * conversion strategies whenever the source is a URL.\n *\n * Design notes:\n *\n * - **Range support detection** is done by issuing a `Range: bytes=0-0`\n *   probe request. We do NOT trust `Accept-Ranges` headers — some servers\n *   support ranges but don't advertise them, others advertise but don't.\n *   The probe request is the canonical signal: a `206 Partial Content`\n *   response means we can stream; anything else fails fast with a clear\n *   error. We never silently fall back to a full download.\n *\n * - **Sequential reads.** libav can issue overlapping `onblockread`\n *   callbacks. The reader serializes them through a single async queue\n *   so a) `ff_block_reader_dev_send` calls are well-ordered and b) we\n *   never have two in-flight fetches for unrelated reads. Throughput\n *   for v1 is \"good enough\"; correctness > parallelism.\n *\n * - **In-flight dedup.** If libav asks for `(pos=1000, len=4096)` twice\n *   in a row before the first request resolves, the second call awaits\n *   the first instead of issuing a duplicate fetch. This handles the\n *   \"demuxer re-reads the same header\" pattern cheaply.\n *\n * - **Read-ahead clamp.** libav's requested length is doubled, then\n *   clamped to `[256 KB, 1 MB]`. Small reads get amortized; pathological\n *   large requests don't OOM us.\n *\n * - **LRU block cache.** Fetched blocks are kept in a Map keyed by start\n *   position, bounded by a byte budget (default 8 MB, configurable via\n *   `cacheBytes`). Map insertion order doubles as recency; re-accessing\n *   a block promotes it via delete+set. Eviction walks oldest-first\n *   until total bytes fit the budget. Typical seek pattern has three\n *   hot regions — header/moov at the front, index at the tail, current\n *   read position — all of which fit comfortably under the default.\n *\n * - **Safe detach.** `detach()` clears `libav.onblockread`, sets a\n *   destroyed flag, and ignores any in-flight fetch resolutions so we\n *   never write into a torn-down demuxer.\n */\n\nconst MIN_READ = 256 * 1024;\nconst MAX_READ = 1 * 1024 * 1024;\nconst DEFAULT_CACHE_BYTES = 8 * 1024 * 1024;\n\ninterface LibavLike {\n  mkblockreaderdev(name: string, size: number): Promise<void>;\n  unlinkreadaheadfile(name: string): Promise<void>;\n  ff_block_reader_dev_send(\n    name: string,\n    pos: number,\n    data: Uint8Array | null,\n    opts?: { errorCode?: number; error?: unknown },\n  ): Promise<void>;\n  onblockread?: (filename: string, pos: number, length: number) => void;\n}\n\nexport interface LibavHttpReaderHandle {\n  /** Total file size (bytes) reported by the server. */\n  readonly size: number;\n  /** Always `\"http-range\"` for now. Reserved for future transports. */\n  readonly transport: \"http-range\";\n  /** Stop serving reads, clear the libav callback, and ignore late fetches. */\n  detach(): Promise<void>;\n}\n\nexport interface AttachLibavHttpReaderOptions {\n  /** Optional `RequestInit` extras (mode, credentials, headers, etc.). */\n  requestInit?: RequestInit;\n  /** Override fetch (for testing). Defaults to globalThis.fetch. */\n  fetchFn?: typeof fetch;\n  /**\n   * Byte budget for the LRU block cache. Defaults to 8 MB. Set to `0`\n   * to disable caching. Raise this (e.g. 32 MB) for apps that play\n   * seek-heavy legacy-container media over the network.\n   */\n  cacheBytes?: number;\n}\n\n/**\n * Result of preparing a libav-readable file from a normalized source.\n * Either an in-memory Blob (created via `mkreadaheadfile`) or a streaming\n * HTTP reader (created via `attachLibavHttpReader`). Callers should\n * `await detach()` when done so resources are cleaned up symmetrically.\n */\nexport interface LibavInputHandle {\n  /** The virtual filename libav sees — pass to `ff_init_demuxer_file`. */\n  readonly filename: string;\n  /** \"blob\" for in-memory, \"http-range\" for streaming URL. */\n  readonly transport: \"blob\" | \"http-range\";\n  /** Total file size in bytes if known, otherwise undefined. */\n  readonly size: number | undefined;\n  /** Tear down the virtual file (and any HTTP reader state). */\n  detach(): Promise<void>;\n}\n\ninterface LibavLikeWithBlob extends LibavLike {\n  mkreadaheadfile(name: string, blob: Blob): Promise<void>;\n}\n\n/**\n * Convenience for the libav-backed strategies. Given a normalized source,\n * either creates an in-memory readahead file (for Blob inputs) or attaches\n * the HTTP block reader (for URL inputs). Returns a handle the caller\n * should detach when done.\n */\nexport async function prepareLibavInput(\n  libav: LibavLikeWithBlob,\n  filename: string,\n  source: import(\"./source.js\").NormalizedSource,\n  transport?: import(\"../types.js\").TransportConfig,\n): Promise<LibavInputHandle> {\n  if (source.kind === \"url\") {\n    const handle = await attachLibavHttpReader(libav, filename, source.url, {\n      requestInit: transport?.requestInit,\n      fetchFn: transport?.fetchFn,\n      cacheBytes: transport?.cacheBytes,\n    });\n    return {\n      filename,\n      transport: \"http-range\",\n      size: handle.size,\n      detach: () => handle.detach(),\n    };\n  }\n  await libav.mkreadaheadfile(filename, source.blob);\n  return {\n    filename,\n    transport: \"blob\",\n    size: source.byteLength,\n    detach: async () => {\n      try { await libav.unlinkreadaheadfile(filename); } catch { /* ignore */ }\n    },\n  };\n}\n\n/**\n * Attach an HTTP block reader to a libav.js instance. After this resolves,\n * libav can `ff_init_demuxer_file(filename)` and the demuxer will pull\n * bytes via Range requests instead of needing a Blob.\n *\n * Fails fast (before any libav setup) if the server doesn't support\n * Range requests.\n */\nexport async function attachLibavHttpReader(\n  libav: LibavLike,\n  filename: string,\n  url: string,\n  options: AttachLibavHttpReaderOptions = {},\n): Promise<LibavHttpReaderHandle> {\n  const fetchFn = options.fetchFn ?? fetch;\n\n  // 1. Probe the server with a single-byte Range request.\n  let probeRes: Response;\n  try {\n    probeRes = await fetchFn(url, {\n      ...options.requestInit,\n      headers: {\n        ...(options.requestInit?.headers ?? {}),\n        Range: \"bytes=0-0\",\n      },\n    });\n  } catch (err) {\n    throw new Error(\n      `libav HTTP reader: failed to reach ${url}: ${(err as Error).message}`,\n    );\n  }\n  if (probeRes.status !== 206) {\n    // 200 means the server ignored Range and would have sent the whole\n    // file. We refuse to silently slurp gigabytes.\n    throw new Error(\n      `libav HTTP reader: ${url} does not support HTTP Range requests ` +\n      `(server returned ${probeRes.status} for a Range probe; need 206 Partial Content). ` +\n      `Remote AVI/ASF/FLV playback requires a server that honors byte-range requests.`,\n    );\n  }\n\n  // 2. Parse total file size from Content-Range: \"bytes 0-0/12345678\"\n  const contentRange = probeRes.headers.get(\"content-range\") ?? \"\";\n  const sizeMatch = contentRange.match(/\\/(\\d+)$/);\n  if (!sizeMatch) {\n    throw new Error(\n      `libav HTTP reader: ${url} returned 206 but no parseable Content-Range header (got: \"${contentRange}\")`,\n    );\n  }\n  const size = parseInt(sizeMatch[1], 10);\n  if (!Number.isFinite(size) || size <= 0) {\n    throw new Error(\n      `libav HTTP reader: ${url} reported invalid file size ${size}`,\n    );\n  }\n\n  // Drain the probe body so the connection can be reused.\n  try { await probeRes.arrayBuffer(); } catch { /* ignore */ }\n\n  // 3. Create the virtual file libav will read from.\n  await libav.mkblockreaderdev(filename, size);\n\n  // ── State ───────────────────────────────────────────────────────────────\n\n  let detached = false;\n  // LRU cache of fetched blocks, keyed by start position. Map insertion\n  // order = recency. Bounded by `cacheBudget` bytes; evicts oldest-first\n  // on overflow. Set budget to 0 to disable caching.\n  const cache = new Map<number, Uint8Array>();\n  let cacheBytes = 0;\n  const cacheBudget = Math.max(0, options.cacheBytes ?? DEFAULT_CACHE_BYTES);\n  // The currently in-flight fetch, if any. Used both for serialization\n  // (we await this before starting another) and for in-flight dedup.\n  let inflight: Promise<void> | null = null;\n\n  function clampReadLength(requested: number): number {\n    const doubled = requested * 2;\n    if (doubled < MIN_READ) return MIN_READ;\n    if (doubled > MAX_READ) return MAX_READ;\n    return doubled;\n  }\n\n  /**\n   * Look up a cached block that fully covers `[pos, pos+length)`. On hit,\n   * promote the block to most-recent and return the slice. On miss, null.\n   */\n  function cacheLookup(pos: number, length: number): Uint8Array | null {\n    for (const [blockPos, bytes] of cache) {\n      if (pos >= blockPos && pos + length <= blockPos + bytes.byteLength) {\n        cache.delete(blockPos);\n        cache.set(blockPos, bytes);\n        const offset = pos - blockPos;\n        return bytes.subarray(offset, offset + length);\n      }\n    }\n    return null;\n  }\n\n  /** Insert a fetched block; evict least-recently-used until under budget. */\n  function cacheInsert(pos: number, bytes: Uint8Array): void {\n    const existing = cache.get(pos);\n    if (existing) {\n      cacheBytes -= existing.byteLength;\n      cache.delete(pos);\n    }\n    cache.set(pos, bytes);\n    cacheBytes += bytes.byteLength;\n    while (cacheBytes > cacheBudget && cache.size > 0) {\n      const oldestKey = cache.keys().next().value as number | undefined;\n      if (oldestKey === undefined) break;\n      const oldest = cache.get(oldestKey);\n      if (!oldest) break;\n      cache.delete(oldestKey);\n      cacheBytes -= oldest.byteLength;\n    }\n  }\n\n  /** Fetch one Range and update the cache. */\n  async function fetchRange(pos: number, length: number): Promise<Uint8Array> {\n    const end = Math.min(pos + length - 1, size - 1);\n    const res = await fetchFn(url, {\n      ...options.requestInit,\n      headers: {\n        ...(options.requestInit?.headers ?? {}),\n        Range: `bytes=${pos}-${end}`,\n      },\n    });\n    if (res.status !== 206 && res.status !== 200) {\n      throw new Error(\n        `libav HTTP reader: Range request bytes=${pos}-${end} returned ${res.status}`,\n      );\n    }\n    const buf = new Uint8Array(await res.arrayBuffer());\n    cacheInsert(pos, buf);\n    return buf;\n  }\n\n  /**\n   * Handle a single libav read request. Serializes against any in-flight\n   * read by chaining off `inflight`. Honors `detached` at every async\n   * boundary so a torn-down reader never writes back into libav.\n   */\n  async function handleRead(name: string, pos: number, length: number): Promise<void> {\n    // Wait for any preceding read to finish so we don't interleave.\n    if (inflight) {\n      try { await inflight; } catch { /* ignore — that read's own caller handled it */ }\n    }\n    if (detached) return;\n\n    // Cache hit — reply directly without a network round-trip.\n    const hit = cacheLookup(pos, length);\n    if (hit) {\n      try { await libav.ff_block_reader_dev_send(name, pos, hit); } catch { /* ignore — libav may have torn down */ }\n      return;\n    }\n\n    // Cache miss — fetch via Range. Read-ahead amortizes small reads.\n    const fetchLen = clampReadLength(length);\n    const fetched = (async () => {\n      try {\n        const buf = await fetchRange(pos, fetchLen);\n        if (detached) return;\n        // Slice exactly what libav asked for and send it back.\n        const reply = buf.subarray(0, Math.min(length, buf.byteLength));\n        try { await libav.ff_block_reader_dev_send(name, pos, reply); } catch { /* ignore */ }\n      } catch (err) {\n        if (detached) return;\n        // Signal EOF + error code to libav so the demuxer surfaces it.\n        try {\n          await libav.ff_block_reader_dev_send(name, pos, null, {\n            error: err,\n          });\n        } catch { /* ignore */ }\n      }\n    })();\n    inflight = fetched;\n    try { await fetched; } finally { if (inflight === fetched) inflight = null; }\n  }\n\n  // 4. Wire the callback. The signature accepts `(name, pos, length)` and\n  // we hand it to handleRead which does all the work asynchronously.\n  // Note: libav.js dispatches this synchronously from a worker message,\n  // so we kick off handleRead but don't await — the queue inside handleRead\n  // serializes things.\n  const previousCallback = libav.onblockread;\n  libav.onblockread = (name: string, pos: number, length: number) => {\n    if (detached || name !== filename) {\n      // Forward to any previous callback (e.g. another reader on the same\n      // libav instance). This is rare in practice but cheap to support.\n      previousCallback?.(name, pos, length);\n      return;\n    }\n    void handleRead(name, pos, length);\n  };\n\n  return {\n    size,\n    transport: \"http-range\",\n    async detach() {\n      if (detached) return;\n      detached = true;\n      // Restore the previous callback (if any) so we don't break unrelated\n      // readers on the same libav instance.\n      libav.onblockread = previousCallback;\n      // Wait for the last in-flight read to settle so we don't tear down\n      // the virtual file while libav is still expecting a response.\n      if (inflight) {\n        try { await inflight; } catch { /* ignore */ }\n      }\n      // Drop the cache and unlink the virtual file.\n      cache.clear();\n      cacheBytes = 0;\n      try { await libav.unlinkreadaheadfile(filename); } catch { /* ignore */ }\n    },\n  };\n}\n"]}