diff --git a/__tests__/hls_qa.test.mjs b/__tests__/hls_qa.test.mjs index a3bca40..32b5557 100644 --- a/__tests__/hls_qa.test.mjs +++ b/__tests__/hls_qa.test.mjs @@ -753,19 +753,21 @@ describe('HLS QA: Deep probe strategies', () => { }); const win = dom.window; - // Mock Hls with cast API + // Real players attach the HLS instance on the video element as a custom property. + // Hls.cast() is a TypeScript identity cast in hls.js (returns input unchanged), + // NOT a reverse-lookup — so we test what actually works in practice. const video = win.document.getElementById('v'); + video.__hlsInstance = { url: 'https://cdn.example.com/video.m3u8' }; win.Hls = class { - static cast(v) { - if (v === video) return { url: 'https://cdn.example.com/cast.m3u8' }; - return null; - } + // cast() is a no-op identity function in the real hls.js library + static cast(v) { return v; } }; const results = runProbeInWin(win); - const found = results.find(r => r.player === 'Hls.cast'); - if (!found) throw new Error('Hls.cast manifest not detected. Results: ' + JSON.stringify(results)); + // Should detect via video element property scan (Strategy B1) + const found = results.find(r => r.player && r.player.includes('video.') && r.url.includes('.m3u8')); + if (!found) throw new Error('Video element HLS instance not detected. Results: ' + JSON.stringify(results)); }); it('should prefer interceptor-collected manifests over inline-script', () => { diff --git a/src/inject/interceptors.js b/src/inject/interceptors.js index 04462fe..d544727 100644 --- a/src/inject/interceptors.js +++ b/src/inject/interceptors.js @@ -34,6 +34,13 @@ } } + // Helper: check if a URL looks like an HLS manifest + function isManifestUrl(url) { + var u = url.toLowerCase(); + return u.includes('.m3u8') || u.includes('.hlsvod') || + u.includes('/hls/') || u.includes('/playlist') || u.includes('/manifest'); + } + // Helper: report manifest to content script via postMessage function reportManifest(url, source) { var resolved = resolveUrl(url); @@ -64,12 +71,7 @@ XMLHttpRequest.prototype.send = function (_body) { if (this._goon_url && typeof this._goon_url === 'string') { var resolved = resolveUrl(this._goon_url); - if ( - resolved && - (resolved.includes('.m3u8') || - resolved.includes('/playlist') || - resolved.includes('/manifest')) - ) { + if (resolved && isManifestUrl(resolved)) { reportManifest(resolved, 'xhr'); } } @@ -82,12 +84,7 @@ var url = typeof resource === 'string' ? resource : resource && resource.url; if (url && typeof url === 'string') { var resolved = resolveUrl(url); - if ( - resolved && - (resolved.includes('.m3u8') || - resolved.includes('/playlist') || - resolved.includes('/manifest')) - ) { + if (resolved && isManifestUrl(resolved)) { reportManifest(resolved, 'fetch'); } } @@ -95,8 +92,12 @@ }; // 3. Intercept HLS.js loadSource (catches HLS.js instances) + // Guard against double-patching: only wrap if the method hasn't been patched yet. + var hlsPatched = false; function patchHlsPrototype() { if (typeof Hls === 'undefined') return; + if (hlsPatched) return; // Already patched — skip + var originalLoadSource = Hls.prototype.loadSource; if (!originalLoadSource) return; @@ -115,6 +116,7 @@ return originalAttachMedia.apply(this, arguments); }; } + hlsPatched = true; } // Patch immediately if HLS is already defined @@ -124,7 +126,6 @@ // Use a combination of: // 1. Longer polling window (30s instead of 3s) to catch lazy-loaded players. // 2. MutationObserver on
for new