mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-09-04 15:05:20 +00:00
feat: rank player candidates by ordered signals, add browser E2E suite
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>
This commit is contained in:
+78
-22
@@ -566,18 +566,21 @@
|
||||
|
||||
};
|
||||
|
||||
tryOnce();
|
||||
tryOnce();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Buffer-aware snap-back (#3). An involuntary pause/seek often coincides with the
|
||||
|
||||
// player buffering — and a player can't actually play while it's stalled. Snapping
|
||||
|
||||
// immediately just fights the buffer (seek → re-buffer → another pause → …), which
|
||||
|
||||
// looks like stutter. Instead: if the player isn't ready, wait until it can play
|
||||
|
||||
// player buffering — and a player can't actually play while it's stalled. Snapping
|
||||
|
||||
// immediately just fights the buffer (seek → re-buffer → another pause → …), which
|
||||
|
||||
// (readyState>=3, not seeking), then snap ONCE to the host's *current* position
|
||||
|
||||
// (re-queried, since the captured target may be stale by the time buffering ends).
|
||||
|
||||
@@ -589,28 +592,81 @@
|
||||
|
||||
if (hcmDeferredSnapPending) return; // already waiting — don't stack polls
|
||||
|
||||
|
||||
if (hcmDeferredSnapPending) return; // already waiting — don't stack polls
|
||||
hcmDeferredSnapPending = true;
|
||||
|
||||
const deadline = Date.now() + HCM_BUFFER_WAIT_MS;
|
||||
|
||||
const deadline = Date.now() + HCM_BUFFER_WAIT_MS;
|
||||
const poll = () => {
|
||||
|
||||
// Abort if the reason to snap is gone: user went solo, we're no longer a
|
||||
const poll = () => {
|
||||
|
||||
|
||||
// gated guest, or the video vanished.
|
||||
|
||||
if (hcmDesynced || !hcmIsGuestGated()) { hcmDeferredSnapPending = false; return; }
|
||||
|
||||
const video = findVideo();
|
||||
|
||||
const ready = video && video.readyState >= 3 && !video.seeking;
|
||||
|
||||
if (ready || Date.now() >= deadline) {
|
||||
|
||||
hcmDeferredSnapPending = false;
|
||||
|
||||
hcmRequestHostSyncWithRetry(); // fresh host position + snap once
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
scheduleLifecycleTimeout(poll, 300);
|
||||
};
|
||||
|
||||
poll();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Entry point: background told us our local action was blocked in host-only.
|
||||
|
||||
function hcmHandleBlocked(action, target) {
|
||||
|
||||
// HOST_BLOCKED is only ever sent to a gated guest (background verifies
|
||||
|
||||
// host-only + !host before sending), so it's authoritative. Adopt the
|
||||
|
||||
// role/mode from it in case our CONTROL_MODE broadcast hasn't landed yet
|
||||
|
||||
// (join race, EC-5) — otherwise we'd miss the dialog/snap-back.
|
||||
|
||||
hcmControlMode = 'host-only';
|
||||
|
||||
hcmAmController = false;
|
||||
|
||||
if (hcmDesynced) return; // already solo, nothing to do
|
||||
|
||||
|
||||
|
||||
const intent = hcmClassifyIntent();
|
||||
|
||||
if (intent === 'live') return; // EC-15: leave the guest alone on live
|
||||
|
||||
if (intent === 'involuntary') {
|
||||
|
||||
// EC-4 loop guard: only the silent auto snap-back is suppressed by the
|
||||
|
||||
// cooldown — the deliberate dialog path below must still go through,
|
||||
|
||||
// otherwise a second deliberate pause inside the cooldown window leaves
|
||||
|
||||
// the user stuck paused with no UI (M-3).
|
||||
// gated guest, or the video vanished.
|
||||
|
||||
if (hcmDesynced || !hcmIsGuestGated()) { hcmDeferredSnapPending = false; return; }
|
||||
|
||||
const video = findVideo();
|
||||
|
||||
const ready = video && video.readyState >= 3 && !video.seeking;
|
||||
|
||||
if (ready || Date.now() >= deadline) {
|
||||
|
||||
hcmDeferredSnapPending = false;
|
||||
|
||||
if (Date.now() < hcmSnapBackCooldownUntil || hcmDeferredSnapPending) return;
|
||||
|
||||
// Buffering/ads/throttle — silently re-sync, no dialog spam.
|
||||
|
||||
const video = findVideo();
|
||||
|
||||
if (video && video.readyState >= 3 && !video.seeking) {
|
||||
|
||||
// Ready now → snap immediately. Use the captured target if it's
|
||||
|
||||
Reference in New Issue
Block a user