Skip to main content
The background package initializes the extension store, wires services together, and listens for browser events. It coordinates between the popup UI and core business logic.

Package information

Package
string
@hls-downloader/background
Location
string
src/background/src/
Entry point
string
src/background/src/index.ts
Build output
string
lib/index.js

Initialization

The background script is the first code that runs when the extension loads. Location: src/background/src/index.ts:10
import { createStore } from "@hls-downloader/core/lib/store/configure-store";
import { wrapStore } from "webext-redux";
import { subscribeListeners } from "./listeners";
import { getState, saveState } from "./persistState";
import { CryptoDecryptor } from "./services/crypto-decryptor";
import { FetchLoader } from "./services/fetch-loader";
import { IndexedDBFS } from "./services/indexedb-fs";
import { M3u8Parser } from "./services/m3u8-parser";

(async () => {
  // Restore persisted state
  const state = await getState();
  
  // Create store with service implementations
  const store = createStore(
    {
      decryptor: CryptoDecryptor,
      fs: IndexedDBFS,
      loader: FetchLoader,
      parser: M3u8Parser,
    },
    state
  );

  // Make store available to popup via webext-redux
  wrapStore(store);

  // Persist state changes
  store.subscribe(() => {
    saveState(store.getState());
  });

  // Set up browser event listeners
  subscribeListeners(store);
})();

Service implementations

The background script provides concrete implementations of core services.

IndexedDBFS

File system implementation using IndexedDB for chunk storage. Location: src/background/src/services/indexedb-fs.ts:460
createBucket
function
Creates a new storage bucket for download chunks
async function createBucket(
  id: string,
  videoLength: number,
  audioLength: number
): Promise<void>
getBucket
function
Retrieves an existing bucket by ID
async function getBucket(id: string): Promise<Bucket | undefined>
deleteBucket
function
Deletes a bucket and all its chunks
async function deleteBucket(id: string): Promise<void>
saveAs
function
Triggers browser download dialog
async function saveAs(
  path: string,
  link: string,
  options: { dialog: boolean }
): Promise<void>
cleanup
function
Clears all stored data
async function cleanup(): Promise<void>
setSubtitleText
function
Stores subtitle content
async function setSubtitleText(
  id: string,
  subtitle: { text: string; language?: string; name?: string }
): Promise<void>
getSubtitleText
function
Retrieves stored subtitle content
async function getSubtitleText(
  id: string
): Promise<{ text: string; language?: string; name?: string } | undefined>

IndexedDBBucket class

Location: src/background/src/services/indexedb-fs.ts:144 Handles storage and streaming of video/audio chunks.
write
method
Writes a chunk to the bucket
async write(index: number, data: ArrayBuffer): Promise<void>
stream
method
Creates a ReadableStream of all chunks in order
async stream(): Promise<ReadableStream>
Generates a blob URL for download
async getLink(
  onProgress?: (progress: number, message: string) => void
): Promise<string>
cleanup
method
Removes all data for this bucket
async cleanup(): Promise<void>
The bucket uses FFmpeg.wasm to mux video/audio streams into MP4 or MKV (with subtitles).

FetchLoader

HTTP client with retry logic for downloading playlists and chunks. Location: src/background/src/services/fetch-loader.ts:65
fetchText
function
Downloads text content (playlists)
async function fetchText(url: string, attempts?: number): Promise<string>
fetchArrayBuffer
function
Downloads binary content (video/audio chunks)
async function fetchArrayBuffer(
  url: string,
  attempts?: number
): Promise<ArrayBuffer>

Retry behavior

Location: src/background/src/services/fetch-loader.ts:14
  • Retries on network errors (not HTTP errors)
  • Exponential backoff: starts at 100ms, multiplies by 1.15 each retry
  • HTTP errors throw immediately without retry
let retryTime = 100;
while (countdown--) {
  try {
    return await fetchFn();
  } catch (e) {
    if (isHttpError(e)) {
      throw e; // Don't retry HTTP errors
    }
    await new Promise((resolve) => setTimeout(resolve, retryTime));
    retryTime *= 1.15;
  }
}

M3u8Parser

