Files
KoalaSync/extension/media-frame-target.js
T
Timo 68f2d9f27c fix(extension): find the player when the all-frames sweep comes back empty
Diagnosed against a fixture rebuilt from the live yummyanime.tv page, with the
ad churn the real site produces. Under that churn the resolver reported
frame=0, hasVideo=false while the video demonstrably existed two frame levels
down: one ad slot tearing down mid-call makes Chromium reject the whole
allFrames sweep, and the resolver then silently fell back to the top frame and
never looked again.

v3.1.2 did not have this failure because webNavigation.getAllFrames() gave it
an explicit frame list. That list is now rebuilt without the permission: every
content script that messages the background carries sender.frameId, so the
background keeps a per-tab registry of frames it has seen and the resolver asks
any frame the sweep missed directly. One rejected probe now costs one frame
instead of the whole page.

Two supporting fixes fell out of the same investigation. Frames reported hidden
by an ancestor that could inspect them directly — the 0x0 same-origin wrapper an
anime host parks unwatched mirrors in — are now excluded without waiting for the
postMessage visibility handshake, which was the tie the resolver kept failing to
break. And leaf frames with no video and no nested frames are left out of that
handshake entirely, so a churning ad slot can no longer make every phase wait on
a frame that is already gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:56:17 +02:00

718 lines
31 KiB
JavaScript

