feat: v1.2.0 — auto-reconnect, recording, monitor bounds, WASAPI loopback

- Auto-reconnect: stream retries with exponential backoff (2s→30s max)
  on unexpected exit; stops only when user clicks Stop
- Recording: global RecordingBar with folder picker, segment duration (min),
  per-stream checkbox, Select All; uses FFmpeg tee muxer for simultaneous
  SRT + segmented .ts file output
- Monitor capture: PowerShell System.Windows.Forms.Screen gives exact
  pixel bounds (x/y/width/height); per-monitor deviceName is now unique
  (desktop_monitor_N); gdigrab uses -offset_x/-offset_y/-video_size
- Window capture: windowTitle now explicitly propagated in handleVideoSelect
- System audio: added WASAPI Loopback option (no VB-Audio required),
  existing virtual-audio-capturer kept as fallback
- Reconnecting event forwarded to renderer; status shows "Reconnecting…"
  with attempt count in logs
- IPC: pick-folder (dialog.showOpenDialog) and open-folder (shell.openPath)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
admin
2026-04-22 12:10:39 +03:00
parent 6b0c2ca0ae
commit acbd3b6349
11 changed files with 659 additions and 264 deletions
+22 -7
View File
@@ -1,4 +1,4 @@
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, dialog } = require('electron')
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, dialog, shell } = require('electron')
const path = require('path')
const fs = require('fs')
const ffmpegManager = require('./ffmpeg')
@@ -124,7 +124,7 @@ function loadConfig() {
} catch (e) {
console.error('Failed to load config:', e)
}
return { streams: [], tolbek: { enabled: false, port: 5000, latency: 200 } }
return { streams: [], tolbek: { enabled: false, port: 5000, latency: 200 }, recording: { folder: '', segmentMinutes: 60 } }
}
function saveConfig(config) {
@@ -323,6 +323,19 @@ ipcMain.handle('get-active-streams', () => {
return ffmpegManager.getActiveStreams()
})
ipcMain.handle('pick-folder', async () => {
const result = await dialog.showOpenDialog(mainWindow || undefined, {
title: 'Выберите папку для записи',
properties: ['openDirectory', 'createDirectory']
})
if (result.canceled || !result.filePaths.length) return null
return result.filePaths[0]
})
ipcMain.handle('open-folder', async (_, folderPath) => {
try { await shell.openPath(folderPath) } catch {}
})
ipcMain.handle('minimize-window', () => {
mainWindow && mainWindow.minimize()
})
@@ -335,21 +348,23 @@ ipcMain.on('update-tray-count', (_, count) => {
updateTrayMenu(count)
})
// Forward FFmpeg log events to renderer
// Forward FFmpeg 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)
updateTrayMenu(ffmpegManager.getActiveCount())
})
ffmpegManager.on('ended', (data) => {
mainWindow && mainWindow.webContents.send('stream-ended', data)
const active = ffmpegManager.getActiveCount()
updateTrayMenu(active)
updateTrayMenu(ffmpegManager.getActiveCount())
})
ffmpegManager.on('reconnecting', (data) => {
mainWindow && mainWindow.webContents.send('stream-reconnecting', data)
})
app.whenReady().then(() => {