fix: real-time HLS interception via content script (architectural overhaul)
CI / test (pull_request) Canceled after 0s

Previous approach (DOM snapshot via executeScript) failed on Pornhub because:
- Video player loads asynchronously via JavaScript
- HLS manifest arrives via XHR/fetch AFTER page render
- <video> element only shows blob: URL, not the actual manifest

New approach:
- Content script injects interceptor into MAIN world at document_start
- Interceptor patches XMLHttpRequest and fetch() to capture .m3u8 URLs
- Periodic DOM scans detect HLS player instances on <video> elements
- Global config object scanning for embedded stream URLs
- Popup queries content script via chrome.tabs.sendMessage for real-time data
- If no DOM videos found but content script has data, create entries from intercepted sources

Changes:
- content.js: Full rewrite with main-world injection + XHR/fetch interception
- index.js: Query content scripts via messages, merge with DOM snapshot
- manifest.json: all_frames + document_start for early injection
This commit is contained in:
hermes committed 2026-08-10 18:53:27 +00:00
1 parent db52db7799
commit c22631d1f1
3 files changed
+354 -72

No files matched your search

+2 -1
View File
@@ -27,7 +27,8 @@
{ {
"matches": ["<all_urls>"], "matches": ["<all_urls>"],
"js": ["content.js"], "js": ["content.js"],
"run_at": "document_idle" "run_at": "document_start",
"all_frames": true
} }
], ],
"icons": { "icons": {
+280 -10
View File
@@ -1,19 +1,289 @@
/** /**
* content.js — Content script injected into matching pages. * content.js — Content script injected into matching pages.
* Communicates with the background service worker via chrome.runtime. * Runs in the ISOLATED world. Coordinates scanning by injecting a script
* Use cautiously: runs in the page's DOM context, so isolate from page globals. * into the MAIN world (which CAN intercept XHR/fetch), then collects results
* via postMessage.
*
* Architecture:
* 1. Inject interceptor.js into MAIN world (intercepts network + DOM)
* 2. Interceptor posts results back via window.postMessage
* 3. This script collects results and forwards to the popup
*/ */
(function () { (function () {
'use strict'; 'use strict';
console.log('[goon-plugin] Content script loaded'); // Collect results from injected scripts
let collectedData = {
hlsManifests: new Set(),
dashManifests: new Set(),
videoSources: [],
posterUrl: null,
};
// Example: inject a small element into the page. let pollingActive = false;
// Uncomment and customize as needed. let pollTimer = null;
//
// const badge = document.createElement('div'); /**
// badge.textContent = 'Goon Plugin Active'; * Inject the interceptor script into the MAIN world to intercept XHR/fetch.
// badge.style.cssText = 'position:fixed;bottom:8px;right:8px;padding:4px 8px;...'; */
// document.body.appendChild(badge); function injectInterceptor() {
const interceptorCode = `
(function() {
// Already injected
if (window.__goonInterceptor) return;
window.__goonInterceptor = true;
const manifests = { hls: new Set(), dash: new Set() };
let scanCount = 0;
// --- XHR interception ---
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this.__goonUrl = typeof url === 'string' ? url : url.toString();
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
const url = this.__goonUrl || '';
if (url.includes('.m3u8') || url.includes('/hls/')) {
manifests.hls.add(url);
}
if (url.includes('.mpd') || url.includes('/dash/')) {
manifests.dash.add(url);
}
this.addEventListener('load', function() {
try {
const resp = this.responseText;
if (resp && typeof resp === 'string') {
const m3u8Re = /https?:[^"'\\s]+\\.m3u8[^"'\\s]*/gi;
let m;
while ((m = m3u8Re.exec(resp)) !== null) {
const cleaned = m[0].replace(/[;,]$/, '');
if (!cleaned.includes('/subtitle') && !cleaned.includes('/captions') && !cleaned.includes('.m3u8x')) {
manifests.hls.add(cleaned);
}
}
}
} catch(e) {}
});
return origSend.apply(this, arguments);
};
// --- Fetch interception ---
const origFetch = window.fetch;
window.fetch = function(input, init) {
const url = typeof input === 'string' ? input : (input && input.url ? input.url.toString() : '');
if (url.includes('.m3u8') || url.includes('/hls/')) manifests.hls.add(url);
if (url.includes('.mpd') || url.includes('/dash/')) manifests.dash.add(url);
return origFetch.apply(this, arguments).then(function(resp) {
if (resp && resp.clone) {
resp.clone().text().then(function(text) {
if (text && typeof text === 'string') {
const m3u8Re = /https?:[^"'\\s]+\\.m3u8[^"'\\s]*/gi;
let m;
while ((m = m3u8Re.exec(text)) !== null) {
const cleaned = m[0].replace(/[;,]$/, '');
if (!cleaned.includes('/subtitle') && !cleaned.includes('/captions') && !cleaned.includes('.m3u8x')) {
manifests.hls.add(cleaned);
}
}
}
}).catch(function(){});
}
return resp;
}).catch(function(e) { return origFetch(input, init); });
};
// --- Scan for HLS instances on video elements ---
function scanVideos() {
scanCount++;
const videos = document.querySelectorAll('video');
const sources = [];
const hlsProps = ['hls', 'hlsInstance', '_hls', '__hls', 'player', '_player', '__player__'];
for (const v of videos) {
let src = v.currentSrc || v.src || '';
let poster = v.poster || '';
let hlsUrl = null;
// Check HLS instances
for (const prop of hlsProps) {
if (v[prop] && typeof v[prop] === 'object') {
const obj = v[prop];
if (obj.url) hlsUrl = obj.url;
else if (obj.opts && obj.opts.url) hlsUrl = obj.opts.url;
else if (obj.config && obj.config.url) hlsUrl = obj.config.url;
else if (obj.levels && obj.levels.length > 0) hlsUrl = obj.levels[0].uri;
else if (obj.sourceLevels && obj.sourceLevels.length > 0) hlsUrl = obj.sourceLevels[0].uri;
if (hlsUrl) break;
}
}
if (hlsUrl) manifests.hls.add(hlsUrl);
sources.push({
src: src,
poster: poster,
width: v.clientWidth || 0,
height: v.clientHeight || 0,
isBlob: src.startsWith('blob:'),
});
}
// --- Scan inline scripts for embedded URLs ---
const m3u8Re = /https?:[^"'\\s]+\\.m3u8[^"'\\s]*/gi;
for (const script of document.querySelectorAll('script')) {
if (script.src) continue;
const text = script.textContent;
if (!text) continue;
let m;
while ((m = m3u8Re.exec(text)) !== null) {
const cleaned = m[0].replace(/[;,]$/, '');
if (!cleaned.includes('/subtitle') && !cleaned.includes('/captions') && !cleaned.includes('.m3u8x')) {
manifests.hls.add(cleaned);
}
}
}
// --- Scan global config objects ---
const configVars = ['hlsConfig', 'hls_player', 'hlsOptions', 'playerConfig', 'streamConfig',
'videoConfig', 'playerOpts', 'phPlayer', 'playerData', 'videoData', 'streamData',
'cloudflareConfig', 'cfPlayer', 'cfStream', 'videoSource', '__videojs__', 'videojs'];
for (const vname of configVars) {
try {
const obj = window[vname];
if (obj && typeof obj === 'object') {
function deepFind(o, depth) {
if (!o || typeof o !== 'object' || depth > 4) return;
for (const k of ['src', 'url', 'source', 'manifest', 'hls', 'm3u8', 'file', 'stream', 'video', 'data']) {
if (o[k]) {
if (typeof o[k] === 'string') {
if (o[k].includes('.m3u8')) manifests.hls.add(o[k]);
if (o[k].includes('.mpd')) manifests.dash.add(o[k]);
} else {
deepFind(o[k], depth + 1);
}
}
}
}
deepFind(obj, 0);
}
} catch(e) {}
}
// Post results to content script via postMessage
window.postMessage({
type: '__GOON_SCAN_RESULT__',
hlsManifests: Array.from(manifests.hls),
dashManifests: Array.from(manifests.dash),
videoSources: sources,
scanCount: scanCount,
}, '*');
}
// Run initial scan + periodic rescans
scanVideos();
setInterval(scanVideos, 1500);
})();
`;
// Inject as a script element into the page (runs in MAIN world)
const script = document.createElement('script');
script.textContent = interceptorCode;
script.setAttribute('type', 'text/javascript');
(document.head || document.documentElement).appendChild(script);
script.parentNode.removeChild(script);
}
/**
* Listen for results from the injected interceptor.
*/
window.addEventListener('message', function(event) {
if (event.source !== window) return;
if (!event.data || event.data.type !== '__GOON_SCAN_RESULT__') return;
const data = event.data;
if (data.hlsManifests) {
for (const url of data.hlsManifests) {
collectedData.hlsManifests.add(url);
}
}
if (data.dashManifests) {
for (const url of data.dashManifests) {
collectedData.dashManifests.add(url);
}
}
if (data.videoSources && data.videoSources.length > 0) {
collectedData.videoSources = data.videoSources;
}
});
/**
* Extract poster URL from the page (content script CAN access DOM).
*/
function extractPoster() {
const posterSelectors = [
'meta[property="og:image"]',
'meta[name="twitter:image"]',
'meta[itemprop="image"]',
'img.poster, img.thumbnail, .video-poster img, .thumbnail img, .preview img',
'[data-poster]',
];
for (const selector of posterSelectors) {
const el = document.querySelector(selector);
if (el) {
const url = el.getAttribute('content') || el.getAttribute('src') || el.getAttribute('data-src') || el.getAttribute('data-poster');
if (url && url.startsWith('http')) {
collectedData.posterUrl = url;
return;
}
}
}
}
// Inject the interceptor on load
injectInterceptor();
// Extract poster from the content script's DOM view
extractPoster();
// Re-extract after a delay to catch dynamic content
setTimeout(extractPoster, 3000);
setTimeout(extractPoster, 6000);
/**
* Get collected data as a serializable object.
*/
function getData() {
extractPoster(); // Fresh scan
return {
hlsManifests: Array.from(collectedData.hlsManifests),
dashManifests: Array.from(collectedData.dashManifests),
videoSources: collectedData.videoSources,
posterUrl: collectedData.posterUrl,
timestamp: Date.now(),
};
}
// Listen for messages from the popup
chrome.runtime.onMessage.addListener(function(message, _sender, sendResponse) {
if (message.type === 'GOON_GET_VIDEO_DATA') {
sendResponse(getData());
return false;
}
if (message.type === 'GOON_INJECT') {
injectInterceptor();
sendResponse({ injected: true });
return false;
}
return false;
});
console.log('[goon-plugin] Content script loaded (isolated world). Interceptor injected.');
})(); })();
+72 -61
View File
@@ -112,47 +112,6 @@ findBtn.addEventListener('click', async () => {
const scrapeFn = () => { const scrapeFn = () => {
const dataKeys = ['src', 'video', 'mp4', 'webm', 'poster']; const dataKeys = ['src', 'video', 'mp4', 'webm', 'poster'];
/** Extract HLS manifest URLs from page using 3 strategies. */
function extractHlsManifest() {
// Strategy 1: <script> blocks containing .m3u8 URLs (most sites embed manifests in inline JS)
const m3u8Regex = /https?:[^"'\s]+\.m3u8[^"'\s]*/gi;
for (const script of document.querySelectorAll('script')) {
if (script.src) continue; // skip external scripts
const matches = script.textContent.match(m3u8Regex);
if (matches && matches.length > 0) {
// Prefer the first .m3u8 URL that looks like a real manifest (not a fragment/track)
for (const m of matches) {
const url = m.replace(/[;,]$/, '');
if (url && !url.includes('/subtitle') && !url.includes('/captions') && !url.includes('.m3u8x')) {
return url;
}
}
}
}
// Strategy 2: Check for HLS player instances on video elements
for (const v of document.querySelectorAll('video')) {
if (v && v.hlsInstance && v.hlsInstance.url) return v.hlsInstance.url;
if (v && v._hls && v._hls.url) return v._hls.url;
}
// Strategy 3: Common global HLS variables
const hlsVarNames = ['hlsConfig', 'hls_player', 'hlsOptions', 'playerConfig', 'streamConfig'];
for (const varName of hlsVarNames) {
try {
const obj = window[varName];
if (obj && typeof obj === 'object') {
for (const key of ['src', 'url', 'source', 'manifest', 'hls', 'm3u8', 'file']) {
const val = obj[key];
if (typeof val === 'string' && val.includes('.m3u8')) return val;
}
}
} catch { /* ignore */ }
}
return null;
}
// Collect all visible video elements, measure by rendered pixel area // Collect all visible video elements, measure by rendered pixel area
const allVideos = Array.from(document.querySelectorAll('video')).filter(v => { const allVideos = Array.from(document.querySelectorAll('video')).filter(v => {
const style = window.getComputedStyle(v); const style = window.getComputedStyle(v);
@@ -173,18 +132,30 @@ findBtn.addEventListener('click', async () => {
const sources = Array.from(document.querySelectorAll('video source[src]')).map(s => s.src); const sources = Array.from(document.querySelectorAll('video source[src]')).map(s => s.src);
const audios = Array.from(document.querySelectorAll('audio')).length; const audios = Array.from(document.querySelectorAll('audio')).length;
// Extract HLS manifest from the page return { videos: largest ? [largest] : [], videoCount: largest ? 1 : 0, sources, audios };
const hlsManifest = extractHlsManifest();
return { videos: largest ? [largest] : [], videoCount: largest ? 1 : 0, sources, audios, hlsManifest };
}; };
// Query content scripts for real-time HLS interception data
async function queryContentScript(tabId) {
try {
return await chrome.tabs.sendMessage(tabId, { type: 'GOON_GET_VIDEO_DATA' });
} catch {
// Content script not available (e.g., chrome:// pages)
return null;
}
}
const results = await Promise.all( const results = await Promise.all(
tabsToScan.map(async t => { tabsToScan.map(async t => {
try { try {
return await chrome.scripting.executeScript({ const [scriptResult, contentData] = await Promise.all([
target: { tabId: t.id, allFrames: true }, chrome.scripting.executeScript({
func: scrapeFn, target: { tabId: t.id, allFrames: true },
}); func: scrapeFn,
}),
queryContentScript(t.id),
]);
return { scriptResult: scriptResult.filter(Boolean), contentData };
} catch { } catch {
console.warn('[goon] skipping tab', t.id, ':', t.url); console.warn('[goon] skipping tab', t.id, ':', t.url);
return null; return null;
@@ -192,36 +163,76 @@ findBtn.addEventListener('click', async () => {
}) })
); );
// Dedup across all tabs by (src+poster) key // Build tab results — merge DOM snapshot with content script interception data
const tabResults = tabsToScan.map((tab, i) => ({ const tabResults = tabsToScan.map((tab, i) => {
tabTitle: tab.title || 'Unknown', const res = results[i];
frames: results[i]?.filter(Boolean) || [], if (!res) return null;
})).filter(g => g.frames.length > 0); return {
tabTitle: tab.title || 'Unknown',
frames: res.scriptResult || [],
contentData: res.contentData || null,
};
}).filter(Boolean);
const scanned = tabResults.length; const scanned = tabResults.length;
const seen = new Map(); const seen = new Map();
let totalFound = 0; let totalFound = 0;
for (const group of tabResults) { for (const group of tabResults) {
// Collect HLS manifests from the content script (real-time interception)
const hlsManifests = (group.contentData && group.contentData.hlsManifests) || [];
const contentPoster = (group.contentData && group.contentData.posterUrl) || null;
// Process DOM snapshot frames
for (const r of group.frames) { for (const r of group.frames) {
if (!r.result) continue; if (!r.result) continue;
// Get the HLS manifest from this frame's result (if any)
const frameHlsManifest = r.result.hlsManifest || null;
for (const v of r.result.videos) { for (const v of r.result.videos) {
totalFound++; totalFound++;
const key = (v.src || '(empty)') + '||' + (v.poster || '(none)'); const key = (v.src || '(empty)') + '||' + (v.poster || '(none)');
if (!seen.has(key)) { if (!seen.has(key)) {
const item = { url: v.src || v.poster || '', poster: v.poster || null, tab: group.tabTitle }; const item = { url: v.src || v.poster || '', poster: v.poster || null, tab: group.tabTitle };
if (v.src) item.src = v.src; if (v.src) item.src = v.src;
// Attach HLS manifest if found in this frame
if (frameHlsManifest) { // Attach HLS data from content script interception
item.hlsManifest = frameHlsManifest; if (hlsManifests.length > 0) {
item.hlsManifest = hlsManifests[0];
item.mediaType = 'hls'; item.mediaType = 'hls';
// When HLS is present, prefer the manifest URL as the primary URL // When HLS is present, prefer the manifest URL as the primary URL
if (!v.src || v.src.startsWith('blob:')) { if (!v.src || v.src.startsWith('blob:')) {
item.url = frameHlsManifest; item.url = hlsManifests[0];
item.src = frameHlsManifest; item.src = hlsManifests[0];
} }
} }
// Use poster from content script if available and better
if (contentPoster && !item.poster) {
item.poster = contentPoster;
}
seen.set(key, item);
}
}
}
// If no videos found in DOM but content script found HLS manifests + video sources,
// create entries from the intercepted data
if (!group.frames.length && group.contentData) {
const contentVideos = group.contentData.videoSources || [];
for (const cv of contentVideos) {
if (!cv.src && !hlsManifests.length) continue;
totalFound++;
const effectiveSrc = hlsManifests[0] || cv.src;
const key = effectiveSrc + '||' + (cv.poster || '(none)');
if (!seen.has(key)) {
const item = {
url: effectiveSrc,
src: effectiveSrc,
poster: cv.poster || contentPoster || null,
tab: group.tabTitle,
};
if (hlsManifests.length > 0) {
item.hlsManifest = hlsManifests[0];
item.mediaType = 'hls';
}
seen.set(key, item); seen.set(key, item);
} }
} }