@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.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/api/stream-persistence.d.ts +68 -0
- package/dist/api/stream-persistence.js +181 -0
- package/dist/api/stream-persistence.js.map +1 -0
- package/dist/api/stream-route-state.d.ts +59 -0
- package/dist/api/stream-route-state.js +1 -0
- package/dist/api/stream-route-state.js.map +1 -0
- package/dist/api/stream-routes.d.ts +43 -0
- package/dist/api/stream-routes.js +609 -0
- package/dist/api/stream-routes.js.map +1 -0
- package/dist/api/streaming-text.d.ts +10 -0
- package/dist/api/streaming-text.js +88 -0
- package/dist/api/streaming-text.js.map +1 -0
- package/dist/api/streaming-types.d.ts +2 -0
- package/dist/api/streaming-types.js +1 -0
- package/dist/api/streaming-types.js.map +1 -0
- package/dist/api/tts-routes.d.ts +25 -0
- package/dist/api/tts-routes.js +158 -0
- package/dist/api/tts-routes.js.map +1 -0
- package/dist/core.d.ts +164 -0
- package/dist/core.js +495 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/dist/services/stream-manager.d.ts +124 -0
- package/dist/services/stream-manager.js +531 -0
- package/dist/services/stream-manager.js.map +1 -0
- package/dist/services/tts-stream-bridge.d.ts +103 -0
- package/dist/services/tts-stream-bridge.js +327 -0
- package/dist/services/tts-stream-bridge.js.map +1 -0
- package/package.json +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 elizaOS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @elizaos/plugin-streaming
|
|
2
|
+
|
|
3
|
+
Unified RTMP streaming: Twitch, YouTube Live, X (Twitter), pump.fun, `streaming.customRtmp`, and `streaming.rtmpSources[]` for named extra ingests.
|
|
4
|
+
|
|
5
|
+
Default export: **`streamingPlugin`**. Preset factories: **`createTwitchDestination`**, **`createYoutubeDestination`**, **`createXStreamDestination`**, **`createPumpfunDestination`**. Helpers: **`createCustomRtmpDestination`**, **`createNamedRtmpDestination`**.
|
|
6
|
+
|
|
7
|
+
Stream keys still come from each platform’s studio/dashboard (no OAuth in this package).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { StreamingDestination } from '../core.js';
|
|
2
|
+
import '@elizaos/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stream persistence layer — overlay layout and visual/voice settings I/O.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from stream-routes.ts to keep that file focused on route handling.
|
|
8
|
+
* All functions here deal with reading/writing JSON files under the
|
|
9
|
+
* `data/stream/` directory.
|
|
10
|
+
*
|
|
11
|
+
* @module api/stream-persistence
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface StreamVoiceSettings {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
provider?: string;
|
|
17
|
+
autoSpeak?: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface StreamVisualSettings {
|
|
20
|
+
theme?: string;
|
|
21
|
+
avatarIndex?: number;
|
|
22
|
+
voice?: StreamVoiceSettings;
|
|
23
|
+
}
|
|
24
|
+
/** Sanitize destination ID for use as a filename segment. */
|
|
25
|
+
declare function safeDestId(id: string): string;
|
|
26
|
+
/** Extract `?destination=<id>` from the raw request URL. */
|
|
27
|
+
declare function parseDestinationQuery(url?: string): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Read overlay layout for a destination.
|
|
30
|
+
* Falls back: destination-specific -> global -> plugin default -> null.
|
|
31
|
+
*/
|
|
32
|
+
declare function readOverlayLayout(destinationId?: string | null, destination?: StreamingDestination): unknown;
|
|
33
|
+
/** Write overlay layout (to destination-specific or global file). */
|
|
34
|
+
declare function writeOverlayLayout(layout: unknown, destinationId?: string | null): void;
|
|
35
|
+
/**
|
|
36
|
+
* Seed the plugin's default overlay layout on first stream start.
|
|
37
|
+
* Only writes if no destination-specific layout file exists yet.
|
|
38
|
+
*/
|
|
39
|
+
declare function seedOverlayDefaults(destination: StreamingDestination): void;
|
|
40
|
+
/**
|
|
41
|
+
* Validate and sanitize a raw settings object into a safe StreamVisualSettings.
|
|
42
|
+
* Only allows known keys with expected types — rejects everything else.
|
|
43
|
+
* Returns null with an error message if validation fails.
|
|
44
|
+
*/
|
|
45
|
+
declare function validateStreamSettings(raw: unknown): {
|
|
46
|
+
settings: StreamVisualSettings;
|
|
47
|
+
error?: undefined;
|
|
48
|
+
} | {
|
|
49
|
+
settings?: undefined;
|
|
50
|
+
error: string;
|
|
51
|
+
};
|
|
52
|
+
declare function readStreamSettings(): StreamVisualSettings;
|
|
53
|
+
declare function writeStreamSettings(settings: StreamVisualSettings): void;
|
|
54
|
+
/**
|
|
55
|
+
* Build the visual config for browser-capture by merging:
|
|
56
|
+
* 1. Server-side stream-settings.json (authoritative)
|
|
57
|
+
* 2. Environment variables (STREAM_THEME, STREAM_AVATAR_INDEX) as fallback
|
|
58
|
+
*
|
|
59
|
+
* Reads the active destination's overlay layout when available.
|
|
60
|
+
*/
|
|
61
|
+
declare function getHeadlessCaptureConfig(destinationId?: string | null): {
|
|
62
|
+
overlayLayout?: string;
|
|
63
|
+
theme?: string;
|
|
64
|
+
avatarIndex?: number;
|
|
65
|
+
destinationId?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export { type StreamVisualSettings, type StreamVoiceSettings, getHeadlessCaptureConfig, parseDestinationQuery, readOverlayLayout, readStreamSettings, safeDestId, seedOverlayDefaults, validateStreamSettings, writeOverlayLayout, writeStreamSettings };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { logger } from "@elizaos/core";
|
|
4
|
+
const OVERLAY_DIR = path.join(
|
|
5
|
+
process.env.ELIZA_DATA_DIR || path.join(process.cwd(), "data"),
|
|
6
|
+
"stream"
|
|
7
|
+
);
|
|
8
|
+
const SETTINGS_FILE = path.join(OVERLAY_DIR, "stream-settings.json");
|
|
9
|
+
function safeDestId(id) {
|
|
10
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
11
|
+
}
|
|
12
|
+
function overlayFileForDestination(destinationId) {
|
|
13
|
+
if (destinationId) {
|
|
14
|
+
return path.join(
|
|
15
|
+
OVERLAY_DIR,
|
|
16
|
+
`overlay-layout-${safeDestId(destinationId)}.json`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return path.join(OVERLAY_DIR, "overlay-layout.json");
|
|
20
|
+
}
|
|
21
|
+
function parseDestinationQuery(url) {
|
|
22
|
+
if (!url) return void 0;
|
|
23
|
+
try {
|
|
24
|
+
const u = new URL(url, "http://localhost");
|
|
25
|
+
return u.searchParams.get("destination") || void 0;
|
|
26
|
+
} catch {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function getOverlayLayoutJson(destinationId) {
|
|
31
|
+
const files = destinationId ? [
|
|
32
|
+
overlayFileForDestination(destinationId),
|
|
33
|
+
overlayFileForDestination(null)
|
|
34
|
+
] : [overlayFileForDestination(null)];
|
|
35
|
+
for (const f of files) {
|
|
36
|
+
try {
|
|
37
|
+
if (fs.existsSync(f)) {
|
|
38
|
+
return fs.readFileSync(f, "utf-8");
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function readOverlayLayout(destinationId, destination) {
|
|
46
|
+
if (destinationId) {
|
|
47
|
+
const destFile = overlayFileForDestination(destinationId);
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(destFile)) {
|
|
50
|
+
return JSON.parse(fs.readFileSync(destFile, "utf-8"));
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
logger.warn(
|
|
54
|
+
`[stream] Failed to read overlay layout for ${destinationId}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const globalFile = overlayFileForDestination(null);
|
|
59
|
+
try {
|
|
60
|
+
if (fs.existsSync(globalFile)) {
|
|
61
|
+
return JSON.parse(fs.readFileSync(globalFile, "utf-8"));
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
logger.warn("[stream] Failed to read global overlay layout file");
|
|
65
|
+
}
|
|
66
|
+
if (destination?.defaultOverlayLayout) {
|
|
67
|
+
return destination.defaultOverlayLayout;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function writeOverlayLayout(layout, destinationId) {
|
|
72
|
+
fs.mkdirSync(OVERLAY_DIR, { recursive: true });
|
|
73
|
+
const file = overlayFileForDestination(destinationId);
|
|
74
|
+
fs.writeFileSync(file, JSON.stringify(layout, null, 2), "utf-8");
|
|
75
|
+
const label = destinationId ? `[${destinationId}]` : "[global]";
|
|
76
|
+
logger.info(`[stream] Overlay layout ${label} saved`);
|
|
77
|
+
}
|
|
78
|
+
function seedOverlayDefaults(destination) {
|
|
79
|
+
if (!destination.defaultOverlayLayout) return;
|
|
80
|
+
const destFile = overlayFileForDestination(destination.id);
|
|
81
|
+
if (fs.existsSync(destFile)) return;
|
|
82
|
+
writeOverlayLayout(destination.defaultOverlayLayout, destination.id);
|
|
83
|
+
logger.info(`[stream] Seeded default overlay layout for ${destination.name}`);
|
|
84
|
+
}
|
|
85
|
+
const SETTINGS_MAX_JSON_BYTES = 4096;
|
|
86
|
+
function validateStreamSettings(raw) {
|
|
87
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
88
|
+
return { error: "Settings must be a non-array object" };
|
|
89
|
+
}
|
|
90
|
+
const serialized = JSON.stringify(raw);
|
|
91
|
+
if (serialized.length > SETTINGS_MAX_JSON_BYTES) {
|
|
92
|
+
return {
|
|
93
|
+
error: `Settings payload exceeds ${SETTINGS_MAX_JSON_BYTES} byte limit`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const input = raw;
|
|
97
|
+
const result = {};
|
|
98
|
+
if ("theme" in input) {
|
|
99
|
+
if (typeof input.theme !== "string" || input.theme.length > 64) {
|
|
100
|
+
return { error: "theme must be a string (max 64 chars)" };
|
|
101
|
+
}
|
|
102
|
+
result.theme = input.theme;
|
|
103
|
+
}
|
|
104
|
+
if ("avatarIndex" in input) {
|
|
105
|
+
if (typeof input.avatarIndex !== "number" || !Number.isInteger(input.avatarIndex) || input.avatarIndex < 0 || input.avatarIndex > 999) {
|
|
106
|
+
return { error: "avatarIndex must be an integer between 0 and 999" };
|
|
107
|
+
}
|
|
108
|
+
result.avatarIndex = input.avatarIndex;
|
|
109
|
+
}
|
|
110
|
+
if ("voice" in input) {
|
|
111
|
+
if (!input.voice || typeof input.voice !== "object" || Array.isArray(input.voice)) {
|
|
112
|
+
return { error: "voice must be an object" };
|
|
113
|
+
}
|
|
114
|
+
const v = input.voice;
|
|
115
|
+
const voice = {
|
|
116
|
+
enabled: false
|
|
117
|
+
};
|
|
118
|
+
if ("enabled" in v) {
|
|
119
|
+
if (typeof v.enabled !== "boolean") {
|
|
120
|
+
return { error: "voice.enabled must be a boolean" };
|
|
121
|
+
}
|
|
122
|
+
voice.enabled = v.enabled;
|
|
123
|
+
}
|
|
124
|
+
if ("autoSpeak" in v) {
|
|
125
|
+
if (typeof v.autoSpeak !== "boolean") {
|
|
126
|
+
return { error: "voice.autoSpeak must be a boolean" };
|
|
127
|
+
}
|
|
128
|
+
voice.autoSpeak = v.autoSpeak;
|
|
129
|
+
}
|
|
130
|
+
if ("provider" in v) {
|
|
131
|
+
if (typeof v.provider !== "string" || v.provider.length > 64) {
|
|
132
|
+
return { error: "voice.provider must be a string (max 64 chars)" };
|
|
133
|
+
}
|
|
134
|
+
voice.provider = v.provider;
|
|
135
|
+
}
|
|
136
|
+
result.voice = voice;
|
|
137
|
+
}
|
|
138
|
+
const knownKeys = /* @__PURE__ */ new Set(["theme", "avatarIndex", "voice"]);
|
|
139
|
+
for (const key of Object.keys(input)) {
|
|
140
|
+
if (!knownKeys.has(key)) {
|
|
141
|
+
return { error: `Unknown settings key: ${key}` };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { settings: result };
|
|
145
|
+
}
|
|
146
|
+
function readStreamSettings() {
|
|
147
|
+
try {
|
|
148
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
149
|
+
return JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
logger.warn("[stream] Failed to read stream settings file");
|
|
153
|
+
}
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
function writeStreamSettings(settings) {
|
|
157
|
+
fs.mkdirSync(OVERLAY_DIR, { recursive: true });
|
|
158
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf-8");
|
|
159
|
+
logger.info("[stream] Stream settings saved");
|
|
160
|
+
}
|
|
161
|
+
function getHeadlessCaptureConfig(destinationId) {
|
|
162
|
+
const settings = readStreamSettings();
|
|
163
|
+
return {
|
|
164
|
+
overlayLayout: getOverlayLayoutJson(destinationId) ?? void 0,
|
|
165
|
+
theme: settings.theme ?? process.env.STREAM_THEME,
|
|
166
|
+
avatarIndex: settings.avatarIndex ?? (process.env.STREAM_AVATAR_INDEX ? parseInt(process.env.STREAM_AVATAR_INDEX, 10) : void 0),
|
|
167
|
+
destinationId: destinationId ?? void 0
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
getHeadlessCaptureConfig,
|
|
172
|
+
parseDestinationQuery,
|
|
173
|
+
readOverlayLayout,
|
|
174
|
+
readStreamSettings,
|
|
175
|
+
safeDestId,
|
|
176
|
+
seedOverlayDefaults,
|
|
177
|
+
validateStreamSettings,
|
|
178
|
+
writeOverlayLayout,
|
|
179
|
+
writeStreamSettings
|
|
180
|
+
};
|
|
181
|
+
//# sourceMappingURL=stream-persistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/stream-persistence.ts"],"sourcesContent":["/**\n * Stream persistence layer — overlay layout and visual/voice settings I/O.\n *\n * Extracted from stream-routes.ts to keep that file focused on route handling.\n * All functions here deal with reading/writing JSON files under the\n * `data/stream/` directory.\n *\n * @module api/stream-persistence\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { logger } from \"@elizaos/core\";\nimport type { StreamingDestination } from \"./streaming-types.js\";\n\n// ---------------------------------------------------------------------------\n// Interfaces\n// ---------------------------------------------------------------------------\n\nexport interface StreamVoiceSettings {\n enabled: boolean;\n provider?: string;\n autoSpeak?: boolean;\n}\n\nexport interface StreamVisualSettings {\n theme?: string;\n avatarIndex?: number;\n voice?: StreamVoiceSettings;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst OVERLAY_DIR = path.join(\n process.env.ELIZA_DATA_DIR || path.join(process.cwd(), \"data\"),\n \"stream\",\n);\n\nconst SETTINGS_FILE = path.join(OVERLAY_DIR, \"stream-settings.json\");\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Sanitize destination ID for use as a filename segment. */\nexport function safeDestId(id: string): string {\n return id.replace(/[^a-zA-Z0-9_-]/g, \"\");\n}\n\n/** Return the layout file path for a given destination (or global default). */\nfunction overlayFileForDestination(destinationId?: string | null): string {\n if (destinationId) {\n return path.join(\n OVERLAY_DIR,\n `overlay-layout-${safeDestId(destinationId)}.json`,\n );\n }\n return path.join(OVERLAY_DIR, \"overlay-layout.json\");\n}\n\n/** Extract `?destination=<id>` from the raw request URL. */\nexport function parseDestinationQuery(url?: string): string | undefined {\n if (!url) return undefined;\n try {\n const u = new URL(url, \"http://localhost\");\n return u.searchParams.get(\"destination\") || undefined;\n } catch {\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Overlay layout persistence (per-destination JSON files)\n// ---------------------------------------------------------------------------\n\n/** Read overlay layout as JSON string for seeding into headless browser. */\nfunction getOverlayLayoutJson(destinationId?: string | null): string | null {\n const files = destinationId\n ? [\n overlayFileForDestination(destinationId),\n overlayFileForDestination(null),\n ]\n : [overlayFileForDestination(null)];\n for (const f of files) {\n try {\n if (fs.existsSync(f)) {\n return fs.readFileSync(f, \"utf-8\");\n }\n } catch {\n // Not available\n }\n }\n return null;\n}\n\n/**\n * Read overlay layout for a destination.\n * Falls back: destination-specific -> global -> plugin default -> null.\n */\nexport function readOverlayLayout(\n destinationId?: string | null,\n destination?: StreamingDestination,\n): unknown {\n if (destinationId) {\n const destFile = overlayFileForDestination(destinationId);\n try {\n if (fs.existsSync(destFile)) {\n return JSON.parse(fs.readFileSync(destFile, \"utf-8\"));\n }\n } catch {\n logger.warn(\n `[stream] Failed to read overlay layout for ${destinationId}`,\n );\n }\n }\n\n const globalFile = overlayFileForDestination(null);\n try {\n if (fs.existsSync(globalFile)) {\n return JSON.parse(fs.readFileSync(globalFile, \"utf-8\"));\n }\n } catch {\n logger.warn(\"[stream] Failed to read global overlay layout file\");\n }\n\n if (destination?.defaultOverlayLayout) {\n return destination.defaultOverlayLayout;\n }\n\n return null;\n}\n\n/** Write overlay layout (to destination-specific or global file). */\nexport function writeOverlayLayout(\n layout: unknown,\n destinationId?: string | null,\n): void {\n fs.mkdirSync(OVERLAY_DIR, { recursive: true });\n const file = overlayFileForDestination(destinationId);\n fs.writeFileSync(file, JSON.stringify(layout, null, 2), \"utf-8\");\n const label = destinationId ? `[${destinationId}]` : \"[global]\";\n logger.info(`[stream] Overlay layout ${label} saved`);\n}\n\n/**\n * Seed the plugin's default overlay layout on first stream start.\n * Only writes if no destination-specific layout file exists yet.\n */\nexport function seedOverlayDefaults(destination: StreamingDestination): void {\n if (!destination.defaultOverlayLayout) return;\n const destFile = overlayFileForDestination(destination.id);\n if (fs.existsSync(destFile)) return;\n writeOverlayLayout(destination.defaultOverlayLayout, destination.id);\n logger.info(`[stream] Seeded default overlay layout for ${destination.name}`);\n}\n\n// ---------------------------------------------------------------------------\n// Stream visual/voice settings persistence\n// ---------------------------------------------------------------------------\n\nconst SETTINGS_MAX_JSON_BYTES = 4096;\n\n/**\n * Validate and sanitize a raw settings object into a safe StreamVisualSettings.\n * Only allows known keys with expected types — rejects everything else.\n * Returns null with an error message if validation fails.\n */\nexport function validateStreamSettings(\n raw: unknown,\n):\n | { settings: StreamVisualSettings; error?: undefined }\n | { settings?: undefined; error: string } {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n return { error: \"Settings must be a non-array object\" };\n }\n\n // Reject if serialized payload is too large\n const serialized = JSON.stringify(raw);\n if (serialized.length > SETTINGS_MAX_JSON_BYTES) {\n return {\n error: `Settings payload exceeds ${SETTINGS_MAX_JSON_BYTES} byte limit`,\n };\n }\n\n const input = raw as Record<string, unknown>;\n const result: StreamVisualSettings = {};\n\n // theme: optional string, max 64 chars\n if (\"theme\" in input) {\n if (typeof input.theme !== \"string\" || input.theme.length > 64) {\n return { error: \"theme must be a string (max 64 chars)\" };\n }\n result.theme = input.theme;\n }\n\n // avatarIndex: optional non-negative integer\n if (\"avatarIndex\" in input) {\n if (\n typeof input.avatarIndex !== \"number\" ||\n !Number.isInteger(input.avatarIndex) ||\n input.avatarIndex < 0 ||\n input.avatarIndex > 999\n ) {\n return { error: \"avatarIndex must be an integer between 0 and 999\" };\n }\n result.avatarIndex = input.avatarIndex;\n }\n\n // voice: optional object with known fields\n if (\"voice\" in input) {\n if (\n !input.voice ||\n typeof input.voice !== \"object\" ||\n Array.isArray(input.voice)\n ) {\n return { error: \"voice must be an object\" };\n }\n const v = input.voice as Record<string, unknown>;\n const voice: StreamVoiceSettings = {\n enabled: false,\n };\n if (\"enabled\" in v) {\n if (typeof v.enabled !== \"boolean\") {\n return { error: \"voice.enabled must be a boolean\" };\n }\n voice.enabled = v.enabled;\n }\n if (\"autoSpeak\" in v) {\n if (typeof v.autoSpeak !== \"boolean\") {\n return { error: \"voice.autoSpeak must be a boolean\" };\n }\n voice.autoSpeak = v.autoSpeak;\n }\n if (\"provider\" in v) {\n if (typeof v.provider !== \"string\" || v.provider.length > 64) {\n return { error: \"voice.provider must be a string (max 64 chars)\" };\n }\n voice.provider = v.provider;\n }\n result.voice = voice;\n }\n\n // Reject unknown top-level keys\n const knownKeys = new Set([\"theme\", \"avatarIndex\", \"voice\"]);\n for (const key of Object.keys(input)) {\n if (!knownKeys.has(key)) {\n return { error: `Unknown settings key: ${key}` };\n }\n }\n\n return { settings: result };\n}\n\nexport function readStreamSettings(): StreamVisualSettings {\n try {\n if (fs.existsSync(SETTINGS_FILE)) {\n return JSON.parse(fs.readFileSync(SETTINGS_FILE, \"utf-8\"));\n }\n } catch {\n logger.warn(\"[stream] Failed to read stream settings file\");\n }\n return {};\n}\n\nexport function writeStreamSettings(settings: StreamVisualSettings): void {\n fs.mkdirSync(OVERLAY_DIR, { recursive: true });\n fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), \"utf-8\");\n logger.info(\"[stream] Stream settings saved\");\n}\n\n/**\n * Build the visual config for browser-capture by merging:\n * 1. Server-side stream-settings.json (authoritative)\n * 2. Environment variables (STREAM_THEME, STREAM_AVATAR_INDEX) as fallback\n *\n * Reads the active destination's overlay layout when available.\n */\nexport function getHeadlessCaptureConfig(destinationId?: string | null): {\n overlayLayout?: string;\n theme?: string;\n avatarIndex?: number;\n destinationId?: string;\n} {\n const settings = readStreamSettings();\n return {\n overlayLayout: getOverlayLayoutJson(destinationId) ?? undefined,\n theme: settings.theme ?? process.env.STREAM_THEME,\n avatarIndex:\n settings.avatarIndex ??\n (process.env.STREAM_AVATAR_INDEX\n ? parseInt(process.env.STREAM_AVATAR_INDEX, 10)\n : undefined),\n destinationId: destinationId ?? undefined,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { StreamingDestination } from '../core.js';
|
|
2
|
+
import '@elizaos/core';
|
|
3
|
+
|
|
4
|
+
interface StreamRouteState {
|
|
5
|
+
streamManager: {
|
|
6
|
+
isRunning(): boolean;
|
|
7
|
+
writeFrame(buf: Buffer): boolean;
|
|
8
|
+
start(config: unknown): Promise<void>;
|
|
9
|
+
stop(): Promise<{
|
|
10
|
+
uptime: number;
|
|
11
|
+
}>;
|
|
12
|
+
getHealth(): {
|
|
13
|
+
running: boolean;
|
|
14
|
+
ffmpegAlive: boolean;
|
|
15
|
+
uptime: number;
|
|
16
|
+
frameCount: number;
|
|
17
|
+
volume: number;
|
|
18
|
+
muted: boolean;
|
|
19
|
+
audioSource: string;
|
|
20
|
+
inputMode: string | null;
|
|
21
|
+
};
|
|
22
|
+
getVolume(): number;
|
|
23
|
+
isMuted(): boolean;
|
|
24
|
+
setVolume(level: number): Promise<void>;
|
|
25
|
+
mute(): Promise<void>;
|
|
26
|
+
unmute(): Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
port?: number;
|
|
29
|
+
captureUrl?: string;
|
|
30
|
+
screenCapture?: {
|
|
31
|
+
isFrameCaptureActive(): boolean;
|
|
32
|
+
startFrameCapture(opts: {
|
|
33
|
+
fps?: number;
|
|
34
|
+
quality?: number;
|
|
35
|
+
endpoint?: string;
|
|
36
|
+
gameUrl?: string;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
stopFrameCapture?(): void;
|
|
39
|
+
};
|
|
40
|
+
destinations: Map<string, StreamingDestination>;
|
|
41
|
+
activeDestinationId?: string;
|
|
42
|
+
activeStreamSource: {
|
|
43
|
+
type: "stream-tab" | "game" | "custom-url";
|
|
44
|
+
url?: string;
|
|
45
|
+
};
|
|
46
|
+
config?: {
|
|
47
|
+
messages?: {
|
|
48
|
+
tts?: Record<string, unknown>;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* When the client saves stream visual settings, mirror `avatarIndex` into
|
|
53
|
+
* Eliza `config.ui` (and live server state). Greeting + identity read
|
|
54
|
+
* `config.ui`, while avatar taps only hit `/api/stream/settings` today.
|
|
55
|
+
*/
|
|
56
|
+
mirrorStreamAvatarToElizaConfig?: (avatarIndex: number) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type { StreamRouteState };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=stream-route-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* Generic streaming infrastructure routes.
|
|
12
|
+
*
|
|
13
|
+
* Shared pipeline for all streaming destinations (custom RTMP, Twitch,
|
|
14
|
+
* YouTube, etc.): capture mode detection, Xvfb management, browser capture,
|
|
15
|
+
* FFmpeg, frame routing, volume/mute.
|
|
16
|
+
*
|
|
17
|
+
* Platform-specific credential fetching lives in destination adapters.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Resolve the active streaming destination from the registry. */
|
|
21
|
+
declare function getActiveDestination(state: StreamRouteState): StreamingDestination | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Detect the best capture mode for the current environment.
|
|
24
|
+
*
|
|
25
|
+
* Priority:
|
|
26
|
+
* 1. STREAM_MODE env var (explicit override)
|
|
27
|
+
* 2. Desktop screen capture bridge -> "pipe" (POST /api/stream/frame -> FFmpeg stdin)
|
|
28
|
+
* 3. Linux with DISPLAY or Xvfb -> "x11grab" (GPU-backed game-stream approach)
|
|
29
|
+
* 4. macOS -> "avfoundation" (native screen capture)
|
|
30
|
+
* 5. Fallback -> "file" (Puppeteer CDP -> temp JPEG -> FFmpeg)
|
|
31
|
+
*/
|
|
32
|
+
/** @internal Exported for testing. */
|
|
33
|
+
declare function detectCaptureMode(): StreamConfig["inputMode"];
|
|
34
|
+
/**
|
|
35
|
+
* Try to start Xvfb on the specified display if not already running (Linux only).
|
|
36
|
+
* Returns true if display is available, false otherwise.
|
|
37
|
+
*/
|
|
38
|
+
/** @internal Exported for testing. */
|
|
39
|
+
declare function ensureXvfb(display: string, resolution: string): Promise<boolean>;
|
|
40
|
+
/** 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 };
|