Commit Graph

9 Commits

Author SHA1 Message Date
Timo e0c68650c5 perf(extension): cut target selection from ~20s to under 3s
Selecting an anime tab took long enough that it read as broken. Measured against
the two anime fixtures, the cost was three multiplying blocks, none of which was
doing useful work.

The visibility handshake ran on every page with more than one frame, including
pages where no frame had a video at all. Its only purpose is to rank and exclude
video candidates, so with nothing to rank it was several seconds of pure waiting
per attempt. It is now skipped unless a candidate exists.

Its pass count was fixed at four, the worst-case same-origin nesting depth. It
now scales to the depth actually observed, which is two on these players, and
each surplus pass was a full round trip across every frame.

The retry budget was spent waiting for a video that no frame had. Retrying
cannot conjure one, and the injected monitor promotes the real player within a
fraction of a second of it appearing, so the loop stops instead — and only
retries when the sweep itself came back thin, which is the case a second pass
can actually fix.

Both probe timeouts were also far too generous. inspectMediaFrame and the
monitor injection are synchronous DOM work: a live frame answers in tens of
milliseconds and anything slower is a frame being torn down, which is exactly
what an ad slot is. 2000ms down to 750ms; a frame dropped there is re-probed on
the next attempt and reports itself through its monitor anyway.

Measured, calm page then heavy ad churn:
  selection  2.5s / 2.6s   (was 2.5s / 7.2s, and ~12s before this series)
  promotion  0.34s / 2.7s  (was 0.35s / 6.4s, and ~15s before this series)

The remaining time is the injection chain itself, not discovery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:03:40 +02:00
Timo 414be96432 fix(extension): close the frame-registry gaps found in a manual audit
Read the whole target workflow end to end rather than re-running the suite.
Five defects, none of which the existing tests could have caught.

A srcless iframe resolves to its parent document's URL, so a hidden ad slot
could mark the page containing it as hidden and exclude the real player. The
hidden-frame filter now requires the frame element to have actually carried a
src, and ignores any frame claiming the href of the document that reported it.

The frame registry was fed only by incoming sender.frameId, which is empty
during the first activation — exactly when the all-frames sweep needs a
fallback. The resolver now reports every frame it reached and those ids are
recorded before anything is injected.

Monitor injection and deactivation both went through the sweep alone. A
rejected sweep therefore left deep frames without a monitor (so they could
never report themselves, keeping the registry empty) and, on the way out, left
stale monitors reporting after a target switch. Both now address known frames
individually as well.

Registry eviction skipped when the oldest entry was the top frame, so the set
could grow without bound, and a cap of 64 meant up to 64 individual probes —
the same cost the removed 0..64 sweep had. The cap is 24, eviction always
removes a non-top frame, and a committed navigation drops the tab's ids so dead
frames from the previous page are not probed forever.

The stuck-activation watchdog only ran inside GET_STATUS, so it never fired
while the popup was closed — the one situation it exists for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:25:19 +02:00
Timo ac0093b043 fix(extension): stop a single unresponsive frame from stalling activation
v3.1.2 worked on the nested anime players. Removing the webNavigation
permission in 4d78970 replaced its per-frame listing with one allFrames call
and left the injection path unbounded, and that is what broke them.

Both properties are restored without the permission. The allFrames sweep now
only discovers the frame list — it already reports frameId and documentId for
every frame it reached — and each probe after it is addressed to a single
frame, so a player or ad frame that never answers can no longer cancel the
others. Every chrome.scripting.executeScript in the injection path is bounded
by a timeout; nine of them could previously stay pending forever, which pinned
activeTargetActivation and left the popup reporting "activating" with nothing
in the log.

A watchdog abandons any activation still running after 30s and turns it into a
reportable error, so that state cannot be permanent again regardless of cause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 00:55:12 +02:00
Timo 096775d39f fix(extension): make anime-style nested players selectable again
Reproduced against the live yummyanime.tv layout, which is:

  top (no video)
  ├── visible same-origin wrapper 830x498 -> cross-origin player
  ├── hidden same-origin wrapper    0x0   -> cross-origin mirror
  └── hidden cross-origin trailer   0x0

Three things kept that page from ever settling on a target.

Equally-ranked players were a hard failure. Several mirrors or dubs loaded at
once is an ordinary layout for these sites, and refusing to activate made them
unusable. The resolver now holds the top frame and waits for one of them to
start playing, which is the signal that breaks the tie.

Inconclusive probes moved the target. A page whose players are still loading
resolves differently from one call to the next, and every difference triggered
a full teardown and reinjection, so activation never finished — the popup sat
on "activating" with nothing in the log. A probe that finds no video now leaves
the target where it is.

The visibility handshake expired mid-probe. Its listener lived 1000ms while the
probe sequence is six separate executeScript round trips; on a heavy page it
was gone before the answer arrived, leaving every frame's visibility unknown —
the exact state that makes two players look equal. It now outlives the sequence.

A settled failure also no longer reports itself as "activating".

Covered by two fixtures built from the real page: one where the player exists
up front, and one where the host only creates it on play, asserting the target
is promoted into the deep cross-origin frame without touching the popup again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 00:47:29 +02:00
Timo 75a9ba5d3d fix(extension): control nested players without permission prompts or churn
Google Drive and YummyAnime host their player in a cross-origin iframe. The
3.1.2 targeting work reached those frames but misdiagnosed and destabilized
them in four separate ways. No manifest permission is added or restored;
webNavigation stays removed.

Access diagnosis was inferred, not measured. Every frame probe error was
swallowed, and any origin that failed to answer was reported as missing host
access. A slow or still-loading player frame therefore produced
"Host access required for youtube.googleapis.com" for an origin the extension
already held. The resolver now asks permissions.contains() before raising an
access error, and treats a granted-but-unresponsive origin as a retry, not a
user decision.

Probes were unbounded. Every executeScript in the resolver now runs under a
timeout, so one unreachable frame can no longer stall an activation, and the
retry budget drops from eight passes to three.

The chat overlay followed the player into its frame, which rendered it on top
of the video and scoped closing and minimizing to that frame. It is now always
installed in the tab's top document, with all chat traffic routed to frame 0,
while only the playback controller goes into the selected media frame.

Nested targets reactivated continuously. Every heartbeat and content event
revalidated the target with a full teardown and reinjection, and the media
monitor treated ordinary play, pause and buffering as frame layout changes.
Both paths now reactivate only when the selected frame or document actually
moves.

Also restores the audio-route retention that keeps a deselected tab audible:
createMediaElementSource() can only be called once per element, so a
reinjected content script must adopt the existing route rather than rebuild it.

Verified with 90 unit tests, 40 browser E2E tests including two new
Drive-shaped fixtures that assert the controller lands in the player frame
while the chat stays in the top document, and npm run verify.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 00:15:10 +02:00
Timo 04694d4439 revert(extension): drop post-3.1.2 frame-targeting band-aids
Restores extension/ to the state directly after webNavigation was removed
(4d78970). The six follow-up commits layered heuristics on an unverified
premise (frame-ID sweeps, multi-phase probes, retry loops) without fixing
the underlying resolver. They are removed so the real fix can be built on
a known state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:53:10 +02:00
Timo e1daeecab1 fix(extension): harden target frame recovery and switching 2026-08-17 23:06:46 +02:00
Timo 4d7897028b fix(extension): remove webNavigation permission 2026-08-17 18:41:40 +02:00
KoalaDev 082b69f509 fix(extension): support cross-origin media frames 2026-08-17 16:49:53 +02:00