Fix Netflix seek handling

This commit is contained in:
KoalaDev
2026-07-02 09:43:11 +02:00
parent 7781d418ba
commit 8a21fbe07f
3 changed files with 106 additions and 25 deletions
+78 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}