The proposal said a registered content script could be scoped to the selected
tab's origin. It cannot, in two ways that both matter.
matches is evaluated against each frame's own URL, not the tab's, so a pattern
for the top-level site would never reach the embedded player frame — the only
frame the change exists to reach. Covering a player origin that is unknown
before discovery requires <all_urls>.
And registration has no tab scoping at all: with several tabs open on the same
site, every one of them gets the script.
The honest cost is therefore that a passive monitor runs in every frame of every
http/https tab while a target is selected, which is a privacy-posture change for
a project that advertises touching only the chosen tab. Recorded so the decision
is made against the real trade rather than the flattering version.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A monitor took the current DOM as its baseline on install, so a video that was
already present counted as "not a change" and was never announced. The discovery
poll added in the previous commit reinstalled monitors every 2s, which meant a
video appearing between two reinstalls was silently swallowed — the reported
debug log had no [Content] lines at all, which is the signature of exactly this.
Monitors now announce a video that is already there when they install, which
also makes the rebuilt-frame case work by construction rather than by timing.
The reinstall interval is raised to 5s now that each install is informative.
Also adds docs/frame-targeting-handoff.md: why v3.1.2 worked immediately with
webNavigation, why reconstructing that single call from sweeps, a learned
registry, per-frame monitors and a poll keeps producing timing windows, and the
proposed structural replacement (chrome.scripting.registerContentScripts with
allFrames, no new permission) together with the project invariant it conflicts
with — which is the owner's decision, not a code change to make unasked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported as "Receiving end does not exist" with targetReady true and no
activation errors: the election named a frame the player had already torn down.
Three defects were stacked, each hidden by the one in front of it.
The election was never released. getReadyTabVideoState() recovered through the
guarded refresh, which reports "unchanged" when no video is reachable, so the
stale frameId/documentId survived; adoption compounded it by setting hasVideo,
after which the target only moves on a frame change. An unreachable content
script — as opposed to a page that simply has no video yet — now releases the
frame election back to the top frame. The tab selection is never touched.
Switching frames destroyed the top frame's scripts. Promoting the target out of
frame 0 called deactivateTargetTab() on the previous target, which sent
TARGET_DEACTIVATE to frame 0 and tore down its content script and the chat
overlay with it. That is why chat delivery failed after promotion, and why
releasing the election pointed at an empty frame. An in-tab frame switch now
leaves the top frame alone.
Discovery could deadlock. Monitors announce new players, but a rebuilt frame is
a new document with no monitor, so the video created in it was never reported —
and nothing then triggered the upkeep that would have installed one. Monitors
are reinstalled on every lifecycle notification with a trailing-edge debounce,
and a bounded discovery poll breaks the cycle when no notification arrives at
all: 2s, capped, only while a tab is selected with no video found, stopping the
moment one is.
The new browser test adopts a nested player, destroys its document the way the
real player does, and asserts both the release and that the rebuilt player is
picked up again without touching the popup. It was flaky until the deadlock was
closed, and now passes repeatedly at a stable 8.3s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Why removing webNavigation broke Drive and the anime hosts, the verified frame
layout of the live KODIK page, each defect that was fixed on the way from tab
selection to playback control, and how the frame list is reconstructed without
the permission.
Also records the one defect still open: an elected frame that the player has
since torn down is never released, because the guarded refresh reports
"unchanged" when no video is reachable and adoption has already set
hasVideo, so nothing moves the target back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Reported from the live site: a media title was recognised and audio processing
worked, but play and pause did nothing. Both halves of the command path were
gated on frame election, and the election had named the top frame.
Outbound, commands went to the elected frame alone, which holds no video, so
they were delivered and ignored. Inbound, isCurrentContentSender() required
sender.frameId to equal the elected frame, so the user's own play and pause
arriving from the real player frame were discarded as a stale sender — which is
why the room never saw them.
Neither direction actually needs the election. Every content-script command
handler already begins with findVideo() and returns when there is none, so a
tab-wide broadcast is delivered to all frames and acted on only by the one that
owns the player. And an inbound media event proves where the player is:
sender.frameId is authoritative, costs no permission and has no timing window,
so the reporting frame is adopted as the target and later commands are addressed
directly again.
Both relaxations apply only while the elected frame reports no video. A good
election still takes the strict path, so the hidden-player rejections are
unaffected.
This is the general answer to losing webNavigation.getAllFrames(). That call
observed the frame tree without touching it, so it never had a failure window;
executeScript has to enter every frame and reliably loses that race against a
player which renavigates and rebuilds its video, as Kodik does. The fix is to
stop depending on the answer rather than to keep chasing it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Inspected the live KODIK layout: the player is a cross-origin frame at depth 2
inside a visible same-origin wrapper, with no sandbox attribute and no video
element at all until playback starts.
The registry wipe added in the previous commit was wrong. tabs.onUpdated reports
status 'loading' for same-document History API navigations as well, which is
exactly what these sites do when you switch mirror or episode part. The wipe
therefore landed while the player frame was being built, leaving the recovery
probe with nothing to fall back on; if the all-frames sweep was rejected at that
moment the target stayed on frame 0 with no way back.
The registry now self-corrects instead: a probe that reached more than the top
frame is authoritative and current, so it supersedes the stored ids. A probe
that only reached frame 0 proves nothing and merges. Stale ids cost one bounded,
isolated probe until the next good resolve replaces them.
This matches the reported symptom, where audio boost and compressor worked on
KODIK — proving the content script was running in the player frame — while the
dev panel still reported frame 0 with no video, because the target had fallen
back after promotion had already succeeded once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>