mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-01 05:40:50 -07:00
fix(overlay): reload imported mpv keys after mpv connects (#278)
* fix(overlay): reload imported mpv keys after mpv connects The overlay fetched mpv's input-bindings before SubMiner connected to mpv, cached an empty list, and never refetched because the connect-time refresh only broadcast session bindings when their compiled signature changed. Keys from input.conf and mpv defaults (9/0 volume, m mute) then did nothing while the overlay had focus, most often in mpv.backend x11 mode. Main now emits mpv-input-bindings:changed whenever the discovered mpv key set or client changes, and the renderer refreshes its imported keys on it. * fix(overlay): subscribe to mpv key changes before initial discovery The renderer registered the mpv-input-bindings:changed listener only after awaiting setupMpvInputForwarding and several other startup calls. If mpv connected in that window, the event was dropped and the imported mpv keys stayed empty.
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Fixed mpv key bindings (input.conf and mpv defaults, e.g. `9`/`0` volume) doing nothing while the overlay had focus when the overlay loaded before SubMiner connected to mpv, common in `mpv.backend: x11` mode. The overlay now reloads mpv's bindings once mpv connects.
|
||||||
@@ -50,7 +50,10 @@ validates discovered keys and translates browser input. The renderer's
|
|||||||
`handlers/mpv-input-forwarding.ts` keeps the session lookup, coalesces asynchronous
|
`handlers/mpv-input-forwarding.ts` keeps the session lookup, coalesces asynchronous
|
||||||
refreshes, and releases held keys on blur or disposal. `handlers/keyboard.ts` runs
|
refreshes, and releases held keys on blur or disposal. `handlers/keyboard.ts` runs
|
||||||
this fallback after SubMiner controls and refreshes on startup, a delayed startup
|
this fallback after SubMiner controls and refreshes on startup, a delayed startup
|
||||||
pass, focus, and binding reload. Discovery does not enter compiled session bindings,
|
pass, focus, binding reload, and the `mpv-input-bindings:changed` event. Main sends
|
||||||
|
that event from `session-bindings-runtime.ts` when mpv's discovered key set changes,
|
||||||
|
including the first discovery after connecting, because the overlay often loads
|
||||||
|
before mpv connects and the compiled session bindings may not change. Discovery does not enter compiled session bindings,
|
||||||
the plugin artifact, persistent config, or session help.
|
the plugin artifact, persistent config, or session help.
|
||||||
|
|
||||||
The subtitle sidebar consumes parsed cues through `SubtitleSidebarSnapshot`. Its `sourceKey`
|
The subtitle sidebar consumes parsed cues through `SubtitleSidebarSnapshot`. Its `sourceKey`
|
||||||
|
|||||||
@@ -5354,6 +5354,8 @@ const { persistSessionBindings, refreshCurrentSessionBindings, refreshMpvSession
|
|||||||
logWarn: (message) => logger.warn(message),
|
logWarn: (message) => logger.warn(message),
|
||||||
onBindingsChanged: (bindings) =>
|
onBindingsChanged: (bindings) =>
|
||||||
overlayManager.broadcastToOverlayWindows(IPC_CHANNELS.event.sessionBindingsChanged, bindings),
|
overlayManager.broadcastToOverlayWindows(IPC_CHANNELS.event.sessionBindingsChanged, bindings),
|
||||||
|
onMpvInputBindingsChanged: () =>
|
||||||
|
overlayManager.broadcastToOverlayWindows(IPC_CHANNELS.event.mpvInputBindingsChanged),
|
||||||
onWarning: (warning) => {
|
onWarning: (warning) => {
|
||||||
if (warning.kind !== 'conflict') return;
|
if (warning.kind !== 'conflict') return;
|
||||||
overlayNotificationsRuntime.showOverlayNotification({
|
overlayNotificationsRuntime.showOverlayNotification({
|
||||||
|
|||||||
@@ -71,6 +71,45 @@ test('persistSessionBindings keeps saved bindings when mpv reload notification f
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('mpv input binding discovery notifies the overlay when the native key set changes', async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-session-native-keys-'));
|
||||||
|
let nativeKeys: unknown = [{ key: '9', cmd: 'add volume -5', priority: 25 }];
|
||||||
|
let notifications = 0;
|
||||||
|
const client = {
|
||||||
|
connected: false,
|
||||||
|
send: () => {},
|
||||||
|
requestProperty: async () => nativeKeys,
|
||||||
|
};
|
||||||
|
const runtime = createSessionBindingsRuntime({
|
||||||
|
configDir: root,
|
||||||
|
getKeybindings: () => [],
|
||||||
|
getConfiguredShortcuts: () => ({ multiCopyTimeoutMs: 1500 }) as never,
|
||||||
|
getResolvedConfig: () => ({ stats: { toggleKey: 's', markWatchedKey: 'w' } }) as ResolvedConfig,
|
||||||
|
getMpvClient: () => client,
|
||||||
|
setSessionBindings: () => {},
|
||||||
|
setSessionBindingsInitialized: () => {},
|
||||||
|
logWarn: () => {},
|
||||||
|
onMpvInputBindingsChanged: () => {
|
||||||
|
notifications += 1;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
runtime.persistSessionBindings([]);
|
||||||
|
await runtime.refreshMpvSessionBindings();
|
||||||
|
assert.equal(notifications, 0, 'nothing to announce before mpv connects');
|
||||||
|
client.connected = true;
|
||||||
|
await runtime.refreshMpvSessionBindings();
|
||||||
|
assert.equal(notifications, 1, 'first discovery after connecting must reach the overlay');
|
||||||
|
await runtime.refreshMpvSessionBindings();
|
||||||
|
assert.equal(notifications, 1, 'unchanged discovery must not create a refresh loop');
|
||||||
|
nativeKeys = [{ key: '0', cmd: 'ignore', priority: 25 }];
|
||||||
|
await runtime.refreshMpvSessionBindings();
|
||||||
|
assert.equal(notifications, 2, 'ignored keys still change what the overlay may forward');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('native prefix conflicts publish the same effective bindings to the overlay and plugin and recover', async () => {
|
test('native prefix conflicts publish the same effective bindings to the overlay and plugin and recover', async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-session-conflict-'));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-session-conflict-'));
|
||||||
const sequence: CompiledSessionBinding = {
|
const sequence: CompiledSessionBinding = {
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export interface SessionBindingsRuntimeDeps {
|
|||||||
setSessionBindingsInitialized: (initialized: boolean) => void;
|
setSessionBindingsInitialized: (initialized: boolean) => void;
|
||||||
logWarn: (message: string, details?: unknown) => void;
|
logWarn: (message: string, details?: unknown) => void;
|
||||||
onBindingsChanged?: (bindings: CompiledSessionBinding[]) => void;
|
onBindingsChanged?: (bindings: CompiledSessionBinding[]) => void;
|
||||||
|
// Fires when mpv's own key bindings change, including the first discovery after
|
||||||
|
// connecting. The overlay's imported mpv keys depend on them even when the
|
||||||
|
// compiled session bindings stay identical.
|
||||||
|
onMpvInputBindingsChanged?: () => void;
|
||||||
onWarning?: (warning: SessionBindingWarning) => void;
|
onWarning?: (warning: SessionBindingWarning) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +45,7 @@ export function createSessionBindingsRuntime(deps: SessionBindingsRuntimeDeps):
|
|||||||
let nativeSnapshot: {
|
let nativeSnapshot: {
|
||||||
client: ReturnType<SessionBindingsRuntimeDeps['getMpvClient']>;
|
client: ReturnType<SessionBindingsRuntimeDeps['getMpvClient']>;
|
||||||
keys: string[];
|
keys: string[];
|
||||||
|
signature: string;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
let pending: {
|
let pending: {
|
||||||
client: ReturnType<SessionBindingsRuntimeDeps['getMpvClient']>;
|
client: ReturnType<SessionBindingsRuntimeDeps['getMpvClient']>;
|
||||||
@@ -135,8 +140,15 @@ export function createSessionBindingsRuntime(deps: SessionBindingsRuntimeDeps):
|
|||||||
try {
|
try {
|
||||||
const raw = await client.requestProperty('input-bindings');
|
const raw = await client.requestProperty('input-bindings');
|
||||||
if (client !== deps.getMpvClient() || !client.connected) return;
|
if (client !== deps.getMpvClient() || !client.connected) return;
|
||||||
nativeSnapshot = { client, keys: parseMpvInputBindingKeys(raw, { includeIgnored: false }) };
|
const signature = JSON.stringify(parseMpvInputBindingKeys(raw));
|
||||||
|
const changed = nativeSnapshot?.client !== client || nativeSnapshot.signature !== signature;
|
||||||
|
nativeSnapshot = {
|
||||||
|
client,
|
||||||
|
keys: parseMpvInputBindingKeys(raw, { includeIgnored: false }),
|
||||||
|
signature,
|
||||||
|
};
|
||||||
publishBindings();
|
publishBindings();
|
||||||
|
if (changed) deps.onMpvInputBindingsChanged?.();
|
||||||
} catch {
|
} catch {
|
||||||
// Keep the last successful snapshot if discovery is temporarily unavailable.
|
// Keep the last successful snapshot if discovery is temporarily unavailable.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -624,6 +624,9 @@ const electronAPI: ElectronAPI = {
|
|||||||
(_event, bindings: import('./types').CompiledSessionBinding[]) => callback(bindings),
|
(_event, bindings: import('./types').CompiledSessionBinding[]) => callback(bindings),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
onMpvInputBindingsChanged: (callback: () => void) => {
|
||||||
|
ipcRenderer.on(IPC_CHANNELS.event.mpvInputBindingsChanged, () => callback());
|
||||||
|
},
|
||||||
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => {
|
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => {
|
||||||
ipcRenderer.on(
|
ipcRenderer.on(
|
||||||
IPC_CHANNELS.event.configHotReload,
|
IPC_CHANNELS.event.configHotReload,
|
||||||
|
|||||||
@@ -1335,6 +1335,9 @@ export function createKeyboardHandlers(
|
|||||||
setupMpvInputForwarding,
|
setupMpvInputForwarding,
|
||||||
refreshConfiguredShortcuts,
|
refreshConfiguredShortcuts,
|
||||||
updateSessionBindings,
|
updateSessionBindings,
|
||||||
|
refreshMpvInputBindings: () => {
|
||||||
|
void importedMpvBindings.refresh();
|
||||||
|
},
|
||||||
syncKeyboardTokenSelection,
|
syncKeyboardTokenSelection,
|
||||||
handleSubtitleContentUpdated,
|
handleSubtitleContentUpdated,
|
||||||
togglePrimarySubtitleBarVisibility,
|
togglePrimarySubtitleBarVisibility,
|
||||||
|
|||||||
@@ -779,6 +779,9 @@ async function init(): Promise<void> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Subscribe before the initial discovery: mpv can connect while it is in flight,
|
||||||
|
// and a missed change event leaves the imported mpv keys empty.
|
||||||
|
window.electronAPI.onMpvInputBindingsChanged(keyboardHandlers.refreshMpvInputBindings);
|
||||||
await keyboardHandlers.setupMpvInputForwarding();
|
await keyboardHandlers.setupMpvInputForwarding();
|
||||||
|
|
||||||
const initialSubtitleStyle = await window.electronAPI.getSubtitleStyle();
|
const initialSubtitleStyle = await window.electronAPI.getSubtitleStyle();
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ export const IPC_CHANNELS = {
|
|||||||
subtitleSidebarToggle: 'subtitle-sidebar:toggle',
|
subtitleSidebarToggle: 'subtitle-sidebar:toggle',
|
||||||
primarySubtitleBarToggle: 'primary-subtitle-bar:toggle',
|
primarySubtitleBarToggle: 'primary-subtitle-bar:toggle',
|
||||||
sessionBindingsChanged: 'session-bindings:changed',
|
sessionBindingsChanged: 'session-bindings:changed',
|
||||||
|
mpvInputBindingsChanged: 'mpv-input-bindings:changed',
|
||||||
configHotReload: 'config:hot-reload',
|
configHotReload: 'config:hot-reload',
|
||||||
overlayNotification: 'overlay:notification',
|
overlayNotification: 'overlay:notification',
|
||||||
notificationHistoryToggle: 'notification-history:toggle',
|
notificationHistoryToggle: 'notification-history:toggle',
|
||||||
|
|||||||
@@ -656,6 +656,7 @@ export interface ElectronAPI {
|
|||||||
) => void;
|
) => void;
|
||||||
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
||||||
onSessionBindingsChanged: (callback: (bindings: CompiledSessionBinding[]) => void) => void;
|
onSessionBindingsChanged: (callback: (bindings: CompiledSessionBinding[]) => void) => void;
|
||||||
|
onMpvInputBindingsChanged: (callback: () => void) => void;
|
||||||
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => void;
|
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user