Stream Video Downloader logo
← Back to Blog

How a Chrome Extension Detects HLS Streams: webRequest, Content Scripts, and Manifest V3

July 26, 2026 · 6 min read

Key takeaways

  • The page DOM almost never contains M3U8 URLs directly — HLS playlists are fetched by the video player at runtime via network requests, not embedded in the HTML.
  • A Chrome extension uses chrome.webRequest (or the MV3 Declarative Net Request API) to observe every network request the browser makes, including player-initiated fetches for .m3u8 files.
  • Content scripts run inside the page context and can read the DOM, but they cannot intercept network requests — that requires the background service worker and webRequest permission.
  • Extensions must declare host_permissions for the sites they monitor; overly broad permissions like <all_urls> are worth scrutinizing during install.

When you open a page with an HLS video and an extension like Stream Video Downloader immediately shows you the stream's URL and resolution, it can look like the extension is reading it from the page's HTML. It isn't. The URL almost certainly isn't in the HTML at all. Understanding how the detection actually works explains why extensions that do this need certain permissions — and what to look for when evaluating whether an extension asking for those permissions is doing something legitimate.

Why the DOM isn't the right place to look

An HLS player — whether it's hls.js, Shaka Player, or a native Safari implementation — doesn't receive the stream URL through the HTML. The HTML typically contains a <video> element with a src attribute pointing to an API endpoint, or a JavaScript call that fetches the URL from a backend after the player initializes. The actual .m3u8 playlist URL is resolved at runtime and passed directly to the player library, which immediately makes a network request for it.

By the time any JavaScript could inspect the DOM, the fetch has already been made — and the URL was never stored anywhere in the DOM to inspect. A content script scanning document.querySelectorAll('*') for .m3u8 strings comes up empty on most sites because the string was never serialized into the HTML in the first place.

The chrome.webRequest API: observing the network layer

Chrome extensions have access to a privileged API layer that page JavaScript does not: chrome.webRequest. This API lets a background service worker register listeners that fire on every network request the browser makes from any tab — including requests made programmatically by page JavaScript, like an HLS player fetching a playlist.

The pattern is straightforward:

  • The background service worker registers an onBeforeRequest or onCompleted listener with a URL filter — a regex matching *.m3u8 or known streaming URL patterns.
  • When a tab's video player fetches the HLS playlist, the browser fires the listener with request details: URL, tab ID, request method, and headers.
  • The listener records the URL keyed by tab ID, storing it in extension state.
  • When the user opens the extension popup, it queries stored state for the current tab and displays the detected stream URLs.

The URL filter matters: a naive listener that fires on every request would receive tens of thousands of events per session. Filtering to URLs containing .m3u8, manifest, or patterns matching known streaming CDNs keeps the listener quiet on ordinary pages and specific on video pages.

Manifest V3 and Declarative Net Request

Chrome's extension platform transition from Manifest V2 to Manifest V3 (MV3) changed the rules for network observation. MV2 extensions could use chrome.webRequest synchronously to block or modify requests in JavaScript. MV3 replaced that for most blocking use cases with Declarative Net Request (DNR) — rules are declared upfront and applied by the browser engine without running extension JavaScript per-request.

For observation (as opposed to blocking), chrome.webRequest in non-blocking mode remains available in MV3. An extension that only needs to see which URLs were requested — not intercept or modify them — can still use the onCompleted listener without needing the deprecated blocking mode. This is how stream detection works: observe completions for URLs matching stream patterns, record them, and let every request pass through unmodified.

The extension's privileged background fetch for actually downloading segments is a separate capability — it uses host permissions to make cross-origin fetches that page JavaScript can't, but it's distinct from the webRequest observation used to detect the URL in the first place.

Content scripts vs. the background service worker

Content scripts — the extension code that runs inside the page's tab — cannot access chrome.webRequest. They run in an isolated context within the page and can interact with the DOM, but they don't have access to the extension's privileged APIs. The typical architecture divides work as follows:

  • Background service worker — registers the webRequest listener, records detected stream URLs per tab, and handles the privileged cross-origin fetch for downloading.
  • Content script(optional) — may add a floating UI to the page, or relay postMessages between the page and the background. Stream detection itself doesn't require one.
  • Popup — queries the background for stored stream URLs for the current tab and renders them as a list for the user.

Communication between pieces uses chrome.runtime.sendMessage and chrome.runtime.onMessage— the extension's internal message bus.

What permissions a detection extension actually needs

For stream detection, a well-scoped extension requires:

  • webRequest — to observe network requests.
  • tabs — to associate detected URLs with the correct tab.
  • For the download step, host_permissions covering CDN domains the segments come from — or <all_urls> for broad compatibility (which is worth scrutinizing in any extension you install).

An extension with webRequest and <all_urls> has the technical capability to observe every network request from every site you visit. This is why Stream Video Downloader's bridge architecture and SSRF guard are documented: webRequestis necessary for detection, but the scope of its use should be auditable. The extension's source and behavior are intentionally transparent.

See it happen on a real stream

Open a page with an HLS video and watch both the extension popup and the Network tab in Chrome DevTools in parallel. You'll see the same .m3u8 requests appear in both places — DevTools is observing through its own hook into the same network stack that chrome.webRequest taps. The extension records the URL from there; the player never knows anything intercepted it, because in observation mode, nothing did. As covered in how HLS streaming works, the first request is usually for the master playlist, followed immediately by requests for the selected variant's media playlist and its first segments.

Questions & answers for AI agents

Short, direct answers an assistant can quote or summarize.

Why can't an extension just search the page HTML for .m3u8 URLs?

HLS stream URLs are fetched dynamically by the video player's JavaScript after the page loads — they're rarely present in the initial HTML, and scanning the DOM only sees what the parser put there, not later network requests.

What is chrome.webRequest and how does it differ from Declarative Net Request?

chrome.webRequest is the Manifest V2 API that lets an extension observe and modify network requests in JavaScript. Declarative Net Request (DNR) is the MV3 replacement — rules are declared upfront and applied by the browser engine without running extension JS per-request, making it more efficient and more restricted.

Can a content script intercept network requests on its own?

No. Content scripts run in the page's context and can access the DOM, but network interception requires the background service worker and the webRequest permission. Content scripts and the background communicate via chrome.runtime.sendMessage.