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
+351
-69
No files matched your search
+2
-1
@@ -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
@@ -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.');
|
||||||
})();
|
})();
|
||||||
+69
-58
@@ -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([
|
||||||
|
chrome.scripting.executeScript({
|
||||||
target: { tabId: t.id, allFrames: true },
|
target: { tabId: t.id, allFrames: true },
|
||||||
func: scrapeFn,
|
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) => {
|
||||||
|
const res = results[i];
|
||||||
|
if (!res) return null;
|
||||||
|
return {
|
||||||
tabTitle: tab.title || 'Unknown',
|
tabTitle: tab.title || 'Unknown',
|
||||||
frames: results[i]?.filter(Boolean) || [],
|
frames: res.scriptResult || [],
|
||||||
})).filter(g => g.frames.length > 0);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user