mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-09-04 15:05:20 +00:00
fix(extension): preserve media detection across scrolling
This commit is contained in:
+14
-4
@@ -681,12 +681,22 @@
|
|||||||
|
|
||||||
if (intent === 'involuntary') {
|
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
|
// otherwise a second deliberate pause inside the cooldown window leaves
|
||||||
|
|
||||||
// cooldown — the deliberate dialog path below must still go through,
|
// the user stuck paused with no UI (M-3).
|
||||||
|
|
||||||
// otherwise a second deliberate pause inside the cooldown window leaves
|
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
|
// Ready now → snap immediately. Use the captured target if it's
|
||||||
|
|
||||||
// usable, otherwise re-query+retry (host state may not be known yet)
|
// usable, otherwise re-query+retry (host state may not be known yet)
|
||||||
|
|||||||
@@ -44,12 +44,16 @@
|
|||||||
const browserReportsVisible = typeof element.checkVisibility === 'function'
|
const browserReportsVisible = typeof element.checkVisibility === 'function'
|
||||||
? element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
? element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
||||||
: true;
|
: true;
|
||||||
|
const layoutWidth = Math.max(window.innerWidth, document.documentElement?.scrollWidth || 0);
|
||||||
|
const layoutHeight = Math.max(window.innerHeight, document.documentElement?.scrollHeight || 0);
|
||||||
|
const scrollX = Number(window.scrollX) || 0;
|
||||||
|
const scrollY = Number(window.scrollY) || 0;
|
||||||
const visible = rect.width > 0
|
const visible = rect.width > 0
|
||||||
&& rect.height > 0
|
&& rect.height > 0
|
||||||
&& rect.bottom > 0
|
&& rect.bottom + scrollY > 0
|
||||||
&& rect.right > 0
|
&& rect.right + scrollX > 0
|
||||||
&& rect.top < window.innerHeight
|
&& rect.top + scrollY < layoutHeight
|
||||||
&& rect.left < window.innerWidth
|
&& rect.left + scrollX < layoutWidth
|
||||||
&& browserReportsVisible
|
&& browserReportsVisible
|
||||||
&& elementStylesAllowRendering(element)
|
&& elementStylesAllowRendering(element)
|
||||||
&& style.display !== 'none'
|
&& style.display !== 'none'
|
||||||
|
|||||||
@@ -61,10 +61,14 @@ export function inspectMediaFrame(expectedVisibilityToken = null) {
|
|||||||
&& !element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) {
|
&& !element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return rect.bottom > 0
|
const layoutWidth = Math.max(view.innerWidth, element.ownerDocument?.documentElement?.scrollWidth || 0);
|
||||||
&& rect.right > 0
|
const layoutHeight = Math.max(view.innerHeight, element.ownerDocument?.documentElement?.scrollHeight || 0);
|
||||||
&& rect.top < view.innerHeight
|
const scrollX = Number(view.scrollX) || 0;
|
||||||
&& rect.left < view.innerWidth;
|
const scrollY = Number(view.scrollY) || 0;
|
||||||
|
return rect.bottom + scrollY > 0
|
||||||
|
&& rect.right + scrollX > 0
|
||||||
|
&& rect.top + scrollY < layoutHeight
|
||||||
|
&& rect.left + scrollX < layoutWidth;
|
||||||
};
|
};
|
||||||
|
|
||||||
const collectVideos = (doc, depth = 0, ancestorVisible = true, videos = [], seen = new Set()) => {
|
const collectVideos = (doc, depth = 0, ancestorVisible = true, videos = [], seen = new Set()) => {
|
||||||
@@ -252,15 +256,19 @@ export function dispatchParentFrameVisibilityProbe(token) {
|
|||||||
const rect = frame.getBoundingClientRect();
|
const rect = frame.getBoundingClientRect();
|
||||||
const style = window.getComputedStyle(frame);
|
const style = window.getComputedStyle(frame);
|
||||||
const area = Math.max(0, rect.width) * Math.max(0, rect.height);
|
const area = Math.max(0, rect.width) * Math.max(0, rect.height);
|
||||||
const intersectsViewport = rect.bottom > 0
|
const layoutWidth = Math.max(window.innerWidth, document.documentElement?.scrollWidth || 0);
|
||||||
&& rect.right > 0
|
const layoutHeight = Math.max(window.innerHeight, document.documentElement?.scrollHeight || 0);
|
||||||
&& rect.top < window.innerHeight
|
const scrollX = Number(window.scrollX) || 0;
|
||||||
&& rect.left < window.innerWidth;
|
const scrollY = Number(window.scrollY) || 0;
|
||||||
|
const intersectsLayout = rect.bottom + scrollY > 0
|
||||||
|
&& rect.right + scrollX > 0
|
||||||
|
&& rect.top + scrollY < layoutHeight
|
||||||
|
&& rect.left + scrollX < layoutWidth;
|
||||||
const browserReportsVisible = typeof frame.checkVisibility === 'function'
|
const browserReportsVisible = typeof frame.checkVisibility === 'function'
|
||||||
? frame.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
? frame.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
||||||
: true;
|
: true;
|
||||||
const directlyVisible = area > 0
|
const directlyVisible = area > 0
|
||||||
&& intersectsViewport
|
&& intersectsLayout
|
||||||
&& browserReportsVisible
|
&& browserReportsVisible
|
||||||
&& style.display !== 'none'
|
&& style.display !== 'none'
|
||||||
&& style.visibility !== 'hidden'
|
&& style.visibility !== 'hidden'
|
||||||
|
|||||||
@@ -127,16 +127,25 @@ assert.strictEqual(
|
|||||||
'a hidden single candidate is not returned as an active player'
|
'a hidden single candidate is not returned as an active player'
|
||||||
);
|
);
|
||||||
|
|
||||||
function attachRenderEnvironment(documentNode, elements, { frameElement = null } = {}) {
|
function attachRenderEnvironment(documentNode, elements, {
|
||||||
|
frameElement = null,
|
||||||
|
scrollWidth = 1000,
|
||||||
|
scrollHeight = 700,
|
||||||
|
scrollX = 0,
|
||||||
|
scrollY = 0
|
||||||
|
} = {}) {
|
||||||
const view = {
|
const view = {
|
||||||
innerWidth: 1000,
|
innerWidth: 1000,
|
||||||
innerHeight: 700,
|
innerHeight: 700,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
frameElement,
|
frameElement,
|
||||||
getComputedStyle(element) {
|
getComputedStyle(element) {
|
||||||
return element._style || { display: 'block', visibility: 'visible', opacity: '1' };
|
return element._style || { display: 'block', visibility: 'visible', opacity: '1' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
documentNode.defaultView = view;
|
documentNode.defaultView = view;
|
||||||
|
documentNode.documentElement = { scrollWidth, scrollHeight };
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
element.ownerDocument = documentNode;
|
element.ownerDocument = documentNode;
|
||||||
element.getBoundingClientRect = () => element._rect || {
|
element.getBoundingClientRect = () => element._rect || {
|
||||||
@@ -175,6 +184,57 @@ assert.strictEqual(
|
|||||||
'a hidden playing preload must not outrank the visible paused player'
|
'a hidden playing preload must not outrank the visible paused player'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const belowFoldPlayer = makeVideo('below-fold-player', 800, 450, {
|
||||||
|
controls: true,
|
||||||
|
duration: 1200
|
||||||
|
});
|
||||||
|
belowFoldPlayer._rect = {
|
||||||
|
width: 800, height: 450, top: 900, left: 0, right: 800, bottom: 1350
|
||||||
|
};
|
||||||
|
const offscreenDecoy = makeVideo('offscreen-decoy', 900, 506, {
|
||||||
|
controls: true,
|
||||||
|
paused: false,
|
||||||
|
duration: 1200
|
||||||
|
});
|
||||||
|
offscreenDecoy._rect = {
|
||||||
|
width: 900, height: 506, top: 0, left: -2000, right: -1100, bottom: 506
|
||||||
|
};
|
||||||
|
const scrollableDocument = {
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector === 'video') return [offscreenDecoy, belowFoldPlayer];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
attachRenderEnvironment(scrollableDocument, [offscreenDecoy, belowFoldPlayer], { scrollHeight: 1500 });
|
||||||
|
assert.strictEqual(
|
||||||
|
findVideo(scrollableDocument),
|
||||||
|
belowFoldPlayer,
|
||||||
|
'a player below the fold remains eligible while a positioned offscreen decoy does not'
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrolledPastPlayer = makeVideo('scrolled-past-player', 800, 450, {
|
||||||
|
controls: true,
|
||||||
|
duration: 1200
|
||||||
|
});
|
||||||
|
scrolledPastPlayer._rect = {
|
||||||
|
width: 800, height: 450, top: -800, left: 0, right: 800, bottom: -350
|
||||||
|
};
|
||||||
|
const scrolledDocument = {
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector === 'video') return [scrolledPastPlayer];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
attachRenderEnvironment(scrolledDocument, [scrolledPastPlayer], {
|
||||||
|
scrollHeight: 1500,
|
||||||
|
scrollY: 900
|
||||||
|
});
|
||||||
|
assert.strictEqual(
|
||||||
|
findVideo(scrolledDocument),
|
||||||
|
scrolledPastPlayer,
|
||||||
|
'a player above the current viewport remains eligible when it is inside the document layout'
|
||||||
|
);
|
||||||
|
|
||||||
const framedHiddenVideo = makeVideo('framed-hidden', 900, 506, {
|
const framedHiddenVideo = makeVideo('framed-hidden', 900, 506, {
|
||||||
controls: true,
|
controls: true,
|
||||||
paused: false,
|
paused: false,
|
||||||
|
|||||||
@@ -35,12 +35,16 @@ export function extractFunction(name, source = fs.readFileSync(contentPath, 'utf
|
|||||||
export const VIDEO_FINDER_EXPORTS = [
|
export const VIDEO_FINDER_EXPORTS = [
|
||||||
'findVideo',
|
'findVideo',
|
||||||
'collectVideoCandidates',
|
'collectVideoCandidates',
|
||||||
|
'getElementRenderBox',
|
||||||
|
'elementStylesAllowRendering',
|
||||||
|
'isElementRendered',
|
||||||
'getRenderedVideoArea',
|
'getRenderedVideoArea',
|
||||||
'getVideoSizeBucket',
|
'getVideoSizeBucket',
|
||||||
'isVideoRendered',
|
'isVideoRendered',
|
||||||
'hasPlayableVideoSource',
|
'hasPlayableVideoSource',
|
||||||
'isBackgroundVideo',
|
'isBackgroundVideo',
|
||||||
'isVideoPlaying',
|
'isVideoPlaying',
|
||||||
|
'isShortUncontrolledVideo',
|
||||||
'compareVideoRanks',
|
'compareVideoRanks',
|
||||||
'pickBestVideo'
|
'pickBestVideo'
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user