@elizaos/plugin-streaming 2.0.0-beta.1 → 2.0.11-beta.7
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 +100 -3
- package/dist/api/stream-persistence.d.ts +12 -17
- package/dist/api/stream-route-state.d.ts +2 -6
- package/dist/api/stream-routes.d.ts +15 -16
- package/dist/api/stream-routes.js.map +1 -1
- package/dist/api/streaming-text.d.ts +4 -6
- package/dist/api/streaming-text.js +1 -1
- package/dist/api/streaming-text.js.map +1 -1
- package/dist/api/streaming-types.d.ts +1 -2
- package/dist/api/tts-routes.d.ts +5 -7
- package/dist/api/tts-routes.js +127 -1
- package/dist/api/tts-routes.js.map +1 -1
- package/dist/core.d.ts +22 -26
- package/dist/core.js +34 -23
- package/dist/core.js.map +1 -1
- package/dist/index.d.ts +15 -21
- package/dist/services/stream-manager.d.ts +5 -9
- package/dist/services/stream-manager.js +3 -6
- package/dist/services/stream-manager.js.map +1 -1
- package/dist/services/tts-stream-bridge.d.ts +14 -14
- package/dist/services/tts-stream-bridge.js +110 -20
- package/dist/services/tts-stream-bridge.js.map +1 -1
- package/package.json +25 -13
package/README.md
CHANGED
|
@@ -1,7 +1,104 @@
|
|
|
1
1
|
# @elizaos/plugin-streaming
|
|
2
2
|
|
|
3
|
-
Unified RTMP streaming
|
|
3
|
+
Unified RTMP live streaming for Eliza agents. Supports Twitch, YouTube Live, X (Twitter), pump.fun, and custom or named RTMP ingest URLs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## What it does
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Adds a `STREAM` agent action so the agent can start/stop streams and check live status in response to natural-language requests ("go live on Twitch", "stop the YouTube stream", "is the X stream live?").
|
|
8
|
+
- Manages a local FFmpeg pipeline: screen/window capture, audio mixing, RTMP push.
|
|
9
|
+
- Supports Eliza Cloud relay mode: the cloud fans one inbound stream to N platform destinations using stored credentials.
|
|
10
|
+
- Provides a `streamStatus` context provider so the agent always knows which streams are live.
|
|
11
|
+
- Handles TTS-to-stream audio: agent speech is generated and piped directly into FFmpeg's audio track.
|
|
12
|
+
|
|
13
|
+
## Capabilities added
|
|
14
|
+
|
|
15
|
+
| Capability | Details |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `STREAM` action | `start`, `stop`, `status` for twitch / youtube / x / pumpfun |
|
|
18
|
+
| `streamStatus` provider | Per-turn JSON snapshot of all platform stream states |
|
|
19
|
+
| Video capture | pipe (desktop UI), avfoundation (macOS), x11grab (Linux/Xvfb), file (headless browser), testsrc |
|
|
20
|
+
| Audio sources | silent, system, microphone, tts (ElevenLabs / OpenAI / Edge / local inference), audio file |
|
|
21
|
+
| Overlay layouts | Per-destination JSON widget layout; seeded from plugin defaults on first start |
|
|
22
|
+
| Stream settings | Visual settings (theme, avatarIndex, voice) persisted in `data/stream/` |
|
|
23
|
+
| Cloud relay | Push to Eliza Cloud ingest; cloud relays to platform RTMP endpoints |
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- **FFmpeg** must be installed and on `PATH`. The plugin throws with install instructions if not found.
|
|
28
|
+
- macOS: `brew install ffmpeg`
|
|
29
|
+
- Linux: `sudo apt install ffmpeg`
|
|
30
|
+
- For TTS audio on streams: ElevenLabs, OpenAI, or Edge TTS API credentials (or a local inference model).
|
|
31
|
+
- For Eliza Cloud relay: `ELIZAOS_CLOUD_API_KEY` and cloud connection.
|
|
32
|
+
|
|
33
|
+
## Enabling the plugin
|
|
34
|
+
|
|
35
|
+
The plugin activates automatically when any streaming destination is configured. Add to your agent config under the `streaming` key:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"streaming": {
|
|
40
|
+
"twitch": { "streamKey": "live_..." },
|
|
41
|
+
"youtube": { "streamKey": "xxxx-xxxx-xxxx-xxxx" },
|
|
42
|
+
"customRtmp": { "rtmpUrl": "rtmp://ingest.example.com/live", "rtmpKey": "my-key" },
|
|
43
|
+
"rtmpSources": [
|
|
44
|
+
{ "id": "my-server", "name": "My Server", "rtmpUrl": "rtmp://...", "rtmpKey": "..." }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or via environment variables (all optional; any one enables the plugin):
|
|
51
|
+
|
|
52
|
+
| Env var | Purpose |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `TWITCH_STREAM_KEY` | Twitch RTMP stream key |
|
|
55
|
+
| `YOUTUBE_STREAM_KEY` | YouTube Live stream key |
|
|
56
|
+
| `YOUTUBE_RTMP_URL` | Override YouTube ingest URL |
|
|
57
|
+
| `X_STREAM_KEY` | X (Twitter) stream key |
|
|
58
|
+
| `X_RTMP_URL` | X RTMP ingest URL |
|
|
59
|
+
| `PUMPFUN_STREAM_KEY` | pump.fun stream key |
|
|
60
|
+
| `PUMPFUN_RTMP_URL` | pump.fun RTMP ingest URL |
|
|
61
|
+
| `CUSTOM_RTMP_URL` | Custom ingest URL |
|
|
62
|
+
| `CUSTOM_RTMP_KEY` | Custom stream key |
|
|
63
|
+
|
|
64
|
+
## Cloud relay vs. direct push
|
|
65
|
+
|
|
66
|
+
Set `<PLATFORM>_STREAMING_BACKEND` to control backend selection:
|
|
67
|
+
|
|
68
|
+
- `direct` — push to the platform's RTMP ingest using a local stream key (default when a key is set).
|
|
69
|
+
- `cloud` — request a per-session relay from Eliza Cloud; requires `ELIZAOS_CLOUD_API_KEY`.
|
|
70
|
+
- `auto` (default) — picks `cloud` when Eliza Cloud is connected and no local key is set; otherwise `direct`.
|
|
71
|
+
|
|
72
|
+
## Stream key setup
|
|
73
|
+
|
|
74
|
+
Stream keys come from each platform's studio or dashboard — there is no OAuth in this package.
|
|
75
|
+
|
|
76
|
+
- Twitch: https://dashboard.twitch.tv/u/YOUR_USERNAME/settings/stream
|
|
77
|
+
- YouTube: https://studio.youtube.com → Go Live → Stream settings
|
|
78
|
+
- X (Twitter): https://studio.twitter.com → Go Live
|
|
79
|
+
- pump.fun: platform stream dashboard
|
|
80
|
+
|
|
81
|
+
## Preset destination factories
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import {
|
|
85
|
+
createTwitchDestination,
|
|
86
|
+
createYoutubeDestination,
|
|
87
|
+
createXStreamDestination,
|
|
88
|
+
createPumpfunDestination,
|
|
89
|
+
createCustomRtmpDestination,
|
|
90
|
+
createNamedRtmpDestination,
|
|
91
|
+
} from "@elizaos/plugin-streaming";
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Pass a factory result to the streaming pipeline's destination map when constructing `StreamRouteState`.
|
|
95
|
+
|
|
96
|
+
## Default export
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import streamingPlugin from "@elizaos/plugin-streaming";
|
|
100
|
+
// or
|
|
101
|
+
import { streamingPlugin } from "@elizaos/plugin-streaming";
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`streamingPlugin` is the `Plugin` object to register with an elizaOS agent runtime.
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { StreamingDestination } from '../core.js';
|
|
2
|
-
import '@elizaos/core';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* Stream persistence layer — overlay layout and visual/voice settings I/O.
|
|
6
3
|
*
|
|
@@ -10,47 +7,47 @@ import '@elizaos/core';
|
|
|
10
7
|
*
|
|
11
8
|
* @module api/stream-persistence
|
|
12
9
|
*/
|
|
13
|
-
|
|
14
|
-
interface StreamVoiceSettings {
|
|
10
|
+
import type { StreamingDestination } from "./streaming-types.js";
|
|
11
|
+
export interface StreamVoiceSettings {
|
|
15
12
|
enabled: boolean;
|
|
16
13
|
provider?: string;
|
|
17
14
|
autoSpeak?: boolean;
|
|
18
15
|
}
|
|
19
|
-
interface StreamVisualSettings {
|
|
16
|
+
export interface StreamVisualSettings {
|
|
20
17
|
theme?: string;
|
|
21
18
|
avatarIndex?: number;
|
|
22
19
|
voice?: StreamVoiceSettings;
|
|
23
20
|
}
|
|
24
21
|
/** Sanitize destination ID for use as a filename segment. */
|
|
25
|
-
declare function safeDestId(id: string): string;
|
|
22
|
+
export declare function safeDestId(id: string): string;
|
|
26
23
|
/** Extract `?destination=<id>` from the raw request URL. */
|
|
27
|
-
declare function parseDestinationQuery(url?: string): string | undefined;
|
|
24
|
+
export declare function parseDestinationQuery(url?: string): string | undefined;
|
|
28
25
|
/**
|
|
29
26
|
* Read overlay layout for a destination.
|
|
30
27
|
* Falls back: destination-specific -> global -> plugin default -> null.
|
|
31
28
|
*/
|
|
32
|
-
declare function readOverlayLayout(destinationId?: string | null, destination?: StreamingDestination): unknown;
|
|
29
|
+
export declare function readOverlayLayout(destinationId?: string | null, destination?: StreamingDestination): unknown;
|
|
33
30
|
/** Write overlay layout (to destination-specific or global file). */
|
|
34
|
-
declare function writeOverlayLayout(layout: unknown, destinationId?: string | null): void;
|
|
31
|
+
export declare function writeOverlayLayout(layout: unknown, destinationId?: string | null): void;
|
|
35
32
|
/**
|
|
36
33
|
* Seed the plugin's default overlay layout on first stream start.
|
|
37
34
|
* Only writes if no destination-specific layout file exists yet.
|
|
38
35
|
*/
|
|
39
|
-
declare function seedOverlayDefaults(destination: StreamingDestination): void;
|
|
36
|
+
export declare function seedOverlayDefaults(destination: StreamingDestination): void;
|
|
40
37
|
/**
|
|
41
38
|
* Validate and sanitize a raw settings object into a safe StreamVisualSettings.
|
|
42
39
|
* Only allows known keys with expected types — rejects everything else.
|
|
43
40
|
* Returns null with an error message if validation fails.
|
|
44
41
|
*/
|
|
45
|
-
declare function validateStreamSettings(raw: unknown): {
|
|
42
|
+
export declare function validateStreamSettings(raw: unknown): {
|
|
46
43
|
settings: StreamVisualSettings;
|
|
47
44
|
error?: undefined;
|
|
48
45
|
} | {
|
|
49
46
|
settings?: undefined;
|
|
50
47
|
error: string;
|
|
51
48
|
};
|
|
52
|
-
declare function readStreamSettings(): StreamVisualSettings;
|
|
53
|
-
declare function writeStreamSettings(settings: StreamVisualSettings): void;
|
|
49
|
+
export declare function readStreamSettings(): StreamVisualSettings;
|
|
50
|
+
export declare function writeStreamSettings(settings: StreamVisualSettings): void;
|
|
54
51
|
/**
|
|
55
52
|
* Build the visual config for browser-capture by merging:
|
|
56
53
|
* 1. Server-side stream-settings.json (authoritative)
|
|
@@ -58,11 +55,9 @@ declare function writeStreamSettings(settings: StreamVisualSettings): void;
|
|
|
58
55
|
*
|
|
59
56
|
* Reads the active destination's overlay layout when available.
|
|
60
57
|
*/
|
|
61
|
-
declare function getHeadlessCaptureConfig(destinationId?: string | null): {
|
|
58
|
+
export declare function getHeadlessCaptureConfig(destinationId?: string | null): {
|
|
62
59
|
overlayLayout?: string;
|
|
63
60
|
theme?: string;
|
|
64
61
|
avatarIndex?: number;
|
|
65
62
|
destinationId?: string;
|
|
66
63
|
};
|
|
67
|
-
|
|
68
|
-
export { type StreamVisualSettings, type StreamVoiceSettings, getHeadlessCaptureConfig, parseDestinationQuery, readOverlayLayout, readStreamSettings, safeDestId, seedOverlayDefaults, validateStreamSettings, writeOverlayLayout, writeStreamSettings };
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { StreamingDestination } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
interface StreamRouteState {
|
|
1
|
+
import type { StreamingDestination } from "./streaming-types.js";
|
|
2
|
+
export interface StreamRouteState {
|
|
5
3
|
streamManager: {
|
|
6
4
|
isRunning(): boolean;
|
|
7
5
|
writeFrame(buf: Buffer): boolean;
|
|
@@ -55,5 +53,3 @@ interface StreamRouteState {
|
|
|
55
53
|
*/
|
|
56
54
|
mirrorStreamAvatarToElizaConfig?: (avatarIndex: number) => void;
|
|
57
55
|
}
|
|
58
|
-
|
|
59
|
-
export type { StreamRouteState };
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import { StreamConfig } from '../services/stream-manager.js';
|
|
3
|
-
import { StreamRouteState } from './stream-route-state.js';
|
|
4
|
-
import { StreamingDestination } from '../core.js';
|
|
5
|
-
export { OverlayLayoutData } from '../core.js';
|
|
6
|
-
import '../services/tts-stream-bridge.js';
|
|
7
|
-
import 'node:stream';
|
|
8
|
-
import '@elizaos/core';
|
|
9
|
-
|
|
10
1
|
/**
|
|
11
2
|
* Generic streaming infrastructure routes.
|
|
12
3
|
*
|
|
@@ -16,9 +7,19 @@ import '@elizaos/core';
|
|
|
16
7
|
*
|
|
17
8
|
* Platform-specific credential fetching lives in destination adapters.
|
|
18
9
|
*/
|
|
19
|
-
|
|
10
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
11
|
+
import type { StreamConfig } from "../services/stream-manager.js";
|
|
12
|
+
import type { StreamRouteState } from "./stream-route-state.js";
|
|
13
|
+
import type { StreamingDestination } from "./streaming-types.js";
|
|
14
|
+
export type { StreamRouteState } from "./stream-route-state.js";
|
|
15
|
+
/**
|
|
16
|
+
* A streaming destination provides RTMP credentials and optional lifecycle
|
|
17
|
+
* hooks. Canonical definition lives in @elizaos/plugin-streaming; re-exported here
|
|
18
|
+
* so existing consumers keep working.
|
|
19
|
+
*/
|
|
20
|
+
export type { OverlayLayoutData, StreamingDestination, } from "./streaming-types.js";
|
|
20
21
|
/** Resolve the active streaming destination from the registry. */
|
|
21
|
-
declare function getActiveDestination(state: StreamRouteState): StreamingDestination | undefined;
|
|
22
|
+
export declare function getActiveDestination(state: StreamRouteState): StreamingDestination | undefined;
|
|
22
23
|
/**
|
|
23
24
|
* Detect the best capture mode for the current environment.
|
|
24
25
|
*
|
|
@@ -30,14 +31,12 @@ declare function getActiveDestination(state: StreamRouteState): StreamingDestina
|
|
|
30
31
|
* 5. Fallback -> "file" (Puppeteer CDP -> temp JPEG -> FFmpeg)
|
|
31
32
|
*/
|
|
32
33
|
/** @internal Exported for testing. */
|
|
33
|
-
declare function detectCaptureMode(): StreamConfig["inputMode"];
|
|
34
|
+
export declare function detectCaptureMode(): StreamConfig["inputMode"];
|
|
34
35
|
/**
|
|
35
36
|
* Try to start Xvfb on the specified display if not already running (Linux only).
|
|
36
37
|
* Returns true if display is available, false otherwise.
|
|
37
38
|
*/
|
|
38
39
|
/** @internal Exported for testing. */
|
|
39
|
-
declare function ensureXvfb(display: string, resolution: string): Promise<boolean>;
|
|
40
|
+
export declare function ensureXvfb(display: string, resolution: string): Promise<boolean>;
|
|
40
41
|
/** Returns `true` if handled, `false` to fall through. */
|
|
41
|
-
declare function handleStreamRoute(req: IncomingMessage, res: ServerResponse, pathname: string, method: string, state: StreamRouteState): Promise<boolean>;
|
|
42
|
-
|
|
43
|
-
export { StreamRouteState, StreamingDestination, detectCaptureMode, ensureXvfb, getActiveDestination, handleStreamRoute };
|
|
42
|
+
export declare function handleStreamRoute(req: IncomingMessage, res: ServerResponse, pathname: string, method: string, state: StreamRouteState): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/api/stream-routes.ts"],"sourcesContent":["/**\n * Generic streaming infrastructure routes.\n *\n * Shared pipeline for all streaming destinations (custom RTMP, Twitch,\n * YouTube, etc.): capture mode detection, Xvfb management, browser capture,\n * FFmpeg, frame routing, volume/mute.\n *\n * Platform-specific credential fetching lives in destination adapters.\n */\n\nimport fs from \"node:fs\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport {\n formatError,\n logger,\n readRequestBody,\n readRequestBodyBuffer,\n sendJson,\n sendJsonError,\n} from \"@elizaos/core\";\nimport type { StreamConfig } from \"../services/stream-manager.js\";\nimport {\n getHeadlessCaptureConfig,\n readStreamSettings,\n seedOverlayDefaults,\n validateStreamSettings,\n writeStreamSettings,\n} from \"./stream-persistence.js\";\nimport type { StreamRouteState } from \"./stream-route-state.js\";\nimport type { StreamingDestination } from \"./streaming-types.js\";\n\nexport type { StreamRouteState } from \"./stream-route-state.js\";\n\ninterface BrowserCaptureModule {\n FRAME_FILE: string;\n startBrowserCapture(config: {\n url: string;\n width?: number;\n height?: number;\n quality?: number;\n endpoint?: string;\n display?: string;\n headless?: boolean | \"new\";\n executablePath?: string;\n userDataDir?: string;\n browserArgs?: string[];\n }): Promise<void>;\n stopBrowserCapture(): Promise<void>;\n}\n\nconst PLUGIN_BROWSER_PACKAGE = \"@elizaos/plugin-browser\";\n\nasync function loadBrowserCapture(): Promise<BrowserCaptureModule> {\n return (await import(\n PLUGIN_BROWSER_PACKAGE\n )) as unknown as BrowserCaptureModule;\n}\n\n// ---------------------------------------------------------------------------\n// MJPEG frame store — shared state for GET /api/stream/screen\n// ---------------------------------------------------------------------------\n\n/**\n * Stores the most-recently received JPEG frame and pushes each new frame\n * to all active MJPEG subscribers (GET /api/stream/screen).\n *\n * Frames arrive via POST /api/stream/frame from:\n * - Electrobun screencapture module (JS canvas → JPEG)\n * - Legacy desktop screencapture bridges\n * - Any client POSTing raw JPEG bytes\n */\nconst MJPEG_BOUNDARY = \"elizaframe\";\n\nconst mjpegSubscribers = new Set<ServerResponse>();\nlet latestFrame: Buffer | null = null;\n\nfunction pushFrameToSubscribers(frame: Buffer): void {\n latestFrame = frame;\n if (mjpegSubscribers.size === 0) return;\n const header = `--${MJPEG_BOUNDARY}\\r\\nContent-Type: image/jpeg\\r\\nContent-Length: ${frame.length}\\r\\n\\r\\n`;\n const headerBuf = Buffer.from(header, \"ascii\");\n const trailer = Buffer.from(\"\\r\\n\", \"ascii\");\n const chunk = Buffer.concat([headerBuf, frame, trailer]);\n const failed: ServerResponse[] = [];\n for (const sub of mjpegSubscribers) {\n try {\n sub.write(chunk);\n } catch {\n failed.push(sub);\n }\n }\n for (const sub of failed) {\n mjpegSubscribers.delete(sub);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public interfaces\n// ---------------------------------------------------------------------------\n\n/**\n * A streaming destination provides RTMP credentials and optional lifecycle\n * hooks. Canonical definition lives in @elizaos/plugin-streaming; re-exported here\n * so existing consumers keep working.\n */\nexport type {\n OverlayLayoutData,\n StreamingDestination,\n} from \"./streaming-types.js\";\n\n/** Resolve the active streaming destination from the registry. */\nexport function getActiveDestination(\n state: StreamRouteState,\n): StreamingDestination | undefined {\n if (state.activeDestinationId) {\n return state.destinations.get(state.activeDestinationId);\n }\n // Fallback: first destination in map (backward compat for single-destination configs)\n const first = state.destinations.values().next();\n return first.done ? undefined : first.value;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction json(res: ServerResponse, data: unknown, status = 200): void {\n sendJson(res, data, status);\n}\n\nfunction error(res: ServerResponse, message: string, status: number): void {\n sendJsonError(res, message, status);\n}\n\n// ---------------------------------------------------------------------------\n// Capture mode detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detect the best capture mode for the current environment.\n *\n * Priority:\n * 1. STREAM_MODE env var (explicit override)\n * 2. Desktop screen capture bridge -> \"pipe\" (POST /api/stream/frame -> FFmpeg stdin)\n * 3. Linux with DISPLAY or Xvfb -> \"x11grab\" (GPU-backed game-stream approach)\n * 4. macOS -> \"avfoundation\" (native screen capture)\n * 5. Fallback -> \"file\" (Puppeteer CDP -> temp JPEG -> FFmpeg)\n */\n/** @internal Exported for testing. */\nexport function detectCaptureMode(): StreamConfig[\"inputMode\"] {\n const explicit = process.env.STREAM_MODE;\n if (explicit === \"ui\" || explicit === \"pipe\") return \"pipe\";\n if (explicit === \"x11grab\") return \"x11grab\";\n if (explicit === \"avfoundation\" || explicit === \"screen\")\n return \"avfoundation\";\n if (explicit === \"file\") return \"file\";\n\n // Desktop bridge -> pipe mode\n if (\"__elizaScreenCapture\" in (globalThis as Record<string, unknown>)) {\n return \"pipe\";\n }\n\n // Linux with a display -> x11grab (Xvfb or native X11)\n if (process.platform === \"linux\" && process.env.DISPLAY) return \"x11grab\";\n\n // macOS -> avfoundation screen capture\n if (process.platform === \"darwin\") return \"avfoundation\";\n\n // Fallback -> headless browser capture -> file mode\n return \"file\";\n}\n\n// ---------------------------------------------------------------------------\n// Xvfb management\n// ---------------------------------------------------------------------------\n\n/**\n * Try to start Xvfb on the specified display if not already running (Linux only).\n * Returns true if display is available, false otherwise.\n */\n/** @internal Exported for testing. */\nexport async function ensureXvfb(\n display: string,\n resolution: string,\n): Promise<boolean> {\n if (process.platform !== \"linux\") return false;\n\n // Validate display format to prevent command injection (must be :<digits>)\n if (!/^:\\d+$/.test(display)) {\n logger.warn(\n `[stream] Invalid display format: ${display} (expected :<number>)`,\n );\n return false;\n }\n\n // Validate resolution early so callers get a clear failure before we\n // touch the display or spawn processes.\n const [w, h] = resolution.split(\"x\");\n if (!w || !h || !/^\\d+$/.test(w) || !/^\\d+$/.test(h)) {\n logger.warn(`[stream] Invalid resolution for Xvfb: ${resolution}`);\n return false;\n }\n\n // Check if the display is already active\n if (process.env.DISPLAY === display) return true;\n\n try {\n const { execSync } = await import(\"node:child_process\");\n // Check if Xvfb is already running on this display\n try {\n execSync(`xdpyinfo -display ${display}`, {\n stdio: \"ignore\",\n timeout: 3000,\n });\n logger.info(`[stream] Xvfb already running on display ${display}`);\n return true;\n } catch {\n // Not running -- start it\n }\n const { spawn: spawnProc } = await import(\"node:child_process\");\n const xvfb = spawnProc(\n \"Xvfb\",\n [display, \"-screen\", \"0\", `${w}x${h}x24`, \"-ac\"],\n {\n stdio: \"ignore\",\n detached: true,\n },\n );\n xvfb.unref();\n\n // Wait for Xvfb to be ready\n await new Promise((r) => setTimeout(r, 1000));\n logger.info(`[stream] Started Xvfb on display ${display} (${resolution})`);\n process.env.DISPLAY = display;\n return true;\n } catch (err) {\n logger.warn(`[stream] Failed to start Xvfb: ${err}`);\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Streaming pipeline (destination-driven)\n// ---------------------------------------------------------------------------\n\n/**\n * Start the full streaming pipeline using the configured destination for\n * RTMP credentials. Handles capture mode detection, Xvfb, browser capture,\n * and FFmpeg configuration.\n */\nasync function startStreamPipeline(\n state: StreamRouteState,\n rtmpUrl: string,\n rtmpKey: string,\n): Promise<{ inputMode: string; audioSource: string }> {\n // Defense-in-depth: validate RTMP scheme before passing to FFmpeg\n if (!/^rtmps?:\\/\\//i.test(rtmpUrl)) {\n throw new Error(\"RTMP URL must use rtmp:// or rtmps:// scheme\");\n }\n\n // Seed plugin-default overlay layout on first stream start\n const activeDest = getActiveDestination(state);\n if (activeDest) {\n seedOverlayDefaults(activeDest);\n }\n const destId = activeDest?.id ?? null;\n\n const mode = detectCaptureMode();\n\n const audioSource = process.env.STREAM_AUDIO_SOURCE ?? \"silent\";\n const audioDevice = process.env.STREAM_AUDIO_DEVICE;\n const volume = parseInt(process.env.STREAM_VOLUME ?? \"80\", 10);\n const resolution = \"1280x720\";\n\n const baseConfig: StreamConfig = {\n rtmpUrl,\n rtmpKey,\n resolution,\n bitrate: \"1500k\",\n audioSource,\n audioDevice,\n volume,\n };\n\n switch (mode) {\n case \"pipe\": {\n // Desktop UI mode: FFmpeg reads frames from stdin via writeFrame().\n logger.info(\"[stream] Capture mode: pipe (desktop UI)\");\n await state.streamManager.start({\n ...baseConfig,\n inputMode: \"pipe\",\n framerate: 15,\n });\n\n // Auto-start desktop frame capture so the UI is streamed without\n // requiring a manual button click in the renderer.\n if (state.screenCapture && !state.screenCapture.isFrameCaptureActive()) {\n try {\n const captureOpts: {\n fps: number;\n quality: number;\n endpoint: string;\n gameUrl?: string;\n } = {\n fps: 15,\n quality: 70,\n endpoint: \"/api/stream/frame\",\n };\n if (\n state.activeStreamSource.type !== \"stream-tab\" &&\n state.activeStreamSource.url\n ) {\n captureOpts.gameUrl = state.activeStreamSource.url;\n }\n await state.screenCapture.startFrameCapture(captureOpts);\n logger.info(\"[stream] Auto-started desktop frame capture\");\n } catch (err) {\n logger.warn(`[stream] Failed to auto-start frame capture: ${err}`);\n }\n } else if (!state.screenCapture) {\n logger.warn(\n \"[stream] ScreenCaptureManager not available -- frame capture must be started manually\",\n );\n }\n break;\n }\n\n case \"x11grab\": {\n // Linux Xvfb mode: capture the virtual display for GPU-backed streams.\n const display = process.env.STREAM_DISPLAY ?? \":99\";\n logger.info(`[stream] Capture mode: x11grab (display ${display})`);\n\n // Ensure Xvfb is running\n await ensureXvfb(display, resolution);\n\n // Launch a browser on the virtual display so there's something to capture\n const captureUrl =\n state.captureUrl ??\n process.env.STREAM_CAPTURE_URL ??\n `http://127.0.0.1:${state.port ?? 2138}`;\n\n try {\n const { startBrowserCapture } = await loadBrowserCapture();\n // Browser capture in x11grab mode just opens the browser on the display --\n // we don't need the frame file since FFmpeg captures the display directly.\n await startBrowserCapture({\n url: captureUrl,\n width: 1280,\n height: 720,\n quality: 70,\n ...getHeadlessCaptureConfig(destId),\n });\n } catch (err) {\n logger.warn(`[stream] Browser launch on ${display} failed: ${err}`);\n }\n\n await state.streamManager.start({\n ...baseConfig,\n inputMode: \"x11grab\",\n display,\n framerate: 30,\n });\n break;\n }\n\n case \"avfoundation\": {\n // macOS native screen capture.\n const videoDevice = process.env.STREAM_VIDEO_DEVICE ?? \"3\";\n logger.info(\n `[stream] Capture mode: avfoundation (device ${videoDevice})`,\n );\n await state.streamManager.start({\n ...baseConfig,\n inputMode: \"avfoundation\",\n videoDevice,\n framerate: 30,\n });\n break;\n }\n\n default: {\n // Headless browser capture -> temp JPEG file -> FFmpeg file mode.\n const captureUrl =\n state.captureUrl ??\n process.env.STREAM_CAPTURE_URL ??\n `http://127.0.0.1:${state.port ?? 2138}`;\n\n logger.info(\n `[stream] Capture mode: file (browser capture -> ${captureUrl})`,\n );\n\n const { startBrowserCapture, FRAME_FILE } = await loadBrowserCapture();\n try {\n await startBrowserCapture({\n url: captureUrl,\n width: 1280,\n height: 720,\n quality: 70,\n ...getHeadlessCaptureConfig(destId),\n });\n // Wait for first frame file to be written\n await new Promise((resolve) => {\n const check = setInterval(() => {\n try {\n if (\n fs.existsSync(FRAME_FILE) &&\n fs.statSync(FRAME_FILE).size > 0\n ) {\n clearInterval(check);\n resolve(true);\n }\n } catch {\n // Frame file not yet ready -- poll again\n }\n }, 200);\n setTimeout(() => {\n clearInterval(check);\n resolve(false);\n }, 10_000);\n });\n } catch (captureErr) {\n logger.warn(`[stream] Browser capture failed: ${captureErr}`);\n }\n\n await state.streamManager.start({\n ...baseConfig,\n inputMode: \"file\",\n frameFile: FRAME_FILE,\n framerate: 30,\n });\n break;\n }\n }\n\n return { inputMode: mode || \"file\", audioSource };\n}\n\n// ---------------------------------------------------------------------------\n// Route handler\n// ---------------------------------------------------------------------------\n\n/** Returns `true` if handled, `false` to fall through. */\nexport async function handleStreamRoute(\n req: IncomingMessage,\n res: ServerResponse,\n pathname: string,\n method: string,\n state: StreamRouteState,\n): Promise<boolean> {\n // Fast-path: skip if not a stream route\n if (\n !pathname.startsWith(\"/api/stream/\") &&\n !pathname.startsWith(\"/api/streaming/\")\n ) {\n return false;\n }\n\n // ── POST /api/stream/frame -- pipe frames to StreamManager + MJPEG ──\n if (method === \"POST\" && pathname === \"/api/stream/frame\") {\n try {\n const buf = await readRequestBodyBuffer(req, {\n maxBytes: 2 * 1024 * 1024,\n });\n if (!buf || buf.length === 0) {\n error(res, \"Empty frame\", 400);\n return true;\n }\n // Always store frame for MJPEG monitoring (GET /api/stream/screen)\n pushFrameToSubscribers(buf);\n // Write to FFmpeg only when RTMP streaming is active\n if (state.streamManager.isRunning()) {\n state.streamManager.writeFrame(buf);\n }\n res.writeHead(200);\n res.end();\n } catch {\n error(res, \"Frame write failed\", 500);\n }\n return true;\n }\n\n // ── GET /api/stream/screen -- MJPEG live view (local + remote agents) ─\n // Serves a continuous multipart/x-mixed-replace stream of JPEG frames.\n // Works independently of RTMP streaming — frames arrive via POST /api/stream/frame.\n // Usage: <img src=\"http://agent-host:2138/api/stream/screen\" />\n if (method === \"GET\" && pathname === \"/api/stream/screen\") {\n res.writeHead(200, {\n \"Content-Type\": `multipart/x-mixed-replace; boundary=${MJPEG_BOUNDARY}`,\n \"Cache-Control\": \"no-store, no-cache\",\n Connection: \"close\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n\n mjpegSubscribers.add(res);\n\n // Send the latest cached frame immediately so there's no blank wait\n if (latestFrame) {\n const header = `--${MJPEG_BOUNDARY}\\r\\nContent-Type: image/jpeg\\r\\nContent-Length: ${latestFrame.length}\\r\\n\\r\\n`;\n res.write(\n Buffer.concat([\n Buffer.from(header, \"ascii\"),\n latestFrame,\n Buffer.from(\"\\r\\n\", \"ascii\"),\n ]),\n );\n }\n\n const cleanup = () => {\n mjpegSubscribers.delete(res);\n };\n req.on(\"close\", cleanup);\n req.on(\"error\", cleanup);\n res.on(\"close\", cleanup);\n res.on(\"error\", cleanup);\n\n // Keep the response open — frames are pushed as they arrive\n return true;\n }\n\n // ── POST /api/stream/live -- start stream via destination ────────────\n if (method === \"POST\" && pathname === \"/api/stream/live\") {\n if (state.streamManager.isRunning()) {\n const health = state.streamManager.getHealth();\n json(res, {\n ok: true,\n live: true,\n message: \"Already streaming\",\n ...health,\n });\n return true;\n }\n\n const dest = getActiveDestination(state);\n if (!dest) {\n error(res, \"No streaming destination configured\", 400);\n return true;\n }\n\n try {\n const { rtmpUrl, rtmpKey } = await dest.getCredentials();\n const { inputMode, audioSource } = await startStreamPipeline(\n state,\n rtmpUrl,\n rtmpKey,\n );\n await dest.onStreamStart?.();\n json(res, {\n ok: true,\n live: true,\n rtmpUrl,\n inputMode,\n audioSource,\n destination: dest.id,\n });\n } catch (err) {\n error(res, formatError(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/offline -- stop stream + notify destination ─────\n if (method === \"POST\" && pathname === \"/api/stream/offline\") {\n try {\n // Stop browser capture\n try {\n const { stopBrowserCapture } = await loadBrowserCapture();\n await stopBrowserCapture();\n } catch {\n // Browser capture may not have been started -- ignore\n }\n // Stop StreamManager\n if (state.streamManager.isRunning()) {\n await state.streamManager.stop();\n }\n // Notify destination\n try {\n await getActiveDestination(state)?.onStreamStop?.();\n } catch {\n // Destination notification failure is non-fatal\n }\n json(res, { ok: true, live: false });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/start -- backward-compat explicit RTMP start ────\n if (method === \"POST\" && pathname === \"/api/stream/start\") {\n try {\n const bodyStr = await readRequestBody(req);\n const body = typeof bodyStr === \"string\" ? JSON.parse(bodyStr) : bodyStr;\n const rtmpUrl = body?.rtmpUrl as string | undefined;\n const rtmpKey = body?.rtmpKey as string | undefined;\n\n if (!rtmpUrl || !rtmpKey) {\n error(res, \"rtmpUrl and rtmpKey are required\", 400);\n return true;\n }\n\n if (!/^rtmps?:\\/\\//i.test(rtmpUrl)) {\n error(res, \"rtmpUrl must use rtmp:// or rtmps:// scheme\", 400);\n return true;\n }\n\n // Validate FFmpeg parameters to prevent filter expression injection\n const VALID_INPUT_MODES = [\"testsrc\", \"avfoundation\", \"pipe\"] as const;\n const inputMode = body?.inputMode ?? \"testsrc\";\n if (!VALID_INPUT_MODES.includes(inputMode)) {\n error(\n res,\n `inputMode must be one of: ${VALID_INPUT_MODES.join(\", \")}`,\n 400,\n );\n return true;\n }\n\n const resolution = (body?.resolution as string) || \"1280x720\";\n if (!/^\\d{3,4}x\\d{3,4}$/.test(resolution)) {\n error(res, \"resolution must match WIDTHxHEIGHT (e.g. 1280x720)\", 400);\n return true;\n }\n\n const bitrate = (body?.bitrate as string) || \"2500k\";\n if (!/^\\d+k$/.test(bitrate)) {\n error(res, \"bitrate must match NUMBERk (e.g. 2500k)\", 400);\n return true;\n }\n\n const framerate = body?.framerate ?? 30;\n if (\n typeof framerate !== \"number\" ||\n !Number.isInteger(framerate) ||\n framerate < 1 ||\n framerate > 60\n ) {\n error(res, \"framerate must be an integer between 1 and 60\", 400);\n return true;\n }\n\n await state.streamManager.start({\n rtmpUrl,\n rtmpKey,\n inputMode,\n resolution,\n bitrate,\n framerate,\n });\n\n json(res, { ok: true, message: \"Stream started\" });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/stop -- backward-compat explicit stop ───────────\n if (method === \"POST\" && pathname === \"/api/stream/stop\") {\n try {\n const result = await state.streamManager.stop();\n json(res, { ok: true, ...result });\n } catch (err) {\n error(res, formatError(err), 500);\n }\n return true;\n }\n\n // ── GET /api/stream/status -- local stream health ────────────────────\n if (method === \"GET\" && pathname === \"/api/stream/status\") {\n const health = state.streamManager.getHealth();\n const activeDest = getActiveDestination(state);\n const destInfo = activeDest\n ? { id: activeDest.id, name: activeDest.name }\n : null;\n json(res, { ok: true, ...health, destination: destInfo });\n return true;\n }\n\n // ── POST /api/stream/volume -- set stream volume (0-100) ─────────────\n if (method === \"POST\" && pathname === \"/api/stream/volume\") {\n try {\n const body = await readRequestBody(req);\n const parsed = typeof body === \"string\" ? JSON.parse(body) : body;\n const level = parsed?.volume;\n if (\n typeof level !== \"number\" ||\n !Number.isFinite(level) ||\n level < 0 ||\n level > 100\n ) {\n error(res, \"volume must be a number between 0 and 100\", 400);\n return true;\n }\n await state.streamManager.setVolume(level);\n json(res, {\n ok: true,\n volume: state.streamManager.getVolume(),\n muted: state.streamManager.isMuted(),\n });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/mute -- mute stream audio ──────────────────────\n if (method === \"POST\" && pathname === \"/api/stream/mute\") {\n try {\n await state.streamManager.mute();\n json(res, {\n ok: true,\n muted: true,\n volume: state.streamManager.getVolume(),\n });\n } catch (err) {\n error(res, formatError(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/unmute -- unmute stream audio ───────────────────\n if (method === \"POST\" && pathname === \"/api/stream/unmute\") {\n try {\n await state.streamManager.unmute();\n json(res, {\n ok: true,\n muted: false,\n volume: state.streamManager.getVolume(),\n });\n } catch (err) {\n error(res, formatError(err), 500);\n }\n return true;\n }\n\n // ── GET /api/streaming/destinations -- list configured destination ───\n if (method === \"GET\" && pathname === \"/api/streaming/destinations\") {\n const destinations = Array.from(state.destinations.values()).map((d) => ({\n id: d.id,\n name: d.name,\n active:\n d.id ===\n (state.activeDestinationId ?? state.destinations.keys().next().value),\n }));\n json(res, { ok: true, destinations });\n return true;\n }\n\n // ── POST /api/streaming/destination -- set active destination ────────\n if (method === \"POST\" && pathname === \"/api/streaming/destination\") {\n try {\n const body = await readRequestBody(req);\n const parsed = typeof body === \"string\" ? JSON.parse(body) : body;\n const destinationId = parsed?.destinationId as string | undefined;\n if (!destinationId) {\n error(res, \"destinationId is required\", 400);\n return true;\n }\n const target = state.destinations.get(destinationId);\n if (target) {\n state.activeDestinationId = destinationId;\n json(res, {\n ok: true,\n destination: { id: target.id, name: target.name },\n });\n } else {\n error(res, `Unknown destination: ${destinationId}`, 404);\n }\n } catch (err) {\n error(res, formatError(err), 500);\n }\n return true;\n }\n\n // ── GET /api/stream/settings -- read stream visual settings ───────────\n if (method === \"GET\" && pathname === \"/api/stream/settings\") {\n try {\n const settings = readStreamSettings();\n json(res, { ok: true, settings });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/settings -- save stream visual settings ──────────\n if (method === \"POST\" && pathname === \"/api/stream/settings\") {\n try {\n const body = await readRequestBody(req);\n const parsed = typeof body === \"string\" ? JSON.parse(body) : body;\n const result = validateStreamSettings(parsed?.settings);\n if (result.error || !result.settings) {\n error(res, result.error ?? \"Invalid settings\", 400);\n return true;\n }\n // Merge with existing settings so partial updates (e.g. just avatarIndex)\n // don't wipe other fields (e.g. voice config).\n const existing = readStreamSettings();\n const merged = { ...existing, ...result.settings };\n writeStreamSettings(merged);\n if (\n typeof merged.avatarIndex === \"number\" &&\n Number.isFinite(merged.avatarIndex)\n ) {\n try {\n state.mirrorStreamAvatarToElizaConfig?.(merged.avatarIndex);\n } catch (err) {\n logger.warn(\n `[stream] mirrorStreamAvatarToElizaConfig failed (stream settings still saved): ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n json(res, { ok: true, settings: merged });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n // ── GET /api/stream/source -- get active stream source ───────────────\n if (method === \"GET\" && pathname === \"/api/stream/source\") {\n json(res, { source: state.activeStreamSource });\n return true;\n }\n\n // ── POST /api/stream/source -- set active stream source ──────────────\n if (method === \"POST\" && pathname === \"/api/stream/source\") {\n try {\n const body = await readRequestBody(req);\n const { sourceType, customUrl } = JSON.parse(\n typeof body === \"string\" ? body : JSON.stringify(body),\n );\n\n if (![\"stream-tab\", \"game\", \"custom-url\"].includes(sourceType)) {\n error(res, \"Invalid sourceType\", 400);\n return true;\n }\n if (sourceType === \"custom-url\" && !customUrl) {\n error(res, \"customUrl required for custom-url source\", 400);\n return true;\n }\n if (sourceType === \"game\" && !customUrl) {\n error(res, \"customUrl required for game source\", 400);\n return true;\n }\n\n // Validate URL scheme to prevent file:// or javascript: URI injection.\n // Only http/https are permitted as capture targets.\n if (\n (sourceType === \"game\" || sourceType === \"custom-url\") &&\n customUrl &&\n !/^https?:\\/\\//i.test(customUrl)\n ) {\n error(res, \"customUrl must use http:// or https:// scheme\", 400);\n return true;\n }\n\n // Stop current frame capture if active\n if (state.screenCapture?.isFrameCaptureActive()) {\n state.screenCapture.stopFrameCapture?.();\n }\n\n // Build capture options\n const captureOpts: {\n fps: number;\n quality: number;\n endpoint: string;\n gameUrl?: string;\n } = {\n fps: 15,\n quality: 70,\n endpoint: \"/api/stream/frame\",\n };\n\n if (sourceType === \"game\" || sourceType === \"custom-url\") {\n captureOpts.gameUrl = customUrl;\n }\n\n // Update state\n state.activeStreamSource = { type: sourceType, url: customUrl };\n\n // Restart frame capture if stream is running\n if (state.streamManager.isRunning() && state.screenCapture) {\n try {\n await state.screenCapture.startFrameCapture(captureOpts);\n } catch (err) {\n logger.warn(\n `[stream] Failed to restart frame capture after source switch: ${err}`,\n );\n }\n }\n\n json(res, { ok: true, source: state.activeStreamSource });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n return false;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/api/stream-routes.ts"],"sourcesContent":["/**\n * Generic streaming infrastructure routes.\n *\n * Shared pipeline for all streaming destinations (custom RTMP, Twitch,\n * YouTube, etc.): capture mode detection, Xvfb management, browser capture,\n * FFmpeg, frame routing, volume/mute.\n *\n * Platform-specific credential fetching lives in destination adapters.\n */\n\nimport fs from \"node:fs\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport {\n formatError,\n logger,\n readRequestBody,\n readRequestBodyBuffer,\n sendJson,\n sendJsonError,\n} from \"@elizaos/core\";\nimport type { StreamConfig } from \"../services/stream-manager.js\";\nimport {\n getHeadlessCaptureConfig,\n readStreamSettings,\n seedOverlayDefaults,\n validateStreamSettings,\n writeStreamSettings,\n} from \"./stream-persistence.js\";\nimport type { StreamRouteState } from \"./stream-route-state.js\";\nimport type { StreamingDestination } from \"./streaming-types.js\";\n\nexport type { StreamRouteState } from \"./stream-route-state.js\";\n\ninterface BrowserCaptureModule {\n FRAME_FILE: string;\n startBrowserCapture(config: {\n url: string;\n width?: number;\n height?: number;\n quality?: number;\n endpoint?: string;\n display?: string;\n headless?: boolean | \"new\";\n executablePath?: string;\n userDataDir?: string;\n browserArgs?: string[];\n }): Promise<void>;\n stopBrowserCapture(): Promise<void>;\n}\n\nconst PLUGIN_BROWSER_PACKAGE = \"@elizaos/plugin-browser\";\n\nasync function loadBrowserCapture(): Promise<BrowserCaptureModule> {\n return (await import(\n PLUGIN_BROWSER_PACKAGE\n )) as unknown as BrowserCaptureModule;\n}\n\n// ---------------------------------------------------------------------------\n// MJPEG frame store — shared state for GET /api/stream/screen\n// ---------------------------------------------------------------------------\n\n/**\n * Stores the most-recently received JPEG frame and pushes each new frame\n * to all active MJPEG subscribers (GET /api/stream/screen).\n *\n * Frames arrive via POST /api/stream/frame from:\n * - Electrobun screencapture module (JS canvas → JPEG)\n * - Legacy desktop screencapture bridges\n * - Any client POSTing raw JPEG bytes\n */\nconst MJPEG_BOUNDARY = \"elizaframe\";\n\nconst mjpegSubscribers = new Set<ServerResponse>();\nlet latestFrame: Buffer | null = null;\n\nfunction pushFrameToSubscribers(frame: Buffer): void {\n latestFrame = frame;\n if (mjpegSubscribers.size === 0) return;\n const header = `--${MJPEG_BOUNDARY}\\r\\nContent-Type: image/jpeg\\r\\nContent-Length: ${frame.length}\\r\\n\\r\\n`;\n const headerBuf = Buffer.from(header, \"ascii\");\n const trailer = Buffer.from(\"\\r\\n\", \"ascii\");\n const chunk = Buffer.concat([headerBuf, frame, trailer]);\n const failed: ServerResponse[] = [];\n for (const sub of mjpegSubscribers) {\n try {\n sub.write(chunk);\n } catch {\n failed.push(sub);\n }\n }\n for (const sub of failed) {\n mjpegSubscribers.delete(sub);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public interfaces\n// ---------------------------------------------------------------------------\n\n/**\n * A streaming destination provides RTMP credentials and optional lifecycle\n * hooks. Canonical definition lives in @elizaos/plugin-streaming; re-exported here\n * so existing consumers keep working.\n */\nexport type {\n OverlayLayoutData,\n StreamingDestination,\n} from \"./streaming-types.js\";\n\n/** Resolve the active streaming destination from the registry. */\nexport function getActiveDestination(\n state: StreamRouteState,\n): StreamingDestination | undefined {\n if (state.activeDestinationId) {\n return state.destinations.get(state.activeDestinationId);\n }\n // Fallback: first destination in map (backward compat for single-destination configs)\n const first = state.destinations.values().next();\n return first.done ? undefined : first.value;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction json(res: ServerResponse, data: unknown, status = 200): void {\n sendJson(res, data, status);\n}\n\nfunction error(res: ServerResponse, message: string, status: number): void {\n sendJsonError(res, message, status);\n}\n\n// ---------------------------------------------------------------------------\n// Capture mode detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detect the best capture mode for the current environment.\n *\n * Priority:\n * 1. STREAM_MODE env var (explicit override)\n * 2. Desktop screen capture bridge -> \"pipe\" (POST /api/stream/frame -> FFmpeg stdin)\n * 3. Linux with DISPLAY or Xvfb -> \"x11grab\" (GPU-backed game-stream approach)\n * 4. macOS -> \"avfoundation\" (native screen capture)\n * 5. Fallback -> \"file\" (Puppeteer CDP -> temp JPEG -> FFmpeg)\n */\n/** @internal Exported for testing. */\nexport function detectCaptureMode(): StreamConfig[\"inputMode\"] {\n const explicit = process.env.STREAM_MODE;\n if (explicit === \"ui\" || explicit === \"pipe\") return \"pipe\";\n if (explicit === \"x11grab\") return \"x11grab\";\n if (explicit === \"avfoundation\" || explicit === \"screen\")\n return \"avfoundation\";\n if (explicit === \"file\") return \"file\";\n\n // Desktop bridge -> pipe mode\n if (\"__elizaScreenCapture\" in (globalThis as Record<string, unknown>)) {\n return \"pipe\";\n }\n\n // Linux with a display -> x11grab (Xvfb or native X11)\n if (process.platform === \"linux\" && process.env.DISPLAY) return \"x11grab\";\n\n // macOS -> avfoundation screen capture\n if (process.platform === \"darwin\") return \"avfoundation\";\n\n // Fallback -> headless browser capture -> file mode\n return \"file\";\n}\n\n// ---------------------------------------------------------------------------\n// Xvfb management\n// ---------------------------------------------------------------------------\n\n/**\n * Try to start Xvfb on the specified display if not already running (Linux only).\n * Returns true if display is available, false otherwise.\n */\n/** @internal Exported for testing. */\nexport async function ensureXvfb(\n display: string,\n resolution: string,\n): Promise<boolean> {\n if (process.platform !== \"linux\") return false;\n\n // Validate display format to prevent command injection (must be :<digits>)\n if (!/^:\\d+$/.test(display)) {\n logger.warn(\n `[stream] Invalid display format: ${display} (expected :<number>)`,\n );\n return false;\n }\n\n // Validate resolution early so callers get a clear failure before we\n // touch the display or spawn processes.\n const [w, h] = resolution.split(\"x\");\n if (!w || !h || !/^\\d+$/.test(w) || !/^\\d+$/.test(h)) {\n logger.warn(`[stream] Invalid resolution for Xvfb: ${resolution}`);\n return false;\n }\n\n // Check if the display is already active\n if (process.env.DISPLAY === display) return true;\n\n try {\n const { execSync } = await import(\"node:child_process\");\n // Check if Xvfb is already running on this display\n try {\n execSync(`xdpyinfo -display ${display}`, {\n stdio: \"ignore\",\n timeout: 3000,\n });\n logger.info(`[stream] Xvfb already running on display ${display}`);\n return true;\n } catch {\n // Not running -- start it\n }\n const { spawn: spawnProc } = await import(\"node:child_process\");\n const xvfb = spawnProc(\n \"Xvfb\",\n [display, \"-screen\", \"0\", `${w}x${h}x24`, \"-ac\"],\n {\n stdio: \"ignore\",\n detached: true,\n },\n );\n xvfb.unref();\n\n // Wait for Xvfb to be ready\n await new Promise((r) => setTimeout(r, 1000));\n logger.info(`[stream] Started Xvfb on display ${display} (${resolution})`);\n process.env.DISPLAY = display;\n return true;\n } catch (err) {\n logger.warn(`[stream] Failed to start Xvfb: ${err}`);\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Streaming pipeline (destination-driven)\n// ---------------------------------------------------------------------------\n\n/**\n * Start the full streaming pipeline using the configured destination for\n * RTMP credentials. Handles capture mode detection, Xvfb, browser capture,\n * and FFmpeg configuration.\n */\nasync function startStreamPipeline(\n state: StreamRouteState,\n rtmpUrl: string,\n rtmpKey: string,\n): Promise<{ inputMode: string; audioSource: string }> {\n // Defense-in-depth: validate RTMP scheme before passing to FFmpeg\n if (!/^rtmps?:\\/\\//i.test(rtmpUrl)) {\n throw new Error(\"RTMP URL must use rtmp:// or rtmps:// scheme\");\n }\n\n // Seed plugin-default overlay layout on first stream start\n const activeDest = getActiveDestination(state);\n if (activeDest) {\n seedOverlayDefaults(activeDest);\n }\n const destId = activeDest?.id ?? null;\n\n const mode = detectCaptureMode();\n\n const audioSource = process.env.STREAM_AUDIO_SOURCE ?? \"silent\";\n const audioDevice = process.env.STREAM_AUDIO_DEVICE;\n const volume = parseInt(process.env.STREAM_VOLUME ?? \"80\", 10);\n const resolution = \"1280x720\";\n\n const baseConfig: StreamConfig = {\n rtmpUrl,\n rtmpKey,\n resolution,\n bitrate: \"1500k\",\n audioSource,\n audioDevice,\n volume,\n };\n\n switch (mode) {\n case \"pipe\": {\n // Desktop UI mode: FFmpeg reads frames from stdin via writeFrame().\n logger.info(\"[stream] Capture mode: pipe (desktop UI)\");\n await state.streamManager.start({\n ...baseConfig,\n inputMode: \"pipe\",\n framerate: 15,\n });\n\n // Auto-start desktop frame capture so the UI is streamed without\n // requiring a manual button click in the renderer.\n if (state.screenCapture && !state.screenCapture.isFrameCaptureActive()) {\n try {\n const captureOpts: {\n fps: number;\n quality: number;\n endpoint: string;\n gameUrl?: string;\n } = {\n fps: 15,\n quality: 70,\n endpoint: \"/api/stream/frame\",\n };\n if (\n state.activeStreamSource.type !== \"stream-tab\" &&\n state.activeStreamSource.url\n ) {\n captureOpts.gameUrl = state.activeStreamSource.url;\n }\n await state.screenCapture.startFrameCapture(captureOpts);\n logger.info(\"[stream] Auto-started desktop frame capture\");\n } catch (err) {\n logger.warn(`[stream] Failed to auto-start frame capture: ${err}`);\n }\n } else if (!state.screenCapture) {\n logger.warn(\n \"[stream] ScreenCaptureManager not available -- frame capture must be started manually\",\n );\n }\n break;\n }\n\n case \"x11grab\": {\n // Linux Xvfb mode: capture the virtual display for GPU-backed streams.\n const display = process.env.STREAM_DISPLAY ?? \":99\";\n logger.info(`[stream] Capture mode: x11grab (display ${display})`);\n\n // Ensure Xvfb is running\n await ensureXvfb(display, resolution);\n\n // Launch a browser on the virtual display so there's something to capture\n const captureUrl =\n state.captureUrl ??\n process.env.STREAM_CAPTURE_URL ??\n `http://127.0.0.1:${state.port ?? 2138}`;\n\n try {\n const { startBrowserCapture } = await loadBrowserCapture();\n // Browser capture in x11grab mode just opens the browser on the display --\n // we don't need the frame file since FFmpeg captures the display directly.\n await startBrowserCapture({\n url: captureUrl,\n width: 1280,\n height: 720,\n quality: 70,\n ...getHeadlessCaptureConfig(destId),\n });\n } catch (err) {\n logger.warn(`[stream] Browser launch on ${display} failed: ${err}`);\n }\n\n await state.streamManager.start({\n ...baseConfig,\n inputMode: \"x11grab\",\n display,\n framerate: 30,\n });\n break;\n }\n\n case \"avfoundation\": {\n // macOS native screen capture.\n const videoDevice = process.env.STREAM_VIDEO_DEVICE ?? \"3\";\n logger.info(\n `[stream] Capture mode: avfoundation (device ${videoDevice})`,\n );\n await state.streamManager.start({\n ...baseConfig,\n inputMode: \"avfoundation\",\n videoDevice,\n framerate: 30,\n });\n break;\n }\n\n default: {\n // Headless browser capture -> temp JPEG file -> FFmpeg file mode.\n const captureUrl =\n state.captureUrl ??\n process.env.STREAM_CAPTURE_URL ??\n `http://127.0.0.1:${state.port ?? 2138}`;\n\n logger.info(\n `[stream] Capture mode: file (browser capture -> ${captureUrl})`,\n );\n\n const { startBrowserCapture, FRAME_FILE } = await loadBrowserCapture();\n try {\n await startBrowserCapture({\n url: captureUrl,\n width: 1280,\n height: 720,\n quality: 70,\n ...getHeadlessCaptureConfig(destId),\n });\n // Wait for first frame file to be written\n await new Promise((resolve) => {\n const check = setInterval(() => {\n try {\n if (\n fs.existsSync(FRAME_FILE) &&\n fs.statSync(FRAME_FILE).size > 0\n ) {\n clearInterval(check);\n resolve(true);\n }\n } catch {\n // Frame writer can race the first stat; keep polling.\n }\n }, 200);\n setTimeout(() => {\n clearInterval(check);\n resolve(false);\n }, 10_000);\n });\n } catch (captureErr) {\n logger.warn(`[stream] Browser capture failed: ${captureErr}`);\n }\n\n await state.streamManager.start({\n ...baseConfig,\n inputMode: \"file\",\n frameFile: FRAME_FILE,\n framerate: 30,\n });\n break;\n }\n }\n\n return { inputMode: mode || \"file\", audioSource };\n}\n\n// ---------------------------------------------------------------------------\n// Route handler\n// ---------------------------------------------------------------------------\n\n/** Returns `true` if handled, `false` to fall through. */\nexport async function handleStreamRoute(\n req: IncomingMessage,\n res: ServerResponse,\n pathname: string,\n method: string,\n state: StreamRouteState,\n): Promise<boolean> {\n // Fast-path: skip if not a stream route\n if (\n !pathname.startsWith(\"/api/stream/\") &&\n !pathname.startsWith(\"/api/streaming/\")\n ) {\n return false;\n }\n\n // ── POST /api/stream/frame -- pipe frames to StreamManager + MJPEG ──\n if (method === \"POST\" && pathname === \"/api/stream/frame\") {\n try {\n const buf = await readRequestBodyBuffer(req, {\n maxBytes: 2 * 1024 * 1024,\n });\n if (!buf || buf.length === 0) {\n error(res, \"Empty frame\", 400);\n return true;\n }\n // Always store frame for MJPEG monitoring (GET /api/stream/screen)\n pushFrameToSubscribers(buf);\n // Write to FFmpeg only when RTMP streaming is active\n if (state.streamManager.isRunning()) {\n state.streamManager.writeFrame(buf);\n }\n res.writeHead(200);\n res.end();\n } catch {\n error(res, \"Frame write failed\", 500);\n }\n return true;\n }\n\n // ── GET /api/stream/screen -- MJPEG live view (local + remote agents) ─\n // Serves a continuous multipart/x-mixed-replace stream of JPEG frames.\n // Works independently of RTMP streaming — frames arrive via POST /api/stream/frame.\n // Usage: <img src=\"http://agent-host:2138/api/stream/screen\" />\n if (method === \"GET\" && pathname === \"/api/stream/screen\") {\n res.writeHead(200, {\n \"Content-Type\": `multipart/x-mixed-replace; boundary=${MJPEG_BOUNDARY}`,\n \"Cache-Control\": \"no-store, no-cache\",\n Connection: \"close\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n\n mjpegSubscribers.add(res);\n\n // Send the latest cached frame immediately so there's no blank wait\n if (latestFrame) {\n const header = `--${MJPEG_BOUNDARY}\\r\\nContent-Type: image/jpeg\\r\\nContent-Length: ${latestFrame.length}\\r\\n\\r\\n`;\n res.write(\n Buffer.concat([\n Buffer.from(header, \"ascii\"),\n latestFrame,\n Buffer.from(\"\\r\\n\", \"ascii\"),\n ]),\n );\n }\n\n const cleanup = () => {\n mjpegSubscribers.delete(res);\n };\n req.on(\"close\", cleanup);\n req.on(\"error\", cleanup);\n res.on(\"close\", cleanup);\n res.on(\"error\", cleanup);\n\n // Keep the response open — frames are pushed as they arrive\n return true;\n }\n\n // ── POST /api/stream/live -- start stream via destination ────────────\n if (method === \"POST\" && pathname === \"/api/stream/live\") {\n if (state.streamManager.isRunning()) {\n const health = state.streamManager.getHealth();\n json(res, {\n ok: true,\n live: true,\n message: \"Already streaming\",\n ...health,\n });\n return true;\n }\n\n const dest = getActiveDestination(state);\n if (!dest) {\n error(res, \"No streaming destination configured\", 400);\n return true;\n }\n\n try {\n const { rtmpUrl, rtmpKey } = await dest.getCredentials();\n const { inputMode, audioSource } = await startStreamPipeline(\n state,\n rtmpUrl,\n rtmpKey,\n );\n await dest.onStreamStart?.();\n json(res, {\n ok: true,\n live: true,\n rtmpUrl,\n inputMode,\n audioSource,\n destination: dest.id,\n });\n } catch (err) {\n error(res, formatError(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/offline -- stop stream + notify destination ─────\n if (method === \"POST\" && pathname === \"/api/stream/offline\") {\n try {\n // Stop browser capture\n try {\n const { stopBrowserCapture } = await loadBrowserCapture();\n await stopBrowserCapture();\n } catch {\n // Browser capture may not have been started -- ignore\n }\n // Stop StreamManager\n if (state.streamManager.isRunning()) {\n await state.streamManager.stop();\n }\n // Notify destination\n try {\n await getActiveDestination(state)?.onStreamStop?.();\n } catch {\n // Destination notification failure is non-fatal\n }\n json(res, { ok: true, live: false });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/start -- backward-compat explicit RTMP start ────\n if (method === \"POST\" && pathname === \"/api/stream/start\") {\n try {\n const bodyStr = await readRequestBody(req);\n const body = typeof bodyStr === \"string\" ? JSON.parse(bodyStr) : bodyStr;\n const rtmpUrl = body?.rtmpUrl as string | undefined;\n const rtmpKey = body?.rtmpKey as string | undefined;\n\n if (!rtmpUrl || !rtmpKey) {\n error(res, \"rtmpUrl and rtmpKey are required\", 400);\n return true;\n }\n\n if (!/^rtmps?:\\/\\//i.test(rtmpUrl)) {\n error(res, \"rtmpUrl must use rtmp:// or rtmps:// scheme\", 400);\n return true;\n }\n\n // Validate FFmpeg parameters to prevent filter expression injection\n const VALID_INPUT_MODES = [\"testsrc\", \"avfoundation\", \"pipe\"] as const;\n const inputMode = body?.inputMode ?? \"testsrc\";\n if (!VALID_INPUT_MODES.includes(inputMode)) {\n error(\n res,\n `inputMode must be one of: ${VALID_INPUT_MODES.join(\", \")}`,\n 400,\n );\n return true;\n }\n\n const resolution = (body?.resolution as string) || \"1280x720\";\n if (!/^\\d{3,4}x\\d{3,4}$/.test(resolution)) {\n error(res, \"resolution must match WIDTHxHEIGHT (e.g. 1280x720)\", 400);\n return true;\n }\n\n const bitrate = (body?.bitrate as string) || \"2500k\";\n if (!/^\\d+k$/.test(bitrate)) {\n error(res, \"bitrate must match NUMBERk (e.g. 2500k)\", 400);\n return true;\n }\n\n const framerate = body?.framerate ?? 30;\n if (\n typeof framerate !== \"number\" ||\n !Number.isInteger(framerate) ||\n framerate < 1 ||\n framerate > 60\n ) {\n error(res, \"framerate must be an integer between 1 and 60\", 400);\n return true;\n }\n\n await state.streamManager.start({\n rtmpUrl,\n rtmpKey,\n inputMode,\n resolution,\n bitrate,\n framerate,\n });\n\n json(res, { ok: true, message: \"Stream started\" });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/stop -- backward-compat explicit stop ───────────\n if (method === \"POST\" && pathname === \"/api/stream/stop\") {\n try {\n const result = await state.streamManager.stop();\n json(res, { ok: true, ...result });\n } catch (err) {\n error(res, formatError(err), 500);\n }\n return true;\n }\n\n // ── GET /api/stream/status -- local stream health ────────────────────\n if (method === \"GET\" && pathname === \"/api/stream/status\") {\n const health = state.streamManager.getHealth();\n const activeDest = getActiveDestination(state);\n const destInfo = activeDest\n ? { id: activeDest.id, name: activeDest.name }\n : null;\n json(res, { ok: true, ...health, destination: destInfo });\n return true;\n }\n\n // ── POST /api/stream/volume -- set stream volume (0-100) ─────────────\n if (method === \"POST\" && pathname === \"/api/stream/volume\") {\n try {\n const body = await readRequestBody(req);\n const parsed = typeof body === \"string\" ? JSON.parse(body) : body;\n const level = parsed?.volume;\n if (\n typeof level !== \"number\" ||\n !Number.isFinite(level) ||\n level < 0 ||\n level > 100\n ) {\n error(res, \"volume must be a number between 0 and 100\", 400);\n return true;\n }\n await state.streamManager.setVolume(level);\n json(res, {\n ok: true,\n volume: state.streamManager.getVolume(),\n muted: state.streamManager.isMuted(),\n });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/mute -- mute stream audio ──────────────────────\n if (method === \"POST\" && pathname === \"/api/stream/mute\") {\n try {\n await state.streamManager.mute();\n json(res, {\n ok: true,\n muted: true,\n volume: state.streamManager.getVolume(),\n });\n } catch (err) {\n error(res, formatError(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/unmute -- unmute stream audio ───────────────────\n if (method === \"POST\" && pathname === \"/api/stream/unmute\") {\n try {\n await state.streamManager.unmute();\n json(res, {\n ok: true,\n muted: false,\n volume: state.streamManager.getVolume(),\n });\n } catch (err) {\n error(res, formatError(err), 500);\n }\n return true;\n }\n\n // ── GET /api/streaming/destinations -- list configured destination ───\n if (method === \"GET\" && pathname === \"/api/streaming/destinations\") {\n const destinations = Array.from(state.destinations.values()).map((d) => ({\n id: d.id,\n name: d.name,\n active:\n d.id ===\n (state.activeDestinationId ?? state.destinations.keys().next().value),\n }));\n json(res, { ok: true, destinations });\n return true;\n }\n\n // ── POST /api/streaming/destination -- set active destination ────────\n if (method === \"POST\" && pathname === \"/api/streaming/destination\") {\n try {\n const body = await readRequestBody(req);\n const parsed = typeof body === \"string\" ? JSON.parse(body) : body;\n const destinationId = parsed?.destinationId as string | undefined;\n if (!destinationId) {\n error(res, \"destinationId is required\", 400);\n return true;\n }\n const target = state.destinations.get(destinationId);\n if (target) {\n state.activeDestinationId = destinationId;\n json(res, {\n ok: true,\n destination: { id: target.id, name: target.name },\n });\n } else {\n error(res, `Unknown destination: ${destinationId}`, 404);\n }\n } catch (err) {\n error(res, formatError(err), 500);\n }\n return true;\n }\n\n // ── GET /api/stream/settings -- read stream visual settings ───────────\n if (method === \"GET\" && pathname === \"/api/stream/settings\") {\n try {\n const settings = readStreamSettings();\n json(res, { ok: true, settings });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n // ── POST /api/stream/settings -- save stream visual settings ──────────\n if (method === \"POST\" && pathname === \"/api/stream/settings\") {\n try {\n const body = await readRequestBody(req);\n const parsed = typeof body === \"string\" ? JSON.parse(body) : body;\n const result = validateStreamSettings(parsed?.settings);\n if (result.error || !result.settings) {\n error(res, result.error ?? \"Invalid settings\", 400);\n return true;\n }\n // Merge with existing settings so partial updates (e.g. just avatarIndex)\n // don't wipe other fields (e.g. voice config).\n const existing = readStreamSettings();\n const merged = { ...existing, ...result.settings };\n writeStreamSettings(merged);\n if (\n typeof merged.avatarIndex === \"number\" &&\n Number.isFinite(merged.avatarIndex)\n ) {\n try {\n state.mirrorStreamAvatarToElizaConfig?.(merged.avatarIndex);\n } catch (err) {\n logger.warn(\n `[stream] mirrorStreamAvatarToElizaConfig failed (stream settings still saved): ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n json(res, { ok: true, settings: merged });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n // ── GET /api/stream/source -- get active stream source ───────────────\n if (method === \"GET\" && pathname === \"/api/stream/source\") {\n json(res, { source: state.activeStreamSource });\n return true;\n }\n\n // ── POST /api/stream/source -- set active stream source ──────────────\n if (method === \"POST\" && pathname === \"/api/stream/source\") {\n try {\n const body = await readRequestBody(req);\n const { sourceType, customUrl } = JSON.parse(\n typeof body === \"string\" ? body : JSON.stringify(body),\n );\n\n if (![\"stream-tab\", \"game\", \"custom-url\"].includes(sourceType)) {\n error(res, \"Invalid sourceType\", 400);\n return true;\n }\n if (sourceType === \"custom-url\" && !customUrl) {\n error(res, \"customUrl required for custom-url source\", 400);\n return true;\n }\n if (sourceType === \"game\" && !customUrl) {\n error(res, \"customUrl required for game source\", 400);\n return true;\n }\n\n // Validate URL scheme to prevent file:// or javascript: URI injection.\n // Only http/https are permitted as capture targets.\n if (\n (sourceType === \"game\" || sourceType === \"custom-url\") &&\n customUrl &&\n !/^https?:\\/\\//i.test(customUrl)\n ) {\n error(res, \"customUrl must use http:// or https:// scheme\", 400);\n return true;\n }\n\n // Stop current frame capture if active\n if (state.screenCapture?.isFrameCaptureActive()) {\n state.screenCapture.stopFrameCapture?.();\n }\n\n // Build capture options\n const captureOpts: {\n fps: number;\n quality: number;\n endpoint: string;\n gameUrl?: string;\n } = {\n fps: 15,\n quality: 70,\n endpoint: \"/api/stream/frame\",\n };\n\n if (sourceType === \"game\" || sourceType === \"custom-url\") {\n captureOpts.gameUrl = customUrl;\n }\n\n // Update state\n state.activeStreamSource = { type: sourceType, url: customUrl };\n\n // Restart frame capture if stream is running\n if (state.streamManager.isRunning() && state.screenCapture) {\n try {\n await state.screenCapture.startFrameCapture(captureOpts);\n } catch (err) {\n logger.warn(\n `[stream] Failed to restart frame capture after source switch: ${err}`,\n );\n }\n }\n\n json(res, { ok: true, source: state.activeStreamSource });\n } catch (err) {\n error(res, String(err), 500);\n }\n return true;\n }\n\n return false;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
type StreamingUpdateKind = "
|
|
2
|
-
interface StreamingUpdate {
|
|
1
|
+
export type StreamingUpdateKind = "unchanged" | "append" | "replace";
|
|
2
|
+
export interface StreamingUpdate {
|
|
3
3
|
kind: StreamingUpdateKind;
|
|
4
4
|
nextText: string;
|
|
5
5
|
emittedText: string;
|
|
6
6
|
}
|
|
7
|
-
declare function mergeStreamingText(existing: string, incoming: string): string;
|
|
8
|
-
declare function resolveStreamingUpdate(existing: string, incoming: string): StreamingUpdate;
|
|
9
|
-
|
|
10
|
-
export { type StreamingUpdate, type StreamingUpdateKind, mergeStreamingText, resolveStreamingUpdate };
|
|
7
|
+
export declare function mergeStreamingText(existing: string, incoming: string): string;
|
|
8
|
+
export declare function resolveStreamingUpdate(existing: string, incoming: string): StreamingUpdate;
|
|
@@ -66,7 +66,7 @@ function mergeStreamingText(existing, incoming) {
|
|
|
66
66
|
function resolveStreamingUpdate(existing, incoming) {
|
|
67
67
|
const nextText = mergeStreamingText(existing, incoming);
|
|
68
68
|
if (nextText === existing) {
|
|
69
|
-
return { kind: "
|
|
69
|
+
return { kind: "unchanged", nextText: existing, emittedText: "" };
|
|
70
70
|
}
|
|
71
71
|
if (nextText.startsWith(existing)) {
|
|
72
72
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/api/streaming-text.ts"],"sourcesContent":["export type StreamingUpdateKind = \"
|
|
1
|
+
{"version":3,"sources":["../../src/api/streaming-text.ts"],"sourcesContent":["export type StreamingUpdateKind = \"unchanged\" | \"append\" | \"replace\";\n\nexport interface StreamingUpdate {\n kind: StreamingUpdateKind;\n nextText: string;\n emittedText: string;\n}\n\nfunction commonPrefixLength(left: string, right: string): number {\n const maxLength = Math.min(left.length, right.length);\n let index = 0;\n while (\n index < maxLength &&\n left.charCodeAt(index) === right.charCodeAt(index)\n ) {\n index += 1;\n }\n return index;\n}\n\nfunction commonSuffixLength(\n left: string,\n right: string,\n sharedPrefixLength: number,\n): number {\n const maxLength = Math.min(\n left.length - sharedPrefixLength,\n right.length - sharedPrefixLength,\n );\n let length = 0;\n while (\n length < maxLength &&\n left.charCodeAt(left.length - 1 - length) ===\n right.charCodeAt(right.length - 1 - length)\n ) {\n length += 1;\n }\n return length;\n}\n\nfunction isLikelySnapshotReplacement(\n existing: string,\n incoming: string,\n): boolean {\n const sharedPrefixLength = commonPrefixLength(existing, incoming);\n const sharedSuffixLength = commonSuffixLength(\n existing,\n incoming,\n sharedPrefixLength,\n );\n const sharedLength = sharedPrefixLength + sharedSuffixLength;\n const minLength = Math.min(existing.length, incoming.length);\n\n return (\n sharedPrefixLength >= 8 ||\n sharedLength >= Math.max(4, Math.ceil(minLength * 0.7))\n );\n}\n\nexport function mergeStreamingText(existing: string, incoming: string): string {\n if (!incoming) return existing;\n if (!existing) return incoming;\n if (incoming === existing) return existing;\n\n if (incoming.startsWith(existing)) {\n return incoming;\n }\n\n if (incoming.includes(existing)) {\n return incoming;\n }\n\n if (existing.startsWith(incoming)) {\n return existing;\n }\n\n const maxOverlap = Math.min(existing.length, incoming.length);\n const existingLength = existing.length;\n for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {\n const existingStart = existingLength - overlap;\n let match = true;\n for (let index = 0; index < overlap; index += 1) {\n if (\n existing.charCodeAt(existingStart + index) !==\n incoming.charCodeAt(index)\n ) {\n match = false;\n break;\n }\n }\n if (!match) continue;\n\n if (overlap === incoming.length) {\n return incoming.length === 1 ? `${existing}${incoming}` : existing;\n }\n\n return `${existing}${incoming.slice(overlap)}`;\n }\n\n if (isLikelySnapshotReplacement(existing, incoming)) {\n return incoming;\n }\n\n return `${existing}${incoming}`;\n}\n\nexport function resolveStreamingUpdate(\n existing: string,\n incoming: string,\n): StreamingUpdate {\n const nextText = mergeStreamingText(existing, incoming);\n if (nextText === existing) {\n return { kind: \"unchanged\", nextText: existing, emittedText: \"\" };\n }\n\n if (nextText.startsWith(existing)) {\n return {\n kind: \"append\",\n nextText,\n emittedText: nextText.slice(existing.length),\n };\n }\n\n return {\n kind: \"replace\",\n nextText,\n emittedText: nextText,\n };\n}\n"],"mappings":"AAQA,SAAS,mBAAmB,MAAc,OAAuB;AAC/D,QAAM,YAAY,KAAK,IAAI,KAAK,QAAQ,MAAM,MAAM;AACpD,MAAI,QAAQ;AACZ,SACE,QAAQ,aACR,KAAK,WAAW,KAAK,MAAM,MAAM,WAAW,KAAK,GACjD;AACA,aAAS;AAAA,EACX;AACA,SAAO;AACT;AAEA,SAAS,mBACP,MACA,OACA,oBACQ;AACR,QAAM,YAAY,KAAK;AAAA,IACrB,KAAK,SAAS;AAAA,IACd,MAAM,SAAS;AAAA,EACjB;AACA,MAAI,SAAS;AACb,SACE,SAAS,aACT,KAAK,WAAW,KAAK,SAAS,IAAI,MAAM,MACtC,MAAM,WAAW,MAAM,SAAS,IAAI,MAAM,GAC5C;AACA,cAAU;AAAA,EACZ;AACA,SAAO;AACT;AAEA,SAAS,4BACP,UACA,UACS;AACT,QAAM,qBAAqB,mBAAmB,UAAU,QAAQ;AAChE,QAAM,qBAAqB;AAAA,IACzB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,eAAe,qBAAqB;AAC1C,QAAM,YAAY,KAAK,IAAI,SAAS,QAAQ,SAAS,MAAM;AAE3D,SACE,sBAAsB,KACtB,gBAAgB,KAAK,IAAI,GAAG,KAAK,KAAK,YAAY,GAAG,CAAC;AAE1D;AAEO,SAAS,mBAAmB,UAAkB,UAA0B;AAC7E,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,aAAa,SAAU,QAAO;AAElC,MAAI,SAAS,WAAW,QAAQ,GAAG;AACjC,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,SAAS,QAAQ,GAAG;AAC/B,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,WAAW,QAAQ,GAAG;AACjC,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,KAAK,IAAI,SAAS,QAAQ,SAAS,MAAM;AAC5D,QAAM,iBAAiB,SAAS;AAChC,WAAS,UAAU,YAAY,UAAU,GAAG,WAAW,GAAG;AACxD,UAAM,gBAAgB,iBAAiB;AACvC,QAAI,QAAQ;AACZ,aAAS,QAAQ,GAAG,QAAQ,SAAS,SAAS,GAAG;AAC/C,UACE,SAAS,WAAW,gBAAgB,KAAK,MACzC,SAAS,WAAW,KAAK,GACzB;AACA,gBAAQ;AACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,MAAO;AAEZ,QAAI,YAAY,SAAS,QAAQ;AAC/B,aAAO,SAAS,WAAW,IAAI,GAAG,QAAQ,GAAG,QAAQ,KAAK;AAAA,IAC5D;AAEA,WAAO,GAAG,QAAQ,GAAG,SAAS,MAAM,OAAO,CAAC;AAAA,EAC9C;AAEA,MAAI,4BAA4B,UAAU,QAAQ,GAAG;AACnD,WAAO;AAAA,EACT;AAEA,SAAO,GAAG,QAAQ,GAAG,QAAQ;AAC/B;AAEO,SAAS,uBACd,UACA,UACiB;AACjB,QAAM,WAAW,mBAAmB,UAAU,QAAQ;AACtD,MAAI,aAAa,UAAU;AACzB,WAAO,EAAE,MAAM,aAAa,UAAU,UAAU,aAAa,GAAG;AAAA,EAClE;AAEA,MAAI,SAAS,WAAW,QAAQ,GAAG;AACjC,WAAO;AAAA,MACL,MAAM;AAAA,MACN;AAAA,MACA,aAAa,SAAS,MAAM,SAAS,MAAM;AAAA,IAC7C;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,aAAa;AAAA,EACf;AACF;","names":[]}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export { OverlayLayoutData, OverlayWidgetInstance, StreamingDestination } from
|
|
2
|
-
import '@elizaos/core';
|
|
1
|
+
export type { OverlayLayoutData, OverlayWidgetInstance, StreamingDestination, } from "../core.ts";
|
package/dist/api/tts-routes.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import http from
|
|
2
|
-
import { ReadJsonBodyOptions } from
|
|
3
|
-
|
|
4
|
-
interface TtsRouteContext {
|
|
1
|
+
import type http from "node:http";
|
|
2
|
+
import { type IAgentRuntime, type ReadJsonBodyOptions } from "@elizaos/core";
|
|
3
|
+
export interface TtsRouteContext {
|
|
5
4
|
req: http.IncomingMessage;
|
|
6
5
|
res: http.ServerResponse;
|
|
7
6
|
method: string;
|
|
8
7
|
pathname: string;
|
|
9
8
|
state: {
|
|
10
9
|
config: Record<string, unknown>;
|
|
10
|
+
runtime?: IAgentRuntime | null;
|
|
11
11
|
};
|
|
12
12
|
json: (res: http.ServerResponse, data: unknown, status?: number) => void;
|
|
13
13
|
error: (res: http.ServerResponse, message: string, status?: number) => void;
|
|
@@ -20,6 +20,4 @@ interface TtsRouteContext {
|
|
|
20
20
|
ELEVENLABS_FETCH_TIMEOUT_MS: number;
|
|
21
21
|
ELEVENLABS_AUDIO_MAX_BYTES: number;
|
|
22
22
|
}
|
|
23
|
-
declare function handleTtsRoutes(ctx: TtsRouteContext): Promise<boolean>;
|
|
24
|
-
|
|
25
|
-
export { type TtsRouteContext, handleTtsRoutes };
|
|
23
|
+
export declare function handleTtsRoutes(ctx: TtsRouteContext): Promise<boolean>;
|