fix: harden frame observation and unblock supported Drive tabs

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>
This commit is contained in:
KoalaDev
2026-08-14 06:45:33 +02:00
parent 809301b641
commit 477dad03a3
5 changed files with 51 additions and 11 deletions
+5
View File
@@ -7,9 +7,14 @@ All notable changes to the KoalaSync browser extension and relay server.
## Unreleased
### Added
- **Extension: Same-origin frame video detection** — Finds and controls players that live inside a first-party iframe instead of the top document, so sites that wrap their player in their own frame sync without site-specific workarounds. Frame documents are watched for late-loading players and re-scanned after a frame reload; cross-origin frames stay out of reach by design.
- **Extension: Editable Hide-Clutter list** — Adds a validated, deduplicated domain editor in Settings, prefilled with the shipped blacklist, with a defaults reset and per-device persistence in `chrome.storage.local`.
- **Extension: Independent default audio boost** — Adds a configurable `020 dB` output gain in half-decibel steps. The boost works with or without the compressor and applies live to the selected video tab.
### Fixed
- **Extension: Google Drive tab selection** — A broad parent domain in the Hide-Clutter list no longer hides a host that has its own supported player path, so Drive videos stay selectable while the filter is on. An exact entry for the host itself still filters it.
- **Extension: Debug report frame visibility** — Video counts now include players inside same-origin frames and the report states whether the selected video sits in a frame.
## [v3.0.2] — 2026-07-31
This release adds three focused chat improvements: encrypted quick-reaction emojis,
+4 -4
View File
@@ -49,13 +49,13 @@
*Ideas and feature requests under evaluation.*
### Cross-frame video detection and control
### Cross-origin frame video detection and control
- **Priority:** P3
- **Category:** Compatibility / Embedded Players
- **Background:** KoalaSync currently injects on demand into the selected tab's top frame. This works for normal top-frame players, including current Emby/Jellyfin usage, but does not cover cases where the real `<video>` lives inside a cross-origin iframe or an `about:blank`/`srcdoc` player frame.
- **Possible approach:** Add an opt-in frame bridge where child frames announce detected videos to the top frame, and the top frame routes remote play/pause/seek commands to the active child video.
- **Status:** Future compatibility work, not needed for current Emby behavior.
- **Background:** KoalaSync injects on demand into the selected tab's top frame. Since the same-origin frame walk shipped, the top-frame script also reaches players inside first-party iframes (`jkanime.net`-style `/jkplayer/` frames, `srcdoc` and `about:blank` frames that inherit the parent origin). What remains uncovered is the real `<video>` living inside a **cross-origin** iframe, where `contentDocument` is unreachable by design.
- **Possible approach:** Add an opt-in frame bridge (`allFrames: true` injection) where child frames announce detected videos to the top frame, and the top frame routes remote play/pause/seek commands to the active child video. Needs a frame-election rule so ad frames cannot claim the session.
- **Status:** Same-origin part completed; cross-origin frame bridge still open. Not needed for current Emby behavior.
### Local extension E2E smoke tests
+21 -6
View File
@@ -1697,23 +1697,37 @@
}
// --- Helper: Wait until video is ready for playback (buffered & seeked) ---
function pollSeekReady(targetTime, timeoutMs = 8000) {
return new Promise((resolve) => {
const interval = 150;
const interval = 150;
let elapsed = 0;
const timer = setInterval(() => {
if (destroyed) {
let elapsed = 0;
const timer = setInterval(() => {
if (destroyed) {
clearInterval(timer);
seekPollTimers.delete(timer);
resolve(false);
return;
}
const video = findVideo(); // Re-query DOM on every iteration
if (!video) {
clearInterval(timer);
seekPollTimers.delete(timer);
resolve(false);
return;
}
const video = findVideo(); // Re-query DOM on every iteration
elapsed += interval;
const current = getSyncCurrentTime(video);
const timeDiff = current !== null ? Math.abs(current - targetTime) : Infinity;
const ready = video.readyState >= 3 && timeDiff < 2.0;
if (ready) {
clearInterval(timer);
seekPollTimers.delete(timer);
resolve(true);
@@ -1764,6 +1778,7 @@
if (message.action === 'RESET_AUDIO_PROCESSING') {
_audioProcessingAllowed = false;
bypassCurrentAudioProcessing();
+7
View File
@@ -39,6 +39,13 @@ assert.match(popupSource, /chrome\.storage\.local\.set\(\{ \[CUSTOM_BLACKLIST_ST
assert.doesNotMatch(popupSource, /chrome\.storage\.sync\.set\(\{ \[CUSTOM_BLACKLIST_STORAGE_KEY\]/, 'custom list is never synced');
assert.match(popupSource, /isUrlBlacklisted\(tab\.url, blacklistDomains\)/, 'tab filtering uses the effective custom list');
// A broad parent domain must not hide a host with a dedicated player path,
// but an exact user entry for that host still filters it.
assert.equal(isUrlBlacklisted('https://drive.google.com/file/d/x/view', BLACKLIST_DOMAINS), false);
assert.equal(isUrlBlacklisted('https://drive.google.com/file/d/x/view', ['drive.google.com']), true);
assert.equal(isUrlBlacklisted('https://docs.google.com/document/d/x', BLACKLIST_DOMAINS), true);
assert.equal(isUrlBlacklisted('https://mail.google.com/mail/u/0', BLACKLIST_DOMAINS), true);
const popupHtml = fs.readFileSync(path.join(repoRoot, 'extension/popup.html'), 'utf8');
assert.match(popupHtml, /id="blacklistDomains"/, 'settings UI contains the editable domain list');
assert.match(popupHtml, /id="blacklistReset"/, 'settings UI contains a defaults reset');
+14 -1
View File
@@ -179,6 +179,15 @@ export const BLACKLIST_DOMAINS = [
'skribbl.io'
];
/**
* Hosts KoalaSync supports through a site-specific player path. A broad parent
* domain in the list (e.g. 'google.com') must not hide them from tab selection.
* An exact entry for the host itself still filters it, so users stay in control.
*/
export const BLACKLIST_SUFFIX_EXCEPTIONS = [
'drive.google.com'
];
export const CUSTOM_BLACKLIST_STORAGE_KEY = 'customBlacklistDomains';
export const MAX_BLACKLIST_DOMAINS = 500;
@@ -246,5 +255,9 @@ export function isUrlBlacklisted(rawUrl, domains = BLACKLIST_DOMAINS) {
return false;
}
return domains.some(domain => hostname === domain || hostname.endsWith(`.${domain}`));
return domains.some(domain => {
if (hostname === domain) return true;
if (!hostname.endsWith(`.${domain}`)) return false;
return !BLACKLIST_SUFFIX_EXCEPTIONS.includes(hostname);
});
}