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:
Timo
2026-08-18 01:05:03 +02:00
parent ac0093b043
commit f450584562
4 changed files with 60 additions and 9 deletions
+3 -2
View File
@@ -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 })');