mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
Fix Netflix seek handling
This commit is contained in:
+78
-8
@@ -1592,6 +1592,68 @@ async function routeToContent(action, payload) {
|
||||
_routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, 0);
|
||||
}
|
||||
|
||||
function isNetflixUrl(url) {
|
||||
try {
|
||||
const host = new URL(url).hostname.toLowerCase();
|
||||
return host === 'netflix.com' || host.endsWith('.netflix.com');
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function installNetflixSeekBridge() {
|
||||
if (window.__koalaNetflixSeekBridgeInstalled) return;
|
||||
window.__koalaNetflixSeekBridgeInstalled = true;
|
||||
|
||||
function getNetflixPlayer() {
|
||||
try {
|
||||
const videoPlayer = window.netflix?.appContext?.state?.playerApp?.getAPI?.().videoPlayer;
|
||||
const sessionId = videoPlayer?.getAllPlayerSessionIds?.()[0];
|
||||
return sessionId ? videoPlayer.getVideoPlayerBySessionId(sessionId) : null;
|
||||
} catch (_e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source !== window) return;
|
||||
const data = event.data;
|
||||
if (!data || data.__koalaNetflixSeek !== 1 || data.kind !== 'seek' || typeof data.time !== 'number') return;
|
||||
try {
|
||||
getNetflixPlayer()?.seek(Math.round(data.time * 1000));
|
||||
} catch (_e) {
|
||||
// Netflix player not ready or private API changed; the next sync tick can retry.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function injectContentScript(tabId) {
|
||||
let isNetflix = false;
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
isNetflix = isNetflixUrl(tab?.url || '');
|
||||
} catch (_e) {
|
||||
// Fall through to the generic content script injection.
|
||||
}
|
||||
|
||||
if (isNetflix) {
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
world: 'MAIN',
|
||||
func: installNetflixSeekBridge
|
||||
});
|
||||
} catch (err) {
|
||||
addLog(`Netflix seek bridge injection failed: ${err.message}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
return chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['content.js']
|
||||
});
|
||||
}
|
||||
|
||||
function _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, retries) {
|
||||
chrome.tabs.sendMessage(tabId, {
|
||||
type: 'SERVER_COMMAND',
|
||||
@@ -1606,10 +1668,7 @@ function _routeToContentInternal(tabId, action, payload, actionTimestamp, comman
|
||||
return;
|
||||
}
|
||||
if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['content.js']
|
||||
}).then(() => {
|
||||
injectContentScript(tabId).then(() => {
|
||||
setTimeout(() => _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, retries + 1), 500);
|
||||
}).catch(_err => {
|
||||
addLog(`Auto-reinject failed for tab ${tabId}`, 'warn');
|
||||
@@ -2196,6 +2255,20 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
addLog('Heartbeat settings error: ' + err.message, 'error');
|
||||
sendResponse({ status: 'ok' });
|
||||
});
|
||||
} else if (message.type === 'INJECT_CONTENT_SCRIPT') {
|
||||
const tabId = Number(message.tabId);
|
||||
if (!Number.isInteger(tabId)) {
|
||||
sendResponse({ status: 'invalid_tab' });
|
||||
return true;
|
||||
}
|
||||
|
||||
injectContentScript(tabId).then(() => {
|
||||
sendResponse({ status: 'ok' });
|
||||
}).catch(err => {
|
||||
addLog(`Failed to inject into tab: ${err.message}`, 'warn');
|
||||
sendResponse({ status: 'error', message: err.message });
|
||||
});
|
||||
return true;
|
||||
} else if (message.type === 'SET_TARGET_TAB') {
|
||||
const previousTabId = currentTabId;
|
||||
currentTabId = message.tabId;
|
||||
@@ -2213,10 +2286,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
|
||||
if (currentTabId) {
|
||||
const selectedTabId = currentTabId;
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId: selectedTabId },
|
||||
files: ['content.js']
|
||||
})
|
||||
injectContentScript(selectedTabId)
|
||||
.then(() => applyAudioSettingsToTab(selectedTabId))
|
||||
.catch(err => {
|
||||
addLog(`Failed to inject into tab: ${err.message}`, 'warn');
|
||||
|
||||
+20
-8
@@ -57,6 +57,22 @@
|
||||
let seekDebounceTimer = null; // debounce timer for rapid seek events
|
||||
let expectedSeekTime = null; // strictly track programmatic seeks
|
||||
|
||||
const NETFLIX_SEEK_BRIDGE = 1;
|
||||
|
||||
function isNetflixHost() {
|
||||
const host = window.location.hostname.toLowerCase();
|
||||
return host === 'netflix.com' || host.endsWith('.netflix.com');
|
||||
}
|
||||
|
||||
function seekVideo(video, targetTime) {
|
||||
expectedSeekTime = targetTime;
|
||||
if (isNetflixHost()) {
|
||||
window.postMessage({ __koalaNetflixSeek: NETFLIX_SEEK_BRIDGE, kind: 'seek', time: targetTime }, '*');
|
||||
return;
|
||||
}
|
||||
video.currentTime = targetTime;
|
||||
}
|
||||
|
||||
// --- Play/Pause Coalescing (leading + trailing) ---
|
||||
// Media players (HLS/DASH, ad insertion, ABR/quality switches, source swaps,
|
||||
// page teardown) fire bursts of native play/pause events within a few hundred
|
||||
@@ -748,8 +764,7 @@
|
||||
ytButton.click();
|
||||
}
|
||||
if (action === EVENTS.SEEK) {
|
||||
expectedSeekTime = data.targetTime;
|
||||
video.currentTime = data.targetTime;
|
||||
seekVideo(video, data.targetTime);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -764,8 +779,7 @@
|
||||
twitchButton.click();
|
||||
}
|
||||
if (action === EVENTS.SEEK) {
|
||||
expectedSeekTime = data.targetTime;
|
||||
video.currentTime = data.targetTime;
|
||||
seekVideo(video, data.targetTime);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -782,8 +796,7 @@
|
||||
_setSuppress('paused');
|
||||
video.pause();
|
||||
} else if (action === EVENTS.SEEK) {
|
||||
expectedSeekTime = data.targetTime;
|
||||
video.currentTime = data.targetTime;
|
||||
seekVideo(video, data.targetTime);
|
||||
}
|
||||
} catch (e) {
|
||||
reportLog(`Media Action Error: ${e.message}`, 'error');
|
||||
@@ -931,10 +944,9 @@
|
||||
return;
|
||||
}
|
||||
_setSuppress('paused');
|
||||
expectedSeekTime = payload.targetTime;
|
||||
video.pause();
|
||||
try {
|
||||
video.currentTime = payload.targetTime;
|
||||
seekVideo(video, payload.targetTime);
|
||||
} catch (e) {
|
||||
reportLog(`Force Sync Seek Error: ${e.message}`, 'error');
|
||||
}
|
||||
|
||||
+8
-9
@@ -1609,10 +1609,14 @@ elements.forceSyncBtn.addEventListener('click', async () => {
|
||||
if (mode === 'jump-to-me') {
|
||||
chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (response) => {
|
||||
if (chrome.runtime.lastError || !response || response.currentTime === undefined) {
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['content.js']
|
||||
}).then(() => {
|
||||
chrome.runtime.sendMessage({ type: 'INJECT_CONTENT_SCRIPT', tabId }, (injectResponse) => {
|
||||
if (chrome.runtime.lastError || !injectResponse || injectResponse.status !== 'ok') {
|
||||
showError(getMessage('ERR_NO_VIDEO_TAB'));
|
||||
forceSyncDone = true;
|
||||
elements.forceSyncBtn.disabled = false;
|
||||
elements.forceSyncBtn.textContent = originalText;
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (retryResponse) => {
|
||||
if (chrome.runtime.lastError) return;
|
||||
@@ -1621,11 +1625,6 @@ elements.forceSyncBtn.addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}).catch(() => {
|
||||
showError(getMessage('ERR_NO_VIDEO_TAB'));
|
||||
forceSyncDone = true;
|
||||
elements.forceSyncBtn.disabled = false;
|
||||
elements.forceSyncBtn.textContent = originalText;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user