fix: HLS playback — CORS proxy for phncdn.com + diagnostic logging

Problem: Even after fixing the hls.js file naming (was 'hls.min.js', now
'hls.js'), videos don't play in the viewer. Likely root cause is CORS
blocking chrome-extension:// requests to phncdn.com CDN URLs that lack
CORS headers for extension origins.

Solution — two-layer fix:

1. Service worker proxy (background.js):
   - New GOON_PROXY_FETCH handler fetches any URL via native fetch()
     (no CORS restrictions in the service worker)
   - Returns base64-encoded response data to the viewer

2. Custom HLS loader (viewer/viewer.js):
   - HlsLoader implements hls.js Loader interface
   - For phncdn.com/pornhubcdn URLs: proxies via GOON_PROXY_FETCH
   - For all other URLs: uses native XHR (bypasses proxy overhead)
   - Config passed to hls.js only when manifest URL needs proxying

3. Diagnostics (viewer/viewer.js):
   - CORS check at page load for phncdn.com and per-video URL
   - Full storage dump of video entries in console
   - Manifest URL logging on each HLS load attempt
   - Proxy activation logging for debugging
This commit is contained in:
kfj001 committed 2026-08-16 16:42:12 -07:00
1 parent fa0a0d76cd
commit b234c8dcae
2 files changed
+204 -1

No files matched your search

