feat: remote control agent — WebSocket client + Remote settings tab

- electron/remote.js: WebSocket client that connects to central server
  - Smart URL builder: domain+443→wss://, IP/non-443→ws://, http/https→ws/wss
  - UUID token auth on connect
  - Auto-reconnect with exponential backoff (3s→60s)
  - Handles commands: start_stream, stop_stream, update_stream, stop_all
  - Sends: devices, stream_status, logs
  - Ping/pong keepalive every 25s
  - Self-signed cert allowed for local IPs (192.168.x, 10.x, .local)
- electron/main.js: IPC handlers remote-connect/disconnect/get-url,
  generate-token; forwards commands to FFmpeg, status to renderer
- electron/preload.js: exposes remoteConnect, remoteDisconnect,
  remoteGetUrl, generateToken, onRemoteStatus, onRemoteCommand
- src/components/RemoteSettings.jsx: new UI tab
  - Server + port fields (default 443, auto wss/ws)
  - URL preview
  - Token display with copy + regenerate
  - Machine name field
  - Connection status bar with animated states
- src/App.jsx: Remote tab added, remote state persisted to config
- src/styles/App.css: remote status bar, token display, actions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
admin
2026-04-23 19:02:57 +03:00
parent e9a9e336da
commit 96b099d892
7 changed files with 667 additions and 5 deletions
+8
View File
@@ -22,6 +22,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
pickFolder: () => ipcRenderer.invoke('pick-folder'),
openFolder: (p) => ipcRenderer.invoke('open-folder', p),
// Remote control
remoteConnect: (cfg) => ipcRenderer.invoke('remote-connect', cfg),
remoteDisconnect: () => ipcRenderer.invoke('remote-disconnect'),
remoteGetUrl: (cfg) => ipcRenderer.invoke('remote-get-url', cfg),
generateToken: () => ipcRenderer.invoke('generate-token'),
// Window controls
minimizeWindow: () => ipcRenderer.invoke('minimize-window'),
hideWindow: () => ipcRenderer.invoke('hide-window'),
@@ -34,6 +40,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
onStreamEnded: (cb) => ipcRenderer.on('stream-ended', (_, d) => cb(d)),
onStreamReconnecting: (cb) => ipcRenderer.on('stream-reconnecting', (_, d) => cb(d)),
onAllStreamsStopped: (cb) => ipcRenderer.on('all-streams-stopped', () => cb()),
onRemoteStatus: (cb) => ipcRenderer.on('remote-status', (_, d) => cb(d)),
onRemoteCommand: (cb) => ipcRenderer.on('remote-command', (_, d) => cb(d)),
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
})