@elizaos/plugin-streaming 2.0.0-beta.1

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.
@@ -0,0 +1,531 @@
1
+ import { execSync, spawn } from "node:child_process";
2
+ import { logger } from "@elizaos/core";
3
+ import { ttsStreamBridge } from "./tts-stream-bridge.js";
4
+ const TAG = "[StreamManager]";
5
+ class StreamManager {
6
+ ffmpeg = null;
7
+ _running = false;
8
+ startedAt = null;
9
+ _frameCount = 0;
10
+ /** Current stream config — stored for restart on volume/audio changes. */
11
+ _config = null;
12
+ /** Current volume level (0–100). */
13
+ _volume = 80;
14
+ /** Whether audio is muted. */
15
+ _muted = false;
16
+ /** Auto-restart state. */
17
+ _restartAttempts = 0;
18
+ _maxRestartAttempts = 5;
19
+ _restartDecayTimer = null;
20
+ _intentionalStop = false;
21
+ /** Pending auto-restart timer — cleared in stop() to prevent races. */
22
+ _restartTimer = null;
23
+ /** Guard: prevents concurrent start() calls from orphaning FFmpeg. */
24
+ _starting = false;
25
+ isRunning() {
26
+ return this._running;
27
+ }
28
+ getUptime() {
29
+ if (!this.startedAt) return 0;
30
+ return Math.floor((Date.now() - this.startedAt) / 1e3);
31
+ }
32
+ getHealth() {
33
+ return {
34
+ running: this._running,
35
+ ffmpegAlive: this.ffmpeg !== null && this.ffmpeg.exitCode === null && !this.ffmpeg.killed,
36
+ uptime: this.getUptime(),
37
+ frameCount: this._frameCount,
38
+ volume: this._volume,
39
+ muted: this._muted,
40
+ audioSource: this._config?.audioSource || "silent",
41
+ inputMode: this._config?.inputMode || null
42
+ };
43
+ }
44
+ getVolume() {
45
+ return this._muted ? 0 : this._volume;
46
+ }
47
+ isMuted() {
48
+ return this._muted;
49
+ }
50
+ /**
51
+ * Set volume (0–100). Restarts FFmpeg if currently streaming to apply the change.
52
+ */
53
+ async setVolume(level) {
54
+ this._volume = Math.max(0, Math.min(100, Math.round(level)));
55
+ logger.info(`${TAG} Volume set to ${this._volume}`);
56
+ if (this._running && this._config) {
57
+ await this.restart();
58
+ }
59
+ }
60
+ /** Mute audio. Restarts FFmpeg if currently streaming. */
61
+ async mute() {
62
+ if (this._muted) return;
63
+ this._muted = true;
64
+ logger.info(`${TAG} Audio muted`);
65
+ if (this._running && this._config) {
66
+ await this.restart();
67
+ }
68
+ }
69
+ /** Unmute audio. Restarts FFmpeg if currently streaming. */
70
+ async unmute() {
71
+ if (!this._muted) return;
72
+ this._muted = false;
73
+ logger.info(`${TAG} Audio unmuted (volume: ${this._volume})`);
74
+ if (this._running && this._config) {
75
+ await this.restart();
76
+ }
77
+ }
78
+ /** Restart the stream with updated config (preserves uptime tracking). */
79
+ async restart() {
80
+ if (!this._config) return;
81
+ const savedStartedAt = this.startedAt;
82
+ const savedFrameCount = this._frameCount;
83
+ this._intentionalStop = true;
84
+ ttsStreamBridge.detach();
85
+ if (this.ffmpeg && !this.ffmpeg.killed && this.ffmpeg.exitCode === null) {
86
+ if (this.ffmpeg.stdin) {
87
+ try {
88
+ this.ffmpeg.stdin.end();
89
+ } catch {
90
+ }
91
+ }
92
+ this.ffmpeg.kill("SIGTERM");
93
+ await Promise.race([
94
+ new Promise((resolve) => this.ffmpeg?.on("exit", resolve)),
95
+ new Promise((resolve) => setTimeout(resolve, 2e3))
96
+ ]);
97
+ if (this.ffmpeg?.exitCode === null) {
98
+ this.ffmpeg.kill("SIGKILL");
99
+ }
100
+ }
101
+ this.ffmpeg = null;
102
+ this._running = false;
103
+ const config = {
104
+ ...this._config,
105
+ volume: this._volume,
106
+ muted: this._muted
107
+ };
108
+ this._intentionalStop = false;
109
+ await this.start(config);
110
+ this.startedAt = savedStartedAt;
111
+ this._frameCount = savedFrameCount;
112
+ logger.info(
113
+ `${TAG} Stream restarted (volume=${this._volume}, muted=${this._muted})`
114
+ );
115
+ }
116
+ /**
117
+ * Write a JPEG frame to FFmpeg's stdin (only works in "pipe" mode).
118
+ * Returns true if the frame was accepted.
119
+ */
120
+ writeFrame(jpegData) {
121
+ if (!this._running || !this.ffmpeg?.stdin) return false;
122
+ if (this.ffmpeg.killed || this.ffmpeg.exitCode !== null) return false;
123
+ try {
124
+ this.ffmpeg.stdin.write(jpegData);
125
+ this._frameCount++;
126
+ if (this._frameCount % 150 === 0) {
127
+ logger.info(`${TAG} Piped ${this._frameCount} frames to FFmpeg`);
128
+ }
129
+ return true;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+ async start(config) {
135
+ if (this._running || this._starting) {
136
+ logger.warn(`${TAG} Already running or starting \u2014 stop first`);
137
+ return;
138
+ }
139
+ this._starting = true;
140
+ try {
141
+ await this._startInner(config);
142
+ } finally {
143
+ this._starting = false;
144
+ }
145
+ }
146
+ async _startInner(config) {
147
+ try {
148
+ execSync("ffmpeg -version", { stdio: "ignore", timeout: 5e3 });
149
+ } catch {
150
+ const installHint = process.platform === "darwin" ? "Install with: brew install ffmpeg" : process.platform === "linux" ? "Install with: sudo apt install ffmpeg (or your distro's package manager)" : "Download from https://ffmpeg.org/download.html";
151
+ throw new Error(
152
+ `FFmpeg not found. Streaming requires FFmpeg to be installed.
153
+ ${installHint}`
154
+ );
155
+ }
156
+ this._config = config;
157
+ this._frameCount = 0;
158
+ this._volume = config.volume ?? this._volume;
159
+ this._muted = config.muted ?? this._muted;
160
+ const resolution = config.resolution || "1280x720";
161
+ const bitrate = config.bitrate || "2500k";
162
+ const framerate = config.framerate || 15;
163
+ const rtmpTarget = `${config.rtmpUrl}/${config.rtmpKey}`;
164
+ const bufsize = `${parseInt(bitrate, 10) * 2}k`;
165
+ const mode = config.inputMode || "testsrc";
166
+ const videoInputArgs = this.buildVideoInputArgs(
167
+ config,
168
+ resolution,
169
+ framerate
170
+ );
171
+ const audioInputArgs = this.buildAudioInputArgs(config);
172
+ const isPipe = mode === "pipe";
173
+ const isScreenCapture = mode === "avfoundation" || mode === "screen" || mode === "x11grab";
174
+ const effectiveVolume = this._muted ? 0 : this._volume / 100;
175
+ const ffmpegArgs = [
176
+ "-thread_queue_size",
177
+ "512",
178
+ // Video input
179
+ ...videoInputArgs,
180
+ // Audio input
181
+ ...audioInputArgs,
182
+ // Video filter: scale for screen capture modes
183
+ ...isScreenCapture ? ["-vf", `scale=${resolution.replace("x", ":")}:flags=fast_bilinear`] : [],
184
+ // Audio filter: volume control
185
+ "-af",
186
+ `volume=${effectiveVolume.toFixed(2)}`,
187
+ // Video encoding (platform-specific)
188
+ ...process.platform === "darwin" ? [
189
+ "-c:v",
190
+ "h264_videotoolbox",
191
+ "-realtime",
192
+ "1",
193
+ "-b:v",
194
+ bitrate,
195
+ "-maxrate",
196
+ bitrate,
197
+ "-bufsize",
198
+ bufsize
199
+ ] : [
200
+ "-c:v",
201
+ "libx264",
202
+ "-preset",
203
+ "veryfast",
204
+ "-tune",
205
+ "zerolatency",
206
+ "-b:v",
207
+ bitrate,
208
+ "-maxrate",
209
+ bitrate,
210
+ "-bufsize",
211
+ bufsize
212
+ ],
213
+ "-s",
214
+ resolution,
215
+ "-pix_fmt",
216
+ "yuv420p",
217
+ "-g",
218
+ "60",
219
+ // Audio encoding
220
+ "-c:a",
221
+ "aac",
222
+ "-b:a",
223
+ "128k",
224
+ // Output
225
+ "-f",
226
+ "flv",
227
+ rtmpTarget
228
+ ];
229
+ const audioSrc = config.audioSource || "silent";
230
+ logger.info(
231
+ `${TAG} Starting FFmpeg RTMP stream (video=${mode}, audio=${audioSrc}, vol=${this._volume}${this._muted ? " MUTED" : ""}) \u2192 ${config.rtmpUrl}`
232
+ );
233
+ logger.info(
234
+ `${TAG} Resolution: ${resolution}, Bitrate: ${bitrate}, FPS: ${framerate}`
235
+ );
236
+ const isTts = (config.audioSource || "silent") === "tts";
237
+ this.ffmpeg = spawn("ffmpeg", ["-y", ...ffmpegArgs], {
238
+ stdio: [
239
+ isPipe ? "pipe" : "ignore",
240
+ "pipe",
241
+ "pipe",
242
+ ...isTts ? ["pipe"] : []
243
+ ]
244
+ });
245
+ this.ffmpeg.stderr?.on("data", (chunk) => {
246
+ const line = chunk.toString().trim();
247
+ if (line) {
248
+ logger.debug(`[FFmpeg] ${line}`);
249
+ }
250
+ });
251
+ this.ffmpeg.on("exit", (code, signal) => {
252
+ if (this._running) {
253
+ logger.warn(
254
+ `${TAG} FFmpeg exited unexpectedly (code=${code}, signal=${signal})`
255
+ );
256
+ this._running = false;
257
+ if (!this._intentionalStop && this._config) {
258
+ this.autoRestart();
259
+ } else {
260
+ this.startedAt = null;
261
+ }
262
+ }
263
+ });
264
+ if (isPipe && this.ffmpeg.stdin) {
265
+ this.ffmpeg.stdin.on("error", (err) => {
266
+ logger.warn(`${TAG} FFmpeg stdin error: ${err.message}`);
267
+ });
268
+ }
269
+ if (isTts && this.ffmpeg.stdio[3]) {
270
+ const pipe3 = this.ffmpeg.stdio[3];
271
+ ttsStreamBridge.attach(pipe3);
272
+ logger.info(`${TAG} TTS bridge attached to pipe:3`);
273
+ }
274
+ await new Promise((r) => setTimeout(r, 1500));
275
+ if (this.ffmpeg.exitCode !== null) {
276
+ const exitCode = this.ffmpeg.exitCode;
277
+ this.ffmpeg = null;
278
+ throw new Error(`${TAG} FFmpeg exited immediately with code ${exitCode}`);
279
+ }
280
+ this._running = true;
281
+ this.startedAt = Date.now();
282
+ this._intentionalStop = false;
283
+ if (this._restartDecayTimer) clearInterval(this._restartDecayTimer);
284
+ this._restartDecayTimer = setInterval(() => {
285
+ if (this._restartAttempts > 0) {
286
+ this._restartAttempts = Math.max(0, this._restartAttempts - 1);
287
+ logger.info(
288
+ `${TAG} Restart counter decayed to ${this._restartAttempts}`
289
+ );
290
+ }
291
+ }, 3e4);
292
+ logger.info(`${TAG} FFmpeg streaming to RTMP \u2014 stream should be live`);
293
+ }
294
+ async stop() {
295
+ const uptime = this.getUptime();
296
+ const frames = this._frameCount;
297
+ ttsStreamBridge.detach();
298
+ if (this.ffmpeg && !this.ffmpeg.killed && this.ffmpeg.exitCode === null) {
299
+ const ffmpegProc = this.ffmpeg;
300
+ if (ffmpegProc.stdin) {
301
+ try {
302
+ ffmpegProc.stdin.end();
303
+ } catch {
304
+ }
305
+ }
306
+ ffmpegProc.kill("SIGTERM");
307
+ await Promise.race([
308
+ new Promise((resolve) => ffmpegProc.on("exit", resolve)),
309
+ new Promise((resolve) => setTimeout(resolve, 3e3))
310
+ ]);
311
+ if (ffmpegProc.exitCode === null) {
312
+ ffmpegProc.kill("SIGKILL");
313
+ }
314
+ }
315
+ this._intentionalStop = true;
316
+ if (this._restartTimer) {
317
+ clearTimeout(this._restartTimer);
318
+ this._restartTimer = null;
319
+ }
320
+ if (this._restartDecayTimer) {
321
+ clearInterval(this._restartDecayTimer);
322
+ this._restartDecayTimer = null;
323
+ }
324
+ this.ffmpeg = null;
325
+ this._running = false;
326
+ this.startedAt = null;
327
+ this._frameCount = 0;
328
+ this._restartAttempts = 0;
329
+ this._config = null;
330
+ logger.info(
331
+ `${TAG} Stream stopped (uptime: ${uptime}s, frames: ${frames})`
332
+ );
333
+ return { uptime };
334
+ }
335
+ /** Attempt to restart FFmpeg after unexpected exit with exponential backoff. */
336
+ autoRestart() {
337
+ if (this._restartAttempts >= this._maxRestartAttempts) {
338
+ logger.error(
339
+ `${TAG} Max restart attempts (${this._maxRestartAttempts}) reached \u2014 giving up`
340
+ );
341
+ this.startedAt = null;
342
+ if (this._restartDecayTimer) {
343
+ clearInterval(this._restartDecayTimer);
344
+ this._restartDecayTimer = null;
345
+ }
346
+ return;
347
+ }
348
+ this._restartAttempts++;
349
+ const delay = Math.min(1e3 * 2 ** (this._restartAttempts - 1), 6e4);
350
+ logger.info(
351
+ `${TAG} Auto-restart attempt ${this._restartAttempts}/${this._maxRestartAttempts} in ${delay}ms`
352
+ );
353
+ this._restartTimer = setTimeout(async () => {
354
+ this._restartTimer = null;
355
+ if (this._intentionalStop || !this._config) return;
356
+ const savedStartedAt = this.startedAt;
357
+ const savedFrameCount = this._frameCount;
358
+ try {
359
+ this.ffmpeg = null;
360
+ await this.start({
361
+ ...this._config,
362
+ volume: this._volume,
363
+ muted: this._muted
364
+ });
365
+ this.startedAt = savedStartedAt;
366
+ this._frameCount = savedFrameCount;
367
+ logger.info(`${TAG} Auto-restart successful`);
368
+ } catch (err) {
369
+ this._running = false;
370
+ logger.error(`${TAG} Auto-restart failed: ${String(err)}`);
371
+ if (!this._intentionalStop && this._config) {
372
+ this.autoRestart();
373
+ }
374
+ }
375
+ }, delay);
376
+ }
377
+ // ---------------------------------------------------------------------------
378
+ // Video input args
379
+ // ---------------------------------------------------------------------------
380
+ buildVideoInputArgs(config, resolution, framerate) {
381
+ const mode = config.inputMode || "testsrc";
382
+ switch (mode) {
383
+ case "pipe": {
384
+ return [
385
+ "-probesize",
386
+ "32",
387
+ "-analyzeduration",
388
+ "0",
389
+ "-f",
390
+ "image2pipe",
391
+ "-c:v",
392
+ "mjpeg",
393
+ "-framerate",
394
+ String(framerate),
395
+ "-i",
396
+ "pipe:0"
397
+ ];
398
+ }
399
+ case "avfoundation":
400
+ case "screen": {
401
+ const videoDevice = config.videoDevice || "3";
402
+ return [
403
+ "-f",
404
+ "avfoundation",
405
+ "-framerate",
406
+ String(framerate),
407
+ "-pixel_format",
408
+ "nv12",
409
+ "-capture_cursor",
410
+ "1",
411
+ "-i",
412
+ `${videoDevice}:none`
413
+ ];
414
+ }
415
+ case "x11grab": {
416
+ const display = config.display || ":99";
417
+ return [
418
+ "-f",
419
+ "x11grab",
420
+ "-video_size",
421
+ resolution,
422
+ "-framerate",
423
+ String(framerate),
424
+ "-draw_mouse",
425
+ "0",
426
+ "-i",
427
+ display
428
+ ];
429
+ }
430
+ case "file": {
431
+ const framePath = config.frameFile || "/tmp/eliza-stream-frame.jpg";
432
+ return [
433
+ "-probesize",
434
+ "32",
435
+ "-analyzeduration",
436
+ "0",
437
+ "-loop",
438
+ "1",
439
+ "-f",
440
+ "image2",
441
+ "-c:v",
442
+ "mjpeg",
443
+ "-framerate",
444
+ String(framerate),
445
+ "-i",
446
+ framePath
447
+ ];
448
+ }
449
+ default: {
450
+ return [
451
+ "-f",
452
+ "lavfi",
453
+ "-i",
454
+ `color=c=0x1a1a2e:s=${resolution}:r=${framerate}`
455
+ ];
456
+ }
457
+ }
458
+ }
459
+ // ---------------------------------------------------------------------------
460
+ // Audio input args
461
+ // ---------------------------------------------------------------------------
462
+ buildAudioInputArgs(config) {
463
+ const source = config.audioSource || "silent";
464
+ switch (source) {
465
+ case "tts": {
466
+ return [
467
+ "-use_wallclock_as_timestamps",
468
+ "1",
469
+ "-probesize",
470
+ "32",
471
+ "-analyzeduration",
472
+ "0",
473
+ "-thread_queue_size",
474
+ "512",
475
+ "-f",
476
+ "s16le",
477
+ "-ar",
478
+ "24000",
479
+ "-ac",
480
+ "1",
481
+ "-i",
482
+ "pipe:3"
483
+ ];
484
+ }
485
+ case "silent": {
486
+ return [
487
+ "-f",
488
+ "lavfi",
489
+ "-i",
490
+ "anullsrc=channel_layout=stereo:sample_rate=44100"
491
+ ];
492
+ }
493
+ case "system": {
494
+ if (process.platform === "darwin") {
495
+ const device2 = config.audioDevice || "0";
496
+ return ["-f", "avfoundation", "-i", `none:${device2}`];
497
+ }
498
+ const device = config.audioDevice || "default";
499
+ return ["-f", "pulse", "-i", device];
500
+ }
501
+ case "microphone": {
502
+ if (process.platform === "darwin") {
503
+ const device2 = config.audioDevice || "0";
504
+ return ["-f", "avfoundation", "-i", `none:${device2}`];
505
+ }
506
+ const device = config.audioDevice || "default";
507
+ return ["-f", "pulse", "-i", device];
508
+ }
509
+ default: {
510
+ if (source.startsWith("/") || source.startsWith("./")) {
511
+ return ["-stream_loop", "-1", "-i", source];
512
+ }
513
+ return [
514
+ "-f",
515
+ "lavfi",
516
+ "-i",
517
+ "anullsrc=channel_layout=stereo:sample_rate=44100"
518
+ ];
519
+ }
520
+ }
521
+ }
522
+ /** Get the TTS stream bridge for external speak triggers. */
523
+ getTtsBridge() {
524
+ return ttsStreamBridge;
525
+ }
526
+ }
527
+ const streamManager = new StreamManager();
528
+ export {
529
+ streamManager
530
+ };
531
+ //# sourceMappingURL=stream-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/services/stream-manager.ts"],"sourcesContent":["/**\n * Stream Manager — cross-platform RTMP streaming via FFmpeg.\n *\n * Supports multiple input modes:\n * - \"pipe\": Receives JPEG frames via writeFrame() → FFmpeg stdin (image2pipe).\n * Used for streaming desktop window contents captured by the host bridge.\n * - \"avfoundation\" / \"screen\": macOS native screen capture.\n * - \"x11grab\": Linux virtual display capture (Xvfb). Used for GPU-backed game streams.\n * - \"file\": Reads a continuously-updated JPEG file (browser-capture).\n * - \"testsrc\": Solid color test pattern (default fallback).\n *\n * Audio support:\n * - \"silent\": Synthetic silent audio (anullsrc) — default.\n * - \"system\": System/desktop audio capture.\n * - \"microphone\": Microphone input.\n * - File path: Play an audio file as stream audio.\n *\n * Volume control:\n * - setVolume(0-100), mute(), unmute() — restarts FFmpeg to apply.\n *\n * Usage:\n * import { streamManager } from \"./services/stream-manager\";\n * await streamManager.start({ rtmpUrl, rtmpKey, inputMode: \"pipe\" });\n * streamManager.writeFrame(jpegBuffer); // called from frame capture\n * streamManager.setVolume(50); // adjust volume mid-stream\n * await streamManager.stop();\n *\n * @module services/stream-manager\n */\n\nimport { type ChildProcess, execSync, spawn } from \"node:child_process\";\nimport { logger } from \"@elizaos/core\";\nimport { type ITtsStreamBridge, ttsStreamBridge } from \"./tts-stream-bridge.js\";\n\nconst TAG = \"[StreamManager]\";\n\nexport type AudioSource = \"silent\" | \"system\" | \"microphone\" | \"tts\";\n\nexport interface StreamConfig {\n rtmpUrl: string;\n rtmpKey: string;\n /** FFmpeg video input source. Defaults to \"testsrc\" (test pattern). */\n inputMode?:\n | \"testsrc\"\n | \"avfoundation\"\n | \"screen\"\n | \"pipe\"\n | \"file\"\n | \"x11grab\";\n /** avfoundation video device index (default \"3\" = Capture screen 0 on macOS) */\n videoDevice?: string;\n /** Path to JPEG frame file (for \"file\" input mode) */\n frameFile?: string;\n /** Resolution (default \"1280x720\") */\n resolution?: string;\n /** Video bitrate (default \"2500k\") */\n bitrate?: string;\n /** Frame rate (default 15) */\n framerate?: number;\n /** X11 display for x11grab mode (e.g., \":99\"). Default \":99\". */\n display?: string;\n /** Audio source. Default \"silent\" (anullsrc). Can also be an absolute file path. */\n audioSource?: AudioSource | string;\n /** Audio device identifier (platform-specific). For macOS avfoundation: device index. For Linux: pulse/alsa device name. */\n audioDevice?: string;\n /** Volume level 0–100. Default 80. Applied as FFmpeg audio filter. */\n volume?: number;\n /** Whether audio is muted. Default false. Overrides volume to 0 when true. */\n muted?: boolean;\n}\n\nclass StreamManager {\n private ffmpeg: ChildProcess | null = null;\n private _running = false;\n private startedAt: number | null = null;\n private _frameCount = 0;\n /** Current stream config — stored for restart on volume/audio changes. */\n private _config: StreamConfig | null = null;\n /** Current volume level (0–100). */\n private _volume = 80;\n /** Whether audio is muted. */\n private _muted = false;\n /** Auto-restart state. */\n private _restartAttempts = 0;\n private _maxRestartAttempts = 5;\n private _restartDecayTimer: ReturnType<typeof setInterval> | null = null;\n private _intentionalStop = false;\n /** Pending auto-restart timer — cleared in stop() to prevent races. */\n private _restartTimer: ReturnType<typeof setTimeout> | null = null;\n /** Guard: prevents concurrent start() calls from orphaning FFmpeg. */\n private _starting = false;\n\n isRunning(): boolean {\n return this._running;\n }\n\n getUptime(): number {\n if (!this.startedAt) return 0;\n return Math.floor((Date.now() - this.startedAt) / 1000);\n }\n\n getHealth() {\n return {\n running: this._running,\n ffmpegAlive:\n this.ffmpeg !== null &&\n this.ffmpeg.exitCode === null &&\n !this.ffmpeg.killed,\n uptime: this.getUptime(),\n frameCount: this._frameCount,\n volume: this._volume,\n muted: this._muted,\n audioSource: this._config?.audioSource || \"silent\",\n inputMode: this._config?.inputMode || null,\n };\n }\n\n getVolume(): number {\n return this._muted ? 0 : this._volume;\n }\n\n isMuted(): boolean {\n return this._muted;\n }\n\n /**\n * Set volume (0–100). Restarts FFmpeg if currently streaming to apply the change.\n */\n async setVolume(level: number): Promise<void> {\n this._volume = Math.max(0, Math.min(100, Math.round(level)));\n logger.info(`${TAG} Volume set to ${this._volume}`);\n if (this._running && this._config) {\n await this.restart();\n }\n }\n\n /** Mute audio. Restarts FFmpeg if currently streaming. */\n async mute(): Promise<void> {\n if (this._muted) return;\n this._muted = true;\n logger.info(`${TAG} Audio muted`);\n if (this._running && this._config) {\n await this.restart();\n }\n }\n\n /** Unmute audio. Restarts FFmpeg if currently streaming. */\n async unmute(): Promise<void> {\n if (!this._muted) return;\n this._muted = false;\n logger.info(`${TAG} Audio unmuted (volume: ${this._volume})`);\n if (this._running && this._config) {\n await this.restart();\n }\n }\n\n /** Restart the stream with updated config (preserves uptime tracking). */\n private async restart(): Promise<void> {\n if (!this._config) return;\n const savedStartedAt = this.startedAt;\n const savedFrameCount = this._frameCount;\n\n // Mark as intentional so the exit handler doesn't trigger autoRestart()\n // concurrently with our manual restart below.\n this._intentionalStop = true;\n\n // Detach TTS bridge before stopping FFmpeg\n ttsStreamBridge.detach();\n\n // Stop FFmpeg without resetting tracking\n if (this.ffmpeg && !this.ffmpeg.killed && this.ffmpeg.exitCode === null) {\n if (this.ffmpeg.stdin) {\n try {\n this.ffmpeg.stdin.end();\n } catch {\n /* ignore */\n }\n }\n this.ffmpeg.kill(\"SIGTERM\");\n await Promise.race([\n new Promise((resolve) => this.ffmpeg?.on(\"exit\", resolve)),\n new Promise((resolve) => setTimeout(resolve, 2000)),\n ]);\n if (this.ffmpeg?.exitCode === null) {\n this.ffmpeg.kill(\"SIGKILL\");\n }\n }\n this.ffmpeg = null;\n this._running = false;\n\n // Restart with current volume/mute applied\n const config = {\n ...this._config,\n volume: this._volume,\n muted: this._muted,\n };\n this._intentionalStop = false;\n await this.start(config);\n\n // Restore tracking\n this.startedAt = savedStartedAt;\n this._frameCount = savedFrameCount;\n logger.info(\n `${TAG} Stream restarted (volume=${this._volume}, muted=${this._muted})`,\n );\n }\n\n /**\n * Write a JPEG frame to FFmpeg's stdin (only works in \"pipe\" mode).\n * Returns true if the frame was accepted.\n */\n writeFrame(jpegData: Buffer): boolean {\n if (!this._running || !this.ffmpeg?.stdin) return false;\n if (this.ffmpeg.killed || this.ffmpeg.exitCode !== null) return false;\n\n try {\n this.ffmpeg.stdin.write(jpegData);\n this._frameCount++;\n if (this._frameCount % 150 === 0) {\n logger.info(`${TAG} Piped ${this._frameCount} frames to FFmpeg`);\n }\n return true;\n } catch {\n return false;\n }\n }\n\n async start(config: StreamConfig): Promise<void> {\n if (this._running || this._starting) {\n logger.warn(`${TAG} Already running or starting — stop first`);\n return;\n }\n this._starting = true;\n try {\n await this._startInner(config);\n } finally {\n this._starting = false;\n }\n }\n\n private async _startInner(config: StreamConfig): Promise<void> {\n // Pre-flight: ensure FFmpeg is installed\n try {\n execSync(\"ffmpeg -version\", { stdio: \"ignore\", timeout: 5000 });\n } catch {\n const installHint =\n process.platform === \"darwin\"\n ? \"Install with: brew install ffmpeg\"\n : process.platform === \"linux\"\n ? \"Install with: sudo apt install ffmpeg (or your distro's package manager)\"\n : \"Download from https://ffmpeg.org/download.html\";\n throw new Error(\n `FFmpeg not found. Streaming requires FFmpeg to be installed.\\n${installHint}`,\n );\n }\n\n this._config = config;\n this._frameCount = 0;\n this._volume = config.volume ?? this._volume;\n this._muted = config.muted ?? this._muted;\n\n const resolution = config.resolution || \"1280x720\";\n const bitrate = config.bitrate || \"2500k\";\n const framerate = config.framerate || 15;\n const rtmpTarget = `${config.rtmpUrl}/${config.rtmpKey}`;\n const bufsize = `${parseInt(bitrate, 10) * 2}k`;\n const mode = config.inputMode || \"testsrc\";\n\n // Build FFmpeg args based on input mode\n const videoInputArgs = this.buildVideoInputArgs(\n config,\n resolution,\n framerate,\n );\n const audioInputArgs = this.buildAudioInputArgs(config);\n const isPipe = mode === \"pipe\";\n const isScreenCapture =\n mode === \"avfoundation\" || mode === \"screen\" || mode === \"x11grab\";\n\n // Effective volume: 0 when muted, otherwise 0–1.0 scale\n const effectiveVolume = this._muted ? 0 : this._volume / 100;\n\n // FFmpeg arg order: all inputs first, then filters, then encoding/output\n const ffmpegArgs = [\n \"-thread_queue_size\",\n \"512\",\n // Video input\n ...videoInputArgs,\n // Audio input\n ...audioInputArgs,\n // Video filter: scale for screen capture modes\n ...(isScreenCapture\n ? [\"-vf\", `scale=${resolution.replace(\"x\", \":\")}:flags=fast_bilinear`]\n : []),\n // Audio filter: volume control\n \"-af\",\n `volume=${effectiveVolume.toFixed(2)}`,\n // Video encoding (platform-specific)\n ...(process.platform === \"darwin\"\n ? [\n \"-c:v\",\n \"h264_videotoolbox\",\n \"-realtime\",\n \"1\",\n \"-b:v\",\n bitrate,\n \"-maxrate\",\n bitrate,\n \"-bufsize\",\n bufsize,\n ]\n : [\n \"-c:v\",\n \"libx264\",\n \"-preset\",\n \"veryfast\",\n \"-tune\",\n \"zerolatency\",\n \"-b:v\",\n bitrate,\n \"-maxrate\",\n bitrate,\n \"-bufsize\",\n bufsize,\n ]),\n \"-s\",\n resolution,\n \"-pix_fmt\",\n \"yuv420p\",\n \"-g\",\n \"60\",\n // Audio encoding\n \"-c:a\",\n \"aac\",\n \"-b:a\",\n \"128k\",\n // Output\n \"-f\",\n \"flv\",\n rtmpTarget,\n ];\n\n const audioSrc = config.audioSource || \"silent\";\n logger.info(\n `${TAG} Starting FFmpeg RTMP stream (video=${mode}, audio=${audioSrc}, vol=${this._volume}${this._muted ? \" MUTED\" : \"\"}) → ${config.rtmpUrl}`,\n );\n logger.info(\n `${TAG} Resolution: ${resolution}, Bitrate: ${bitrate}, FPS: ${framerate}`,\n );\n\n const isTts = (config.audioSource || \"silent\") === \"tts\";\n\n // In pipe mode, FFmpeg reads from stdin; otherwise stdin is ignored.\n // TTS mode adds a 4th stdio fd (pipe:3) for raw PCM audio input.\n this.ffmpeg = spawn(\"ffmpeg\", [\"-y\", ...ffmpegArgs], {\n stdio: [\n isPipe ? \"pipe\" : \"ignore\",\n \"pipe\",\n \"pipe\",\n ...(isTts ? ([\"pipe\"] as const) : []),\n ],\n });\n\n // Log all FFmpeg stderr for debugging\n this.ffmpeg.stderr?.on(\"data\", (chunk: Buffer) => {\n const line = chunk.toString().trim();\n if (line) {\n logger.debug(`[FFmpeg] ${line}`);\n }\n });\n\n this.ffmpeg.on(\"exit\", (code, signal) => {\n if (this._running) {\n logger.warn(\n `${TAG} FFmpeg exited unexpectedly (code=${code}, signal=${signal})`,\n );\n this._running = false;\n if (!this._intentionalStop && this._config) {\n this.autoRestart();\n } else {\n this.startedAt = null;\n }\n }\n });\n\n // Handle stdin errors gracefully in pipe mode\n if (isPipe && this.ffmpeg.stdin) {\n this.ffmpeg.stdin.on(\"error\", (err) => {\n logger.warn(`${TAG} FFmpeg stdin error: ${err.message}`);\n });\n }\n\n // Attach TTS bridge to pipe:3 for PCM audio\n if (isTts && this.ffmpeg.stdio[3]) {\n const pipe3 = this.ffmpeg.stdio[3] as import(\"node:stream\").Writable;\n ttsStreamBridge.attach(pipe3);\n logger.info(`${TAG} TTS bridge attached to pipe:3`);\n }\n\n // Wait a moment to confirm it started\n await new Promise((r) => setTimeout(r, 1500));\n\n if (this.ffmpeg.exitCode !== null) {\n const exitCode = this.ffmpeg.exitCode;\n this.ffmpeg = null;\n throw new Error(`${TAG} FFmpeg exited immediately with code ${exitCode}`);\n }\n\n this._running = true;\n this.startedAt = Date.now();\n this._intentionalStop = false;\n // Decay restart counter every 30s of healthy running\n if (this._restartDecayTimer) clearInterval(this._restartDecayTimer);\n this._restartDecayTimer = setInterval(() => {\n if (this._restartAttempts > 0) {\n this._restartAttempts = Math.max(0, this._restartAttempts - 1);\n logger.info(\n `${TAG} Restart counter decayed to ${this._restartAttempts}`,\n );\n }\n }, 30_000);\n logger.info(`${TAG} FFmpeg streaming to RTMP — stream should be live`);\n }\n\n async stop(): Promise<{ uptime: number }> {\n const uptime = this.getUptime();\n const frames = this._frameCount;\n\n // Detach TTS bridge before killing FFmpeg\n ttsStreamBridge.detach();\n\n if (this.ffmpeg && !this.ffmpeg.killed && this.ffmpeg.exitCode === null) {\n const ffmpegProc = this.ffmpeg;\n // Close stdin first in pipe mode to signal EOF\n if (ffmpegProc.stdin) {\n try {\n ffmpegProc.stdin.end();\n } catch {\n /* ignore */\n }\n }\n ffmpegProc.kill(\"SIGTERM\");\n await Promise.race([\n new Promise((resolve) => ffmpegProc.on(\"exit\", resolve)),\n new Promise((resolve) => setTimeout(resolve, 3000)),\n ]);\n if (ffmpegProc.exitCode === null) {\n ffmpegProc.kill(\"SIGKILL\");\n }\n }\n\n this._intentionalStop = true;\n if (this._restartTimer) {\n clearTimeout(this._restartTimer);\n this._restartTimer = null;\n }\n if (this._restartDecayTimer) {\n clearInterval(this._restartDecayTimer);\n this._restartDecayTimer = null;\n }\n this.ffmpeg = null;\n this._running = false;\n this.startedAt = null;\n this._frameCount = 0;\n this._restartAttempts = 0;\n this._config = null;\n logger.info(\n `${TAG} Stream stopped (uptime: ${uptime}s, frames: ${frames})`,\n );\n return { uptime };\n }\n\n /** Attempt to restart FFmpeg after unexpected exit with exponential backoff. */\n private autoRestart(): void {\n if (this._restartAttempts >= this._maxRestartAttempts) {\n logger.error(\n `${TAG} Max restart attempts (${this._maxRestartAttempts}) reached — giving up`,\n );\n this.startedAt = null;\n if (this._restartDecayTimer) {\n clearInterval(this._restartDecayTimer);\n this._restartDecayTimer = null;\n }\n return;\n }\n\n this._restartAttempts++;\n const delay = Math.min(1000 * 2 ** (this._restartAttempts - 1), 60_000);\n logger.info(\n `${TAG} Auto-restart attempt ${this._restartAttempts}/${this._maxRestartAttempts} in ${delay}ms`,\n );\n\n this._restartTimer = setTimeout(async () => {\n this._restartTimer = null;\n if (this._intentionalStop || !this._config) return;\n\n const savedStartedAt = this.startedAt;\n const savedFrameCount = this._frameCount;\n\n try {\n this.ffmpeg = null;\n await this.start({\n ...this._config,\n volume: this._volume,\n muted: this._muted,\n });\n // Restore tracking so uptime is continuous\n this.startedAt = savedStartedAt;\n this._frameCount = savedFrameCount;\n logger.info(`${TAG} Auto-restart successful`);\n } catch (err) {\n this._running = false;\n logger.error(`${TAG} Auto-restart failed: ${String(err)}`);\n // start() failed before spawning FFmpeg — no exit event will fire,\n // so manually chain the next restart attempt if retries remain.\n if (!this._intentionalStop && this._config) {\n this.autoRestart();\n }\n }\n }, delay);\n }\n\n // ---------------------------------------------------------------------------\n // Video input args\n // ---------------------------------------------------------------------------\n\n private buildVideoInputArgs(\n config: StreamConfig,\n resolution: string,\n framerate: number,\n ): string[] {\n const mode = config.inputMode || \"testsrc\";\n\n switch (mode) {\n case \"pipe\": {\n // Read JPEG frames from stdin via image2pipe.\n // -c:v mjpeg is mandatory: image2pipe cannot auto-detect JPEG from piped data.\n // -probesize/-analyzeduration eliminate the default 5MB probe buffer that\n // causes FFmpeg to stall for ~100 frames before decoding starts.\n return [\n \"-probesize\",\n \"32\",\n \"-analyzeduration\",\n \"0\",\n \"-f\",\n \"image2pipe\",\n \"-c:v\",\n \"mjpeg\",\n \"-framerate\",\n String(framerate),\n \"-i\",\n \"pipe:0\",\n ];\n }\n case \"avfoundation\":\n case \"screen\": {\n // macOS native screen capture via avfoundation.\n // videoDevice \"3\" = Capture screen 0; \":none\" = no audio from avfoundation.\n const videoDevice = config.videoDevice || \"3\";\n return [\n \"-f\",\n \"avfoundation\",\n \"-framerate\",\n String(framerate),\n \"-pixel_format\",\n \"nv12\",\n \"-capture_cursor\",\n \"1\",\n \"-i\",\n `${videoDevice}:none`,\n ];\n }\n case \"x11grab\": {\n // Linux virtual display capture (Xvfb) for GPU-backed game streams.\n // Requires: Xvfb :99 -screen 0 1280x720x24 -ac &\n // Then run a browser/TUI on display :99.\n const display = config.display || \":99\";\n return [\n \"-f\",\n \"x11grab\",\n \"-video_size\",\n resolution,\n \"-framerate\",\n String(framerate),\n \"-draw_mouse\",\n \"0\",\n \"-i\",\n display,\n ];\n }\n case \"file\": {\n // Read from a continuously-updated JPEG file (written by browser-capture).\n const framePath = config.frameFile || \"/tmp/eliza-stream-frame.jpg\";\n return [\n \"-probesize\",\n \"32\",\n \"-analyzeduration\",\n \"0\",\n \"-loop\",\n \"1\",\n \"-f\",\n \"image2\",\n \"-c:v\",\n \"mjpeg\",\n \"-framerate\",\n String(framerate),\n \"-i\",\n framePath,\n ];\n }\n default: {\n // Solid color test pattern (dark navy)\n return [\n \"-f\",\n \"lavfi\",\n \"-i\",\n `color=c=0x1a1a2e:s=${resolution}:r=${framerate}`,\n ];\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Audio input args\n // ---------------------------------------------------------------------------\n\n private buildAudioInputArgs(config: StreamConfig): string[] {\n const source = config.audioSource || \"silent\";\n\n switch (source) {\n case \"tts\": {\n // Raw PCM from TTS bridge via pipe:3 (4th stdio fd).\n // Format must match tts-stream-bridge output: s16le, 24kHz, mono.\n // -use_wallclock_as_timestamps 1: raw PCM has no timestamps, so FFmpeg\n // uses wall-clock time to sync with the video stream.\n // -probesize/-analyzeduration: eliminate probe buffering for immediate start.\n // -thread_queue_size: prevent queue overflow from high-frequency tick writes.\n return [\n \"-use_wallclock_as_timestamps\",\n \"1\",\n \"-probesize\",\n \"32\",\n \"-analyzeduration\",\n \"0\",\n \"-thread_queue_size\",\n \"512\",\n \"-f\",\n \"s16le\",\n \"-ar\",\n \"24000\",\n \"-ac\",\n \"1\",\n \"-i\",\n \"pipe:3\",\n ];\n }\n case \"silent\": {\n // Synthetic silent audio — always works, no hardware required.\n return [\n \"-f\",\n \"lavfi\",\n \"-i\",\n \"anullsrc=channel_layout=stereo:sample_rate=44100\",\n ];\n }\n case \"system\": {\n // System/desktop audio capture.\n if (process.platform === \"darwin\") {\n // macOS: requires BlackHole or similar virtual audio device.\n // audioDevice is the avfoundation audio device index (e.g., \"2\").\n const device = config.audioDevice || \"0\";\n return [\"-f\", \"avfoundation\", \"-i\", `none:${device}`];\n }\n // Linux: PulseAudio monitor source captures desktop audio.\n const device = config.audioDevice || \"default\";\n return [\"-f\", \"pulse\", \"-i\", device];\n }\n case \"microphone\": {\n // Microphone input.\n if (process.platform === \"darwin\") {\n const device = config.audioDevice || \"0\";\n return [\"-f\", \"avfoundation\", \"-i\", `none:${device}`];\n }\n const device = config.audioDevice || \"default\";\n return [\"-f\", \"pulse\", \"-i\", device];\n }\n default: {\n // Treat as a file path — play audio file as stream audio.\n // Supports mp3, wav, ogg, flac, etc.\n if (source.startsWith(\"/\") || source.startsWith(\"./\")) {\n return [\"-stream_loop\", \"-1\", \"-i\", source];\n }\n // Fallback to silent if source is unrecognized.\n return [\n \"-f\",\n \"lavfi\",\n \"-i\",\n \"anullsrc=channel_layout=stereo:sample_rate=44100\",\n ];\n }\n }\n }\n\n /** Get the TTS stream bridge for external speak triggers. */\n getTtsBridge(): ITtsStreamBridge {\n return ttsStreamBridge;\n }\n}\n\n// Module singleton\nexport const streamManager = new StreamManager();\n"],"mappings":"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","names":["device"]}