fix(overlay): prevent Windows mouse lag during click-through tracking (#201)

This commit is contained in:
2026-08-16 01:34:51 -07:00
committed by GitHub
parent 82f6b4705a
commit 2938e7a32a
12 changed files with 206 additions and 65 deletions
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Fixed system-wide mouse lag on Windows while SubMiner is running: the overlay no longer installs Electron's global mouse hook for click-through forwarding, and the mpv window tracker no longer blocks the app on repeated PowerShell command-line lookups.
@@ -129,7 +129,11 @@ coming and prefetching would otherwise idle for the rest of the cue.
path, empty or stale bounding shapes produced invisible or clipped subtitles even though the path, empty or stale bounding shapes produced invisible or clipped subtitles even though the
overlay window remained mapped above mpv. overlay window remained mapped above mpv.
- Pointer pass-through should continue to use `setIgnoreMouseEvents(true, { forward: true })` and - Pointer pass-through should continue to use `setIgnoreMouseEvents(true, { forward: true })` and
the Linux cursor-poll fallback, not bounding-shape clipping. the Linux cursor-poll fallback, not bounding-shape clipping. Note that on Windows click-through
must go through `applyOverlayClickThrough()` (`src/core/services/overlay-click-through.ts`),
which omits `forward: true` there: Electron implements forwarding with a global low-level mouse
hook that lags mouse input system-wide whenever the main thread stalls; the Windows cursor poll
handles overlay wake-up instead.
- Visible-overlay show/reset marks Linux pointer passthrough state dirty even when the logical - Visible-overlay show/reset marks Linux pointer passthrough state dirty even when the logical
interaction state is already inactive. The next cursor-poll tick must still reapply interaction state is already inactive. The next cursor-poll tick must still reapply
`setIgnoreMouseEvents(true, { forward: true })`; otherwise a newly shown Electron overlay can keep `setIgnoreMouseEvents(true, { forward: true })`; otherwise a newly shown Electron overlay can keep
+8 -1
View File
@@ -34,6 +34,7 @@ import {
parseSubsyncManualRunRequest, parseSubsyncManualRunRequest,
parseYoutubePickerResolveRequest, parseYoutubePickerResolveRequest,
} from '../../shared/ipc/validators'; } from '../../shared/ipc/validators';
import { applyOverlayClickThrough } from './overlay-click-through';
const { ipcMain } = electron; const { ipcMain } = electron;
@@ -442,7 +443,13 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
const senderWindow = const senderWindow =
electron.BrowserWindow?.fromWebContents((event as IpcMainEvent).sender) ?? null; electron.BrowserWindow?.fromWebContents((event as IpcMainEvent).sender) ?? null;
if (senderWindow && !senderWindow.isDestroyed()) { if (senderWindow && !senderWindow.isDestroyed()) {
senderWindow.setIgnoreMouseEvents(ignore, parsedOptions); // Route forwarding requests through the platform-aware helper so Windows never
// installs Electron's global mouse hook (see overlay-click-through.ts).
if (ignore && parsedOptions?.forward) {
applyOverlayClickThrough(senderWindow);
} else {
senderWindow.setIgnoreMouseEvents(ignore, parsedOptions);
}
} }
deps.onOverlayMouseInteractionChanged?.(!ignore, senderWindow); deps.onOverlayMouseInteractionChanged?.(!ignore, senderWindow);
}, },
@@ -0,0 +1,21 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyOverlayClickThrough } from './overlay-click-through';
test('applyOverlayClickThrough requests forwarding only off Windows', () => {
const calls: Array<{ ignore: boolean; forward: boolean }> = [];
const window = {
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
calls.push({ ignore, forward: options?.forward === true });
},
};
applyOverlayClickThrough(window, true);
applyOverlayClickThrough(window, false);
assert.deepEqual(calls, [
{ ignore: true, forward: false },
{ ignore: true, forward: true },
]);
});
@@ -0,0 +1,27 @@
type ClickThroughWindow = {
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
};
/**
* Puts an overlay window into click-through mode. Forwarded mouse-move ({ forward: true }) is
* what lets renderer hover tracking wake a click-through overlay, but on Windows Electron
* implements it with a global WH_MOUSE_LL hook whose callback runs on the main-process message
* loop, so any main-thread stall delays mouse input system-wide (electron/electron#10183).
* Windows instead wakes the overlay via the main-process cursor poll
* (tickWindowsOverlayPointerInteraction), so no forwarding is requested there. macOS still
* needs forwarding for renderer hover tracking; Linux ignores the flag entirely
* (electron/electron#16777).
*
* Pass isWindowsPlatform when the caller already carries a platform flag (tests simulate
* platforms through it); otherwise the real process.platform decides.
*/
export function applyOverlayClickThrough(
window: ClickThroughWindow,
isWindowsPlatform?: boolean,
): void {
if (isWindowsPlatform ?? process.platform === 'win32') {
window.setIgnoreMouseEvents(true);
} else {
window.setIgnoreMouseEvents(true, { forward: true });
}
}
+7 -7
View File
@@ -848,7 +848,7 @@ test('Windows visible overlay stays click-through and binds to mpv while tracked
} as never); } as never);
assert.ok(calls.includes('opacity:0')); assert.ok(calls.includes('opacity:0'));
assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('mouse-ignore:true:plain'));
assert.ok(calls.includes('show-inactive')); assert.ok(calls.includes('show-inactive'));
assert.ok(calls.includes('sync-windows-z-order')); assert.ok(calls.includes('sync-windows-z-order'));
assert.ok(!calls.includes('move-top')); assert.ok(!calls.includes('move-top'));
@@ -1060,7 +1060,7 @@ test('tracked Windows overlay refresh rebinds while already visible', () => {
isWindowsPlatform: true, isWindowsPlatform: true,
} as never); } as never);
assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('mouse-ignore:true:plain'));
assert.ok(calls.includes('sync-windows-z-order')); assert.ok(calls.includes('sync-windows-z-order'));
assert.ok(!calls.includes('move-top')); assert.ok(!calls.includes('move-top'));
assert.ok(!calls.includes('show')); assert.ok(!calls.includes('show'));
@@ -1134,7 +1134,7 @@ test('forced passthrough still reapplies while visible on Windows', () => {
forceMousePassthrough: true, forceMousePassthrough: true,
} as never); } as never);
assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('mouse-ignore:true:plain'));
assert.ok(!calls.includes('always-on-top:false')); assert.ok(!calls.includes('always-on-top:false'));
assert.ok(!calls.includes('move-top')); assert.ok(!calls.includes('move-top'));
assert.ok(calls.includes('sync-windows-z-order')); assert.ok(calls.includes('sync-windows-z-order'));
@@ -1339,7 +1339,7 @@ test('tracked Windows overlay rebinds without hiding when tracker focus changes'
assert.ok(!calls.includes('always-on-top:false')); assert.ok(!calls.includes('always-on-top:false'));
assert.ok(!calls.includes('move-top')); assert.ok(!calls.includes('move-top'));
assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('mouse-ignore:true:plain'));
assert.ok(calls.includes('sync-windows-z-order')); assert.ok(calls.includes('sync-windows-z-order'));
assert.ok(!calls.includes('ensure-level')); assert.ok(!calls.includes('ensure-level'));
assert.ok(!calls.includes('enforce-order')); assert.ok(!calls.includes('enforce-order'));
@@ -1489,7 +1489,7 @@ test('tracked Windows overlay reshows click-through even if focus state is stale
isWindowsPlatform: true, isWindowsPlatform: true,
} as never); } as never);
assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('mouse-ignore:true:plain'));
assert.ok(calls.includes('show-inactive')); assert.ok(calls.includes('show-inactive'));
assert.ok(!calls.includes('show')); assert.ok(!calls.includes('show'));
}); });
@@ -1532,7 +1532,7 @@ test('tracked Windows overlay binds above mpv even when tracker focus lags', ()
assert.ok(!calls.includes('always-on-top:false')); assert.ok(!calls.includes('always-on-top:false'));
assert.ok(!calls.includes('move-top')); assert.ok(!calls.includes('move-top'));
assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('mouse-ignore:true:plain'));
assert.ok(calls.includes('sync-windows-z-order')); assert.ok(calls.includes('sync-windows-z-order'));
assert.ok(!calls.includes('ensure-level')); assert.ok(!calls.includes('ensure-level'));
}); });
@@ -2193,7 +2193,7 @@ test('Windows preserves visible overlay and rebinds to mpv while tracker transie
assert.ok(!calls.includes('show')); assert.ok(!calls.includes('show'));
assert.ok(!calls.includes('always-on-top:false')); assert.ok(!calls.includes('always-on-top:false'));
assert.ok(!calls.includes('move-top')); assert.ok(!calls.includes('move-top'));
assert.ok(calls.includes('mouse-ignore:true:forward')); assert.ok(calls.includes('mouse-ignore:true:plain'));
assert.ok(calls.includes('sync-windows-z-order')); assert.ok(calls.includes('sync-windows-z-order'));
assert.ok(!calls.includes('ensure-level')); assert.ok(!calls.includes('ensure-level'));
assert.ok(calls.includes('sync-shortcuts')); assert.ok(calls.includes('sync-shortcuts'));
+5 -4
View File
@@ -1,6 +1,7 @@
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import { BaseWindowTracker } from '../../window-trackers'; import { BaseWindowTracker } from '../../window-trackers';
import { WindowGeometry } from '../../types'; import { WindowGeometry } from '../../types';
import { applyOverlayClickThrough } from './overlay-click-through';
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags'; import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
const WINDOWS_OVERLAY_REVEAL_DELAY_MS = 48; const WINDOWS_OVERLAY_REVEAL_DELAY_MS = 48;
@@ -117,7 +118,7 @@ export function updateVisibleOverlayVisibility(args: {
clearPendingWindowsOverlayReveal(mainWindow); clearPendingWindowsOverlayReveal(mainWindow);
setOverlayWindowOpacity(mainWindow, 0); setOverlayWindowOpacity(mainWindow, 0);
} }
mainWindow.setIgnoreMouseEvents(true, { forward: true }); applyOverlayClickThrough(mainWindow, args.isWindowsPlatform);
releaseOverlayWindowLevel(mainWindow); releaseOverlayWindowLevel(mainWindow);
mainWindow.hide(); mainWindow.hide();
args.syncOverlayShortcuts(); args.syncOverlayShortcuts();
@@ -215,7 +216,7 @@ export function updateVisibleOverlayVisibility(args: {
shouldPreserveWindowsOverlayDuringFocusHandoff || shouldPreserveWindowsOverlayDuringFocusHandoff ||
(hasWindowsForegroundProcessSignal && windowsForegroundProcessName === 'mpv'); (hasWindowsForegroundProcessSignal && windowsForegroundProcessName === 'mpv');
if (shouldIgnoreMouseEvents) { if (shouldIgnoreMouseEvents) {
mainWindow.setIgnoreMouseEvents(true, { forward: true }); applyOverlayClickThrough(mainWindow, args.isWindowsPlatform);
} else { } else {
mainWindow.setIgnoreMouseEvents(false); mainWindow.setIgnoreMouseEvents(false);
} }
@@ -263,7 +264,7 @@ export function updateVisibleOverlayVisibility(args: {
if (hasNonNativeInputRegion) { if (hasNonNativeInputRegion) {
mainWindow.setIgnoreMouseEvents(false); mainWindow.setIgnoreMouseEvents(false);
} else { } else {
mainWindow.setIgnoreMouseEvents(true, { forward: true }); applyOverlayClickThrough(mainWindow, args.isWindowsPlatform);
} }
if (args.isWindowsPlatform) { if (args.isWindowsPlatform) {
scheduleWindowsOverlayReveal( scheduleWindowsOverlayReveal(
@@ -424,7 +425,7 @@ export function updateVisibleOverlayVisibility(args: {
return; return;
} }
args.setTrackerNotReadyWarningShown(false); args.setTrackerNotReadyWarningShown(false);
mainWindow.setIgnoreMouseEvents(true, { forward: true }); applyOverlayClickThrough(mainWindow, args.isWindowsPlatform);
releaseOverlayWindowLevel(mainWindow); releaseOverlayWindowLevel(mainWindow);
mainWindow.hide(); mainWindow.hide();
args.syncOverlayShortcuts(); args.syncOverlayShortcuts();
+2 -1
View File
@@ -331,6 +331,7 @@ import {
acquireYoutubeSubtitleTrack, acquireYoutubeSubtitleTrack,
acquireYoutubeSubtitleTracks, acquireYoutubeSubtitleTracks,
} from './core/services/youtube/generate'; } from './core/services/youtube/generate';
import { applyOverlayClickThrough } from './core/services/overlay-click-through';
import { createYoutubeMediaCacheService } from './core/services/youtube/media-cache'; import { createYoutubeMediaCacheService } from './core/services/youtube/media-cache';
import { resolveYoutubePlaybackUrl } from './core/services/youtube/playback-resolve'; import { resolveYoutubePlaybackUrl } from './core/services/youtube/playback-resolve';
import { probeYoutubeTracks } from './core/services/youtube/track-probe'; import { probeYoutubeTracks } from './core/services/youtube/track-probe';
@@ -5469,7 +5470,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
senderWindow === modalWindow && senderWindow === modalWindow &&
!senderWindow.isDestroyed() !senderWindow.isDestroyed()
) { ) {
senderWindow.setIgnoreMouseEvents(true, { forward: true }); applyOverlayClickThrough(senderWindow);
senderWindow.hide(); senderWindow.hide();
} }
handleOverlayModalClosedHandler(modal); handleOverlayModalClosedHandler(modal);
+4 -3
View File
@@ -2,6 +2,7 @@ import type { BrowserWindow } from 'electron';
import type { OverlayHostedModal } from '../shared/ipc/contracts'; import type { OverlayHostedModal } from '../shared/ipc/contracts';
import type { WindowGeometry } from '../types'; import type { WindowGeometry } from '../types';
import type { HyprlandPlacementStatus } from '../core/services/hyprland-window-placement'; import type { HyprlandPlacementStatus } from '../core/services/hyprland-window-placement';
import { applyOverlayClickThrough } from '../core/services/overlay-click-through';
import { import {
OVERLAY_WINDOW_CONTENT_READY_FLAG, OVERLAY_WINDOW_CONTENT_READY_FLAG,
OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG, OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG,
@@ -299,7 +300,7 @@ export function createOverlayModalRuntimeService(
} }
elevateModalWindow(window); elevateModalWindow(window);
if (options.passThroughMouseEvents) { if (options.passThroughMouseEvents) {
window.setIgnoreMouseEvents(true, { forward: true }); applyOverlayClickThrough(window, platform === 'win32');
} else { } else {
window.setIgnoreMouseEvents(false); window.setIgnoreMouseEvents(false);
} }
@@ -360,7 +361,7 @@ export function createOverlayModalRuntimeService(
mainWindowMousePassthroughForcedByModal = false; mainWindowMousePassthroughForcedByModal = false;
return; return;
} }
mainWindow.setIgnoreMouseEvents(true, { forward: true }); applyOverlayClickThrough(mainWindow, platform === 'win32');
mainWindowMousePassthroughForcedByModal = true; mainWindowMousePassthroughForcedByModal = true;
return; return;
} }
@@ -517,7 +518,7 @@ export function createOverlayModalRuntimeService(
clearPendingModalWindowReveal(); clearPendingModalWindowReveal();
if (modalWindow && !modalWindow.isDestroyed()) { if (modalWindow && !modalWindow.isDestroyed()) {
if (reuseModalWindowAfterClose) { if (reuseModalWindowAfterClose) {
modalWindow.setIgnoreMouseEvents(true, { forward: true }); applyOverlayClickThrough(modalWindow, false);
modalWindow.hide(); modalWindow.hide();
markModalWindowPrimed(modalWindow); markModalWindowPrimed(modalWindow);
} else { } else {
+3 -1
View File
@@ -1,3 +1,5 @@
import { applyOverlayClickThrough } from '../../core/services/overlay-click-through';
type StatsOverlayVisibilityWindow = { type StatsOverlayVisibilityWindow = {
isDestroyed: () => boolean; isDestroyed: () => boolean;
isVisible: () => boolean; isVisible: () => boolean;
@@ -8,7 +10,7 @@ function makeOverlayMousePassive(window: StatsOverlayVisibilityWindow | null): v
if (!window || window.isDestroyed() || !window.isVisible()) { if (!window || window.isDestroyed() || !window.isVisible()) {
return; return;
} }
window.setIgnoreMouseEvents(true, { forward: true }); applyOverlayClickThrough(window);
} }
export function createStatsOverlayVisibilityChangeHandler(deps: { export function createStatsOverlayVisibilityChangeHandler(deps: {
@@ -1,6 +1,7 @@
import { type BrowserWindow, screen } from 'electron'; import { type BrowserWindow, screen } from 'electron';
import { execFile } from 'node:child_process'; import { execFile } from 'node:child_process';
import { startOverlayWindowTracker as startOverlayWindowTrackerCore } from '../../core/services'; import { startOverlayWindowTracker as startOverlayWindowTrackerCore } from '../../core/services';
import { applyOverlayClickThrough } from '../../core/services/overlay-click-through';
import { isHeadlessInitialCommand, type CliArgs } from '../../cli/args'; import { isHeadlessInitialCommand, type CliArgs } from '../../cli/args';
import type { OverlayContentMeasurement, WindowGeometry } from '../../types'; import type { OverlayContentMeasurement, WindowGeometry } from '../../types';
import { createWindowTracker as createWindowTrackerCore } from '../../window-trackers'; import { createWindowTracker as createWindowTrackerCore } from '../../window-trackers';
@@ -603,7 +604,7 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
if (active) { if (active) {
mainWindow.setIgnoreMouseEvents(false); mainWindow.setIgnoreMouseEvents(false);
} else { } else {
mainWindow.setIgnoreMouseEvents(true, { forward: true }); applyOverlayClickThrough(mainWindow);
} }
} }
+118 -46
View File
@@ -1,4 +1,4 @@
import { execFileSync } from 'node:child_process'; import { execFile } from 'node:child_process';
import koffi from 'koffi'; import koffi from 'koffi';
import { matchesMpvSocketPathInCommandLine } from './mpv-socket-match'; import { matchesMpvSocketPathInCommandLine } from './mpv-socket-match';
@@ -173,43 +173,125 @@ function getProcessNameByPid(pid: number): string | null {
} }
} }
const processCommandLineCache = new Map<number, string>(); // Short-lived cache so the 250ms poll doesn't re-query every top-level window's process
// on each pass. The TTL bounds staleness from PID reuse.
const PROCESS_NAME_CACHE_TTL_MS = 5_000;
const PROCESS_NAME_CACHE_PRUNE_THRESHOLD = 512;
const processNameCache = new Map<number, { name: string | null; expiresAtMs: number }>();
function getCachedProcessNameByPid(pid: number): string | null {
const nowMs = Date.now();
const cached = processNameCache.get(pid);
if (cached && cached.expiresAtMs > nowMs) {
return cached.name;
}
const name = getProcessNameByPid(pid);
processNameCache.set(pid, { name, expiresAtMs: nowMs + PROCESS_NAME_CACHE_TTL_MS });
return name;
}
function pruneExpiredProcessNames(nowMs: number): void {
if (processNameCache.size <= PROCESS_NAME_CACHE_PRUNE_THRESHOLD) return;
for (const [pid, entry] of processNameCache) {
if (entry.expiresAtMs <= nowMs) {
processNameCache.delete(pid);
}
}
}
type ProcessCommandLineCacheEntry =
| { state: 'resolved'; commandLine: string; expiresAtMs: number; refreshInFlight: boolean }
| { state: 'pending' }
| { state: 'failed'; retryAtMs: number; backoffMs: number };
const COMMAND_LINE_RETRY_INITIAL_BACKOFF_MS = 2_000;
const COMMAND_LINE_RETRY_MAX_BACKOFF_MS = 30_000;
// A process command line never changes, so a resolved entry only has to expire to survive
// Windows PID reuse (a dead mpv's PID handed to a new instance, whose stale socket path would
// otherwise match the wrong window forever). Longer than the process-name TTL because each
// refresh costs a PowerShell spawn, and the cached value keeps being served while the refresh
// runs, so expiry never interrupts window matching.
const COMMAND_LINE_CACHE_TTL_MS = 60_000;
const processCommandLineCache = new Map<number, ProcessCommandLineCacheEntry>();
function queryProcessCommandLine(
pid: number,
onResult: (commandLine: string | null) => void,
): void {
execFile(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Bypass',
'-Command',
`$process = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($process -and $process.CommandLine) { [Console]::Out.Write($process.CommandLine) }`,
],
{
encoding: 'utf8',
windowsHide: true,
timeout: 1500,
},
(error, stdout) => {
const output = error ? '' : stdout.trim();
onResult(output.length > 0 ? output : null);
},
);
}
// Resolves a process command line via a background PowerShell lookup. Returns null until the
// first lookup completes; the caller's next poll picks up the cached result. Failures are
// negative-cached with exponential backoff: the synchronous version of this lookup could
// block the main thread for its full 1.5s timeout on every 250ms poll, which (combined with
// the forward:true mouse hook) stalled mouse input system-wide.
function getProcessCommandLineByPid(pid: number): string | null { function getProcessCommandLineByPid(pid: number): string | null {
if (processCommandLineCache.has(pid)) { const entry = processCommandLineCache.get(pid);
return processCommandLineCache.get(pid) ?? null; const nowMs = Date.now();
if (entry?.state === 'resolved') {
if (nowMs >= entry.expiresAtMs && !entry.refreshInFlight) {
entry.refreshInFlight = true;
queryProcessCommandLine(pid, (commandLine) => {
processCommandLineCache.set(pid, {
state: 'resolved',
// A failed refresh is usually a transient query error rather than a dead process
// (a gone process owns no window, so it is never looked up again). Keep the last
// known command line and re-check after the next TTL.
commandLine: commandLine ?? entry.commandLine,
expiresAtMs: Date.now() + COMMAND_LINE_CACHE_TTL_MS,
refreshInFlight: false,
});
});
}
return entry.commandLine;
} }
let commandLine: string | null = null; if (entry?.state === 'pending') return null;
try { if (entry?.state === 'failed' && nowMs < entry.retryAtMs) return null;
const output = execFileSync(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Bypass',
'-Command',
`$process = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($process -and $process.CommandLine) { [Console]::Out.Write($process.CommandLine) }`,
],
{
encoding: 'utf8',
windowsHide: true,
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 1500,
},
).trim();
commandLine = output.length > 0 ? output : null;
} catch {
commandLine = null;
}
if (commandLine !== null) { const nextBackoffMs =
processCommandLineCache.set(pid, commandLine); entry?.state === 'failed'
} else { ? Math.min(entry.backoffMs * 2, COMMAND_LINE_RETRY_MAX_BACKOFF_MS)
processCommandLineCache.delete(pid); : COMMAND_LINE_RETRY_INITIAL_BACKOFF_MS;
} processCommandLineCache.set(pid, { state: 'pending' });
return commandLine; queryProcessCommandLine(pid, (commandLine) => {
if (commandLine !== null) {
processCommandLineCache.set(pid, {
state: 'resolved',
commandLine,
expiresAtMs: Date.now() + COMMAND_LINE_CACHE_TTL_MS,
refreshInFlight: false,
});
} else {
processCommandLineCache.set(pid, {
state: 'failed',
retryAtMs: Date.now() + nextBackoffMs,
backoffMs: nextBackoffMs,
});
}
});
return null;
} }
export function findMpvWindows(targetSocketPath?: string | null): MpvPollResult { export function findMpvWindows(targetSocketPath?: string | null): MpvPollResult {
@@ -217,8 +299,7 @@ export function findMpvWindows(targetSocketPath?: string | null): MpvPollResult
const matches: MpvWindowMatch[] = []; const matches: MpvWindowMatch[] = [];
let hasMinimized = false; let hasMinimized = false;
let hasFocused = false; let hasFocused = false;
const processNameCache = new Map<number, string | null>(); pruneExpiredProcessNames(Date.now());
const processCommandLineLookupCache = new Map<number, string | null>();
const cb = koffi.register((hwnd: number, _lParam: number) => { const cb = koffi.register((hwnd: number, _lParam: number) => {
if (!IsWindowVisible(hwnd)) return true; if (!IsWindowVisible(hwnd)) return true;
@@ -228,21 +309,12 @@ export function findMpvWindows(targetSocketPath?: string | null): MpvPollResult
const pidValue = pid[0]!; const pidValue = pid[0]!;
if (pidValue === 0) return true; if (pidValue === 0) return true;
let processName = processNameCache.get(pidValue); const processName = getCachedProcessNameByPid(pidValue);
if (processName === undefined) {
processName = getProcessNameByPid(pidValue);
processNameCache.set(pidValue, processName);
}
if (!processName || processName.toLowerCase() !== 'mpv') return true; if (!processName || processName.toLowerCase() !== 'mpv') return true;
let commandLine: string | null = null; let commandLine: string | null = null;
if (targetSocketPath) { if (targetSocketPath) {
commandLine = processCommandLineLookupCache.get(pidValue) ?? null; commandLine = getProcessCommandLineByPid(pidValue);
if (!processCommandLineLookupCache.has(pidValue)) {
commandLine = getProcessCommandLineByPid(pidValue);
processCommandLineLookupCache.set(pidValue, commandLine);
}
if (!commandLine || !matchesMpvSocketPathInCommandLine(commandLine, targetSocketPath)) { if (!commandLine || !matchesMpvSocketPathInCommandLine(commandLine, targetSocketPath)) {
return true; return true;
} }