mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-28 11:37:16 +00:00
fix(extension): instant mirror switching and prevent uncaught timeout errors
This commit is contained in:
+19
-18
@@ -152,7 +152,7 @@ function forgetFrameIds(tabId) {
|
|||||||
// into a storm.
|
// into a storm.
|
||||||
// Reinstalling is now informative rather than amnesic, but it is still work in
|
// Reinstalling is now informative rather than amnesic, but it is still work in
|
||||||
// every frame; keep it well clear of the discovery poll's own cadence.
|
// every frame; keep it well clear of the discovery poll's own cadence.
|
||||||
const MONITOR_REFRESH_INTERVAL_MS = 5000;
|
const MONITOR_REFRESH_INTERVAL_MS = 1200;
|
||||||
const lastMonitorRefreshByTab = new Map();
|
const lastMonitorRefreshByTab = new Map();
|
||||||
const pendingMonitorRefreshByTab = new Map();
|
const pendingMonitorRefreshByTab = new Map();
|
||||||
|
|
||||||
@@ -864,12 +864,9 @@ function isCurrentContentSender(sender) {
|
|||||||
const matchesElectedFrame = senderFrameId === normalizeFrameId(currentTargetFrameId)
|
const matchesElectedFrame = senderFrameId === normalizeFrameId(currentTargetFrameId)
|
||||||
&& (!currentTargetDocumentId || sender.documentId === currentTargetDocumentId);
|
&& (!currentTargetDocumentId || sender.documentId === currentTargetDocumentId);
|
||||||
if (matchesElectedFrame) return true;
|
if (matchesElectedFrame) return true;
|
||||||
// The elected frame holds no video, so the election is wrong or stale and a
|
// Any frame in the selected target tab reporting playback activity
|
||||||
// frame that is reporting media activity knows better. Frame election is the
|
// is a valid content sender (e.g. user interacted with or switched to another mirror).
|
||||||
// fragile half of this system; sender.frameId is authoritative, costs no
|
return true;
|
||||||
// permission and has no timing window. Trust it rather than dropping the
|
|
||||||
// user's own play and pause because they came from the real player frame.
|
|
||||||
return currentTargetHasVideo !== true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -883,13 +880,16 @@ function adoptReportingFrame(sender) {
|
|||||||
if (!sender?.tab) return false;
|
if (!sender?.tab) return false;
|
||||||
const senderTabId = normalizeTabId(sender.tab.id);
|
const senderTabId = normalizeTabId(sender.tab.id);
|
||||||
if (senderTabId === null || senderTabId !== normalizeTabId(currentTabId)) return false;
|
if (senderTabId === null || senderTabId !== normalizeTabId(currentTabId)) return false;
|
||||||
if (currentTargetHasVideo === true) return false;
|
|
||||||
const senderFrameId = normalizeFrameId(sender.frameId);
|
const senderFrameId = normalizeFrameId(sender.frameId);
|
||||||
if (senderFrameId === normalizeFrameId(currentTargetFrameId)) return false;
|
if (senderFrameId === normalizeFrameId(currentTargetFrameId)
|
||||||
|
&& (!currentTargetDocumentId || sender.documentId === currentTargetDocumentId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
currentTargetFrameId = senderFrameId;
|
currentTargetFrameId = senderFrameId;
|
||||||
currentTargetDocumentId = typeof sender.documentId === 'string' ? sender.documentId : null;
|
currentTargetDocumentId = typeof sender.documentId === 'string' ? sender.documentId : null;
|
||||||
currentTargetHasVideo = true;
|
currentTargetHasVideo = true;
|
||||||
|
stopMediaDiscoveryPoll();
|
||||||
rememberFrameId(senderTabId, senderFrameId);
|
rememberFrameId(senderTabId, senderFrameId);
|
||||||
addLog(`Adopted frame ${senderFrameId} as the media target; it reported playback`, 'info');
|
addLog(`Adopted frame ${senderFrameId} as the media target; it reported playback`, 'info');
|
||||||
chrome.storage.session.set({
|
chrome.storage.session.set({
|
||||||
@@ -2611,8 +2611,11 @@ function executeScriptWithTimeout(options, timeoutMs = SCRIPT_INJECTION_TIMEOUT_
|
|||||||
reject(error);
|
reject(error);
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
});
|
});
|
||||||
|
const scriptPromise = chrome.scripting.executeScript(options);
|
||||||
|
// Attach catch to prevent unhandled promise rejection if timeout wins the race and script fails later (e.g. on tab close)
|
||||||
|
scriptPromise.catch(() => {});
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
chrome.scripting.executeScript(options),
|
scriptPromise,
|
||||||
timeout
|
timeout
|
||||||
]).finally(() => {
|
]).finally(() => {
|
||||||
if (timeoutId !== null) clearTimeout(timeoutId);
|
if (timeoutId !== null) clearTimeout(timeoutId);
|
||||||
@@ -3291,16 +3294,14 @@ async function selectedMediaTargetMoved(tabId) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (normalizeTabId(currentTabId) !== normalizeTabId(tabId)) return false;
|
if (normalizeTabId(currentTabId) !== normalizeTabId(tabId)) return false;
|
||||||
// An inconclusive probe is not a reason to move. A page whose players are
|
// An inconclusive probe is not a reason to move when on top frame.
|
||||||
// still loading, or that offers several equally-ranked mirrors, resolves
|
// However, if a nested frame was elected (currentTargetFrameId !== 0) and is now
|
||||||
// differently from one moment to the next; acting on that flips the target
|
// no longer an accessible candidate with video, the target has vacated.
|
||||||
// back and forth and leaves activation running forever.
|
|
||||||
if (resolved.hasVideo !== true) {
|
if (resolved.hasVideo !== true) {
|
||||||
// No player anywhere yet. This runs on every lifecycle wake-up, so it is
|
|
||||||
// the reliable place to make sure freshly rebuilt documents carry a
|
|
||||||
// monitor — without one, the video created there next is never reported
|
|
||||||
// and the target can never come back.
|
|
||||||
refreshMediaFrameMonitors(tabId).catch(() => {});
|
refreshMediaFrameMonitors(tabId).catch(() => {});
|
||||||
|
if (normalizeFrameId(currentTargetFrameId) !== 0 || currentTargetHasVideo === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (currentTargetHasVideo !== true) return true;
|
if (currentTargetHasVideo !== true) return true;
|
||||||
|
|||||||
@@ -513,7 +513,9 @@ function executeWithTimeout(task, timeoutMs, label) {
|
|||||||
const timeout = new Promise((_, reject) => {
|
const timeout = new Promise((_, reject) => {
|
||||||
timeoutId = setTimeout(() => reject(probeTimeoutError(label, timeoutMs)), timeoutMs);
|
timeoutId = setTimeout(() => reject(probeTimeoutError(label, timeoutMs)), timeoutMs);
|
||||||
});
|
});
|
||||||
return Promise.race([task(), timeout]).finally(() => {
|
const taskPromise = Promise.resolve().then(task);
|
||||||
|
taskPromise.catch(() => {});
|
||||||
|
return Promise.race([taskPromise, timeout]).finally(() => {
|
||||||
if (timeoutId !== null) clearTimeout(timeoutId);
|
if (timeoutId !== null) clearTimeout(timeoutId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -476,6 +476,37 @@ test('re-elects the visible cross-origin player after an iframe switch', async (
|
|||||||
expect(await first.locator('video').evaluate(video => video.paused)).toBe(true);
|
expect(await first.locator('video').evaluate(video => video.paused)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('immediately adopts and syncs when switching mirrors while first mirror was active and playing', async ({ context, extensionId, baseURL }) => {
|
||||||
|
const url = `${baseURL}/pages/cross-origin-switching.html`;
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto(url);
|
||||||
|
await page.waitForFunction(() => window.__fixtureReady === true);
|
||||||
|
const first = page.frames().find(frame => frame.url().includes('/frames/player-frame.html?slot=first'));
|
||||||
|
const second = page.frames().find(frame => frame.url().includes('/frames/player-frame-2.html?slot=second'));
|
||||||
|
const { tabId } = await selectTargetTab(context, extensionId, url);
|
||||||
|
|
||||||
|
await expect.poll(() => first.locator('video').getAttribute('data-koala-attached')).toBe('true');
|
||||||
|
await sendServerCommand(context, extensionId, tabId, 'play');
|
||||||
|
await expect.poll(() => first.locator('video').evaluate(video => !video.paused)).toBe(true);
|
||||||
|
|
||||||
|
const firstStatus = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
|
||||||
|
expect(firstStatus.targetHasVideo).toBe(true);
|
||||||
|
|
||||||
|
// Switch mirror and play second video directly in the new frame
|
||||||
|
await page.evaluate(() => window.switchPlayer());
|
||||||
|
await second.locator('video').evaluate(video => video.play());
|
||||||
|
|
||||||
|
// Should immediately adopt the second mirror
|
||||||
|
await expect.poll(async () => {
|
||||||
|
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
|
||||||
|
return status.targetFrameId;
|
||||||
|
}).not.toBe(firstStatus.targetFrameId);
|
||||||
|
|
||||||
|
// Commands must now control the second frame without delay
|
||||||
|
await sendServerCommand(context, extensionId, tabId, 'pause');
|
||||||
|
await expect.poll(() => second.locator('video').evaluate(video => video.paused)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test('keeps commands flowing during continuous player-frame geometry changes', async ({ context, extensionId, baseURL }) => {
|
test('keeps commands flowing during continuous player-frame geometry changes', async ({ context, extensionId, baseURL }) => {
|
||||||
const url = `${baseURL}/pages/cross-origin-switching.html`;
|
const url = `${baseURL}/pages/cross-origin-switching.html`;
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|||||||
Reference in New Issue
Block a user