fix(extension): bound frame injection and retry failures

This commit is contained in:
Timo
2026-08-17 23:37:09 +02:00
parent e1daeecab1
commit 804c36c694
3 changed files with 117 additions and 21 deletions
+70 -11
View File
@@ -80,6 +80,8 @@ let currentTargetHasVideo = false;
// popup look as if the user never selected a tab.
let requestedTargetTabId = null;
let requestedTargetTitle = null;
let requestedTargetRetryBlockedTabId = null;
let requestedTargetRetryBlockedMessage = null;
let pendingRequestedActivationCount = 0;
let targetActivationGeneration = 0;
let activeTargetActivation = null;
@@ -224,6 +226,7 @@ function ensureState() {
'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'reconnectAttempts', 'currentTabId', 'currentTabTitle',
'currentTargetFrameId', 'currentTargetDocumentId', 'currentTargetHasVideo',
'requestedTargetTabId', 'requestedTargetTitle',
'requestedTargetRetryBlockedTabId', 'requestedTargetRetryBlockedMessage',
'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount', 'roomIdleSince', 'lastContentHeartbeatAt',
'hcmDesynced', 'chatActivityTimeline'
], (data) => {
@@ -246,6 +249,11 @@ function ensureState() {
&& typeof data.requestedTargetTitle === 'string'
? data.requestedTargetTitle
: null;
requestedTargetRetryBlockedTabId = normalizeTabId(data.requestedTargetRetryBlockedTabId);
requestedTargetRetryBlockedMessage = requestedTargetRetryBlockedTabId !== null
&& typeof data.requestedTargetRetryBlockedMessage === 'string'
? data.requestedTargetRetryBlockedMessage
: null;
if (data.currentTabTitle !== undefined) {
currentTabTitle = currentTabId !== null && typeof data.currentTabTitle === 'string'
? data.currentTabTitle
@@ -2340,6 +2348,31 @@ function createTargetActivationSupersededError() {
return error;
}
const SCRIPT_INJECTION_TIMEOUT_MS = 5000;
function executeScriptWithTimeout(options, timeoutMs = SCRIPT_INJECTION_TIMEOUT_MS) {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
return chrome.scripting.executeScript(options);
}
let timeoutId = null;
const label = Array.isArray(options?.files) && options.files.length > 0
? options.files.join(', ')
: 'function injection';
const timeout = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
const error = new Error(`Script injection timed out after ${timeoutMs}ms (${label})`);
error.code = 'script_injection_timeout';
reject(error);
}, timeoutMs);
});
return Promise.race([
chrome.scripting.executeScript(options),
timeout
]).finally(() => {
if (timeoutId !== null) clearTimeout(timeoutId);
});
}
async function injectMediaFrameMonitors(tabId, contentTarget) {
// The all-frames target is only a best-effort sweep: one inaccessible
// frame can make Chromium reject the entire sweep. Always include the
@@ -2353,12 +2386,15 @@ async function injectMediaFrameMonitors(tabId, contentTarget) {
let injectedCount = 0;
for (const target of targets) {
try {
await chrome.scripting.executeScript({
await executeScriptWithTimeout({
target,
files: ['media-frame-monitor.js']
});
}, 2000);
injectedCount++;
} catch {
} catch (error) {
if (error?.code === 'script_injection_timeout') {
addLog(`Media-frame monitor injection timed out for ${JSON.stringify(target)}`, 'warn');
}
// One denied widget frame must not block the selected player.
}
}
@@ -2424,12 +2460,12 @@ async function injectContentScript(tabId, {
}
if (needsPageApiSeek) {
try {
await chrome.scripting.executeScript({
await executeScriptWithTimeout({
target: scriptTarget,
world: 'MAIN',
files: ['page-api-seek-overrides.js']
});
await chrome.scripting.executeScript({
await executeScriptWithTimeout({
target: scriptTarget,
world: 'MAIN',
func: installPageApiSeekBridge
@@ -2440,16 +2476,16 @@ async function injectContentScript(tabId, {
}
}
await chrome.scripting.executeScript({
await executeScriptWithTimeout({
target: scriptTarget,
files: ['page-api-seek-overrides.js']
});
await chrome.scripting.executeScript({
await executeScriptWithTimeout({
target: scriptTarget,
func: setPageApiSeekEnabled,
args: [pageApiSeekReady]
});
const injectionResults = await chrome.scripting.executeScript({
const injectionResults = await executeScriptWithTimeout({
target: scriptTarget,
files: ['chat-format.js', 'chat-overlay.js', 'content.js']
});
@@ -2652,9 +2688,13 @@ async function rememberRequestedTarget(tabId, tabTitle) {
if (normalizedTabId === null) return false;
requestedTargetTabId = normalizedTabId;
requestedTargetTitle = typeof tabTitle === 'string' ? tabTitle : null;
requestedTargetRetryBlockedTabId = null;
requestedTargetRetryBlockedMessage = null;
await chrome.storage.session.set({
requestedTargetTabId,
requestedTargetTitle
requestedTargetTitle,
requestedTargetRetryBlockedTabId: null,
requestedTargetRetryBlockedMessage: null
});
return true;
}
@@ -2666,9 +2706,13 @@ async function clearRequestedTarget(expectedTabId = null) {
}
requestedTargetTabId = null;
requestedTargetTitle = null;
requestedTargetRetryBlockedTabId = null;
requestedTargetRetryBlockedMessage = null;
await chrome.storage.session.set({
requestedTargetTabId: null,
requestedTargetTitle: null
requestedTargetTitle: null,
requestedTargetRetryBlockedTabId: null,
requestedTargetRetryBlockedMessage: null
});
return true;
}
@@ -2678,7 +2722,8 @@ async function retryRequestedTarget() {
if (selectedTabId === null
|| normalizeTabId(currentTabId) === selectedTabId
|| pendingRequestedActivationCount > 0
|| activeTargetActivation) {
|| activeTargetActivation
|| normalizeTabId(requestedTargetRetryBlockedTabId) === selectedTabId) {
return null;
}
@@ -2702,6 +2747,14 @@ async function retryRequestedTarget() {
}
return response;
} catch (error) {
if (error?.code !== HOST_ACCESS_REQUIRED_STATUS) {
requestedTargetRetryBlockedTabId = selectedTabId;
requestedTargetRetryBlockedMessage = error?.message || 'Script injection failed';
await chrome.storage.session.set({
requestedTargetRetryBlockedTabId,
requestedTargetRetryBlockedMessage
});
}
addLog(`Requested target retry failed: ${error.message}`, 'warn');
return injectionFailureResponse(error);
}
@@ -3391,7 +3444,12 @@ async function handleAsyncMessage(message, sender, sendResponse) {
? 'ready'
: pendingTarget?.tabId === targetTabId
? 'access_required'
: normalizeTabId(requestedTargetRetryBlockedTabId) === targetTabId
? 'error'
: 'activating';
const targetActivationError = normalizeTabId(requestedTargetRetryBlockedTabId) === targetTabId
? requestedTargetRetryBlockedMessage
: null;
sendResponse({
status,
peerId,
@@ -3403,6 +3461,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
targetHasVideo: currentTargetHasVideo,
targetReady,
targetActivationState,
targetActivationError,
pendingTargetTabId: pendingTarget?.tabId ?? null,
pendingTargetHost: pendingTarget?.host ?? null,
pendingTargetOriginPattern: pendingTarget?.originPattern ?? null,
+36 -9
View File
@@ -2,6 +2,7 @@ export const MEDIA_FRAME_ACCESS_REQUIRED = 'media_frame_access_required';
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 = 1500;
function normalizeFrameId(value) {
return Number.isInteger(value) && value >= 0 ? value : 0;
@@ -463,10 +464,29 @@ function mergeFrameResults(...groups) {
return Array.from(merged.values());
}
async function executeInAccessibleFrames(chromeApi, targets, func, args) {
function executeWithTimeout(task, timeoutMs, label) {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return task();
let timeoutId = null;
const timeout = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
const error = new Error(`${label} timed out after ${timeoutMs}ms`);
error.code = 'media_frame_probe_timeout';
reject(error);
}, timeoutMs);
});
return Promise.race([task(), timeout]).finally(() => {
if (timeoutId !== null) clearTimeout(timeoutId);
});
}
async function executeInAccessibleFrames(chromeApi, targets, func, args, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
const settled = await Promise.all(targets.map(async target => {
try {
const result = await chromeApi.scripting.executeScript({ target, func, args });
const result = await executeWithTimeout(
() => chromeApi.scripting.executeScript({ target, func, args }),
timeoutMs,
`Frame probe for ${JSON.stringify(target)}`
);
return Array.isArray(result) ? result : [];
} catch {
return [];
@@ -478,7 +498,8 @@ async function executeInAccessibleFrames(chromeApi, targets, func, args) {
export async function resolveMediaContentTarget(chromeApi, tabId, {
attempts = 8,
retryDelayMs = 200,
probeDelayMs = 60
probeDelayMs = 60,
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS
} = {}) {
let fallback = null;
let missingAccess = null;
@@ -489,13 +510,15 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
chromeApi,
[{ tabId, frameIds: [0] }],
inspectMediaFrame,
[null]
[null],
probeTimeoutMs
);
const allFrameResults = await executeInAccessibleFrames(
chromeApi,
listMediaFrameScriptTargets(tabId),
inspectMediaFrame,
[null]
[null],
probeTimeoutMs
);
let results = mergeFrameResults(topResults, allFrameResults);
const embeddedFrameCount = topResults.reduce(
@@ -507,7 +530,8 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
chromeApi,
listFrameProbeTargets(tabId, embeddedFrameCount),
inspectMediaFrame,
[null]
[null],
probeTimeoutMs
);
results = mergeFrameResults(results, individuallyProbed);
}
@@ -521,7 +545,8 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
chromeApi,
frameTargets,
installParentFrameVisibilityProbe,
[token]
[token],
probeTimeoutMs
);
// Four passes match the maximum same-origin recursion depth.
for (let pass = 0; pass < 4; pass++) {
@@ -529,7 +554,8 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
chromeApi,
frameTargets,
dispatchParentFrameVisibilityProbe,
[token]
[token],
probeTimeoutMs
);
await new Promise(resolve => setTimeout(resolve, probeDelayMs));
}
@@ -537,7 +563,8 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
chromeApi,
frameTargets,
inspectMediaFrame,
[token]
[token],
probeTimeoutMs
);
if (inspected.length > 0) results = mergeFrameResults(results, inspected);
} catch {
+11 -1
View File
@@ -2808,13 +2808,23 @@ function refreshDebugInfo() {
}
return;
}
if (res?.targetActivationState === 'error') {
if (elements.videoDebug) {
elements.videoDebug.textContent = res.targetActivationError
? `Injection fehlgeschlagen: ${res.targetActivationError}`
: 'Video-Injection fehlgeschlagen.';
}
return;
}
if (elements.videoDebug) elements.videoDebug.textContent = getMessage('DEBUG_NO_TAB');
return;
}
if (res.targetReady !== true) {
if (elements.videoDebug) {
elements.videoDebug.textContent = getMessage('DEBUG_TARGET_ACTIVATING');
elements.videoDebug.textContent = res.targetActivationState === 'error'
? `Injection fehlgeschlagen: ${res.targetActivationError || 'unbekannter Fehler'}`
: getMessage('DEBUG_TARGET_ACTIVATING');
}
return;
}