@elizaos/plugin-streaming 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
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":[]}
@@ -0,0 +1,10 @@
1
+ type StreamingUpdateKind = "noop" | "append" | "replace";
2
+ interface StreamingUpdate {
3
+ kind: StreamingUpdateKind;
4
+ nextText: string;
5
+ emittedText: string;
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 };
@@ -0,0 +1,88 @@
1
+ function commonPrefixLength(left, right) {
2
+ const maxLength = Math.min(left.length, right.length);
3
+ let index = 0;
4
+ while (index < maxLength && left.charCodeAt(index) === right.charCodeAt(index)) {
5
+ index += 1;
6
+ }
7
+ return index;
8
+ }
9
+ function commonSuffixLength(left, right, sharedPrefixLength) {
10
+ const maxLength = Math.min(
11
+ left.length - sharedPrefixLength,
12
+ right.length - sharedPrefixLength
13
+ );
14
+ let length = 0;
15
+ while (length < maxLength && left.charCodeAt(left.length - 1 - length) === right.charCodeAt(right.length - 1 - length)) {
16
+ length += 1;
17
+ }
18
+ return length;
19
+ }
20
+ function isLikelySnapshotReplacement(existing, incoming) {
21
+ const sharedPrefixLength = commonPrefixLength(existing, incoming);
22
+ const sharedSuffixLength = commonSuffixLength(
23
+ existing,
24
+ incoming,
25
+ sharedPrefixLength
26
+ );
27
+ const sharedLength = sharedPrefixLength + sharedSuffixLength;
28
+ const minLength = Math.min(existing.length, incoming.length);
29
+ return sharedPrefixLength >= 8 || sharedLength >= Math.max(4, Math.ceil(minLength * 0.7));
30
+ }
31
+ function mergeStreamingText(existing, incoming) {
32
+ if (!incoming) return existing;
33
+ if (!existing) return incoming;
34
+ if (incoming === existing) return existing;
35
+ if (incoming.startsWith(existing)) {
36
+ return incoming;
37
+ }
38
+ if (incoming.includes(existing)) {
39
+ return incoming;
40
+ }
41
+ if (existing.startsWith(incoming)) {
42
+ return existing;
43
+ }
44
+ const maxOverlap = Math.min(existing.length, incoming.length);
45
+ const existingLength = existing.length;
46
+ for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
47
+ const existingStart = existingLength - overlap;
48
+ let match = true;
49
+ for (let index = 0; index < overlap; index += 1) {
50
+ if (existing.charCodeAt(existingStart + index) !== incoming.charCodeAt(index)) {
51
+ match = false;
52
+ break;
53
+ }
54
+ }
55
+ if (!match) continue;
56
+ if (overlap === incoming.length) {
57
+ return incoming.length === 1 ? `${existing}${incoming}` : existing;
58
+ }
59
+ return `${existing}${incoming.slice(overlap)}`;
60
+ }
61
+ if (isLikelySnapshotReplacement(existing, incoming)) {
62
+ return incoming;
63
+ }
64
+ return `${existing}${incoming}`;
65
+ }
66
+ function resolveStreamingUpdate(existing, incoming) {
67
+ const nextText = mergeStreamingText(existing, incoming);
68
+ if (nextText === existing) {
69
+ return { kind: "noop", nextText: existing, emittedText: "" };
70
+ }
71
+ if (nextText.startsWith(existing)) {
72
+ return {
73
+ kind: "append",
74
+ nextText,
75
+ emittedText: nextText.slice(existing.length)
76
+ };
77
+ }
78
+ return {
79
+ kind: "replace",
80
+ nextText,
81
+ emittedText: nextText
82
+ };
83
+ }
84
+ export {
85
+ mergeStreamingText,
86
+ resolveStreamingUpdate
87
+ };
88
+ //# sourceMappingURL=streaming-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/streaming-text.ts"],"sourcesContent":["export type StreamingUpdateKind = \"noop\" | \"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: \"noop\", 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,QAAQ,UAAU,UAAU,aAAa,GAAG;AAAA,EAC7D;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":[]}
@@ -0,0 +1,2 @@
1
+ export { OverlayLayoutData, OverlayWidgetInstance, StreamingDestination } from '../core.js';
2
+ import '@elizaos/core';
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=streaming-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,25 @@
1
+ import http from 'node:http';
2
+ import { ReadJsonBodyOptions } from '@elizaos/core';
3
+
4
+ interface TtsRouteContext {
5
+ req: http.IncomingMessage;
6
+ res: http.ServerResponse;
7
+ method: string;
8
+ pathname: string;
9
+ state: {
10
+ config: Record<string, unknown>;
11
+ };
12
+ json: (res: http.ServerResponse, data: unknown, status?: number) => void;
13
+ error: (res: http.ServerResponse, message: string, status?: number) => void;
14
+ readJsonBody: <T extends object>(req: http.IncomingMessage, res: http.ServerResponse, options?: ReadJsonBodyOptions) => Promise<T | null>;
15
+ isRedactedSecretValue: (value: unknown) => boolean;
16
+ fetchWithTimeoutGuard: (url: string, init: RequestInit, timeoutMs: number) => Promise<Response>;
17
+ streamResponseBodyWithByteLimit: (upstream: Response, res: http.ServerResponse, maxBytes: number, timeoutMs: number) => Promise<void>;
18
+ responseContentLength: (headers: Pick<Headers, "get">) => number | null;
19
+ isAbortError: (error: unknown) => boolean;
20
+ ELEVENLABS_FETCH_TIMEOUT_MS: number;
21
+ ELEVENLABS_AUDIO_MAX_BYTES: number;
22
+ }
23
+ declare function handleTtsRoutes(ctx: TtsRouteContext): Promise<boolean>;
24
+
25
+ export { type TtsRouteContext, handleTtsRoutes };
@@ -0,0 +1,158 @@
1
+ import { sanitizeSpeechText } from "@elizaos/core";
2
+ async function handleTtsRoutes(ctx) {
3
+ const { req, res, method, pathname, state, json, error, readJsonBody } = ctx;
4
+ if (method === "GET" && pathname === "/api/tts/config") {
5
+ const messages = state.config && typeof state.config === "object" ? state.config.messages : void 0;
6
+ const tts = messages && typeof messages === "object" ? messages.tts ?? void 0 : void 0;
7
+ const elevenlabs = tts && typeof tts === "object" ? tts.elevenlabs ?? void 0 : void 0;
8
+ const edge = tts && typeof tts === "object" ? tts.edge ?? void 0 : void 0;
9
+ const openai = tts && typeof tts === "object" ? tts.openai ?? void 0 : void 0;
10
+ json(res, {
11
+ provider: typeof tts?.provider === "string" ? tts.provider : void 0,
12
+ mode: typeof tts?.mode === "string" ? tts.mode : void 0,
13
+ auto: typeof tts?.auto === "string" ? tts.auto : void 0,
14
+ enabled: tts?.enabled === true,
15
+ elevenlabs: elevenlabs ? {
16
+ apiKey: typeof elevenlabs.apiKey === "string" && elevenlabs.apiKey.trim() && !ctx.isRedactedSecretValue(elevenlabs.apiKey) ? "[REDACTED]" : void 0,
17
+ voiceId: typeof elevenlabs.voiceId === "string" ? elevenlabs.voiceId : void 0,
18
+ modelId: typeof elevenlabs.modelId === "string" ? elevenlabs.modelId : void 0,
19
+ stability: typeof elevenlabs.voiceSettings?.stability === "number" ? elevenlabs.voiceSettings.stability : void 0,
20
+ similarityBoost: typeof elevenlabs.voiceSettings?.similarityBoost === "number" ? elevenlabs.voiceSettings.similarityBoost : void 0,
21
+ speed: typeof elevenlabs.voiceSettings?.speed === "number" ? elevenlabs.voiceSettings.speed : void 0
22
+ } : void 0,
23
+ edge: edge ? {
24
+ voice: typeof edge.voice === "string" ? edge.voice : void 0,
25
+ lang: typeof edge.lang === "string" ? edge.lang : void 0,
26
+ rate: typeof edge.rate === "string" ? edge.rate : void 0,
27
+ pitch: typeof edge.pitch === "string" ? edge.pitch : void 0,
28
+ volume: typeof edge.volume === "string" ? edge.volume : void 0
29
+ } : void 0,
30
+ openai: openai ? {
31
+ apiKey: typeof openai.apiKey === "string" && openai.apiKey.trim() && !ctx.isRedactedSecretValue(openai.apiKey) ? "[REDACTED]" : void 0,
32
+ model: typeof openai.model === "string" ? openai.model : void 0,
33
+ voice: typeof openai.voice === "string" ? openai.voice : void 0
34
+ } : void 0
35
+ });
36
+ return true;
37
+ }
38
+ if (method === "POST" && pathname === "/api/tts/elevenlabs") {
39
+ const body = await readJsonBody(req, res);
40
+ if (!body) return true;
41
+ const text = typeof body.text === "string" ? sanitizeSpeechText(body.text) : "";
42
+ if (!text) {
43
+ error(res, "Missing text", 400);
44
+ return true;
45
+ }
46
+ const messages = state.config && typeof state.config === "object" ? state.config.messages : void 0;
47
+ const tts = messages && typeof messages === "object" ? messages.tts ?? void 0 : void 0;
48
+ const eleven = tts && typeof tts === "object" ? tts.elevenlabs ?? void 0 : void 0;
49
+ const requestedApiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
50
+ const configuredApiKey = typeof eleven?.apiKey === "string" ? eleven.apiKey.trim() : "";
51
+ const envApiKey = typeof process.env.ELEVENLABS_API_KEY === "string" ? process.env.ELEVENLABS_API_KEY.trim() : "";
52
+ const resolvedApiKey = requestedApiKey && !ctx.isRedactedSecretValue(requestedApiKey) ? requestedApiKey : configuredApiKey && !ctx.isRedactedSecretValue(configuredApiKey) ? configuredApiKey : envApiKey && !ctx.isRedactedSecretValue(envApiKey) ? envApiKey : "";
53
+ if (!resolvedApiKey) {
54
+ error(
55
+ res,
56
+ "ElevenLabs API key is not available. Set ELEVENLABS_API_KEY in Secrets.",
57
+ 400
58
+ );
59
+ return true;
60
+ }
61
+ const voiceId = typeof body.voiceId === "string" && body.voiceId.trim() || typeof eleven?.voiceId === "string" && eleven.voiceId.trim() || "EXAVITQu4vr4xnSDxMaL";
62
+ const modelId = typeof body.modelId === "string" && body.modelId.trim() || typeof eleven?.modelId === "string" && eleven.modelId.trim() || "eleven_flash_v2_5";
63
+ const outputFormat = typeof body.outputFormat === "string" && body.outputFormat.trim() || "mp3_22050_32";
64
+ const requestedVoiceSettings = body.voice_settings && typeof body.voice_settings === "object" && !Array.isArray(body.voice_settings) ? body.voice_settings : void 0;
65
+ const voiceSettings = {};
66
+ const stability = requestedVoiceSettings?.stability;
67
+ if (typeof stability === "number" && stability >= 0 && stability <= 1) {
68
+ voiceSettings.stability = stability;
69
+ }
70
+ const similarityBoost = requestedVoiceSettings?.similarity_boost;
71
+ if (typeof similarityBoost === "number" && similarityBoost >= 0 && similarityBoost <= 1) {
72
+ voiceSettings.similarity_boost = similarityBoost;
73
+ }
74
+ const speed = requestedVoiceSettings?.speed;
75
+ if (typeof speed === "number" && speed >= 0.5 && speed <= 2) {
76
+ voiceSettings.speed = speed;
77
+ }
78
+ const payload = {
79
+ text,
80
+ model_id: modelId,
81
+ apply_text_normalization: body.apply_text_normalization === "on" || body.apply_text_normalization === "off" ? body.apply_text_normalization : "auto"
82
+ };
83
+ if (Object.keys(voiceSettings).length > 0) {
84
+ payload.voice_settings = voiceSettings;
85
+ }
86
+ try {
87
+ const upstreamUrl = new URL(
88
+ `https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream`
89
+ );
90
+ upstreamUrl.searchParams.set("output_format", outputFormat);
91
+ const upstream = await ctx.fetchWithTimeoutGuard(
92
+ upstreamUrl.toString(),
93
+ {
94
+ method: "POST",
95
+ headers: {
96
+ "xi-api-key": resolvedApiKey,
97
+ "Content-Type": "application/json",
98
+ Accept: "audio/mpeg"
99
+ },
100
+ body: JSON.stringify(payload)
101
+ },
102
+ ctx.ELEVENLABS_FETCH_TIMEOUT_MS
103
+ );
104
+ if (!upstream.ok) {
105
+ const upstreamBody = await upstream.text().catch(() => "");
106
+ error(
107
+ res,
108
+ `ElevenLabs request failed (${upstream.status}): ${upstreamBody.slice(0, 240)}`,
109
+ upstream.status === 429 ? 429 : 502
110
+ );
111
+ return true;
112
+ }
113
+ const contentType = upstream.headers.get("content-type") || "audio/mpeg";
114
+ const contentLength = ctx.responseContentLength(upstream.headers);
115
+ if (contentLength !== null && contentLength > ctx.ELEVENLABS_AUDIO_MAX_BYTES) {
116
+ error(
117
+ res,
118
+ `ElevenLabs response exceeds maximum size of ${ctx.ELEVENLABS_AUDIO_MAX_BYTES} bytes`,
119
+ 502
120
+ );
121
+ return true;
122
+ }
123
+ res.writeHead(200, {
124
+ "Content-Type": contentType,
125
+ "Cache-Control": "no-store",
126
+ ...contentLength !== null ? { "Content-Length": String(contentLength) } : {}
127
+ });
128
+ await ctx.streamResponseBodyWithByteLimit(
129
+ upstream,
130
+ res,
131
+ ctx.ELEVENLABS_AUDIO_MAX_BYTES,
132
+ ctx.ELEVENLABS_FETCH_TIMEOUT_MS
133
+ );
134
+ res.end();
135
+ return true;
136
+ } catch (err) {
137
+ if (res.headersSent) {
138
+ res.destroy(
139
+ err instanceof Error ? err : new Error(
140
+ `ElevenLabs proxy error: ${typeof err === "string" ? err : String(err)}`
141
+ )
142
+ );
143
+ return true;
144
+ }
145
+ error(
146
+ res,
147
+ `ElevenLabs proxy error: ${err instanceof Error ? err.message : String(err)}`,
148
+ ctx.isAbortError(err) ? 504 : 502
149
+ );
150
+ return true;
151
+ }
152
+ }
153
+ return false;
154
+ }
155
+ export {
156
+ handleTtsRoutes
157
+ };
158
+ //# sourceMappingURL=tts-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/tts-routes.ts"],"sourcesContent":["import type http from \"node:http\";\nimport { type ReadJsonBodyOptions, sanitizeSpeechText } from \"@elizaos/core\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TtsRouteContext {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n method: string;\n pathname: string;\n state: { config: Record<string, unknown> };\n json: (res: http.ServerResponse, data: unknown, status?: number) => void;\n error: (res: http.ServerResponse, message: string, status?: number) => void;\n readJsonBody: <T extends object>(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n options?: ReadJsonBodyOptions,\n ) => Promise<T | null>;\n isRedactedSecretValue: (value: unknown) => boolean;\n fetchWithTimeoutGuard: (\n url: string,\n init: RequestInit,\n timeoutMs: number,\n ) => Promise<Response>;\n streamResponseBodyWithByteLimit: (\n upstream: Response,\n res: http.ServerResponse,\n maxBytes: number,\n timeoutMs: number,\n ) => Promise<void>;\n responseContentLength: (headers: Pick<Headers, \"get\">) => number | null;\n isAbortError: (error: unknown) => boolean;\n ELEVENLABS_FETCH_TIMEOUT_MS: number;\n ELEVENLABS_AUDIO_MAX_BYTES: number;\n}\n\n// ---------------------------------------------------------------------------\n// Route handler\n// ---------------------------------------------------------------------------\n\nexport async function handleTtsRoutes(ctx: TtsRouteContext): Promise<boolean> {\n const { req, res, method, pathname, state, json, error, readJsonBody } = ctx;\n\n // ── GET /api/tts/config ───────────────────────────────────────────────\n if (method === \"GET\" && pathname === \"/api/tts/config\") {\n const messages =\n state.config && typeof state.config === \"object\"\n ? ((state.config as Record<string, unknown>).messages as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const tts =\n messages && typeof messages === \"object\"\n ? ((messages.tts as Record<string, unknown>) ?? undefined)\n : undefined;\n\n const elevenlabs =\n tts && typeof tts === \"object\"\n ? ((tts.elevenlabs as Record<string, unknown>) ?? undefined)\n : undefined;\n const edge =\n tts && typeof tts === \"object\"\n ? ((tts.edge as Record<string, unknown>) ?? undefined)\n : undefined;\n const openai =\n tts && typeof tts === \"object\"\n ? ((tts.openai as Record<string, unknown>) ?? undefined)\n : undefined;\n\n json(res, {\n provider: typeof tts?.provider === \"string\" ? tts.provider : undefined,\n mode: typeof tts?.mode === \"string\" ? tts.mode : undefined,\n auto: typeof tts?.auto === \"string\" ? tts.auto : undefined,\n enabled: tts?.enabled === true,\n elevenlabs: elevenlabs\n ? {\n apiKey:\n typeof elevenlabs.apiKey === \"string\" &&\n elevenlabs.apiKey.trim() &&\n !ctx.isRedactedSecretValue(elevenlabs.apiKey)\n ? \"[REDACTED]\"\n : undefined,\n voiceId:\n typeof elevenlabs.voiceId === \"string\"\n ? elevenlabs.voiceId\n : undefined,\n modelId:\n typeof elevenlabs.modelId === \"string\"\n ? elevenlabs.modelId\n : undefined,\n stability:\n typeof (\n elevenlabs.voiceSettings as Record<string, unknown> | undefined\n )?.stability === \"number\"\n ? ((elevenlabs.voiceSettings as Record<string, unknown>)\n .stability as number)\n : undefined,\n similarityBoost:\n typeof (\n elevenlabs.voiceSettings as Record<string, unknown> | undefined\n )?.similarityBoost === \"number\"\n ? ((elevenlabs.voiceSettings as Record<string, unknown>)\n .similarityBoost as number)\n : undefined,\n speed:\n typeof (\n elevenlabs.voiceSettings as Record<string, unknown> | undefined\n )?.speed === \"number\"\n ? ((elevenlabs.voiceSettings as Record<string, unknown>)\n .speed as number)\n : undefined,\n }\n : undefined,\n edge: edge\n ? {\n voice: typeof edge.voice === \"string\" ? edge.voice : undefined,\n lang: typeof edge.lang === \"string\" ? edge.lang : undefined,\n rate: typeof edge.rate === \"string\" ? edge.rate : undefined,\n pitch: typeof edge.pitch === \"string\" ? edge.pitch : undefined,\n volume: typeof edge.volume === \"string\" ? edge.volume : undefined,\n }\n : undefined,\n openai: openai\n ? {\n apiKey:\n typeof openai.apiKey === \"string\" &&\n openai.apiKey.trim() &&\n !ctx.isRedactedSecretValue(openai.apiKey)\n ? \"[REDACTED]\"\n : undefined,\n model: typeof openai.model === \"string\" ? openai.model : undefined,\n voice: typeof openai.voice === \"string\" ? openai.voice : undefined,\n }\n : undefined,\n });\n return true;\n }\n\n // ── POST /api/tts/elevenlabs ─────────────────────────────────────────\n if (method === \"POST\" && pathname === \"/api/tts/elevenlabs\") {\n const body = await readJsonBody<{\n text?: string;\n voiceId?: string;\n modelId?: string;\n outputFormat?: string;\n apiKey?: string;\n apply_text_normalization?: \"auto\" | \"on\" | \"off\";\n voice_settings?: {\n stability?: number;\n similarity_boost?: number;\n speed?: number;\n };\n }>(req, res);\n if (!body) return true;\n\n const text =\n typeof body.text === \"string\" ? sanitizeSpeechText(body.text) : \"\";\n if (!text) {\n error(res, \"Missing text\", 400);\n return true;\n }\n\n const messages =\n state.config && typeof state.config === \"object\"\n ? ((state.config as Record<string, unknown>).messages as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const tts =\n messages && typeof messages === \"object\"\n ? ((messages.tts as Record<string, unknown>) ?? undefined)\n : undefined;\n const eleven =\n tts && typeof tts === \"object\"\n ? ((tts.elevenlabs as Record<string, unknown>) ?? undefined)\n : undefined;\n\n const requestedApiKey =\n typeof body.apiKey === \"string\" ? body.apiKey.trim() : \"\";\n const configuredApiKey =\n typeof eleven?.apiKey === \"string\" ? eleven.apiKey.trim() : \"\";\n const envApiKey =\n typeof process.env.ELEVENLABS_API_KEY === \"string\"\n ? process.env.ELEVENLABS_API_KEY.trim()\n : \"\";\n\n const resolvedApiKey =\n requestedApiKey && !ctx.isRedactedSecretValue(requestedApiKey)\n ? requestedApiKey\n : configuredApiKey && !ctx.isRedactedSecretValue(configuredApiKey)\n ? configuredApiKey\n : envApiKey && !ctx.isRedactedSecretValue(envApiKey)\n ? envApiKey\n : \"\";\n\n if (!resolvedApiKey) {\n error(\n res,\n \"ElevenLabs API key is not available. Set ELEVENLABS_API_KEY in Secrets.\",\n 400,\n );\n return true;\n }\n\n const voiceId =\n (typeof body.voiceId === \"string\" && body.voiceId.trim()) ||\n (typeof eleven?.voiceId === \"string\" && eleven.voiceId.trim()) ||\n \"EXAVITQu4vr4xnSDxMaL\";\n const modelId =\n (typeof body.modelId === \"string\" && body.modelId.trim()) ||\n (typeof eleven?.modelId === \"string\" && eleven.modelId.trim()) ||\n \"eleven_flash_v2_5\";\n const outputFormat =\n (typeof body.outputFormat === \"string\" && body.outputFormat.trim()) ||\n \"mp3_22050_32\";\n\n const requestedVoiceSettings =\n body.voice_settings &&\n typeof body.voice_settings === \"object\" &&\n !Array.isArray(body.voice_settings)\n ? body.voice_settings\n : undefined;\n\n const voiceSettings: Record<string, number> = {};\n const stability = requestedVoiceSettings?.stability;\n if (typeof stability === \"number\" && stability >= 0 && stability <= 1) {\n voiceSettings.stability = stability;\n }\n const similarityBoost = requestedVoiceSettings?.similarity_boost;\n if (\n typeof similarityBoost === \"number\" &&\n similarityBoost >= 0 &&\n similarityBoost <= 1\n ) {\n voiceSettings.similarity_boost = similarityBoost;\n }\n const speed = requestedVoiceSettings?.speed;\n if (typeof speed === \"number\" && speed >= 0.5 && speed <= 2) {\n voiceSettings.speed = speed;\n }\n\n const payload: Record<string, unknown> = {\n text,\n model_id: modelId,\n apply_text_normalization:\n body.apply_text_normalization === \"on\" ||\n body.apply_text_normalization === \"off\"\n ? body.apply_text_normalization\n : \"auto\",\n };\n if (Object.keys(voiceSettings).length > 0) {\n payload.voice_settings = voiceSettings;\n }\n\n try {\n const upstreamUrl = new URL(\n `https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream`,\n );\n upstreamUrl.searchParams.set(\"output_format\", outputFormat);\n\n const upstream = await ctx.fetchWithTimeoutGuard(\n upstreamUrl.toString(),\n {\n method: \"POST\",\n headers: {\n \"xi-api-key\": resolvedApiKey,\n \"Content-Type\": \"application/json\",\n Accept: \"audio/mpeg\",\n },\n body: JSON.stringify(payload),\n },\n ctx.ELEVENLABS_FETCH_TIMEOUT_MS,\n );\n\n if (!upstream.ok) {\n const upstreamBody = await upstream.text().catch(() => \"\");\n error(\n res,\n `ElevenLabs request failed (${upstream.status}): ${upstreamBody.slice(0, 240)}`,\n upstream.status === 429 ? 429 : 502,\n );\n return true;\n }\n\n const contentType = upstream.headers.get(\"content-type\") || \"audio/mpeg\";\n const contentLength = ctx.responseContentLength(upstream.headers);\n if (\n contentLength !== null &&\n contentLength > ctx.ELEVENLABS_AUDIO_MAX_BYTES\n ) {\n error(\n res,\n `ElevenLabs response exceeds maximum size of ${ctx.ELEVENLABS_AUDIO_MAX_BYTES} bytes`,\n 502,\n );\n return true;\n }\n\n res.writeHead(200, {\n \"Content-Type\": contentType,\n \"Cache-Control\": \"no-store\",\n ...(contentLength !== null\n ? { \"Content-Length\": String(contentLength) }\n : {}),\n });\n\n await ctx.streamResponseBodyWithByteLimit(\n upstream,\n res,\n ctx.ELEVENLABS_AUDIO_MAX_BYTES,\n ctx.ELEVENLABS_FETCH_TIMEOUT_MS,\n );\n res.end();\n return true;\n } catch (err) {\n if (res.headersSent) {\n res.destroy(\n err instanceof Error\n ? err\n : new Error(\n `ElevenLabs proxy error: ${typeof err === \"string\" ? err : String(err)}`,\n ),\n );\n return true;\n }\n error(\n res,\n `ElevenLabs proxy error: ${err instanceof Error ? err.message : String(err)}`,\n ctx.isAbortError(err) ? 504 : 502,\n );\n return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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","names":[]}
package/dist/core.d.ts ADDED
@@ -0,0 +1,164 @@
1
+ import { IAgentRuntime, Plugin, Action, Provider } from '@elizaos/core';
2
+
3
+ /**
4
+ * Shared RTMP streaming utilities: destinations, cloud relay, overlay presets,
5
+ * and pipeline control actions (local FFmpeg via dashboard API).
6
+ */
7
+
8
+ interface OverlayWidgetInstance {
9
+ id: string;
10
+ type: string;
11
+ enabled: boolean;
12
+ position: {
13
+ x: number;
14
+ y: number;
15
+ width: number;
16
+ height: number;
17
+ };
18
+ zIndex: number;
19
+ config: Record<string, unknown>;
20
+ }
21
+ interface OverlayLayoutData {
22
+ version: 1;
23
+ name: string;
24
+ widgets: OverlayWidgetInstance[];
25
+ }
26
+ interface StreamingDestination {
27
+ id: string;
28
+ name: string;
29
+ getCredentials(): Promise<{
30
+ rtmpUrl: string;
31
+ rtmpKey: string;
32
+ }>;
33
+ onStreamStart?(): Promise<void>;
34
+ onStreamStop?(): Promise<void>;
35
+ /** Per-destination default overlay layout, seeded on first stream start. */
36
+ defaultOverlayLayout?: OverlayLayoutData;
37
+ }
38
+ interface StreamingPluginConfig {
39
+ /** Short lowercase identifier, e.g. "twitch" or "youtube" */
40
+ platformId: string;
41
+ /** Display name, e.g. "Twitch" or "YouTube" */
42
+ platformName: string;
43
+ /** Env var that holds the stream key, e.g. "TWITCH_STREAM_KEY" */
44
+ streamKeyEnvVar: string;
45
+ /** Default RTMP ingest URL for this platform */
46
+ defaultRtmpUrl: string;
47
+ /** Optional env var for a custom RTMP URL (YouTube supports this) */
48
+ rtmpUrlEnvVar?: string;
49
+ /** Override the elizaOS plugin name (defaults to `${platformId}-streaming`) */
50
+ pluginName?: string;
51
+ /** Per-destination default overlay layout, seeded on first stream start. */
52
+ defaultOverlayLayout?: OverlayLayoutData;
53
+ /**
54
+ * When true, the plugin auto-selects between direct RTMP push and the
55
+ * Eliza Cloud RTMP relay backend based on `<UPPER>_STREAMING_BACKEND`
56
+ * (`direct` | `cloud` | `auto`, default `auto`).
57
+ *
58
+ * - `direct` — push to platform RTMP ingest using a local stream key (Mode A).
59
+ * - `cloud` — request a per-session relay from Eliza Cloud (Mode B).
60
+ * The cloud fans the inbound stream out to N destinations.
61
+ * - `auto` — pick `cloud` when Eliza Cloud is connected AND no local
62
+ * stream key is set; otherwise pick `direct`.
63
+ *
64
+ * Existing users with a local `<PLATFORM>_STREAM_KEY` keep the direct path
65
+ * unchanged; cloud relay only activates when they enable cloud and have no
66
+ * local key.
67
+ */
68
+ cloudRelay?: boolean;
69
+ }
70
+ /**
71
+ * Build a preset overlay layout with the given widget types enabled.
72
+ * Widget types not listed in `enabledTypes` are included but disabled.
73
+ */
74
+ declare function buildPresetLayout(name: string, enabledTypes: string[]): OverlayLayoutData;
75
+ declare function createNamedRtmpDestination(params: {
76
+ id: string;
77
+ name?: string;
78
+ rtmpUrl: string;
79
+ rtmpKey: string;
80
+ }): StreamingDestination;
81
+ declare function createCustomRtmpDestination(config?: {
82
+ rtmpUrl?: string;
83
+ rtmpKey?: string;
84
+ }): StreamingDestination;
85
+ declare function createStreamingDestination(cfg: StreamingPluginConfig, overrides?: {
86
+ streamKey?: string;
87
+ rtmpUrl?: string;
88
+ }): StreamingDestination;
89
+ /**
90
+ * Configuration for the Eliza Cloud relay-backed streaming destination.
91
+ *
92
+ * The destination POSTs to `/v1/apis/streaming/sessions` to acquire a
93
+ * per-session ingest URL + stream key. The cloud forwards the inbound
94
+ * stream to the user's stored destinations for `platformId`.
95
+ */
96
+ interface CloudRelayDestinationCfg {
97
+ /** Short lowercase platform identifier — e.g. "twitch", "youtube". */
98
+ platformId: string;
99
+ /** Display name — e.g. "Twitch", "YouTube". */
100
+ platformName: string;
101
+ /** Active runtime — used to read ELIZAOS_CLOUD_* settings. */
102
+ runtime: IAgentRuntime;
103
+ /** Optional per-destination default overlay layout. */
104
+ defaultOverlayLayout?: OverlayLayoutData;
105
+ }
106
+ /**
107
+ * Build a `StreamingDestination` whose RTMP credentials come from the
108
+ * Eliza Cloud relay (Mode B). The cloud-issued credentials point at the
109
+ * SRS ingest, NOT at the platform's RTMP endpoint — the cloud relays the
110
+ * inbound stream to platform RTMP servers using stored per-org credentials.
111
+ *
112
+ * Lifecycle:
113
+ * - `getCredentials()` — POST `/v1/apis/streaming/sessions` →
114
+ * `{ sessionId, ingestUrl, streamKey }`, returned to the caller as
115
+ * `{ rtmpUrl: ingestUrl, rtmpKey: streamKey }`.
116
+ * - `onStreamStop()` — DELETE `/v1/apis/streaming/sessions/{id}`.
117
+ *
118
+ * Throws if Eliza Cloud is not connected.
119
+ */
120
+ declare function createCloudRelayDestination(cfg: CloudRelayDestinationCfg): StreamingDestination;
121
+ type StreamingBackend = "direct" | "cloud" | "auto";
122
+ /**
123
+ * Resolve which streaming backend to use for a given platform at runtime.
124
+ *
125
+ * Reads `<UPPER>_STREAMING_BACKEND` (e.g. `TWITCH_STREAMING_BACKEND`) — one
126
+ * of `direct`, `cloud`, or `auto` (default `auto`).
127
+ *
128
+ * `auto` picks `cloud` iff Eliza Cloud is connected AND no local stream key
129
+ * is set in `cfg.streamKeyEnvVar`. Otherwise it picks `direct`.
130
+ */
131
+ declare function resolveStreamingBackend(runtime: IAgentRuntime, cfg: StreamingPluginConfig): "direct" | "cloud";
132
+ declare function streamingPipelineLocalPort(): number;
133
+ declare const STREAMING_PLATFORMS: readonly ["twitch", "youtube", "x", "pumpfun"];
134
+ type StreamingPlatform = (typeof STREAMING_PLATFORMS)[number];
135
+ type StreamingOp = "start" | "stop" | "status";
136
+ interface BuildStreamOpActionParams {
137
+ validate?: () => Promise<boolean>;
138
+ }
139
+ declare function buildStreamOpAction(params?: BuildStreamOpActionParams): Action;
140
+ /**
141
+ * Provider that renders the live status of every supported streaming platform
142
+ * as JSON context. The pipeline currently exposes a single shared
143
+ * `/api/stream/status` endpoint, so each platform row reflects that same
144
+ * snapshot tagged with its destination label.
145
+ */
146
+ declare const streamStatusProvider: Provider;
147
+ /**
148
+ * Build a complete elizaOS Plugin for a streaming destination.
149
+ *
150
+ * Returns:
151
+ * - `plugin` -- the Plugin object to register with elizaOS
152
+ * - `createDestination` -- the destination factory (for the streaming pipeline)
153
+ */
154
+ /** Result of {@link createStreamingPlugin} — plugin + a backend-aware destination factory. */
155
+ interface CreatedStreamingPlugin {
156
+ plugin: Plugin;
157
+ createDestination: (runtime?: IAgentRuntime, overrides?: {
158
+ streamKey?: string;
159
+ rtmpUrl?: string;
160
+ }) => StreamingDestination;
161
+ }
162
+ declare function createStreamingPlugin(cfg: StreamingPluginConfig): CreatedStreamingPlugin;
163
+
164
+ export { type BuildStreamOpActionParams, type CloudRelayDestinationCfg, type CreatedStreamingPlugin, type OverlayLayoutData, type OverlayWidgetInstance, STREAMING_PLATFORMS, type StreamingBackend, type StreamingDestination, type StreamingOp, type StreamingPlatform, type StreamingPluginConfig, buildPresetLayout, buildStreamOpAction, createCloudRelayDestination, createCustomRtmpDestination, createNamedRtmpDestination, createStreamingDestination, createStreamingPlugin, resolveStreamingBackend, streamStatusProvider, streamingPipelineLocalPort };