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

+72 -61
View File
@@ -112,47 +112,6 @@ findBtn.addEventListener('click', async () => {
const scrapeFn = () => {
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
const allVideos = Array.from(document.querySelectorAll('video')).filter(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 audios = Array.from(document.querySelectorAll('audio')).length;
// Extract HLS manifest from the page
const hlsManifest = extractHlsManifest();
return { videos: largest ? [largest] : [], videoCount: largest ? 1 : 0, sources, audios, hlsManifest };
return { videos: largest ? [largest] : [], videoCount: largest ? 1 : 0, sources, audios };
};
// 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(
tabsToScan.map(async t => {
try {
return await chrome.scripting.executeScript({
target: { tabId: t.id, allFrames: true },
func: scrapeFn,
});
const [scriptResult, contentData] = await Promise.all([
chrome.scripting.executeScript({
target: { tabId: t.id, allFrames: true },
func: scrapeFn,
}),
queryContentScript(t.id),
]);
return { scriptResult: scriptResult.filter(Boolean), contentData };
} catch {
console.warn('[goon] skipping tab', t.id, ':', t.url);
return null;
@@ -192,36 +163,76 @@ findBtn.addEventListener('click', async () => {
})
);
// Dedup across all tabs by (src+poster) key
const tabResults = tabsToScan.map((tab, i) => ({
tabTitle: tab.title || 'Unknown',
frames: results[i]?.filter(Boolean) || [],
})).filter(g => g.frames.length > 0);
// Build tab results — merge DOM snapshot with content script interception data
const tabResults = tabsToScan.map((tab, i) => {
const res = results[i];
if (!res) return null;
return {
tabTitle: tab.title || 'Unknown',
frames: res.scriptResult || [],
contentData: res.contentData || null,
};
}).filter(Boolean);
const scanned = tabResults.length;
const seen = new Map();
let totalFound = 0;
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) {
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) {
totalFound++;
const key = (v.src || '(empty)') + '||' + (v.poster || '(none)');
if (!seen.has(key)) {
const item = { url: v.src || v.poster || '', poster: v.poster || null, tab: group.tabTitle };
if (v.src) item.src = v.src;
// Attach HLS manifest if found in this frame
if (frameHlsManifest) {
item.hlsManifest = frameHlsManifest;
// Attach HLS data from content script interception
if (hlsManifests.length > 0) {
item.hlsManifest = hlsManifests[0];
item.mediaType = 'hls';
// When HLS is present, prefer the manifest URL as the primary URL
if (!v.src || v.src.startsWith('blob:')) {
item.url = frameHlsManifest;
item.src = frameHlsManifest;
item.url = hlsManifests[0];
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);
}
}