const { spawn, exec } = require('child_process') const path = require('path') const fs = require('fs') let ffmpegPath = null function getFfmpegPath() { if (ffmpegPath) return ffmpegPath const resourcesPath = process.resourcesPath || path.join(__dirname, '..') const candidates = [ path.join(resourcesPath, 'ffmpeg-bin', process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'), path.join(__dirname, '..', 'ffmpeg-bin', process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'), ] for (const p of candidates) { if (fs.existsSync(p)) { ffmpegPath = p return p } } ffmpegPath = process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg' return ffmpegPath } function runCommand(cmd, args) { return new Promise((resolve) => { const proc = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] }) let stdout = '' let stderr = '' proc.stdout.on('data', d => stdout += d) proc.stderr.on('data', d => stderr += d) proc.on('close', () => resolve({ stdout, stderr })) proc.on('error', () => resolve({ stdout: '', stderr: '' })) // Timeout setTimeout(() => { proc.kill(); resolve({ stdout, stderr }) }, 10000) }) } async function getWindowsDevices() { const ffmpeg = getFfmpegPath() const { stderr } = await runCommand(ffmpeg, [ '-list_devices', 'true', '-f', 'dshow', '-i', 'dummy' ]) const video = [] const audio = [] const lines = stderr.split('\n') let section = null for (const line of lines) { if (line.includes('"') && line.includes('(video)')) { section = 'video' const match = line.match(/"([^"]+)"\s*\(video\)/) if (match) { video.push({ name: match[1], deviceName: match[1], type: 'device' }) } } else if (line.includes('"') && line.includes('(audio)')) { section = 'audio' const match = line.match(/"([^"]+)"\s*\(audio\)/) if (match) { audio.push({ name: match[1], deviceName: match[1], type: 'device' }) } } } // Add system audio loopback devices (WASAPI) // These appear as "virtual-audio-capturer" or similar dshow devices // Also detect via registry / wmic audio.push({ name: 'System Audio (Loopback)', deviceName: 'virtual-audio-capturer', type: 'system', description: 'Capture all system audio output' }) // Add screen capture options const screenDevices = [ { name: 'Desktop (Full Screen)', type: 'desktop', deviceName: 'desktop' }, ] // Try to enumerate monitors const monitors = await getWindowsMonitors() monitors.forEach((m, i) => { screenDevices.push({ name: `Monitor ${i + 1}${m.label ? ': ' + m.label : ''}`, type: 'desktop', deviceName: `desktop`, monitorIndex: i }) }) return { video: [...screenDevices, ...video], audio } } async function getWindowsMonitors() { return new Promise((resolve) => { exec('wmic desktopmonitor get Name,ScreenHeight,ScreenWidth /format:list', (err, stdout) => { if (err) { resolve([]); return } const monitors = [] const blocks = stdout.split('\r\n\r\n').filter(b => b.trim()) for (const block of blocks) { const nameMatch = block.match(/Name=(.+)/) if (nameMatch) monitors.push({ label: nameMatch[1].trim() }) } resolve(monitors) }) setTimeout(() => resolve([]), 5000) }) } async function getMacDevices() { const ffmpeg = getFfmpegPath() const { stderr } = await runCommand(ffmpeg, [ '-f', 'avfoundation', '-list_devices', 'true', '-i', '' ]) const video = [] const audio = [] const lines = stderr.split('\n') let section = null for (const line of lines) { if (line.includes('AVFoundation video devices')) { section = 'video' continue } if (line.includes('AVFoundation audio devices')) { section = 'audio' continue } const match = line.match(/\[(\d+)\]\s+(.+)/) if (match) { const index = match[1] const name = match[2].trim() if (section === 'video') { const isScreen = name.toLowerCase().includes('screen') || name.toLowerCase().includes('capture') video.push({ name, deviceIndex: index, type: isScreen ? 'desktop' : 'device' }) } else if (section === 'audio') { audio.push({ name, deviceIndex: index, type: 'device' }) } } } return { video, audio } } async function getLinuxDevices() { const video = [] const audio = [] // V4L2 devices try { const v4l2Devices = await new Promise((resolve) => { exec('ls /dev/video* 2>/dev/null', (err, stdout) => { if (err) { resolve([]); return } resolve(stdout.trim().split('\n').filter(Boolean)) }) }) for (const dev of v4l2Devices) { const { stdout } = await runCommand('v4l2-ctl', ['--device', dev, '--info']) const nameMatch = stdout.match(/Card type\s*:\s*(.+)/) video.push({ name: nameMatch ? nameMatch[1].trim() : dev, devicePath: dev, type: 'device' }) } } catch (e) {} // Add screen capture const display = process.env.DISPLAY || ':0' video.unshift({ name: `Desktop (${display})`, type: 'desktop', devicePath: display }) // PulseAudio devices try { const { stdout: paList } = await runCommand('pactl', ['list', 'short', 'sources']) const lines = paList.split('\n').filter(Boolean) for (const line of lines) { const parts = line.split('\t') if (parts.length >= 2) { const deviceName = parts[1] const isMonitor = deviceName.includes('.monitor') audio.push({ name: deviceName, deviceName, type: isMonitor ? 'system' : 'device', description: isMonitor ? 'System audio (loopback)' : '' }) } } } catch (e) { audio.push({ name: 'default', deviceName: 'default', type: 'device' }) } return { video, audio } } async function getDevices() { let result if (process.platform === 'win32') { result = await getWindowsDevices() } else if (process.platform === 'darwin') { result = await getMacDevices() } else { result = await getLinuxDevices() } // Always add "None" options result.video.unshift({ name: '— No Video —', type: 'none', deviceName: 'none' }) result.audio.unshift({ name: '— No Audio —', type: 'none', deviceName: 'none' }) return result } async function getWindows() { if (process.platform === 'win32') { return getWindowsWindowsList() } else if (process.platform === 'darwin') { return getMacWindowsList() } else { return getLinuxWindowsList() } } function getWindowsWindowsList() { return new Promise((resolve) => { const script = ` $windows = Get-Process | Where-Object {$_.MainWindowTitle -ne ""} | Select-Object ProcessName, MainWindowTitle $windows | ForEach-Object { Write-Output "$($_.MainWindowTitle)" } ` exec(`powershell -Command "${script.replace(/\n/g, ' ')}"`, (err, stdout) => { if (err) { resolve([]); return } const windows = stdout.trim().split('\n') .map(w => w.trim()) .filter(Boolean) .map(title => ({ title, type: 'window' })) resolve(windows) }) }) } function getMacWindowsList() { return new Promise((resolve) => { exec(`osascript -e 'tell application "System Events" to get name of every process whose has UI elements is true'`, (err, stdout) => { if (err) { resolve([]); return } const apps = stdout.trim().split(', ') .filter(Boolean) .map(title => ({ title, type: 'window' })) resolve(apps) }) }) } function getLinuxWindowsList() { return new Promise((resolve) => { exec('wmctrl -l 2>/dev/null', (err, stdout) => { if (err) { resolve([]); return } const windows = stdout.trim().split('\n') .map(line => { const parts = line.split(/\s+/) const title = parts.slice(3).join(' ') return { title, type: 'window' } }) .filter(w => w.title) resolve(windows) }) }) } module.exports = { getDevices, getWindows }