audio playback fixes
This commit is contained in:
@@ -88,7 +88,7 @@ function ensureBackend(): AudioBackend {
|
||||
// ── Process-exit teardown ─────────────────────────────────────────────
|
||||
// `q` (the quit action) calls `process.exit(0)`, which bypasses Solid's
|
||||
// onCleanup — where `backend.dispose()` would otherwise kill the spawned
|
||||
// player (mpv/ffplay/afplay). Without this hook those child processes
|
||||
// player (mpv). Without this hook those child processes
|
||||
// survive the host and keep playing audio after the TUI has quit. The
|
||||
// `exit` event fires synchronously on `process.exit(N)`; the signal
|
||||
// handlers cover Ctrl-C / kill, which otherwise terminate without running
|
||||
|
||||
@@ -5,43 +5,43 @@
|
||||
* regardless of which component is focused. Uses the event bus to
|
||||
* decouple key detection from audio control logic.
|
||||
*
|
||||
* Keys are only handled when an episode is loaded (or for play/pause,
|
||||
* always). This prevents accidental volume/seek changes when there's
|
||||
* nothing playing.
|
||||
* Volume and speed are app-level settings — adjustable with or without
|
||||
* an episode loaded (they apply to the next playback and persist). Seek
|
||||
* is playback-dependent, so it still requires a loaded episode.
|
||||
*/
|
||||
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
import { emit } from "../utils/event-bus"
|
||||
import { useKeyboard } from "@opentui/solid";
|
||||
import { emit } from "../utils/event-bus";
|
||||
|
||||
export type MediaKeyAction =
|
||||
| "media.toggle"
|
||||
| "media.volumeUp"
|
||||
| "media.volumeDown"
|
||||
| "media.seekForward"
|
||||
| "media.seekBackward"
|
||||
| "media.speedCycle"
|
||||
| "media.toggle"
|
||||
| "media.volumeUp"
|
||||
| "media.volumeDown"
|
||||
| "media.seekForward"
|
||||
| "media.seekBackward"
|
||||
| "media.speedCycle";
|
||||
|
||||
/** Key-to-action mappings for multimedia controls */
|
||||
const MEDIA_KEY_MAP: Record<string, MediaKeyAction> = {
|
||||
// Common terminal media keys — these overlap with Player.tsx local
|
||||
// bindings, but Player guards on `props.focused` so the global
|
||||
// handler fires independently when the player tab is *not* active.
|
||||
//
|
||||
// When Player IS focused both handlers fire, but since the audio
|
||||
// actions are idempotent (toggle = toggle, seek = additive) having
|
||||
// them called twice for the same keypress is avoided by the event
|
||||
// bus approach — the audio hook only processes event-bus events, and
|
||||
// Player.tsx calls audio methods directly. We therefore guard with
|
||||
// a "playerFocused" flag passed via options.
|
||||
}
|
||||
// Common terminal media keys — these overlap with Player.tsx local
|
||||
// bindings, but Player guards on `props.focused` so the global
|
||||
// handler fires independently when the player tab is *not* active.
|
||||
//
|
||||
// When Player IS focused both handlers fire, but since the audio
|
||||
// actions are idempotent (toggle = toggle, seek = additive) having
|
||||
// them called twice for the same keypress is avoided by the event
|
||||
// bus approach — the audio hook only processes event-bus events, and
|
||||
// Player.tsx calls audio methods directly. We therefore guard with
|
||||
// a "playerFocused" flag passed via options.
|
||||
};
|
||||
|
||||
export interface MultimediaKeysOptions {
|
||||
/** When true, skip handling (Player.tsx handles keys locally) */
|
||||
playerFocused?: () => boolean
|
||||
/** When true, skip handling (text input has focus) */
|
||||
inputFocused?: () => boolean
|
||||
/** Whether an episode is currently loaded */
|
||||
hasEpisode?: () => boolean
|
||||
/** When true, skip handling (Player.tsx handles keys locally) */
|
||||
playerFocused?: () => boolean;
|
||||
/** When true, skip handling (text input has focus) */
|
||||
inputFocused?: () => boolean;
|
||||
/** Whether an episode is currently loaded */
|
||||
hasEpisode?: () => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,50 +49,47 @@ export interface MultimediaKeysOptions {
|
||||
* event bus. Call once at the app level (e.g. in App.tsx).
|
||||
*/
|
||||
export function useMultimediaKeys(options: MultimediaKeysOptions = {}) {
|
||||
useKeyboard((key) => {
|
||||
// Don't intercept when a text input owns the keyboard
|
||||
if (options.inputFocused?.()) return
|
||||
useKeyboard((key) => {
|
||||
// Don't intercept when a text input owns the keyboard
|
||||
if (options.inputFocused?.()) return;
|
||||
|
||||
// Don't intercept when Player component handles its own keys
|
||||
if (options.playerFocused?.()) return
|
||||
// Don't intercept when Player component handles its own keys
|
||||
if (options.playerFocused?.()) return;
|
||||
|
||||
// Ctrl/Meta combos are app-level shortcuts, not media keys
|
||||
if (key.ctrl || key.meta) return
|
||||
// Ctrl/Meta combos are app-level shortcuts, not media keys
|
||||
if (key.ctrl || key.meta) return;
|
||||
|
||||
switch (key.name) {
|
||||
case "space":
|
||||
// Toggle play/pause — always valid (may start a loaded episode)
|
||||
emit("media.toggle", {})
|
||||
break
|
||||
switch (key.name) {
|
||||
case "space":
|
||||
// Toggle play/pause — always valid (may start a loaded episode)
|
||||
emit("media.toggle", {});
|
||||
break;
|
||||
|
||||
case "up":
|
||||
if (!options.hasEpisode?.()) return
|
||||
emit("media.volumeUp", {})
|
||||
break
|
||||
case "up":
|
||||
emit("media.volumeUp", {});
|
||||
break;
|
||||
|
||||
case "down":
|
||||
if (!options.hasEpisode?.()) return
|
||||
emit("media.volumeDown", {})
|
||||
break
|
||||
case "down":
|
||||
emit("media.volumeDown", {});
|
||||
break;
|
||||
|
||||
case "left":
|
||||
if (!options.hasEpisode?.()) return
|
||||
emit("media.seekBackward", {})
|
||||
break
|
||||
case "left":
|
||||
if (!options.hasEpisode?.()) return;
|
||||
emit("media.seekBackward", {});
|
||||
break;
|
||||
|
||||
case "right":
|
||||
if (!options.hasEpisode?.()) return
|
||||
emit("media.seekForward", {})
|
||||
break
|
||||
case "right":
|
||||
if (!options.hasEpisode?.()) return;
|
||||
emit("media.seekForward", {});
|
||||
break;
|
||||
|
||||
case "s":
|
||||
if (!options.hasEpisode?.()) return
|
||||
emit("media.speedCycle", {})
|
||||
break
|
||||
case "s":
|
||||
emit("media.speedCycle", {});
|
||||
break;
|
||||
|
||||
default:
|
||||
// Not a media key — do nothing
|
||||
break
|
||||
}
|
||||
})
|
||||
default:
|
||||
// Not a media key — do nothing
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user