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:
+79
-3
@@ -1,8 +1,9 @@
|
||||
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, dialog, shell } = require('electron')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const ffmpegManager = require('./ffmpeg')
|
||||
const deviceManager = require('./devices')
|
||||
const ffmpegManager = require('./ffmpeg')
|
||||
const deviceManager = require('./devices')
|
||||
const remoteClient = require('./remote')
|
||||
|
||||
// ─── Icon helpers ──────────────────────────────────────────────────────────────
|
||||
// Draw a pixel into an RGBA buffer
|
||||
@@ -124,7 +125,12 @@ function loadConfig() {
|
||||
} catch (e) {
|
||||
console.error('Failed to load config:', e)
|
||||
}
|
||||
return { streams: [], tolbek: { enabled: false, port: 5000, latency: 200 }, recording: { folder: '', segmentMinutes: 60 } }
|
||||
return {
|
||||
streams: [],
|
||||
tolbek: { enabled: false, port: 5000, latency: 200 },
|
||||
recording: { folder: '', segmentMinutes: 60 },
|
||||
remote: { enabled: false, server: '', port: '', token: require('./remote').RemoteClient.generateToken(), machineName: '' }
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig(config) {
|
||||
@@ -336,6 +342,29 @@ ipcMain.handle('open-folder', async (_, folderPath) => {
|
||||
try { await shell.openPath(folderPath) } catch {}
|
||||
})
|
||||
|
||||
// ─── Remote control IPC ───────────────────────────────────────────────────────
|
||||
ipcMain.handle('remote-connect', async (_, { server, port, token, machineName }) => {
|
||||
try {
|
||||
remoteClient.connect({ server, port, token, machineName })
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('remote-disconnect', () => {
|
||||
remoteClient.disconnect()
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
ipcMain.handle('remote-get-url', (_, { server, port }) => {
|
||||
return require('./remote').buildWsUrl(server, port)
|
||||
})
|
||||
|
||||
ipcMain.handle('generate-token', () => {
|
||||
return require('./remote').RemoteClient.generateToken()
|
||||
})
|
||||
|
||||
ipcMain.handle('minimize-window', () => {
|
||||
mainWindow && mainWindow.minimize()
|
||||
})
|
||||
@@ -367,6 +396,53 @@ ffmpegManager.on('reconnecting', (data) => {
|
||||
mainWindow && mainWindow.webContents.send('stream-reconnecting', data)
|
||||
})
|
||||
|
||||
// ─── Remote client events ─────────────────────────────────────────────────────
|
||||
remoteClient.on('status', (data) => {
|
||||
mainWindow && mainWindow.webContents.send('remote-status', data)
|
||||
})
|
||||
|
||||
// Server requested device list → fetch and send back
|
||||
remoteClient.on('request_devices', async () => {
|
||||
try {
|
||||
const devices = await deviceManager.getDevices()
|
||||
remoteClient.sendDevices(devices)
|
||||
} catch {}
|
||||
})
|
||||
|
||||
// Server sent a command → execute locally
|
||||
remoteClient.on('command', async ({ action, config, id, changes }) => {
|
||||
try {
|
||||
if (action === 'start_stream') {
|
||||
// Update UI first
|
||||
mainWindow && mainWindow.webContents.send('remote-command', { action: 'start_stream', config })
|
||||
const result = await ffmpegManager.startStream(config)
|
||||
updateTrayMenu(ffmpegManager.getActiveCount())
|
||||
remoteClient.sendStreamStatus(config.id, 'running')
|
||||
} else if (action === 'stop_stream') {
|
||||
ffmpegManager.stopStream(id)
|
||||
updateTrayMenu(ffmpegManager.getActiveCount())
|
||||
remoteClient.sendStreamStatus(id, 'idle')
|
||||
mainWindow && mainWindow.webContents.send('remote-command', { action: 'stop_stream', id })
|
||||
} else if (action === 'update_stream') {
|
||||
mainWindow && mainWindow.webContents.send('remote-command', { action: 'update_stream', id, changes })
|
||||
} else if (action === 'stop_all') {
|
||||
ffmpegManager.stopAllStreams()
|
||||
updateTrayMenu(0)
|
||||
mainWindow && mainWindow.webContents.send('all-streams-stopped')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Remote] Command error:', e)
|
||||
}
|
||||
})
|
||||
|
||||
// Forward stream events to remote server
|
||||
ffmpegManager.on('ended', (data) => {
|
||||
remoteClient.sendStreamStatus(data.id, 'idle')
|
||||
})
|
||||
ffmpegManager.on('error', (data) => {
|
||||
remoteClient.sendStreamStatus(data.id, 'error')
|
||||
})
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
createTray()
|
||||
|
||||
Reference in New Issue
Block a user