export const MEDIA_FRAME_ACCESS_REQUIRED = 'media_frame_access_required'; export const MEDIA_FRAME_AMBIGUOUS = 'media_frame_ambiguous'; const MIN_PLAYER_FRAME_AREA = 320 * 180; const MIN_PLAYER_ASPECT_RATIO = 1.15; const MAX_PLAYER_ASPECT_RATIO = 2.6; function normalizeFrameId(value) { return Number.isInteger(value) && value >= 0 ? value : 0; } function safeOrigin(value) { try { const url = new URL(value); return (url.protocol === 'http:' || url.protocol === 'https:') ? url.origin : null; } catch { return null; } } function originPattern(value) { try { const url = new URL(value); if (url.protocol !== 'http:' && url.protocol !== 'https:') return null; // WebExtension match patterns intentionally omit ports. Chromium treats // the host pattern port-independently; Firefox rejects explicit ports. return `${url.protocol}//${url.hostname}/*`; } catch { return null; } } function isGoogleDrivePlayerUrl(value) { try { const url = new URL(value); if (url.hostname.toLowerCase() !== 'youtube.googleapis.com' || (url.pathname !== '/embed' && !url.pathname.startsWith('/embed/'))) { return false; } const parentOrigin = url.searchParams.get('origin') || url.searchParams.get('post_message_origin'); return parentOrigin === 'https://drive.google.com'; } catch { return false; } } /** * Runs inside every frame through chrome.scripting.executeScript. Keep this * function self-contained: extension functions outside its body are not * available in the injected isolated world. */ export function inspectMediaFrame(expectedVisibilityToken = null) { const elementIsVisible = (element, rect) => { if (!rect || rect.width <= 0 || rect.height <= 0) return false; const view = element.ownerDocument?.defaultView || window; const style = view.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) { return false; } if (typeof element.checkVisibility === 'function' && !element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) { return false; } const layoutWidth = Math.max(view.innerWidth, element.ownerDocument?.documentElement?.scrollWidth || 0); const layoutHeight = Math.max(view.innerHeight, element.ownerDocument?.documentElement?.scrollHeight || 0); const scrollX = Number(view.scrollX) || 0; const scrollY = Number(view.scrollY) || 0; return rect.bottom + scrollY > 0 && rect.right + scrollX > 0 && rect.top + scrollY < layoutHeight && rect.left + scrollX < layoutWidth; }; const collectVideos = (doc, depth = 0, ancestorVisible = true, videos = [], seen = new Set()) => { if (depth >= 4 || typeof doc.querySelectorAll !== 'function') return videos; for (const video of doc.querySelectorAll('video')) { if (!seen.has(video)) { seen.add(video); videos.push({ video, ancestorVisible }); } } const hosts = doc.querySelectorAll('[id*="player" i], [class*="player" i], [id*="video" i], [class*="video" i], [id*="media" i], [class*="media" i], [id*="stream" i], [class*="stream" i], ytd-player, netflix-player, emby-player, jellyfin-player, video-player'); for (const host of hosts) { if (!host.shadowRoot) continue; for (const video of host.shadowRoot.querySelectorAll('video')) { if (!seen.has(video)) { seen.add(video); videos.push({ video, ancestorVisible }); } } } for (const frame of doc.querySelectorAll('iframe, frame')) { try { const frameRect = frame.getBoundingClientRect(); const frameVisible = ancestorVisible && elementIsVisible(frame, frameRect); const frameDoc = frame.contentDocument; if (frameDoc) collectVideos(frameDoc, depth + 1, frameVisible, videos, seen); } catch { // Cross-origin media is inspected in its own execution result. } } return videos; }; const videoDetails = collectVideos(document).map(({ video, ancestorVisible }) => { const rect = video.getBoundingClientRect(); const rendered = ancestorVisible && elementIsVisible(video, rect); const hasSource = !!(video.currentSrc || video.src || video.srcObject || video.querySelector?.('source[src]')); const background = !!video.loop && !!video.muted && !video.controls; const duration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : 0; const shortUncontrolled = !video.controls && duration > 0 && duration < 300; const renderedArea = Math.max(0, rect.width) * Math.max(0, rect.height); return { hasSource, rendered, background, shortUncontrolled, sizeBucket: Math.round(Math.sqrt(renderedArea) / 40), playing: video.paused === false && video.ended !== true, controls: !!video.controls, readyState: Number.isInteger(video.readyState) ? video.readyState : 0, duration, renderedArea }; }); const compareVideo = (left, right) => { const leftRank = [ left.hasSource ? 1 : 0, left.rendered ? 1 : 0, left.background ? 0 : 1, left.shortUncontrolled ? 0 : 1, left.playing ? 1 : 0, left.controls ? 1 : 0, left.readyState, left.duration, left.sizeBucket ]; const rightRank = [ right.hasSource ? 1 : 0, right.rendered ? 1 : 0, right.background ? 0 : 1, right.shortUncontrolled ? 0 : 1, right.playing ? 1 : 0, right.controls ? 1 : 0, right.readyState, right.duration, right.sizeBucket ]; for (let index = 0; index < leftRank.length; index++) { if (leftRank[index] !== rightRank[index]) return rightRank[index] - leftRank[index]; } return 0; }; videoDetails.sort(compareVideo); // Recursively list direct and same-origin-descendant frame elements. This // lets the background identify a large inaccessible player origin even if // an all-frame probe is rejected by the browser's site-access policy. const embeddedFrames = []; const collectEmbeddedFrames = (doc, depth = 0, ancestorVisible = true) => { if (depth >= 4 || typeof doc.querySelectorAll !== 'function') return; for (const frame of doc.querySelectorAll('iframe, frame')) { const rect = frame.getBoundingClientRect(); const directVisible = elementIsVisible(frame, rect); const visible = ancestorVisible && directVisible; let href = ''; try { href = new URL(frame.src || '', doc.location.href).href; } catch { href = ''; } embeddedFrames.push({ href, origin: (() => { try { return new URL(href).origin; } catch { return null; } })(), area: Math.max(0, rect.width) * Math.max(0, rect.height), width: Math.max(0, rect.width), height: Math.max(0, rect.height), visible, depth: depth + 1, mediaHint: frame.allowFullscreen === true || frame.hasAttribute?.('allowfullscreen') || /autoplay|fullscreen|picture-in-picture|encrypted-media/i.test(frame.getAttribute('allow') || '') || /player|video|stream|watch|embed|media|xfp/i.test([ frame.id, frame.name, frame.className, frame.title, href ].join(' ')) }); try { const frameDoc = frame.contentDocument; if (frameDoc) collectEmbeddedFrames(frameDoc, depth + 1, visible); } catch { // Cross-origin descendants are represented by their frame URL. } } }; collectEmbeddedFrames(document); const storedParentVisibility = window.__koalaParentFrameVisibility; const parentVisibility = expectedVisibilityToken && storedParentVisibility?.token === expectedVisibilityToken ? storedParentVisibility : null; return { href: window.location.href, origin: window.location.origin, isTop: window.top === window, videoCount: videoDetails.length, bestVideo: videoDetails[0] || null, frameArea: Math.max(0, window.innerWidth) * Math.max(0, window.innerHeight), parentFrameVisible: window.top === window ? true : parentVisibility?.visible ?? null, parentFrameArea: window.top === window ? Math.max(0, window.innerWidth) * Math.max(0, window.innerHeight) : (Number.isFinite(parentVisibility?.area) ? parentVisibility.area : null), embeddedFrames }; } /** Runs inside every frame before the visibility dispatch. */ export function installParentFrameVisibilityProbe(token) { try { window.__koalaFrameVisibilityCleanup?.(); } catch { /* stale probe */ } window.__koalaParentFrameVisibility = { token, visible: null, area: null }; let timeout = null; const cleanup = () => { window.removeEventListener('message', handler); if (timeout !== null) clearTimeout(timeout); if (window.__koalaFrameVisibilityCleanup === cleanup) { delete window.__koalaFrameVisibilityCleanup; } }; const handler = (event) => { if (event.source !== window.parent || event.data?.type !== 'KOALASYNC_FRAME_VISIBILITY' || event.data?.token !== token) { return; } window.__koalaParentFrameVisibility = { token, visible: event.data.visible === true, area: Number.isFinite(event.data.area) ? event.data.area : 0 }; }; window.addEventListener('message', handler); timeout = setTimeout(cleanup, 1000); window.__koalaFrameVisibilityCleanup = cleanup; } /** Runs inside every frame; each parent reports geometry to its direct children. */ export function dispatchParentFrameVisibilityProbe(token) { const ancestor = window.top === window ? { visible: true, area: Infinity } : window.__koalaParentFrameVisibility; for (const frame of document.querySelectorAll('iframe, frame')) { try { const rect = frame.getBoundingClientRect(); const style = window.getComputedStyle(frame); const area = Math.max(0, rect.width) * Math.max(0, rect.height); const layoutWidth = Math.max(window.innerWidth, document.documentElement?.scrollWidth || 0); const layoutHeight = Math.max(window.innerHeight, document.documentElement?.scrollHeight || 0); const scrollX = Number(window.scrollX) || 0; const scrollY = Number(window.scrollY) || 0; const intersectsLayout = rect.bottom + scrollY > 0 && rect.right + scrollX > 0 && rect.top + scrollY < layoutHeight && rect.left + scrollX < layoutWidth; const browserReportsVisible = typeof frame.checkVisibility === 'function' ? frame.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) : true; const directlyVisible = area > 0 && intersectsLayout && browserReportsVisible && style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity) !== 0; const visible = directlyVisible && ancestor?.visible !== false; const inheritedArea = Number.isFinite(ancestor?.area) ? ancestor.area : area; const effectiveArea = Math.min(area, inheritedArea); frame.contentWindow?.postMessage({ type: 'KOALASYNC_FRAME_VISIBILITY', token, visible, area: effectiveArea }, '*'); } catch { // A detached or browser-owned frame is not a candidate. } } } function mediaCandidateRank(entry) { const result = entry.result; const video = result.bestVideo; const visibility = result.parentFrameVisible === true ? 2 : result.parentFrameVisible === false ? 0 : 1; return [ visibility, video.hasSource ? 1 : 0, video.rendered ? 1 : 0, video.background ? 0 : 1, video.shortUncontrolled ? 0 : 1, video.playing ? 1 : 0, video.controls ? 1 : 0, video.readyState, video.duration, video.sizeBucket, result.isTop ? 1 : 0, Number.isFinite(result.parentFrameArea) ? result.parentFrameArea : result.frameArea ]; } function compareRanks(left, right) { const leftRank = mediaCandidateRank(left); const rightRank = mediaCandidateRank(right); for (let index = 0; index < leftRank.length; index++) { if (leftRank[index] !== rightRank[index]) return rightRank[index] - leftRank[index]; } return 0; } function sameMeaningfulRank(left, right) { const leftRank = mediaCandidateRank(left); const rightRank = mediaCandidateRank(right); // Ignore duration, size, top-frame preference, and raw frame area. Two // otherwise identical frames are unsafe to distinguish by preload metadata. return leftRank.slice(0, 8).every((value, index) => value === rightRank[index]); } export function selectMediaFrame(injectionResults) { const candidates = (Array.isArray(injectionResults) ? injectionResults : []) .filter(entry => Number.isInteger(entry?.frameId) && entry?.result?.bestVideo?.rendered === true) .filter(entry => entry.result.parentFrameVisible !== false) .sort(compareRanks); if (candidates.length === 0) return null; if (candidates.length > 1 && candidates[0].result.parentFrameVisible !== true && candidates[1].result.parentFrameVisible !== true && sameMeaningfulRank(candidates[0], candidates[1])) { return null; } return candidates[0]; } function findMissingPlayerAccess(results) { const accessibleOrigins = new Set(); for (const entry of results) { const origin = safeOrigin(entry?.result?.href); if (origin) accessibleOrigins.add(origin); } const missingByOrigin = new Map(); for (const entry of results) { for (const frame of entry?.result?.embeddedFrames || []) { if (!frame.visible || accessibleOrigins.has(frame.origin) || !frame.origin) continue; const aspectRatio = frame.height > 0 ? frame.width / frame.height : 0; const drivePlayer = isGoogleDrivePlayerUrl(frame.href); const looksLikePlayer = frame.mediaHint === true && frame.area >= MIN_PLAYER_FRAME_AREA && aspectRatio >= MIN_PLAYER_ASPECT_RATIO && aspectRatio <= MAX_PLAYER_ASPECT_RATIO; if (!drivePlayer && !looksLikePlayer) continue; const previous = missingByOrigin.get(frame.origin); if (!previous || frame.area > previous.area || drivePlayer) { missingByOrigin.set(frame.origin, { ...frame, drivePlayer }); } } } const missing = Array.from(missingByOrigin.values()).sort((left, right) => { if (left.drivePlayer !== right.drivePlayer) return left.drivePlayer ? -1 : 1; return right.area - left.area; }); if (missing.length === 0) return null; if (!missing[0].drivePlayer && missing.length > 1 && missing[0].area < missing[1].area * 1.5) { return null; } return { host: new URL(missing[0].origin).hostname, originPattern: originPattern(missing[0].origin), area: missing[0].area, drivePlayer: missing[0].drivePlayer === true }; } function shouldPreferMissingAccess(access, selected) { if (!access) return false; if (access.drivePlayer || !selected?.result?.bestVideo) return true; const video = selected.result.bestVideo; if (!video.hasSource || !video.rendered || video.background) return true; const selectedArea = Number.isFinite(video.renderedArea) ? video.renderedArea : 0; const weakAccessibleCandidate = !video.controls && video.duration > 0 && video.duration < 300; return weakAccessibleCandidate && access.area >= Math.max(MIN_PLAYER_FRAME_AREA, selectedArea * 1.5); } function accessRequiredError(access) { const error = new Error(`Embedded player access is required for ${access.host}`); error.code = MEDIA_FRAME_ACCESS_REQUIRED; error.host = access.host; error.originPattern = access.originPattern; return error; } function ambiguousFrameError() { const error = new Error('The active embedded video frame could not be identified safely'); error.code = MEDIA_FRAME_AMBIGUOUS; return error; } function contentTarget(tabId, selected) { const frameId = normalizeFrameId(selected?.frameId); const documentId = typeof selected?.documentId === 'string' ? selected.documentId : null; return { frameId, documentId, frameUrl: typeof selected?.result?.href === 'string' ? selected.result.href : null, hasVideo: !!selected?.result?.bestVideo, scriptTarget: documentId ? { tabId, documentIds: [documentId] } : (frameId === 0 ? { tabId } : { tabId, frameIds: [frameId] }) }; } export async function listMediaFrameScriptTargets(chromeApi, tabId) { if (chromeApi.webNavigation?.getAllFrames) { try { const frames = await chromeApi.webNavigation.getAllFrames({ tabId }); if (Array.isArray(frames) && frames.length > 0) { return frames .filter(frame => Number.isInteger(frame?.frameId)) .map(frame => (typeof frame.documentId === 'string' && frame.documentId ? { tabId, documentIds: [frame.documentId] } : { tabId, frameIds: [frame.frameId] })); } } catch { // Older browsers fall back to the all-frames probe below. } } return [{ tabId, allFrames: true }]; } async function executeInAccessibleFrames(chromeApi, targets, func, args) { const settled = await Promise.all(targets.map(async target => { try { return await chromeApi.scripting.executeScript({ target, func, args }); } catch { return []; } })); return settled.flat(); } export async function resolveMediaContentTarget(chromeApi, tabId, { attempts = 8, retryDelayMs = 200, probeDelayMs = 60 } = {}) { let fallback = null; let missingAccess = null; let ambiguous = false; for (let attempt = 0; attempt < attempts; attempt++) { const scriptTargets = await listMediaFrameScriptTargets(chromeApi, tabId); let results = await executeInAccessibleFrames( chromeApi, scriptTargets, inspectMediaFrame, [null] ); if (results.length === 0) { try { results = await chromeApi.scripting.executeScript({ target: { tabId }, func: inspectMediaFrame, args: [null] }); } catch { return contentTarget(tabId, null); } } if (results.length > 1) { const token = `${tabId}:${attempt}:${Date.now()}:${Math.random()}`; try { await executeInAccessibleFrames( chromeApi, scriptTargets, installParentFrameVisibilityProbe, [token] ); // Four passes match the maximum same-origin recursion depth. for (let pass = 0; pass < 4; pass++) { await executeInAccessibleFrames( chromeApi, scriptTargets, dispatchParentFrameVisibilityProbe, [token] ); await new Promise(resolve => setTimeout(resolve, probeDelayMs)); } const inspected = await executeInAccessibleFrames( chromeApi, scriptTargets, inspectMediaFrame, [token] ); if (inspected.length > 0) results = inspected; } catch { // Initial results remain usable, but equally-ranked unknown // frames will be rejected below rather than guessed. } } const selected = selectMediaFrame(results); const videoCandidates = results.filter(entry => entry?.result?.bestVideo?.rendered === true && entry.result.parentFrameVisible !== false); const currentMissingAccess = findMissingPlayerAccess(results); missingAccess = currentMissingAccess; fallback = selected; ambiguous = !selected && videoCandidates.length > 1; if (selected) { if (selected.result.bestVideo.hasSource && selected.result.bestVideo.rendered && !shouldPreferMissingAccess(currentMissingAccess, selected)) { return contentTarget(tabId, selected); } } if (attempt < attempts - 1) { await new Promise(resolve => setTimeout(resolve, retryDelayMs)); } } if (missingAccess) throw accessRequiredError(missingAccess); if (fallback) return contentTarget(tabId, fallback); if (ambiguous) throw ambiguousFrameError(); return contentTarget(tabId, null); }