feat: full HLS.js support — detection via interceptors + deep probe, playback via hls.js in viewer #2
No files matched your search
+46
-4
@@ -31,16 +31,25 @@
|
|||||||
* extension-originated <script src="..."> as content scripts — not as
|
* extension-originated <script src="..."> as content scripts — not as
|
||||||
* page inline code — so the page's CSP does not apply.
|
* page inline code — so the page's CSP does not apply.
|
||||||
*
|
*
|
||||||
|
* IMPORTANT: Remove the script element ONLY after the load event fires.
|
||||||
|
* Removing it synchronously can prevent the browser from starting the fetch,
|
||||||
|
* especially in Edge/Chrome with strict timing.
|
||||||
|
*
|
||||||
* Runs BEFORE any page JS (document_start), so we catch all HLS activity.
|
* Runs BEFORE any page JS (document_start), so we catch all HLS activity.
|
||||||
*/
|
*/
|
||||||
function injectInterceptors() {
|
function injectInterceptors() {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = chrome.runtime.getURL('interceptors.js');
|
script.src = chrome.runtime.getURL('interceptors.js');
|
||||||
script.setAttribute('type', 'text/javascript');
|
script.setAttribute('type', 'text/javascript');
|
||||||
(document.head || document.documentElement).appendChild(script);
|
script.onload = function () {
|
||||||
// Remove the script element after execution but keep interceptors active
|
// Script loaded and executed — safe to remove the element now
|
||||||
|
if (script.parentNode) {
|
||||||
script.parentNode.removeChild(script);
|
script.parentNode.removeChild(script);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
// Keep the element in the DOM — onload will remove it after execution
|
||||||
|
}
|
||||||
|
|
||||||
// Inject immediately at document_start
|
// Inject immediately at document_start
|
||||||
injectInterceptors();
|
injectInterceptors();
|
||||||
@@ -59,6 +68,7 @@
|
|||||||
timestamp: event.data.timestamp
|
timestamp: event.data.timestamp
|
||||||
});
|
});
|
||||||
console.log('[goon-plugin] Intercepted HLS manifest via', source, ':', url);
|
console.log('[goon-plugin] Intercepted HLS manifest via', source, ':', url);
|
||||||
|
console.log('[goon-plugin] Total intercepted manifests so far:', interceptedManifests.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -96,14 +106,28 @@
|
|||||||
*/
|
*/
|
||||||
function getVideoData() {
|
function getVideoData() {
|
||||||
const poster = extractPoster();
|
const poster = extractPoster();
|
||||||
|
console.log('[goon-content] getVideoData called. Poster:', poster);
|
||||||
|
console.log('[goon-content] Intercepted manifests count:', interceptedManifests.length);
|
||||||
|
if (interceptedManifests.length > 0) {
|
||||||
|
console.log('[goon-content] Manifest details:', JSON.stringify(interceptedManifests.map(m => ({ url: m.url, source: m.source }))));
|
||||||
|
}
|
||||||
|
|
||||||
// DOM video scan (isolated world — can access DOM but not page JS)
|
// DOM video scan (isolated world — can access DOM but not page JS)
|
||||||
const videoElements = [];
|
const videoElements = [];
|
||||||
try {
|
try {
|
||||||
const videos = document.querySelectorAll('video');
|
const videos = document.querySelectorAll('video');
|
||||||
|
console.log('[goon-content] Found', videos.length, '<video> elements in DOM');
|
||||||
for (const video of videos) {
|
for (const video of videos) {
|
||||||
const style = window.getComputedStyle(video);
|
const style = window.getComputedStyle(video);
|
||||||
if (style.display !== 'none' && video.clientWidth > 0 && video.clientHeight > 0) {
|
const isVisible = style.display !== 'none' && video.clientWidth > 0 && video.clientHeight > 0;
|
||||||
|
console.log('[goon-content] Video:', {
|
||||||
|
src: (video.currentSrc || video.src || '').substring(0, 120),
|
||||||
|
visible: isVisible,
|
||||||
|
dimensions: video.clientWidth + 'x' + video.clientHeight,
|
||||||
|
networkState: video.networkState,
|
||||||
|
readyState: video.readyState,
|
||||||
|
});
|
||||||
|
if (isVisible) {
|
||||||
// Extract buffered ranges as serializable arrays
|
// Extract buffered ranges as serializable arrays
|
||||||
const bufferedRanges = [];
|
const bufferedRanges = [];
|
||||||
if (video.buffered && video.buffered.length > 0) {
|
if (video.buffered && video.buffered.length > 0) {
|
||||||
@@ -122,7 +146,25 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch { /* ignore DOM access errors */ }
|
} catch(e) { /* ignore DOM access errors */
|
||||||
|
console.warn('[goon-content] DOM scan error:', e && e.message ? e.message : e);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[goon-content] Visible video elements:', videoElements.length);
|
||||||
|
// Expose interceptor data globally so the popup's deep probe can read it.
|
||||||
|
window.__goon_plugin_data = window.__goon_plugin_data || {
|
||||||
|
manifests: [],
|
||||||
|
sources: [],
|
||||||
|
_contentType: 'content-script',
|
||||||
|
};
|
||||||
|
// Merge our intercepted manifests into the shared data store (the interceptors
|
||||||
|
// write their own URLs here; we add content-script-level XHR intercepts).
|
||||||
|
for (const m of interceptedManifests) {
|
||||||
|
if (!window.__goon_plugin_data.manifests.includes(m.url)) {
|
||||||
|
window.__goon_plugin_data.manifests.push(m.url);
|
||||||
|
window.__goon_plugin_data.sources.push(m.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
posterUrl: poster,
|
posterUrl: poster,
|
||||||
|
|||||||
+142
-1
@@ -373,6 +373,145 @@ function m3u8DeepProbeFn() {
|
|||||||
} catch(e) { /* ignore */ }
|
} catch(e) { /* ignore */ }
|
||||||
probeLog('Strategy F (dynamic HLS fallback): found', strategyFCount, 'new manifests');
|
probeLog('Strategy F (dynamic HLS fallback): found', strategyFCount, 'new manifests');
|
||||||
|
|
||||||
|
// Strategy G: Non-enumerable + Symbol property detection on video elements.
|
||||||
|
// Object.keys() only returns enumerable string properties. Players like es6player
|
||||||
|
// hide instances via Object.defineProperty(enumerable:false) or Symbol keys.
|
||||||
|
// This runs in main-world via executeScript, so getOwnPropertyNames/Symbols work.
|
||||||
|
let strategyGCount = 0;
|
||||||
|
try {
|
||||||
|
for (const v of document.querySelectorAll('video')) {
|
||||||
|
// Non-enumerable string properties
|
||||||
|
const nonEnumProps = Object.getOwnPropertyNames(v);
|
||||||
|
for (const key of nonEnumProps) {
|
||||||
|
if (STANDARD_VIDEO_ATTRS.has(key)) continue;
|
||||||
|
try {
|
||||||
|
const obj = v[key];
|
||||||
|
if (!obj || typeof obj !== 'object') continue;
|
||||||
|
|
||||||
|
// Check if this object looks like an HLS instance (same heuristics as interceptors)
|
||||||
|
const isHlsLike = typeof obj.url === 'string' && obj.url.length > 5 ||
|
||||||
|
(obj.levels && Array.isArray(obj.levels)) ||
|
||||||
|
typeof obj.attachMedia === 'function' ||
|
||||||
|
typeof obj.loadSource === 'function' ||
|
||||||
|
typeof obj.getConfig === 'function' ||
|
||||||
|
typeof obj.detachMedia === 'function' ||
|
||||||
|
typeof obj.destroy === 'function' ||
|
||||||
|
(obj.networkControls && typeof obj.networkControls === 'object');
|
||||||
|
|
||||||
|
if (isHlsLike) {
|
||||||
|
probeLog('Strategy G: found HLS-like object on video via non-enumerable prop', key);
|
||||||
|
// Extract URL from non-enumerable property
|
||||||
|
if (typeof obj.url === 'string' && obj.url.length > 5) {
|
||||||
|
const resolved = resolveUrl(obj.url);
|
||||||
|
if (resolved && !seen.has(resolved)) {
|
||||||
|
seen.add(resolved);
|
||||||
|
manifests.push({ url: resolved, player: `video[G].${key}.url` });
|
||||||
|
strategyGCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Extract level URIs
|
||||||
|
if (obj.levels && Array.isArray(obj.levels)) {
|
||||||
|
for (let gi = 0; gi < obj.levels.length; gi++) {
|
||||||
|
const lvl = obj.levels[gi];
|
||||||
|
if (lvl && lvl.uri && !seen.has(lvl.uri)) {
|
||||||
|
const resolved = resolveUrl(lvl.uri) || lvl.uri;
|
||||||
|
seen.add(resolved);
|
||||||
|
manifests.push({ url: resolved, player: `video[G].${key}.levels[${gi}].uri` });
|
||||||
|
strategyGCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Also deep-walk nested objects for URL-like values (even without HLS signatures)
|
||||||
|
if (typeof obj === 'object' && obj !== null) {
|
||||||
|
walk(obj, `video[G].${key}`, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) { /* circular or inaccessible */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symbol properties
|
||||||
|
const symbolProps = Object.getOwnPropertySymbols(v);
|
||||||
|
for (const symKey of symbolProps) {
|
||||||
|
try {
|
||||||
|
const symObj = v[symKey];
|
||||||
|
if (!symObj || typeof symObj !== 'object') continue;
|
||||||
|
probeLog('Strategy G: video has Symbol prop', symKey.toString());
|
||||||
|
|
||||||
|
// Check HLS signatures on symbols too
|
||||||
|
if (typeof symObj.url === 'string' && symObj.url.length > 5) {
|
||||||
|
const resolved = resolveUrl(symObj.url);
|
||||||
|
if (resolved && !seen.has(resolved)) {
|
||||||
|
seen.add(resolved);
|
||||||
|
manifests.push({ url: resolved, player: `video[G].${symKey.toString()}.url` });
|
||||||
|
strategyGCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (symObj.levels && Array.isArray(symObj.levels)) {
|
||||||
|
for (let gi = 0; gi < symObj.levels.length; gi++) {
|
||||||
|
const lvl = symObj.levels[gi];
|
||||||
|
if (lvl && lvl.uri && !seen.has(lvl.uri)) {
|
||||||
|
const resolved = resolveUrl(lvl.uri) || lvl.uri;
|
||||||
|
seen.add(resolved);
|
||||||
|
manifests.push({ url: resolved, player: `video[G].${symKey.toString()}.levels[${gi}].uri` });
|
||||||
|
strategyGCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Deep-walk nested objects
|
||||||
|
walk(symObj, `video[G].${symKey.toString()}`, 0);
|
||||||
|
} catch(e) { /* inaccessible */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) { /* ignore */ }
|
||||||
|
probeLog('Strategy G (non-enumerable/Symbol props): found', strategyGCount, 'new manifests');
|
||||||
|
|
||||||
|
// Strategy H: Full window global scan — enumerate ALL objects on window, not just
|
||||||
|
// known player names. Filter out built-ins and walk any object that looks like a
|
||||||
|
// player container (.url, .levels, .config with HLS keys, .media reference).
|
||||||
|
let strategyHCount = 0;
|
||||||
|
try {
|
||||||
|
const windowKeys = Object.keys(window);
|
||||||
|
for (const key of windowKeys) {
|
||||||
|
const val = window[key];
|
||||||
|
// Skip primitives
|
||||||
|
if (typeof val === 'function' || typeof val === 'string' ||
|
||||||
|
typeof val === 'number' || typeof val === 'boolean' ||
|
||||||
|
val === null || val === undefined) continue;
|
||||||
|
// Skip DOM elements directly on window
|
||||||
|
if (val && typeof val.nodeType === 'number') continue;
|
||||||
|
if (typeof val !== 'object') continue;
|
||||||
|
|
||||||
|
// Quick check: does this object have any player-like keys?
|
||||||
|
let hasPlayerKey = false;
|
||||||
|
try {
|
||||||
|
const subKeys = Object.keys(val).slice(0, 50); // limit scan depth for performance
|
||||||
|
hasPlayerKey = subKeys.some(k => {
|
||||||
|
const lk = k.toLowerCase();
|
||||||
|
return lk.includes('hls') || lk.includes('player') || lk.includes('video') ||
|
||||||
|
lk.includes('manifest') || lk.includes('source') || lk.includes('level') ||
|
||||||
|
lk.includes('playlist') || lk.includes('stream') || lk === 'url' ||
|
||||||
|
lk === 'config' || lk === 'media' || lk === 'levels' ||
|
||||||
|
lk === 'width' || lk === 'height' || lk === 'volume';
|
||||||
|
});
|
||||||
|
} catch { hasPlayerKey = false; }
|
||||||
|
if (!hasPlayerKey) continue;
|
||||||
|
|
||||||
|
probeLog('Strategy H: window global', key, 'has player-like keys');
|
||||||
|
// Deep-walk this object for HLS instances and manifest URLs
|
||||||
|
walk(val, `window[H].${key}`, 0);
|
||||||
|
if (typeof val.url === 'string' && isManifestUrl(val.url)) {
|
||||||
|
const resolved = resolveUrl(val.url);
|
||||||
|
if (resolved && !seen.has(resolved)) {
|
||||||
|
seen.add(resolved);
|
||||||
|
manifests.push({ url: resolved, player: `window[H].${key}.url` });
|
||||||
|
strategyHCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) { /* ignore */ }
|
||||||
|
probeLog('Strategy H (full window scan): found', strategyHCount, 'new manifests');
|
||||||
|
|
||||||
probeLog('Deep probe complete. Total unique manifests found:', manifests.length);
|
probeLog('Deep probe complete. Total unique manifests found:', manifests.length);
|
||||||
if (manifests.length > 0) {
|
if (manifests.length > 0) {
|
||||||
probeLog('Manifest summary:', JSON.stringify(manifests.map(m => ({ url: m.url.substring(0, 100), player: m.player }))));
|
probeLog('Manifest summary:', JSON.stringify(manifests.map(m => ({ url: m.url.substring(0, 100), player: m.player }))));
|
||||||
@@ -560,7 +699,9 @@ function dedupManifests(allManifests) {
|
|||||||
const playerRank = {
|
const playerRank = {
|
||||||
'__goon_plugin_data': 0,
|
'__goon_plugin_data': 0,
|
||||||
'content-script-intercept': 0,
|
'content-script-intercept': 0,
|
||||||
'video._goon_hls_manifest': 1,
|
'webRequest': 1,
|
||||||
|
'phncdn_reconstructed': 2,
|
||||||
|
'video._goon_hls_manifest': 3,
|
||||||
'inline-script': 99,
|
'inline-script': 99,
|
||||||
};
|
};
|
||||||
unique.sort((a, b) => {
|
unique.sort((a, b) => {
|
||||||
|
|||||||
Reference in new issue
Block a user