mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-16 01:55:51 -07:00
fix(overlay): prevent Windows mouse lag during click-through tracking (#201)
This commit is contained in:
@@ -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
|
||||
overlay window remained mapped above mpv.
|
||||
- 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
|
||||
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
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
parseSubsyncManualRunRequest,
|
||||
parseYoutubePickerResolveRequest,
|
||||
} from '../../shared/ipc/validators';
|
||||
import { applyOverlayClickThrough } from './overlay-click-through';
|
||||
|
||||
const { ipcMain } = electron;
|
||||
|
||||
@@ -442,7 +443,13 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
|
||||
const senderWindow =
|
||||
electron.BrowserWindow?.fromWebContents((event as IpcMainEvent).sender) ?? null;
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -848,7 +848,7 @@ test('Windows visible overlay stays click-through and binds to mpv while tracked
|
||||
} as never);
|
||||
|
||||
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('sync-windows-z-order'));
|
||||
assert.ok(!calls.includes('move-top'));
|
||||
@@ -1060,7 +1060,7 @@ test('tracked Windows overlay refresh rebinds while already visible', () => {
|
||||
isWindowsPlatform: true,
|
||||
} 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('move-top'));
|
||||
assert.ok(!calls.includes('show'));
|
||||
@@ -1134,7 +1134,7 @@ test('forced passthrough still reapplies while visible on Windows', () => {
|
||||
forceMousePassthrough: true,
|
||||
} 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('move-top'));
|
||||
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('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('ensure-level'));
|
||||
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,
|
||||
} 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'));
|
||||
});
|
||||
@@ -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('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('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('always-on-top:false'));
|
||||
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('ensure-level'));
|
||||
assert.ok(calls.includes('sync-shortcuts'));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import { BaseWindowTracker } from '../../window-trackers';
|
||||
import { WindowGeometry } from '../../types';
|
||||
import { applyOverlayClickThrough } from './overlay-click-through';
|
||||
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||
|
||||
const WINDOWS_OVERLAY_REVEAL_DELAY_MS = 48;
|
||||
@@ -117,7 +118,7 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
clearPendingWindowsOverlayReveal(mainWindow);
|
||||
setOverlayWindowOpacity(mainWindow, 0);
|
||||
}
|
||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
applyOverlayClickThrough(mainWindow, args.isWindowsPlatform);
|
||||
releaseOverlayWindowLevel(mainWindow);
|
||||
mainWindow.hide();
|
||||
args.syncOverlayShortcuts();
|
||||
@@ -215,7 +216,7 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
shouldPreserveWindowsOverlayDuringFocusHandoff ||
|
||||
(hasWindowsForegroundProcessSignal && windowsForegroundProcessName === 'mpv');
|
||||
if (shouldIgnoreMouseEvents) {
|
||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
applyOverlayClickThrough(mainWindow, args.isWindowsPlatform);
|
||||
} else {
|
||||
mainWindow.setIgnoreMouseEvents(false);
|
||||
}
|
||||
@@ -263,7 +264,7 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
if (hasNonNativeInputRegion) {
|
||||
mainWindow.setIgnoreMouseEvents(false);
|
||||
} else {
|
||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
applyOverlayClickThrough(mainWindow, args.isWindowsPlatform);
|
||||
}
|
||||
if (args.isWindowsPlatform) {
|
||||
scheduleWindowsOverlayReveal(
|
||||
@@ -424,7 +425,7 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
return;
|
||||
}
|
||||
args.setTrackerNotReadyWarningShown(false);
|
||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
applyOverlayClickThrough(mainWindow, args.isWindowsPlatform);
|
||||
releaseOverlayWindowLevel(mainWindow);
|
||||
mainWindow.hide();
|
||||
args.syncOverlayShortcuts();
|
||||
|
||||
+2
-1
@@ -331,6 +331,7 @@ import {
|
||||
acquireYoutubeSubtitleTrack,
|
||||
acquireYoutubeSubtitleTracks,
|
||||
} from './core/services/youtube/generate';
|
||||
import { applyOverlayClickThrough } from './core/services/overlay-click-through';
|
||||
import { createYoutubeMediaCacheService } from './core/services/youtube/media-cache';
|
||||
import { resolveYoutubePlaybackUrl } from './core/services/youtube/playback-resolve';
|
||||
import { probeYoutubeTracks } from './core/services/youtube/track-probe';
|
||||
@@ -5469,7 +5470,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
||||
senderWindow === modalWindow &&
|
||||
!senderWindow.isDestroyed()
|
||||
) {
|
||||
senderWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
applyOverlayClickThrough(senderWindow);
|
||||
senderWindow.hide();
|
||||
}
|
||||
handleOverlayModalClosedHandler(modal);
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { BrowserWindow } from 'electron';
|
||||
import type { OverlayHostedModal } from '../shared/ipc/contracts';
|
||||
import type { WindowGeometry } from '../types';
|
||||
import type { HyprlandPlacementStatus } from '../core/services/hyprland-window-placement';
|
||||
import { applyOverlayClickThrough } from '../core/services/overlay-click-through';
|
||||
import {
|
||||
OVERLAY_WINDOW_CONTENT_READY_FLAG,
|
||||
OVERLAY_WINDOW_DOCUMENT_LOADED_FLAG,
|
||||
@@ -299,7 +300,7 @@ export function createOverlayModalRuntimeService(
|
||||
}
|
||||
elevateModalWindow(window);
|
||||
if (options.passThroughMouseEvents) {
|
||||
window.setIgnoreMouseEvents(true, { forward: true });
|
||||
applyOverlayClickThrough(window, platform === 'win32');
|
||||
} else {
|
||||
window.setIgnoreMouseEvents(false);
|
||||
}
|
||||
@@ -360,7 +361,7 @@ export function createOverlayModalRuntimeService(
|
||||
mainWindowMousePassthroughForcedByModal = false;
|
||||
return;
|
||||
}
|
||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
applyOverlayClickThrough(mainWindow, platform === 'win32');
|
||||
mainWindowMousePassthroughForcedByModal = true;
|
||||
return;
|
||||
}
|
||||
@@ -517,7 +518,7 @@ export function createOverlayModalRuntimeService(
|
||||
clearPendingModalWindowReveal();
|
||||
if (modalWindow && !modalWindow.isDestroyed()) {
|
||||
if (reuseModalWindowAfterClose) {
|
||||
modalWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
applyOverlayClickThrough(modalWindow, false);
|
||||
modalWindow.hide();
|
||||
markModalWindowPrimed(modalWindow);
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { applyOverlayClickThrough } from '../../core/services/overlay-click-through';
|
||||
|
||||
type StatsOverlayVisibilityWindow = {
|
||||
isDestroyed: () => boolean;
|
||||
isVisible: () => boolean;
|
||||
@@ -8,7 +10,7 @@ function makeOverlayMousePassive(window: StatsOverlayVisibilityWindow | null): v
|
||||
if (!window || window.isDestroyed() || !window.isVisible()) {
|
||||
return;
|
||||
}
|
||||
window.setIgnoreMouseEvents(true, { forward: true });
|
||||
applyOverlayClickThrough(window);
|
||||
}
|
||||
|
||||
export function createStatsOverlayVisibilityChangeHandler(deps: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type BrowserWindow, screen } from 'electron';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { startOverlayWindowTracker as startOverlayWindowTrackerCore } from '../../core/services';
|
||||
import { applyOverlayClickThrough } from '../../core/services/overlay-click-through';
|
||||
import { isHeadlessInitialCommand, type CliArgs } from '../../cli/args';
|
||||
import type { OverlayContentMeasurement, WindowGeometry } from '../../types';
|
||||
import { createWindowTracker as createWindowTrackerCore } from '../../window-trackers';
|
||||
@@ -603,7 +604,7 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
|
||||
if (active) {
|
||||
mainWindow.setIgnoreMouseEvents(false);
|
||||
} else {
|
||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
applyOverlayClickThrough(mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+118
-46
@@ -1,4 +1,4 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { execFile } from 'node:child_process';
|
||||
import koffi from 'koffi';
|
||||
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 {
|
||||
if (processCommandLineCache.has(pid)) {
|
||||
return processCommandLineCache.get(pid) ?? null;
|
||||
const entry = processCommandLineCache.get(pid);
|
||||
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;
|
||||
try {
|
||||
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 (entry?.state === 'pending') return null;
|
||||
if (entry?.state === 'failed' && nowMs < entry.retryAtMs) return null;
|
||||
|
||||
if (commandLine !== null) {
|
||||
processCommandLineCache.set(pid, commandLine);
|
||||
} else {
|
||||
processCommandLineCache.delete(pid);
|
||||
}
|
||||
return commandLine;
|
||||
const nextBackoffMs =
|
||||
entry?.state === 'failed'
|
||||
? Math.min(entry.backoffMs * 2, COMMAND_LINE_RETRY_MAX_BACKOFF_MS)
|
||||
: COMMAND_LINE_RETRY_INITIAL_BACKOFF_MS;
|
||||
processCommandLineCache.set(pid, { state: 'pending' });
|
||||
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 {
|
||||
@@ -217,8 +299,7 @@ export function findMpvWindows(targetSocketPath?: string | null): MpvPollResult
|
||||
const matches: MpvWindowMatch[] = [];
|
||||
let hasMinimized = false;
|
||||
let hasFocused = false;
|
||||
const processNameCache = new Map<number, string | null>();
|
||||
const processCommandLineLookupCache = new Map<number, string | null>();
|
||||
pruneExpiredProcessNames(Date.now());
|
||||
|
||||
const cb = koffi.register((hwnd: number, _lParam: number) => {
|
||||
if (!IsWindowVisible(hwnd)) return true;
|
||||
@@ -228,21 +309,12 @@ export function findMpvWindows(targetSocketPath?: string | null): MpvPollResult
|
||||
const pidValue = pid[0]!;
|
||||
if (pidValue === 0) return true;
|
||||
|
||||
let processName = processNameCache.get(pidValue);
|
||||
if (processName === undefined) {
|
||||
processName = getProcessNameByPid(pidValue);
|
||||
processNameCache.set(pidValue, processName);
|
||||
}
|
||||
|
||||
const processName = getCachedProcessNameByPid(pidValue);
|
||||
if (!processName || processName.toLowerCase() !== 'mpv') return true;
|
||||
|
||||
let commandLine: string | null = null;
|
||||
if (targetSocketPath) {
|
||||
commandLine = processCommandLineLookupCache.get(pidValue) ?? null;
|
||||
if (!processCommandLineLookupCache.has(pidValue)) {
|
||||
commandLine = getProcessCommandLineByPid(pidValue);
|
||||
processCommandLineLookupCache.set(pidValue, commandLine);
|
||||
}
|
||||
commandLine = getProcessCommandLineByPid(pidValue);
|
||||
if (!commandLine || !matchesMpvSocketPathInCommandLine(commandLine, targetSocketPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user