mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-19 15:46:15 +00:00
fix(extension): stop the video-state poll from restarting the target
getReadyTabVideoState() treated "no video found" as a broken injection and forced a full reactivation. On a page that legitimately has no video yet — an anime or Drive page before playback starts — that fired on every call, and the dev panel polls it on a timer. The result was an endless teardown and reinjection cycle: the target never settled, the popup showed "activating" forever, and the panel reported "Target tab changed before content script recovery completed" because each read raced the reactivation it had triggered. Only an unreachable content script justifies recovery now, and that recovery no longer reinjects unless the selected frame actually moved. Audited against v3.1.2, which worked on these pages. The only unjustified deviation left was the retry budget, which had been cut from eight passes to three and shortened the window for a late-loading player; it is back at eight, now bounded by a wall-clock deadline instead of being unbounded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+14
-5
@@ -2059,17 +2059,26 @@ async function getReadyTabVideoState(tabId, expectedGeneration = targetActivatio
|
||||
return { error: 'Target tab changed before video state could be read' };
|
||||
}
|
||||
let state = await getTabVideoState(tabId);
|
||||
if (!state || state.error || state.found === false) {
|
||||
const activation = await refreshCurrentMediaTarget(tabId);
|
||||
if (activation?.status !== 'ok') {
|
||||
// "No video" is a legitimate answer, not a broken injection: an anime or
|
||||
// Drive page has no video element until the viewer starts playback. Forcing
|
||||
// a reactivation for it made every poll of this function tear the content
|
||||
// script down and reinject it, which kept the target permanently activating.
|
||||
// Only an unreachable content script justifies recovery.
|
||||
if (!state || state.error) {
|
||||
const activation = await refreshCurrentMediaTarget(tabId, { onlyIfTargetMoved: true });
|
||||
if (activation?.status !== 'ok' && activation?.status !== 'unchanged') {
|
||||
return { error: 'Target tab changed before content script recovery completed' };
|
||||
}
|
||||
// An unchanged target reports no generation of its own.
|
||||
const generation = Number.isInteger(activation.generation)
|
||||
? activation.generation
|
||||
: targetActivationGeneration;
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
if (!isCurrentTargetIdentity(tabId, activation.generation)) {
|
||||
if (!isCurrentTargetIdentity(tabId, generation)) {
|
||||
return { error: 'Target tab changed before video state could be read' };
|
||||
}
|
||||
state = await getTabVideoState(tabId);
|
||||
if (!isCurrentTargetIdentity(tabId, activation.generation)) {
|
||||
if (!isCurrentTargetIdentity(tabId, generation)) {
|
||||
return { error: 'Target tab changed while video state was being read' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,11 +526,17 @@ async function originAccessIsWithheld(chromeApi, originPattern) {
|
||||
}
|
||||
|
||||
export async function resolveMediaContentTarget(chromeApi, tabId, {
|
||||
attempts = 3,
|
||||
// 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
|
||||
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
|
||||
// ...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;
|
||||
@@ -636,6 +642,9 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isFinite(deadlineMs) && deadlineMs > 0 && Date.now() - startedAt >= deadlineMs) {
|
||||
break;
|
||||
}
|
||||
if (attempt < attempts - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('target tab lifecycle', () => {
|
||||
it('does not reactivate the target for ordinary playback churn', () => {
|
||||
expect(backgroundSource).toContain('async function selectedMediaTargetMoved(tabId)');
|
||||
expect(backgroundSource).toContain('onlyIfTargetMoved = false');
|
||||
expect(backgroundSource.match(/onlyIfTargetMoved: true/g)?.length).toBe(4);
|
||||
expect(backgroundSource.match(/onlyIfTargetMoved: true/g)?.length).toBe(5);
|
||||
// Playback state must stay out of the candidate signature, otherwise
|
||||
// every play/pause looks like a frame layout change.
|
||||
expect(monitorSource).not.toContain('element.paused ? 0 : 1');
|
||||
@@ -60,7 +60,8 @@ describe('target tab lifecycle', () => {
|
||||
);
|
||||
expect(resolverSource).toContain('async function originAccessIsWithheld(chromeApi, originPattern)');
|
||||
expect(resolverSource).toContain('probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS');
|
||||
expect(resolverSource).toContain('attempts = 3');
|
||||
expect(resolverSource).toContain('attempts = 8');
|
||||
expect(resolverSource).toContain('deadlineMs = 12000');
|
||||
// A swallowed probe error is what turned a slow player frame into a
|
||||
// permission prompt for an origin the extension already held.
|
||||
expect(resolverSource).toContain('errors.push({ target, error })');
|
||||
|
||||
Reference in New Issue
Block a user