export const MEDIA_FRAME_ACCESS_REQUIRED = 'media_frame_access_required';
export const MEDIA_FRAME_PROBE_TIMEOUT = 'media_frame_probe_timeout';
const MIN_PLAYER_FRAME_AREA = 320 * 180;
const MIN_PLAYER_ASPECT_RATIO = 1.15;
const MAX_PLAYER_ASPECT_RATIO = 2.6;
const DEFAULT_PROBE_TIMEOUT_MS = 2000;
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);
// The listener has to outlive the whole probe sequence: install, four
// dispatch passes and the final inspection, each a separate executeScript
// round trip. On a heavy page those add up well past a second, and a
// listener that expired first left every frame's visibility unknown — which
// is exactly the state that makes two players look equally ranked.
timeout = setTimeout(cleanup, 15000);
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]);
}
/**
* Frames whose own element was seen as hidden by an ancestor that could inspect
* it directly. A same-origin wrapper collapsed to 0x0 — the usual way an anime
* host parks the mirrors you are not watching — is reported here by the top
* frame itself, so the hidden player can be ruled out without waiting for the
* postMessage visibility handshake to complete.
*/
function hiddenFrameHrefs(injectionResults) {
const visibility = new Map();
for (const entry of Array.isArray(injectionResults) ? injectionResults : []) {
for (const frame of entry?.result?.embeddedFrames || []) {
if (typeof frame?.href !== 'string' || !frame.href) continue;
// Any ancestor reporting it visible wins over one reporting it hidden.
visibility.set(frame.href, (visibility.get(frame.href) === true) || frame.visible === true);
}
}
const hidden = new Set();
for (const [href, visible] of visibility) if (!visible) hidden.add(href);
return hidden;
}
export function selectMediaFrame(injectionResults) {
const hidden = hiddenFrameHrefs(injectionResults);
const candidates = (Array.isArray(injectionResults) ? injectionResults : [])
.filter(entry => Number.isInteger(entry?.frameId)
&& entry?.result?.bestVideo?.rendered === true)
.filter(entry => entry.result.parentFrameVisible !== false)
.filter(entry => !hidden.has(entry.result.href))
.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 (!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;
// Drive never plays the file in its own document, so its embedded player
// outranks a weak local candidate. It must not outrank a real one: a Drive
// tab can host an ordinary accessible video next to a file preview.
if (access.drivePlayer) {
return weakAccessibleCandidate || selectedArea < MIN_PLAYER_FRAME_AREA;
}
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 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 function listMediaFrameScriptTargets(tabId) {
return [{ tabId, allFrames: true }];
}
/** Pins one probe to one frame, preferring the exact document when known. */
function frameScriptTarget(tabId, entry) {
const frameId = normalizeFrameId(entry?.frameId);
return typeof entry?.documentId === 'string' && entry.documentId
? { tabId, documentIds: [entry.documentId] }
: { tabId, frameIds: [frameId] };
}
/**
* Later results replace earlier ones for the same frame. A frame that navigated
* between two probes must not appear twice, because two stale copies of one
* frame look exactly like two competing players.
*/
function mergeFrameResults(...groups) {
const merged = new Map();
for (const group of groups) {
for (const entry of Array.isArray(group) ? group : []) {
if (!Number.isInteger(entry?.frameId)) continue;
merged.set(`frame:${entry.frameId}`, entry);
}
}
return Array.from(merged.values());
}
function probeTimeoutError(label, timeoutMs) {
const error = new Error(`${label} timed out after ${timeoutMs}ms`);
error.code = MEDIA_FRAME_PROBE_TIMEOUT;
return error;
}
function executeWithTimeout(task, timeoutMs, label) {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return task();
let timeoutId = null;
const timeout = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(probeTimeoutError(label, timeoutMs)), timeoutMs);
});
return Promise.race([task(), timeout]).finally(() => {
if (timeoutId !== null) clearTimeout(timeoutId);
});
}
async function executeInAccessibleFrames(chromeApi, targets, func, args, timeoutMs) {
const errors = [];
const settled = await Promise.all(targets.map(async target => {
try {
const result = await executeWithTimeout(
() => chromeApi.scripting.executeScript({ target, func, args }),
timeoutMs,
`Frame probe ${JSON.stringify(target)}`
);
return Array.isArray(result) ? result : [];
} catch (error) {
// A failed probe is recorded, never silently dropped. Only the
// caller can tell "withheld origin" from "frame is still loading",
// and guessing that difference is what produced false permission
// prompts for players the extension was already allowed to touch.
errors.push({ target, error });
return [];
}
}));
return { results: settled.flat(), errors };
}
/**
* Asks the browser whether an origin is genuinely withheld.
*
* Returns true when the grant is missing, false when it is held, and null when
* the browser cannot answer. A frame that did not respond to a probe is not
* evidence of a missing grant: that inference is what made Drive and
* YummyAnime demand access for an origin the extension already had.
*/
async function originAccessIsWithheld(chromeApi, originPattern) {
if (typeof originPattern !== 'string' || !originPattern) return null;
if (typeof chromeApi?.permissions?.contains !== 'function') return null;
try {
const granted = await executeWithTimeout(
() => Promise.resolve(chromeApi.permissions.contains({ origins: [originPattern] })),
1000,
`Permission check for ${originPattern}`
);
if (granted === true) return false;
if (granted === false) return true;
return null;
} catch {
return null;
}
}
export async function resolveMediaContentTarget(chromeApi, tabId, {
// v3.1.2's retry budget: a player frame can take several seconds to appear,
// and giving up early is what turns a slow page into "no video found".
attempts = 8,
retryDelayMs = 200,
probeDelayMs = 60,
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
// Frame ids the background has seen in this tab. They rescue the probe when
// the all-frames sweep is rejected wholesale by one unrelated frame.
knownFrameIds = [],
// ...but the budget is now wall-clock bounded, so a page whose frames all
// time out cannot hold the activation open for minutes.
deadlineMs = 12000
} = {}) {
const startedAt = Date.now();
let fallback = null;
let missingAccess = null;
let ambiguous = false;
let unresolvedGrantedHost = null;
for (let attempt = 0; attempt < attempts; attempt++) {
const scriptTargets = listMediaFrameScriptTargets(tabId);
let { results } = await executeInAccessibleFrames(
chromeApi,
scriptTargets,
inspectMediaFrame,
[null],
probeTimeoutMs
);
// Any frame the sweep missed but that we know exists gets asked directly.
// One rejected probe then costs one frame, not the whole page.
const missingFrameIds = knownFrameIds.filter(frameId => Number.isInteger(frameId)
&& !results.some(entry => entry.frameId === frameId));
if (missingFrameIds.length > 0) {
const { results: recovered } = await executeInAccessibleFrames(
chromeApi,
missingFrameIds.map(frameId => ({ tabId, frameIds: [frameId] })),
inspectMediaFrame,
[null],
probeTimeoutMs
);
if (recovered.length > 0) results = mergeFrameResults(results, recovered);
}
if (results.length === 0) {
// The all-frames sweep answered for nothing at all, so fall back to
// the top document alone. Every probe is time-boxed: an unreachable
// player frame must never stall the whole activation.
try {
const topResults = await executeWithTimeout(
() => chromeApi.scripting.executeScript({
target: { tabId },
func: inspectMediaFrame,
args: [null]
}),
probeTimeoutMs,
'Top-frame probe'
);
results = Array.isArray(topResults) ? topResults : [];
if (results.length === 0) return contentTarget(tabId, null);
} catch {
return contentTarget(tabId, null);
}
}
if (results.length > 1) {
const token = `${tabId}:${attempt}:${Date.now()}:${Math.random()}`;
// Address each discovered frame on its own from here on. A single
// allFrames call is all-or-nothing: one player or ad frame that
// never answers takes the whole probe down with it. v3.1.2 avoided
// that by listing frames through webNavigation — but the sweep
// above already reports frameId and documentId for every frame it
// reached, so the same isolation costs no permission at all.
// A leaf frame with no video and no nested frames can never be a
// candidate nor an ancestor of one. Ad slots are exactly that, and
// they churn constantly, so every phase below would otherwise wait
// on a frame that was already being torn down.
const relevant = results.filter(entry => (entry?.result?.videoCount || 0) > 0
|| (entry?.result?.embeddedFrames?.length || 0) > 0
|| entry?.result?.isTop === true);
const frameTargets = (relevant.length > 0 ? relevant : results)
.map(entry => frameScriptTarget(tabId, entry));
try {
const visibilityTimeoutMs = Math.min(probeTimeoutMs, 750);
await executeInAccessibleFrames(
chromeApi,
frameTargets,
installParentFrameVisibilityProbe,
[token],
visibilityTimeoutMs
);
// Four passes match the maximum same-origin recursion depth.
for (let pass = 0; pass < 4; pass++) {
await executeInAccessibleFrames(
chromeApi,
frameTargets,
dispatchParentFrameVisibilityProbe,
[token],
visibilityTimeoutMs
);
await new Promise(resolve => setTimeout(resolve, probeDelayMs));
}
const { results: inspected } = await executeInAccessibleFrames(
chromeApi,
frameTargets,
inspectMediaFrame,
[token],
probeTimeoutMs
);
if (inspected.length > 0) results = mergeFrameResults(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);
let currentMissingAccess = findMissingPlayerAccess(results);
if (currentMissingAccess) {
const withheld = await originAccessIsWithheld(
chromeApi,
currentMissingAccess.originPattern
);
if (withheld === false) {
// The grant is already held, so the player frame is merely slow,
// still navigating, or gone. Retrying is correct here; prompting
// for a permission the user already gave is not.
unresolvedGrantedHost = currentMissingAccess.host;
currentMissingAccess = null;
}
}
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 (Number.isFinite(deadlineMs) && deadlineMs > 0 && Date.now() - startedAt >= deadlineMs) {
break;
}
if (attempt < attempts - 1) {
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
}
}
if (missingAccess) throw accessRequiredError(missingAccess);
if (fallback) return contentTarget(tabId, fallback);
// A player whose origin is already granted but which never answered is a
// timing problem, not a user decision. Keep the tab selected on its top
// frame so the injected monitor can promote the real player once it loads,
// instead of failing the activation or prompting for nothing.
if (unresolvedGrantedHost) return contentTarget(tabId, null);
// Several equally-ranked players — anime mirrors, alternative dubs — are a
// normal page layout, not an error. Refusing to activate made those pages
// unusable, and flipping between candidates restarted the target forever.
// Hold the top frame and let the monitor promote the one that starts
// playing, which is the signal that breaks the tie.
if (ambiguous) return { ...contentTarget(tabId, null), ambiguous: true };
return contentTarget(tabId, null);
}