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>
A peer can be left paused at the end of an episode while the other moves on,
and nothing recovers it. The episode guard, the title on the wire and the
comparison all exist already, so the interesting part is why the guard lets
that one pause through — and that cannot be settled without logs from both
sides while it happens.
Records what was verified from the code, the three candidate causes that are
still hypotheses, the exact log lines that separate them, the planned fix
order, and the approaches already ruled out (forcing the comparison would
break every platform that exposes no media title).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sticky player selection, Firefox E2E coverage and a two-peer relay E2E are
all deliberate omissions rather than oversights. Writing them down keeps the
current suite from reading as broader coverage than it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The weighted score summed incomparable units, so size could outvote traits
that disqualify an element outright. Measured on a real page: a display:none
preload reports its full 1080p intrinsic size and scored 2073600, beating a
visible unmuted player at 509920.
Selection now compares an ordered list of signals, highest priority first:
has a source, is rendered, is not a silent background loop, rendered size
bucket, is playing, has controls, duration. Rendered size replaces intrinsic
resolution, and mute state is gone from the ranking entirely: it is a viewer
preference, not evidence about which element is the player.
It stays a ranking rather than a filter, so a page of only bad candidates
still yields one and findVideo never returns null where a video exists.
The new tests/e2e suite runs the shipped finder against real fixture pages
and drives the packed extension for injection, reinjection and remote
play/pause/seek into a first-party frame. All five scoring scenarios fail
against the previous implementation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Saving the editor used to persist the full effective list, which froze the
shipped defaults at that moment: every default added in a later version
never reached anyone who had touched the list once.
Storage now keeps only { removedDefaults, addedDomains }, and the effective
list is resolved against whatever ships in the running version. Entries are
tagged default or user, the editor groups them under comment headers, and
'#' lines are ignored on save. Lists saved by earlier versions are migrated
to the delta form on first read and the legacy key is removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Audit follow-ups on the same-origin frame walk:
- prune detached frames from the load-hook set so ad/SPA frame churn
no longer grows it for the page's lifetime
- hook load on nested frames, not just top-level ones
- re-observe from scratch after a frame reload instead of leaving the
replaced document's tree registered
- reset the frame registry when the heartbeat error path disconnects
Also: a broad parent domain in the blacklist no longer hides a host with
its own supported player path (drive.google.com behind google.com), while
an exact user entry for that host still filters it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Design brief for the in-page E2E encrypted chat: threat model, key
derivation via URL fragment, #j2 link format, backward compatibility
with old extensions, and the constraints the discarded feature/soonTMChat
branch got wrong.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Peer usernames are remote-controlled and were interpolated into an
innerHTML string in updateLobbyUI. The server only truncates them to 30
chars, so a peer could inject markup into everyone else's popup: enough
to load a remote image (leaking viewer IPs) or spoof readiness badges.
Inline handlers were already blocked by the MV3 default CSP.
Build the peer items with the DOM API, matching the pattern the sibling
peer list already uses.
Add the two checks that would have caught this before upload:
- eslint no-unsanitized, which reproduces the AMO warning at lint time
- addons-linter on the built XPI in verify-release, with
--warnings-as-errors since it exits 0 on warnings and AMO rejects them
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The v2.5.2 Disney+ page-API integration leaked blob-relative <video>
time into force sync, seeks, and heartbeats when the page-API bridge
had no fresh data, so force sync on Disney+ appeared broken.
- getSyncCurrentTime/getSyncDuration now refuse native values on Disney+
(return null/0) so stale bridge data degrades to a clean no-op instead
of broadcasting garbage to peers. The get_current_time handler and
episode/lobby/hcmIsLive paths are routed through the same accessor.
- Validate FORCE_SYNC_PREPARE/SEEK payloads as finite before relaying;
the internal coercion no longer treats '' as 0.
- Stop double-routing FORCE_SYNC_PREPARE from the popup path (the
generic popup route now covers only play/pause/seek).
- popup force-sync: exclude null/empty peer times from the jump-to-others
median (Number(null)===0 was dragging the target to 0), guard against
NaN end-to-end, clear the dangling reset timer on failure, and retry
without re-injecting when the content script responds but the Disney+
bridge has not yet delivered a finite time.
- hcmIsLive skips the native-duration live signal on Disney+ only,
preserving YouTube/Twitch Infinity-duration live detection.
Disney-specific logic remains strictly gated to disneyplus.com; no
Netflix/YouTube/Twitch/generic path is affected.