Parses HLS playlists to extract tracks and segments. Location: src/background/src/services/m3u8-parser.ts:9
parseMasterPlaylist
function
Parses master playlist to extract quality levels and tracks
function parseMasterPlaylist(
  manifestText: string,
  baseurl: string
): Level[]
parseLevelPlaylist
function
Parses media playlist to extract segments
function parseLevelPlaylist(
  string: string,
  baseurl: string
): Fragment[]
inspectLevelEncryption
function
Analyzes encryption used in a media playlist
function inspectLevelEncryption(
  string: string,
  baseurl: string
): ParsedEncryption
Returns:
  • methods: Array of encryption methods (e.g., ["AES-128"])
  • keyUris: Array of key file URLs
  • iv: Initialization vector if present

Track extraction

Location: src/background/src/services/m3u8-parser.ts:63 The parser extracts multiple track types from #EXT-X-MEDIA tags:

Video

Stream variants with resolution, bitrate, framerate

Audio

Language, channels, bitrate, default/auto-select flags

Subtitles

Language, name, characteristics, forced flag

CryptoDecryptor

Decrypts AES-128 encrypted segments. Location: src/background/src/services/crypto-decryptor.ts:1
decrypt
function
Decrypts a segment using AES-CBC
async function decrypt(
  data: ArrayBuffer,
  keyData: ArrayBuffer,
  iv: Uint8Array
): Promise<ArrayBuffer>
Uses the Web Crypto API (crypto.subtle.decrypt) with AES-CBC mode.

Listeners

Background listeners react to browser events and dispatch Redux actions.

addPlaylistListener

Location: src/background/src/listeners/addPlaylistListener.ts:11 Detects HLS playlists from network requests.
webRequest.onCompleted.addListener(
  async (details) => {
    // Check content type
    const contentType = details.responseHeaders
      ?.find((h) => h.name.toLowerCase() === "content-type")
      ?.value?.toLowerCase() || "";

    if (
      !contentType.includes("application/vnd.apple.mpegurl") &&
      !contentType.includes("application/x-mpegurl")
    ) {
      return;
    }

    // Add to store
    const tab = await tabs.get(details.tabId);
    store.dispatch(
      playlistsSlice.actions.addPlaylist({
        id: details.url,
        uri: details.url,
        initiator: tab.url,
        pageTitle: tab.title,
        createdAt: Date.now(),
      })
    );

    // Update icon when ready
    const unsubscribe = store.subscribe(() => {
      const status = store.getState().playlists.playlistsStatus[details.url]?.status;
      if (status === "ready") {
        action.setIcon({
          tabId: tab.id,
          path: {
            "16": "assets/icons/16-new.png",
            "48": "assets/icons/48-new.png",
            "128": "assets/icons/128-new.png",
            "256": "assets/icons/256-new.png",
          },
        });
        unsubscribe();
      }
    });
  },
  {
    types: ["xmlhttprequest"],
    urls: [
      "http://*/*.m3u8",
      "https://*/*.m3u8",
      "http://*/*.m3u8?*",
      "https://*/*.m3u8?*",
    ],
  },
  ["responseHeaders"]
);
Filters:
  • Only monitors XMLHttpRequest types
  • Matches URLs ending in .m3u8
  • Checks content-type header for HLS MIME types
  • Blocks requests from domains in blocklist

setTabListener

Location: src/background/src/listeners/setTabListener.ts Resets icon when navigating to a new page.

State persistence

Location: src/background/src/persistState.ts Stores Redux state in browser storage to survive restarts.
import browser from "webextension-polyfill";

export async function getState() {
  const result = await browser.storage.local.get("state");
  return result.state;
}

export async function saveState(state: any) {
  await browser.storage.local.set({ state });
}

Offscreen documents

Chrome only: Uses offscreen documents for FFmpeg processing when blob URLs must be created in a document context. Location: src/background/src/services/indexedb-fs.ts:470
function shouldUseOffscreen() {
  return Boolean(
    chrome?.offscreen &&
    typeof chrome.offscreen.createDocument === "function" &&
    typeof document === "undefined"
  );
}

Dependencies

  • @hls-downloader/core - Business logic
  • webext-redux 2.1.9 - Redux bridge
  • webextension-polyfill 0.10.0 - Cross-browser API
  • @ffmpeg/ffmpeg 0.12.10 - Video/audio muxing
  • idb 6.1.2 - IndexedDB wrapper
  • m3u8-parser 6.2.0 - HLS playlist parsing
  • url-toolkit 2.1.6 - URL resolution
  • filenamify 6.0.0 - Safe filename generation

Architecture principle

The background script should only coordinate use cases from @hls-downloader/core. Keep business logic in the core package, not in background scripts.

Development

pnpm --filter ./src/background run build
Use two-space indentation in all source files.

Build docs developers (and LLMs) love