+31
View File
@@ -176,6 +176,37 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
return true; return true;
} }
case 'GOON_PROXY_FETCH': {
// Proxy a fetch request through the service worker (bypasses CORS).
// Returns base64-encoded response data. Used by the viewer when
// extension-origin requests to video CDNs are blocked by CORS.
(async () => {
const url = message.url;
try {
const resp = await fetch(url);
if (!resp.ok) {
sendResponse({ ok: false, status: resp.status });
return;
}
const buffer = await resp.arrayBuffer();
const bytes = new Uint8Array(buffer);
// Convert to base64 (btoa can only handle strings up to ~65k chars)
let binary = '';
const CHUNK_SIZE = 32768;
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
const chunk = bytes.subarray(i, i + CHUNK_SIZE);
binary += String.fromCharCode.apply(null, Array.from(chunk));
}
const base64 = btoa(binary);
sendResponse({ ok: true, data: base64, mimeType: resp.headers.get('content-type') || '' });
} catch(e) {
console.warn('[goon-bg] proxyFetch failed:', e && e.message ? e.message : e);
sendResponse({ ok: false, error: e && e.message ? e.message : String(e) });
}
})();
return true;
}
default: default:
console.warn('[goon-plugin] Unknown message type:', message.type); console.warn('[goon-plugin] Unknown message type:', message.type);
} }
+173 -1
View File
@@ -52,6 +52,134 @@ const gridN = parseInt(params.get('grid'), 10) || 2;
const tags = params.get('tags') ? JSON.parse(params.get('tags')) : []; const tags = params.get('tags') ? JSON.parse(params.get('tags')) : [];
const panelsEl = document.getElementById('panels'); const panelsEl = document.getElementById('panels');
/** Track all Hls instances so we can destroy them on unload. */
/**
* Custom HLS loader that proxies phncdn.com URLs through the service worker
* (bypassing CORS) and fetches everything else natively.
* Implements hls.js Loader interface: load(config), abort().
*/
function HlsLoader(config) {
this._config = config;
this._xhr = null;
}
HlsLoader.prototype.abort = function () {
if (this._xhr) { this._xhr.abort(); this._xhr = null; }
};
HlsLoader.prototype.load = function (config) {
this.abort();
this._loaderContext = config.type + ': ' + (config.url || '').substring(0, 100);
this._onSuccess = config.onSuccess;
this._onError = config.onError;
this._onTimeout = config.onTimeout;
this._timeout = config.timeout || 10000;
const url = config.url;
const needProxy = url.includes('phncdn') || url.includes('pornhubcdn');
if (needProxy) {
// Use service worker proxy to bypass CORS
console.log('[goon-viewer] Proxying via SW:', url.substring(0, 120));
chrome.runtime.sendMessage({ type: 'GOON_PROXY_FETCH', url }, (resp) => {
if (chrome.runtime.lastError || !resp || !resp.ok) {
const err = new Error('Proxy fetch failed: ' + (chrome.runtime.lastError?.message || resp?.error || 'unknown'));
console.error('[goon-viewer] Proxy error:', err.message);
this._onError(config, { url, context: this._loaderContext, body: null, stats: { trequest: Date.now(), tfirst: 0, tload: Date.now(), loadingPeriod: 0 }, error: err });
return;
}
// Decode base64 → ArrayBuffer
let binary = '';
const data = resp.data || '';
const CHUNK = 32768;
for (let i = 0; i < data.length; i += CHUNK) {
binary += String.fromCharCode.apply(null, Array.from(data.subarray?.(i, i + CHUNK) || new Uint8Array().subarray(0, Math.min(CHUNK, data.length - i))));
}
// Simpler approach: use atob on the full string
try {
binary = atob(data);
} catch {}
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
this._onSuccess(config, { response: { headers: {}, body: bytes.buffer }, trequest: Date.now(), tfirst: Date.now(), tload: Date.now(), loadingPeriod: Date.now() - config.trequest });
});
} else {
// Native XHR for non-proxy URLs
const xhr = new XMLHttpRequest();
this._xhr = xhr;
xhr.open('GET', url, true);
xhr.withCredentials = false;
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
this._onSuccess(config, { response: { headers: xhr.getAllResponseHeaders(), body: xhr.response }, trequest: config.trequest, tfirst: Date.now(), tload: Date.now(), loadingPeriod: Date.now() - config.trequest });
} else {
this._onError(config, { url, context: this._loaderContext, body: null, stats: { trequest: config.trequest, tfirst: 0, tload: Date.now(), loadingPeriod: 0 }, error: new Error('HTTP ' + xhr.status) });
}
};
xhr.onerror = () => {
this._onError(config, { url, context: this._loaderContext, body: null, stats: { trequest: config.trequest, tfirst: 0, tload: Date.now(), loadingPeriod: 0 }, error: new Error('XHR network error') });
};
xhr.ontimeout = () => {
this._onTimeout(config, { url, context: this._loaderContext, stats: { trequest: config.trequest, tfirst: 0, tload: Date.now(), loadingPeriod: 0 } });
};
xhr.timeout = this._timeout;
xhr.responseType = needProxy ? 'arraybuffer' : 'arraybuffer';
xhr.send();
}
};
HlsLoader.prototype.destroy = function () { this.abort(); };
HlsLoader.canSupportLoader = function () { return true; };
// === CORS diagnostics — check if phncdn.com allows extension-origin requests ===
(async function checkCors() {
try {
const resp = await fetch('https://phncdn.com/images/favicon.ico', { method: 'HEAD', mode: 'cors' });
console.log('[goon-viewer] CORS check phncdn.com:', resp.ok ? 'ALLOWED (status ' + resp.status + ')' : 'BLOCKED');
} catch(e) {
console.warn('[goon-viewer] CORS check phncdn.com: BLOCKED —', e && e.message ? e.message : e);
}
})();
// === HLS segment proxy helper ===
// Proxies a video URL through the service worker (no CORS restrictions).
// Returns an ArrayBuffer of the response content.
async function proxyFetch(url) {
try {
const resp = await chrome.runtime.sendMessage({ type: 'GOON_PROXY_FETCH', url });
if (resp && resp.data && resp.ok) {
// Decode base64 → ArrayBuffer
const binary = atob(resp.data);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
return null;
} catch(e) {
console.warn('[goon-viewer] proxyFetch failed for:', url.substring(0, 120), e && e.message ? e.message : e);
return null;
}
}
// === Check if any manifest URLs require proxying (phncdn.com / cdn) ===
function urlsNeedProxy(urls) {
return urls.some(u => u && typeof u === 'string' && (u.includes('phncdn') || u.includes('pornhubcdn')));
}
/** Check whether fetching a URL from the extension page works (CORS test). */
async function corsCheck(url) {
try {
const resp = await fetch(url, { method: 'HEAD', mode: 'cors' });
return resp.ok;
} catch {
return false;
}
}
/** Track all Hls instances so we can destroy them on unload. */ /** Track all Hls instances so we can destroy them on unload. */
const hlsInstances = new Map(); const hlsInstances = new Map();
@@ -682,9 +810,23 @@ function playHls(video, manifestUrl, panel) {
return; return;
} }
// Check whether this manifest URL needs proxying (phncdn.com without CORS headers
// blocks extension-origin requests from the viewer page).
const needProxy = manifestUrl.includes('phncdn') || manifestUrl.includes('pornhubcdn');
if (needProxy) {
console.log('[goon-viewer] Manifest needs proxying via service worker');
}
destroyHls(video); destroyHls(video);
const hls = new window.Hls(HLS_CONFIG); const config = Object.assign({}, HLS_CONFIG);
if (needProxy) {
// Custom fetch loader that uses the service worker proxy for phncdn URLs.
config.loader = window.Hls.DefaultConfig.loader || HlsLoader;
}
const hls = new window.Hls(config);
hlsInstances.set(video, hls); hlsInstances.set(video, hls);
hls.on(window.Hls.Events.MANIFEST_PARSED, () => { hls.on(window.Hls.Events.MANIFEST_PARSED, () => {
@@ -810,6 +952,36 @@ async function init() {
overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;background:rgba(0,0,0,0.85);color:#fff;padding:8px 16px;font-family:sans-serif;font-size:14px;z-index:999;'; overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;background:rgba(0,0,0,0.85);color:#fff;padding:8px 16px;font-family:sans-serif;font-size:14px;z-index:999;';
document.body.style.position = 'relative'; document.body.style.position = 'relative';
document.body.insertBefore(overlay, document.body.firstChild); document.body.insertBefore(overlay, document.body.firstChild);
// === DIAGNOSTIC: Check storage + CORS ===
console.log('[goon-viewer] Viewer ID:', id, 'Grid:', gridN + 'x' + gridN);
console.log('[goon-viewer] Raw stored data:', JSON.stringify(videoUrls));
if (videoUrls.length > 0) {
for (const entry of videoUrls) {
const src = (entry.src || '').substring(0, 120);
console.log('[goon-viewer] Entry:', {
src: src,
mediaType: entry.mediaType,
hlsManifest: (entry.hlsManifest || '').substring(0, 120),
poster: (entry.poster || '').substring(0, 80),
});
}
// Test CORS on all video URLs
const urlsToTest = videoUrls.map(v => v.src || v.hlsManifest || 'none').filter(u => u !== 'none' && typeof u === 'string');
for (const url of [...new Set(urlsToTest)]) {
const corsOk = await corsCheck(url);
console.log('[goon-viewer] CORS', corsOk ? 'OK' : 'BLOCKED:', url.substring(0, 120));
}
// Check if any URLs point to phncdn.com (proxy will be needed if CORS blocked)
const needsProxy = urlsToTest.some(u => u.includes('phncdn') || u.includes('pornhubcdn'));
console.log('[goon-viewer] Proxy required:', needsProxy);
} else {
console.warn('[goon-viewer] No video entries in storage for ID:', id, '— data may have been cleared or never saved');
}
overlay.textContent = `Found ${videoUrls.length} video URL(s) in ${gridN}x${gridN} grid`; overlay.textContent = `Found ${videoUrls.length} video URL(s) in ${gridN}x${gridN} grid`;
statusElRef = overlay; statusElRef = overlay;