// Streaming-media detection, background half. docs/05 §3: sniff `.m3u8`/`.mpd` and the // HLS/DASH content types on the wire, then tell the tab's content script a manifest was // seen. The extension never parses the manifest itself — media.listVariants (daemon-side) // does that; this module only recognizes "this response is probably a manifest". const MANIFEST_EXTENSIONS = ['.m3u8', '.mpd']; const MANIFEST_CONTENT_TYPES = [ 'application/vnd.apple.mpegurl', 'application/x-mpegurl', 'audio/mpegurl', 'application/dash+xml', ]; /** Extension check ignores the query string — `manifest.m3u8?token=…` still counts. */ export function isManifestUrl(url: string): boolean { let pathname: string; try { pathname = new URL(url).pathname.toLowerCase(); } catch { return false; } return MANIFEST_EXTENSIONS.some((ext) => pathname.endsWith(ext)); } export function isManifestContentType(contentType: string | null | undefined): boolean { if (!contentType) return false; const base = contentType.split(';')[0]!.trim().toLowerCase(); return MANIFEST_CONTENT_TYPES.includes(base); } export function looksLikeManifest(url: string, contentType: string | null | undefined): boolean { return isManifestUrl(url) || isManifestContentType(contentType); } export interface DetectedManifest { tabId: number; url: string; headers: Array<{ name: string; value: string }>; } interface HeadersReceivedDetails { tabId: number; url: string; responseHeaders?: Array<{ name: string; value: string }>; } export interface MediaWebRequest { onHeadersReceived: { addListener( cb: (details: HeadersReceivedDetails) => void, filter: { urls: string[]; types?: string[] }, extraInfoSpec?: string[], ): void; }; } /** * Watches responses for HLS/DASH manifests and reports each hit once per (tab, url) — * a page can request the same manifest repeatedly (HLS live-refresh) and the content * script only needs to hear about it once to show the panel. */ export class MediaWatcher { private readonly seen = new Set(); constructor(private readonly onDetected: (m: DetectedManifest) => void) {} attach(webRequest: MediaWebRequest): void { webRequest.onHeadersReceived.addListener( (details) => this.handle(details), { urls: [''] }, ['responseHeaders'], ); } private handle(details: HeadersReceivedDetails): void { if (details.tabId < 0) return; // not a request associated with any tab const headers = details.responseHeaders ?? []; const contentType = headers.find((h) => h.name.toLowerCase() === 'content-type')?.value; if (!looksLikeManifest(details.url, contentType)) return; const key = `${details.tabId}:${details.url}`; if (this.seen.has(key)) return; this.seen.add(key); this.onDetected({ tabId: details.tabId, url: details.url, headers }); } }