const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, dialog } = require('electron') const path = require('path') const fs = require('fs') const ffmpegManager = require('./ffmpeg') const deviceManager = require('./devices') // Inline tray icon as SVG → base64 DataURL (no external file needed) function makeTrayIcon() { const size = 16 const svg = ` ` const dataUrl = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}` const img = nativeImage.createFromDataURL(dataUrl) if (process.platform === 'darwin') { img.setTemplateImage(true) } return img } // Inline icon for main window (32x32) function makeWindowIcon() { const svg = ` ` const dataUrl = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}` return nativeImage.createFromDataURL(dataUrl) } const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged let mainWindow = null let tray = null let isQuitting = false // Persistent config storage const configPath = path.join(app.getPath('userData'), 'config.json') function loadConfig() { try { if (fs.existsSync(configPath)) { return JSON.parse(fs.readFileSync(configPath, 'utf8')) } } catch (e) { console.error('Failed to load config:', e) } return { streams: [], tolbek: { enabled: false, port: 5000, latency: 200 } } } function saveConfig(config) { try { fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8') } catch (e) { console.error('Failed to save config:', e) } } function createWindow() { mainWindow = new BrowserWindow({ width: 1100, height: 750, minWidth: 800, minHeight: 600, backgroundColor: '#1a1a2e', frame: false, titleBarStyle: 'hidden', titleBarOverlay: { color: '#16213e', symbolColor: '#e0e0e0', height: 36 }, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false }, icon: makeWindowIcon() }) if (isDev) { mainWindow.loadURL('http://localhost:5173') mainWindow.webContents.openDevTools({ mode: 'detach' }) } else { mainWindow.loadFile(path.join(__dirname, '../dist/index.html')) } // Hide to tray on close instead of quitting mainWindow.on('close', (event) => { if (!isQuitting) { event.preventDefault() mainWindow.hide() if (process.platform === 'darwin') { app.dock.hide() } } }) } function createTray() { const trayImage = makeTrayIcon() tray = new Tray(trayImage) tray.setToolTip('SRT Streamer') updateTrayMenu() tray.on('click', () => { if (mainWindow) { if (mainWindow.isVisible()) { mainWindow.focus() } else { showWindow() } } }) } function showWindow() { if (mainWindow) { mainWindow.show() mainWindow.focus() if (process.platform === 'darwin') { app.dock.show() } } } function updateTrayMenu(activeCount = 0) { if (!tray) return const statusLabel = activeCount > 0 ? `Active streams: ${activeCount}` : 'No active streams' const contextMenu = Menu.buildFromTemplate([ { label: 'SRT Streamer', enabled: false }, { label: statusLabel, enabled: false }, { type: 'separator' }, { label: 'Show', click: () => showWindow() }, { type: 'separator' }, { label: 'Stop All Streams', enabled: activeCount > 0, click: () => { ffmpegManager.stopAllStreams() mainWindow && mainWindow.webContents.send('all-streams-stopped') } }, { type: 'separator' }, { label: 'Quit', click: () => { const activeCount = ffmpegManager.getActiveCount() if (activeCount > 0) { const choice = dialog.showMessageBoxSync({ type: 'question', buttons: ['Quit', 'Cancel'], defaultId: 1, cancelId: 1, title: 'Quit SRT Streamer', message: `${activeCount} stream(s) are active.`, detail: 'All running streams will be stopped. Are you sure you want to quit?' }) if (choice !== 0) return } isQuitting = true ffmpegManager.stopAllStreams() app.quit() } } ]) tray.setContextMenu(contextMenu) } // IPC Handlers ipcMain.handle('get-config', () => loadConfig()) ipcMain.handle('save-config', (_, config) => { saveConfig(config); return true }) ipcMain.handle('get-devices', async () => { try { return await deviceManager.getDevices() } catch (e) { console.error('Device enumeration error:', e) return { video: [], audio: [] } } }) ipcMain.handle('get-windows', async () => { try { return await deviceManager.getWindows() } catch (e) { return [] } }) ipcMain.handle('start-stream', async (_, streamConfig) => { try { const result = await ffmpegManager.startStream(streamConfig) const active = ffmpegManager.getActiveCount() updateTrayMenu(active) mainWindow && mainWindow.webContents.send('stream-status', { id: streamConfig.id, status: 'running' }) return { success: true } } catch (e) { return { success: false, error: e.message } } }) ipcMain.handle('stop-stream', async (_, streamId) => { try { ffmpegManager.stopStream(streamId) const active = ffmpegManager.getActiveCount() updateTrayMenu(active) return { success: true } } catch (e) { return { success: false, error: e.message } } }) ipcMain.handle('start-tolbek', async (_, config) => { try { const result = await ffmpegManager.startTolbek(config) return { success: true } } catch (e) { return { success: false, error: e.message } } }) ipcMain.handle('stop-tolbek', async () => { try { ffmpegManager.stopTolbek() return { success: true } } catch (e) { return { success: false, error: e.message } } }) ipcMain.handle('get-active-streams', () => { return ffmpegManager.getActiveStreams() }) ipcMain.handle('minimize-window', () => { mainWindow && mainWindow.minimize() }) ipcMain.handle('hide-window', () => { mainWindow && mainWindow.hide() }) ipcMain.on('update-tray-count', (_, count) => { updateTrayMenu(count) }) // Forward FFmpeg log events to renderer ffmpegManager.on('log', (data) => { mainWindow && mainWindow.webContents.send('stream-log', data) }) ffmpegManager.on('error', (data) => { mainWindow && mainWindow.webContents.send('stream-error', data) const active = ffmpegManager.getActiveCount() updateTrayMenu(active) }) ffmpegManager.on('ended', (data) => { mainWindow && mainWindow.webContents.send('stream-ended', data) const active = ffmpegManager.getActiveCount() updateTrayMenu(active) }) app.whenReady().then(() => { createWindow() createTray() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } else { showWindow() } }) }) app.on('window-all-closed', (e) => { // Keep app running in tray if (process.platform !== 'darwin') { e.preventDefault() } }) app.on('before-quit', () => { isQuitting = true ffmpegManager.stopAllStreams() })