@elizaos/plugin-streaming 2.0.3-beta.2 → 2.0.3-beta.3

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.
@@ -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 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
+ {"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(PLUGIN_BROWSER_PACKAGE)) 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":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elizaos/plugin-streaming",
3
3
  "description": "RTMP streaming for elizaOS (Twitch, YouTube, X, pump.fun, custom and named ingest URLs)",
4
- "version": "2.0.3-beta.2",
4
+ "version": "2.0.3-beta.3",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
@@ -37,9 +37,9 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
- "@elizaos/cloud-routing": "2.0.3-beta.2",
41
- "@elizaos/core": "2.0.3-beta.2",
42
- "@elizaos/plugin-browser": "2.0.3-beta.2"
40
+ "@elizaos/cloud-routing": "2.0.3-beta.3",
41
+ "@elizaos/core": "2.0.3-beta.3",
42
+ "@elizaos/plugin-browser": "2.0.3-beta.3"
43
43
  },
44
44
  "devDependencies": {
45
45
  "tsup": "^8.5.1",
@@ -114,5 +114,5 @@
114
114
  }
115
115
  }
116
116
  },
117
- "gitHead": "82fe0f44215954c2417328203f5bd6510985c1fc"
117
+ "gitHead": "f54b0f4eaed317d59fa7dbcdce20f4cdb0734420"
118
118
  }