mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
video color filters
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
||||||
<title>Roomba Rover</title>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-Dd_cj-I7.js"></script>
|
<script type="module" crossorigin src="/assets/index-CIuRMYki.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BLKXqzg3.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Dd2Mm-LQ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const KEY_ACTIONS = [
|
|||||||
{ id: 'cameraUp', label: 'Camera Up', group: 'Camera' },
|
{ id: 'cameraUp', label: 'Camera Up', group: 'Camera' },
|
||||||
{ id: 'cameraDown', label: 'Camera Down', group: 'Camera' },
|
{ id: 'cameraDown', label: 'Camera Down', group: 'Camera' },
|
||||||
{ id: 'nightVisionToggle', label: 'Toggle Night Vision', group: 'Camera' },
|
{ id: 'nightVisionToggle', label: 'Toggle Night Vision', group: 'Camera' },
|
||||||
|
{ id: 'videoFilterCycle', label: 'Cycle Video Filter', group: 'Camera' },
|
||||||
{ id: 'hornHonk', label: 'Horn (Hold)', group: 'Audio' },
|
{ id: 'hornHonk', label: 'Horn (Hold)', group: 'Audio' },
|
||||||
{ id: 'micPtt', label: 'Mic Push To Talk', group: 'Audio' },
|
{ id: 'micPtt', label: 'Mic Push To Talk', group: 'Audio' },
|
||||||
{ id: 'driveMacro', label: 'Drive Macro', group: 'Macros' },
|
{ id: 'driveMacro', label: 'Drive Macro', group: 'Macros' },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
|
|||||||
import { useVideoRequests } from '../../hooks/useVideoRequests.js';
|
import { useVideoRequests } from '../../hooks/useVideoRequests.js';
|
||||||
import { useRoverSnapshots } from '../../hooks/useRoverSnapshots.js';
|
import { useRoverSnapshots } from '../../hooks/useRoverSnapshots.js';
|
||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import { AUDIO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
import { AUDIO_SETTINGS_DEFAULTS, VIDEO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||||
import {
|
import {
|
||||||
RESTART_DELAY_MS,
|
RESTART_DELAY_MS,
|
||||||
UNMUTE_RETRY_MS,
|
UNMUTE_RETRY_MS,
|
||||||
@@ -14,6 +14,27 @@ import {
|
|||||||
DUCK_RELEASE_FADE_MS,
|
DUCK_RELEASE_FADE_MS,
|
||||||
} from './constants.js';
|
} from './constants.js';
|
||||||
|
|
||||||
|
const VIDEO_FILTER_STYLES = {
|
||||||
|
// The empty filter keeps the browser's native video presentation untouched when the user
|
||||||
|
// wants normal color or when a rover's camera color is already useful.
|
||||||
|
none: '',
|
||||||
|
// Grayscale is the most reliable way to remove the pink cast from a no-IR-filter camera
|
||||||
|
// because it depends on luminance instead of trying to guess the original scene colors.
|
||||||
|
grayscale: 'grayscale(1) contrast(1.08)',
|
||||||
|
// Greenscale is intentionally implemented as a CSS tint over grayscale rather than canvas
|
||||||
|
// processing. That keeps latency low, works for both <video> and snapshot <img>, and avoids
|
||||||
|
// interfering with WebRTC playback.
|
||||||
|
greenscale: 'grayscale(1) sepia(1) hue-rotate(70deg) saturate(2.2) brightness(0.95) contrast(1.1)',
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeVideoFilter(value) {
|
||||||
|
// Persisted settings may outlive code changes, so every media render validates the stored
|
||||||
|
// value before using it in a style. Unknown values fall back to full color.
|
||||||
|
return Object.prototype.hasOwnProperty.call(VIDEO_FILTER_STYLES, value)
|
||||||
|
? value
|
||||||
|
: VIDEO_SETTINGS_DEFAULTS.colorFilter;
|
||||||
|
}
|
||||||
|
|
||||||
export default function RoverMediaPlayer({
|
export default function RoverMediaPlayer({
|
||||||
roverId = null,
|
roverId = null,
|
||||||
sessionInfo = null,
|
sessionInfo = null,
|
||||||
@@ -81,6 +102,16 @@ export default function RoverMediaPlayer({
|
|||||||
const hasDedicatedAudio = Boolean(resolvedAudioSessionInfo?.url);
|
const hasDedicatedAudio = Boolean(resolvedAudioSessionInfo?.url);
|
||||||
const usingSnapshot = videoMode === 'snapshot' || (!videoMode && !resolvedSessionInfo?.url);
|
const usingSnapshot = videoMode === 'snapshot' || (!videoMode && !resolvedSessionInfo?.url);
|
||||||
const { value: audioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
|
const { value: audioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
|
||||||
|
const { value: videoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS);
|
||||||
|
const videoFilter = normalizeVideoFilter(videoSettings?.colorFilter);
|
||||||
|
const videoFilterStyle = VIDEO_FILTER_STYLES[videoFilter];
|
||||||
|
const mediaStyle = videoFilterStyle
|
||||||
|
? {
|
||||||
|
// Apply the filter only to the camera pixels. The parent overlays remain unfiltered so
|
||||||
|
// telemetry, chat, and warning text do not lose contrast or inherit the green tint.
|
||||||
|
filter: videoFilterStyle,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
const masterVolume = Number.isFinite(audioSettings?.masterVolume)
|
const masterVolume = Number.isFinite(audioSettings?.masterVolume)
|
||||||
? audioSettings.masterVolume
|
? audioSettings.masterVolume
|
||||||
: AUDIO_SETTINGS_DEFAULTS.masterVolume;
|
: AUDIO_SETTINGS_DEFAULTS.masterVolume;
|
||||||
@@ -541,6 +572,7 @@ export default function RoverMediaPlayer({
|
|||||||
src={resolvedSnapshotFeed.objectUrl}
|
src={resolvedSnapshotFeed.objectUrl}
|
||||||
alt={resolvedLabel}
|
alt={resolvedLabel}
|
||||||
className="h-full w-full object-contain"
|
className="h-full w-full object-contain"
|
||||||
|
style={mediaStyle}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -556,6 +588,7 @@ export default function RoverMediaPlayer({
|
|||||||
autoPlay
|
autoPlay
|
||||||
controls={false}
|
controls={false}
|
||||||
className="h-full w-full object-contain"
|
className="h-full w-full object-contain"
|
||||||
|
style={mediaStyle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showConnectingOverlay ? (
|
{showConnectingOverlay ? (
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import Tabs, { Tab, TabList, TabPanel, TabPanels } from '../Tabs/index.jsx';
|
|||||||
import SessionSnapshot from '../SessionSnapshot/index.jsx';
|
import SessionSnapshot from '../SessionSnapshot/index.jsx';
|
||||||
import SocketLogPanel from '../SocketLogPanel/index.jsx';
|
import SocketLogPanel from '../SocketLogPanel/index.jsx';
|
||||||
import CardFrame from '../CardFrame/index.jsx';
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
import KeyPill from '../vip/VipAudioUploadCard/KeyPill.jsx';
|
||||||
import { useHudMapSetting } from '../../hooks/useHudMapSetting.js';
|
import { useHudMapSetting } from '../../hooks/useHudMapSetting.js';
|
||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import { useSocket } from '../../context/SocketContext.jsx';
|
import { useSocket } from '../../context/SocketContext.jsx';
|
||||||
import { AUDIO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
import { AUDIO_SETTINGS_DEFAULTS, VIDEO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||||
|
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||||
|
|
||||||
const manualTabs = [
|
const manualTabs = [
|
||||||
{ key: 'start', label: 'Start OI' },
|
{ key: 'start', label: 'Start OI' },
|
||||||
@@ -25,9 +27,28 @@ const manualTabs = [
|
|||||||
{ key: 'dock', label: 'Dock' },
|
{ key: 'dock', label: 'Dock' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const VIDEO_FILTER_OPTIONS = [
|
||||||
|
// "Color" is the pass-through mode users can return to when the scene has usable color.
|
||||||
|
{ key: 'none', label: 'Color' },
|
||||||
|
// Grayscale removes the pink IR-contaminated cast while preserving luminance detail.
|
||||||
|
{ key: 'grayscale', label: 'Gray' },
|
||||||
|
// Greenscale keeps the same luminance-first idea as grayscale, then tints it green for
|
||||||
|
// users who find green-on-black easier to visually parse in bright outdoor scenes.
|
||||||
|
{ key: 'greenscale', label: 'Green' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeVideoFilter(value) {
|
||||||
|
// Settings are stored in a browser cookie and can contain stale or hand-edited values.
|
||||||
|
// Falling back here keeps the dropdown and media player predictable instead of rendering
|
||||||
|
// with an unknown mode.
|
||||||
|
return VIDEO_FILTER_OPTIONS.some((option) => option.key === value)
|
||||||
|
? value
|
||||||
|
: VIDEO_SETTINGS_DEFAULTS.colorFilter;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsPanel() {
|
export default function SettingsPanel() {
|
||||||
const {
|
const {
|
||||||
state: { roverId },
|
state: { keymap, roverId },
|
||||||
actions: { sendOiCommand, setSensorStream },
|
actions: { sendOiCommand, setSensorStream },
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const canControl = Boolean(roverId);
|
const canControl = Boolean(roverId);
|
||||||
@@ -40,6 +61,7 @@ export default function SettingsPanel() {
|
|||||||
driveMacroBackoffEnabled: true,
|
driveMacroBackoffEnabled: true,
|
||||||
});
|
});
|
||||||
const { value: audioSettings, save: saveAudioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
|
const { value: audioSettings, save: saveAudioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
|
||||||
|
const { value: videoSettings, save: saveVideoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS);
|
||||||
const connectionTransport = pageSettings?.connectionTransport || 'websocket';
|
const connectionTransport = pageSettings?.connectionTransport || 'websocket';
|
||||||
const swapMobileControlColumns = Boolean(pageSettings?.swapMobileControlColumns);
|
const swapMobileControlColumns = Boolean(pageSettings?.swapMobileControlColumns);
|
||||||
const driveMacroBackoffEnabled =
|
const driveMacroBackoffEnabled =
|
||||||
@@ -58,6 +80,8 @@ export default function SettingsPanel() {
|
|||||||
const mainBrushDuckAmount = Number.isFinite(audioSettings?.mainBrushDuckAmount)
|
const mainBrushDuckAmount = Number.isFinite(audioSettings?.mainBrushDuckAmount)
|
||||||
? Math.max(0, Math.min(1, audioSettings.mainBrushDuckAmount))
|
? Math.max(0, Math.min(1, audioSettings.mainBrushDuckAmount))
|
||||||
: AUDIO_SETTINGS_DEFAULTS.mainBrushDuckAmount;
|
: AUDIO_SETTINGS_DEFAULTS.mainBrushDuckAmount;
|
||||||
|
const videoColorFilter = normalizeVideoFilter(videoSettings?.colorFilter);
|
||||||
|
const videoFilterCycleKeyLabel = formatKeyLabel(keymap?.videoFilterCycle?.[0]);
|
||||||
|
|
||||||
const sensorButtons = useMemo(
|
const sensorButtons = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -101,6 +125,18 @@ export default function SettingsPanel() {
|
|||||||
const checked = Boolean(event.target.checked);
|
const checked = Boolean(event.target.checked);
|
||||||
savePageSettings((current) => ({ ...(current ?? {}), driveMacroBackoffEnabled: checked }));
|
savePageSettings((current) => ({ ...(current ?? {}), driveMacroBackoffEnabled: checked }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVideoFilterChange = (event) => {
|
||||||
|
const nextFilter = normalizeVideoFilter(event.target.value);
|
||||||
|
|
||||||
|
// Merge into the current video namespace so future video preferences can coexist with this
|
||||||
|
// filter choice. This follows the same persisted-settings shape used by page/audio options.
|
||||||
|
saveVideoSettings((current) => ({
|
||||||
|
...(current ?? {}),
|
||||||
|
colorFilter: nextFilter,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultTab="keybindings">
|
<Tabs defaultTab="keybindings">
|
||||||
<TabList>
|
<TabList>
|
||||||
@@ -134,6 +170,34 @@ export default function SettingsPanel() {
|
|||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-slate-500">Mobile HUD keeps the map on by default.</p>
|
<p className="text-xs text-slate-500">Mobile HUD keeps the map on by default.</p>
|
||||||
</CardFrame>
|
</CardFrame>
|
||||||
|
<CardFrame title="Video" bodyClassName="space-y-0.5 text-sm">
|
||||||
|
{/* The dropdown mirrors the other Page settings controls and writes through the
|
||||||
|
existing cookie-backed settings provider, so the choice survives reloads without
|
||||||
|
adding rover-side state. */}
|
||||||
|
<label className="flex items-center justify-between gap-0.5 text-slate-200">
|
||||||
|
<span>Rover video filter</span>
|
||||||
|
<select
|
||||||
|
value={videoColorFilter}
|
||||||
|
onChange={handleVideoFilterChange}
|
||||||
|
className="field-input text-sm"
|
||||||
|
>
|
||||||
|
{VIDEO_FILTER_OPTIONS.map((option) => (
|
||||||
|
<option key={option.key} value={option.key}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
{/* Use the live keymap value here instead of hardcoding the default key because
|
||||||
|
this shortcut is user-configurable in the Keybindings tab. */}
|
||||||
|
<span className="inline-flex flex-wrap items-center gap-0.5">
|
||||||
|
<span>Use</span>
|
||||||
|
{videoFilterCycleKeyLabel ? <KeyPill label={videoFilterCycleKeyLabel} /> : null}
|
||||||
|
<span>to cycle filters. Applies only to rover camera pixels; HUD overlays stay full color.</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</CardFrame>
|
||||||
<CardFrame title="Mobile controls" bodyClassName="space-y-0.5 text-sm">
|
<CardFrame title="Mobile controls" bodyClassName="space-y-0.5 text-sm">
|
||||||
<label className="flex items-center gap-0.5 text-slate-200">
|
<label className="flex items-center gap-0.5 text-slate-200">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export const DEFAULT_KEYMAP = {
|
|||||||
cameraUp: ['u'],
|
cameraUp: ['u'],
|
||||||
cameraDown: ['j'],
|
cameraDown: ['j'],
|
||||||
nightVisionToggle: ['e'],
|
nightVisionToggle: ['e'],
|
||||||
|
videoFilterCycle: ['2'],
|
||||||
hornHonk: ['h'],
|
hornHonk: ['h'],
|
||||||
micPtt: ['m'],
|
micPtt: ['m'],
|
||||||
driveMacro: ['f'],
|
driveMacro: ['f'],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
|
|||||||
import { isKeyboardCaptureLocked } from './keyboardCaptureLock.js';
|
import { isKeyboardCaptureLocked } from './keyboardCaptureLock.js';
|
||||||
import { isTextInputElement } from './inputFocusUtils.js';
|
import { isTextInputElement } from './inputFocusUtils.js';
|
||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
import { INPUT_SETTINGS_DEFAULTS, VIDEO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||||
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
||||||
import {
|
import {
|
||||||
SONG_DEFAULT_DURATION,
|
SONG_DEFAULT_DURATION,
|
||||||
@@ -26,6 +26,23 @@ const TILT_INTERVAL_MIN = 5;
|
|||||||
const TILT_INTERVAL_MAX = 500;
|
const TILT_INTERVAL_MAX = 500;
|
||||||
const TILT_SPEED_MIN = 1;
|
const TILT_SPEED_MIN = 1;
|
||||||
const TILT_SPEED_MAX = 100;
|
const TILT_SPEED_MAX = 100;
|
||||||
|
const VIDEO_FILTER_SEQUENCE = ['none', 'grayscale', 'greenscale'];
|
||||||
|
|
||||||
|
function normalizeVideoFilter(value) {
|
||||||
|
// The setting is persisted in a browser cookie and can become stale if filter names change.
|
||||||
|
// Normalizing before cycling keeps the shortcut deterministic instead of getting stuck on an
|
||||||
|
// unknown value.
|
||||||
|
return VIDEO_FILTER_SEQUENCE.includes(value) ? value : VIDEO_SETTINGS_DEFAULTS.colorFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextVideoFilter(value) {
|
||||||
|
const current = normalizeVideoFilter(value);
|
||||||
|
const currentIndex = VIDEO_FILTER_SEQUENCE.indexOf(current);
|
||||||
|
|
||||||
|
// The modulo wrap intentionally makes the shortcut a simple single-key cycle:
|
||||||
|
// Color -> Gray -> Green -> Color. That is faster while driving than needing separate keys.
|
||||||
|
return VIDEO_FILTER_SEQUENCE[(currentIndex + 1) % VIDEO_FILTER_SEQUENCE.length];
|
||||||
|
}
|
||||||
|
|
||||||
function clampSpeed(value, fallback) {
|
function clampSpeed(value, fallback) {
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
@@ -142,6 +159,7 @@ export default function KeyboardInputManager() {
|
|||||||
const { homeAssistantSetState } = useSessionActions();
|
const { homeAssistantSetState } = useSessionActions();
|
||||||
const { focusChat, blurChat, isChatFocused } = useChat();
|
const { focusChat, blurChat, isChatFocused } = useChat();
|
||||||
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||||
|
const { save: saveVideoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS);
|
||||||
const keymap = useMemo(() => normalizeKeymapEntries(state.keymap), [state.keymap]);
|
const keymap = useMemo(() => normalizeKeymapEntries(state.keymap), [state.keymap]);
|
||||||
const actionTokens = useMemo(() => {
|
const actionTokens = useMemo(() => {
|
||||||
const tokens = new Set();
|
const tokens = new Set();
|
||||||
@@ -187,7 +205,6 @@ export default function KeyboardInputManager() {
|
|||||||
|
|
||||||
const driveFromKeys = useCallback(() => {
|
const driveFromKeys = useCallback(() => {
|
||||||
const tokensSnapshot = new Set(activeTokensRef.current);
|
const tokensSnapshot = new Set(activeTokensRef.current);
|
||||||
const boostActive = bindingActive(keymap.boostModifier, tokensSnapshot);
|
|
||||||
const slowActive = bindingActive(keymap.slowModifier, tokensSnapshot);
|
const slowActive = bindingActive(keymap.slowModifier, tokensSnapshot);
|
||||||
const speedOptions = slowActive
|
const speedOptions = slowActive
|
||||||
? { baseSpeed: keyboardSpeeds.precisionSpeed, boostSpeed: keyboardSpeeds.precisionSpeed }
|
? { baseSpeed: keyboardSpeeds.precisionSpeed, boostSpeed: keyboardSpeeds.precisionSpeed }
|
||||||
@@ -279,7 +296,7 @@ export default function KeyboardInputManager() {
|
|||||||
const finalNote = setSongNote(next);
|
const finalNote = setSongNote(next);
|
||||||
sendSong([{ note: finalNote, duration: SONG_DEFAULT_DURATION }], { slot: 0 });
|
sendSong([{ note: finalNote, duration: SONG_DEFAULT_DURATION }], { slot: 0 });
|
||||||
},
|
},
|
||||||
[sendSong, setSongNote, state.song?.note],
|
[sendSong, setSongNote, state.song],
|
||||||
);
|
);
|
||||||
|
|
||||||
const ensureSongLoop = useCallback(() => {
|
const ensureSongLoop = useCallback(() => {
|
||||||
@@ -351,6 +368,15 @@ export default function KeyboardInputManager() {
|
|||||||
[homeAssistantSetState, homeAssistant],
|
[homeAssistantSetState, homeAssistant],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const cycleVideoFilter = useCallback(() => {
|
||||||
|
// Update through the same settings namespace as the Page settings dropdown so the keyboard
|
||||||
|
// shortcut and menu never maintain separate copies of the selected filter.
|
||||||
|
saveVideoSettings((current) => ({
|
||||||
|
...(current ?? {}),
|
||||||
|
colorFilter: nextVideoFilter(current?.colorFilter),
|
||||||
|
}));
|
||||||
|
}, [saveVideoSettings]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
if (isKeyboardCaptureLocked()) return;
|
if (isKeyboardCaptureLocked()) return;
|
||||||
@@ -381,6 +407,8 @@ export default function KeyboardInputManager() {
|
|||||||
dockAssist.toggleAssist();
|
dockAssist.toggleAssist();
|
||||||
} else if (newlyPressed.some((token) => keymap.nightVisionToggle?.has(token))) {
|
} else if (newlyPressed.some((token) => keymap.nightVisionToggle?.has(token))) {
|
||||||
toggleNightVision();
|
toggleNightVision();
|
||||||
|
} else if (newlyPressed.some((token) => keymap.videoFilterCycle?.has(token))) {
|
||||||
|
cycleVideoFilter();
|
||||||
} else if (newlyPressed.some((token) => keymap.hornHonk?.has(token))) {
|
} else if (newlyPressed.some((token) => keymap.hornHonk?.has(token))) {
|
||||||
if (!hornActiveRef.current) {
|
if (!hornActiveRef.current) {
|
||||||
const started = startHorn();
|
const started = startHorn();
|
||||||
@@ -440,7 +468,11 @@ export default function KeyboardInputManager() {
|
|||||||
keymap.dockMacro,
|
keymap.dockMacro,
|
||||||
keymap.driveMacro,
|
keymap.driveMacro,
|
||||||
keymap.hornHonk,
|
keymap.hornHonk,
|
||||||
|
keymap.homeAssistantOff,
|
||||||
|
keymap.homeAssistantOn,
|
||||||
keymap.micPtt,
|
keymap.micPtt,
|
||||||
|
keymap.nightVisionToggle,
|
||||||
|
keymap.videoFilterCycle,
|
||||||
resetAll,
|
resetAll,
|
||||||
runMacro,
|
runMacro,
|
||||||
setMicPttActive,
|
setMicPttActive,
|
||||||
@@ -449,7 +481,9 @@ export default function KeyboardInputManager() {
|
|||||||
stopSongLoop,
|
stopSongLoop,
|
||||||
startHorn,
|
startHorn,
|
||||||
stopHorn,
|
stopHorn,
|
||||||
|
toggleNightVision,
|
||||||
triggerHomeAssistantCycle,
|
triggerHomeAssistantCycle,
|
||||||
|
cycleVideoFilter,
|
||||||
dockAssist,
|
dockAssist,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -100,3 +100,10 @@ export const AUDIO_SETTINGS_DEFAULTS = {
|
|||||||
mainBrushDuckEnabled: true,
|
mainBrushDuckEnabled: true,
|
||||||
mainBrushDuckAmount: 0.75,
|
mainBrushDuckAmount: 0.75,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const VIDEO_SETTINGS_DEFAULTS = {
|
||||||
|
// Keep the default as unfiltered color because most rovers still provide useful color
|
||||||
|
// information. The filter is an operator preference, so it belongs in persisted UI
|
||||||
|
// settings instead of being inferred from a rover stream or camera URL.
|
||||||
|
colorFilter: 'none',
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user