fix: real-time HLS interception via content script (architectural overhaul)
CI / test (pull_request) Canceled after 0s
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:
1 parent
db52db7799
commit
c22631d1f1
3 files changed
+354
-72
No files matched your search
+72
-61
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user