@elizaos/plugin-streaming 2.0.0-beta.1 → 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.
- package/README.md +100 -3
- package/dist/api/stream-persistence.d.ts +12 -17
- package/dist/api/stream-route-state.d.ts +2 -6
- package/dist/api/stream-routes.d.ts +15 -16
- package/dist/api/stream-routes.js.map +1 -1
- package/dist/api/streaming-text.d.ts +4 -6
- package/dist/api/streaming-text.js +1 -1
- package/dist/api/streaming-text.js.map +1 -1
- package/dist/api/streaming-types.d.ts +1 -2
- package/dist/api/tts-routes.d.ts +5 -7
- package/dist/api/tts-routes.js +127 -1
- package/dist/api/tts-routes.js.map +1 -1
- package/dist/core.d.ts +22 -26
- package/dist/core.js +34 -23
- package/dist/core.js.map +1 -1
- package/dist/index.d.ts +15 -21
- package/dist/services/stream-manager.d.ts +5 -9
- package/dist/services/stream-manager.js +3 -6
- package/dist/services/stream-manager.js.map +1 -1
- package/dist/services/tts-stream-bridge.d.ts +14 -14
- package/dist/services/tts-stream-bridge.js +110 -20
- package/dist/services/tts-stream-bridge.js.map +1 -1
- package/package.json +25 -13
package/dist/core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * Shared RTMP streaming utilities: destinations, cloud relay, overlay presets,\n * and pipeline control actions (local FFmpeg via dashboard API).\n */\n\nimport { isCloudConnected } from \"@elizaos/cloud-routing\";\nimport type {\n Action,\n ActionParameter,\n ActionResult,\n Content,\n HandlerCallback,\n HandlerOptions,\n IAgentRuntime,\n JsonValue,\n Memory,\n Plugin,\n Provider,\n ProviderResult,\n State,\n} from \"@elizaos/core\";\n\n// ── Overlay layout data (JSON-serializable, no React refs) ──────────────────\n\nexport interface OverlayWidgetInstance {\n id: string;\n type: string;\n enabled: boolean;\n position: { x: number; y: number; width: number; height: number };\n zIndex: number;\n config: Record<string, unknown>;\n}\n\nexport interface OverlayLayoutData {\n version: 1;\n name: string;\n widgets: OverlayWidgetInstance[];\n}\n\n// ── Shared types ────────────────────────────────────────────────────────────\n// Canonical definition — stream-routes.ts re-exports this interface.\n\nexport interface StreamingDestination {\n id: string;\n name: string;\n getCredentials(): Promise<{ rtmpUrl: string; rtmpKey: string }>;\n onStreamStart?(): Promise<void>;\n onStreamStop?(): Promise<void>;\n /** Per-destination default overlay layout, seeded on first stream start. */\n defaultOverlayLayout?: OverlayLayoutData;\n}\n\nexport interface StreamingPluginConfig {\n /** Short lowercase identifier, e.g. \"twitch\" or \"youtube\" */\n platformId: string;\n /** Display name, e.g. \"Twitch\" or \"YouTube\" */\n platformName: string;\n /** Env var that holds the stream key, e.g. \"TWITCH_STREAM_KEY\" */\n streamKeyEnvVar: string;\n /** Default RTMP ingest URL for this platform */\n defaultRtmpUrl: string;\n /** Optional env var for a custom RTMP URL (YouTube supports this) */\n rtmpUrlEnvVar?: string;\n /** Override the elizaOS plugin name (defaults to `${platformId}-streaming`) */\n pluginName?: string;\n /** Per-destination default overlay layout, seeded on first stream start. */\n defaultOverlayLayout?: OverlayLayoutData;\n /**\n * When true, the plugin auto-selects between direct RTMP push and the\n * Eliza Cloud RTMP relay backend based on `<UPPER>_STREAMING_BACKEND`\n * (`direct` | `cloud` | `auto`, default `auto`).\n *\n * - `direct` — push to platform RTMP ingest using a local stream key (Mode A).\n * - `cloud` — request a per-session relay from Eliza Cloud (Mode B).\n * The cloud fans the inbound stream out to N destinations.\n * - `auto` — pick `cloud` when Eliza Cloud is connected AND no local\n * stream key is set; otherwise pick `direct`.\n *\n * Existing users with a local `<PLATFORM>_STREAM_KEY` keep the direct path\n * unchanged; cloud relay only activates when they enable cloud and have no\n * local key.\n */\n cloudRelay?: boolean;\n}\n\n// ── Preset layout builder ───────────────────────────────────────────────────\n\n/** All known built-in widget types. */\nconst WIDGET_DEFAULTS: Record<\n string,\n { position: OverlayWidgetInstance[\"position\"]; zIndex: number }\n> = {\n \"thought-bubble\": {\n position: { x: 2, y: 2, width: 30, height: 20 },\n zIndex: 10,\n },\n \"action-ticker\": {\n position: { x: 0, y: 85, width: 100, height: 15 },\n zIndex: 5,\n },\n \"alert-popup\": {\n position: { x: 30, y: 10, width: 40, height: 20 },\n zIndex: 20,\n },\n \"viewer-count\": {\n position: { x: 88, y: 2, width: 10, height: 6 },\n zIndex: 15,\n },\n branding: { position: { x: 2, y: 90, width: 20, height: 8 }, zIndex: 2 },\n \"custom-html\": {\n position: { x: 50, y: 50, width: 30, height: 20 },\n zIndex: 1,\n },\n \"peon-hud\": {\n position: { x: 82, y: 10, width: 16, height: 30 },\n zIndex: 12,\n },\n \"peon-glass\": {\n position: { x: 2, y: 2, width: 32, height: 40 },\n zIndex: 16,\n },\n \"peon-sakura\": {\n position: { x: 0, y: 0, width: 25, height: 50 },\n zIndex: 3,\n },\n};\n\nlet _presetCounter = 0;\n\n/**\n * Build a preset overlay layout with the given widget types enabled.\n * Widget types not listed in `enabledTypes` are included but disabled.\n */\nexport function buildPresetLayout(\n name: string,\n enabledTypes: string[],\n): OverlayLayoutData {\n const enabledSet = new Set(enabledTypes);\n const widgets: OverlayWidgetInstance[] = Object.entries(WIDGET_DEFAULTS).map(\n ([type, defaults]) => {\n _presetCounter += 1;\n return {\n id: `preset${_presetCounter.toString(36)}`,\n type,\n enabled: enabledSet.has(type),\n position: { ...defaults.position },\n zIndex: defaults.zIndex,\n config: {},\n };\n },\n );\n return { version: 1, name, widgets };\n}\n\n// ── Named / custom RTMP (config-driven ingest) ───────────────────────────────\n\nexport function createNamedRtmpDestination(params: {\n id: string;\n name?: string;\n rtmpUrl: string;\n rtmpKey: string;\n}): StreamingDestination {\n const trimmedId = params.id.trim();\n const label = (params.name ?? trimmedId).trim() || trimmedId;\n return {\n id: trimmedId,\n name: label,\n async getCredentials() {\n const rtmpUrl = params.rtmpUrl.trim();\n const rtmpKey = params.rtmpKey.trim();\n if (!rtmpUrl || !rtmpKey) {\n throw new Error(`${label}: RTMP URL and stream key are required`);\n }\n return { rtmpUrl, rtmpKey };\n },\n };\n}\n\nexport function createCustomRtmpDestination(config?: {\n rtmpUrl?: string;\n rtmpKey?: string;\n}): StreamingDestination {\n return {\n id: \"custom-rtmp\",\n name: \"Custom RTMP\",\n async getCredentials() {\n const rtmpUrl = (\n config?.rtmpUrl ??\n process.env.CUSTOM_RTMP_URL ??\n \"\"\n ).trim();\n const rtmpKey = (\n config?.rtmpKey ??\n process.env.CUSTOM_RTMP_KEY ??\n \"\"\n ).trim();\n if (!rtmpUrl || !rtmpKey) {\n throw new Error(\n \"Custom RTMP requires rtmpUrl and rtmpKey in streaming.customRtmp config or CUSTOM_RTMP_* env\",\n );\n }\n return { rtmpUrl, rtmpKey };\n },\n };\n}\n\n// ── Destination factory ─────────────────────────────────────────────────────\n\nexport function createStreamingDestination(\n cfg: StreamingPluginConfig,\n overrides?: { streamKey?: string; rtmpUrl?: string },\n): StreamingDestination {\n return {\n id: cfg.platformId,\n name: cfg.platformName,\n defaultOverlayLayout: cfg.defaultOverlayLayout,\n\n async getCredentials() {\n const streamKey = (\n overrides?.streamKey ??\n process.env[cfg.streamKeyEnvVar] ??\n \"\"\n ).trim();\n if (!streamKey) {\n throw new Error(`${cfg.platformName} stream key not configured`);\n }\n\n const rtmpUrl = (\n overrides?.rtmpUrl ??\n (cfg.rtmpUrlEnvVar ? process.env[cfg.rtmpUrlEnvVar] : undefined) ??\n cfg.defaultRtmpUrl\n ).trim();\n if (!rtmpUrl) {\n throw new Error(`${cfg.platformName} RTMP URL not configured`);\n }\n\n return { rtmpUrl, rtmpKey: streamKey };\n },\n // Platforms detect stream automatically via RTMP ingest -- no API calls needed\n };\n}\n\n// ── Cloud relay destination ────────────────────────────────────────────────\n\nconst CLOUD_BASE_FALLBACK = \"https://www.elizacloud.ai/api/v1\";\n\nfunction readSetting(runtime: IAgentRuntime, key: string): string | null {\n const raw = runtime.getSetting(key);\n if (raw === null || raw === undefined) return null;\n const str = String(raw).trim();\n return str.length > 0 ? str : null;\n}\n\nfunction getCloudBaseUrl(runtime: IAgentRuntime): string {\n const override = readSetting(runtime, \"ELIZAOS_CLOUD_BASE_URL\");\n return (override ?? CLOUD_BASE_FALLBACK).replace(/\\/+$/, \"\");\n}\n\nfunction getCloudApiKey(runtime: IAgentRuntime): string {\n const apiKey = readSetting(runtime, \"ELIZAOS_CLOUD_API_KEY\");\n if (apiKey === null) {\n throw new Error(\n \"Eliza Cloud relay requested but ELIZAOS_CLOUD_API_KEY is not set\",\n );\n }\n return apiKey;\n}\n\ninterface CreateRelaySessionResponse {\n sessionId: string;\n streamKey: string;\n ingestUrl: string;\n wsUrl?: string;\n}\n\n/**\n * Configuration for the Eliza Cloud relay-backed streaming destination.\n *\n * The destination POSTs to `/v1/apis/streaming/sessions` to acquire a\n * per-session ingest URL + stream key. The cloud forwards the inbound\n * stream to the user's stored destinations for `platformId`.\n */\nexport interface CloudRelayDestinationCfg {\n /** Short lowercase platform identifier — e.g. \"twitch\", \"youtube\". */\n platformId: string;\n /** Display name — e.g. \"Twitch\", \"YouTube\". */\n platformName: string;\n /** Active runtime — used to read ELIZAOS_CLOUD_* settings. */\n runtime: IAgentRuntime;\n /** Optional per-destination default overlay layout. */\n defaultOverlayLayout?: OverlayLayoutData;\n}\n\n/**\n * Build a `StreamingDestination` whose RTMP credentials come from the\n * Eliza Cloud relay (Mode B). The cloud-issued credentials point at the\n * SRS ingest, NOT at the platform's RTMP endpoint — the cloud relays the\n * inbound stream to platform RTMP servers using stored per-org credentials.\n *\n * Lifecycle:\n * - `getCredentials()` — POST `/v1/apis/streaming/sessions` →\n * `{ sessionId, ingestUrl, streamKey }`, returned to the caller as\n * `{ rtmpUrl: ingestUrl, rtmpKey: streamKey }`.\n * - `onStreamStop()` — DELETE `/v1/apis/streaming/sessions/{id}`.\n *\n * Throws if Eliza Cloud is not connected.\n */\nexport function createCloudRelayDestination(\n cfg: CloudRelayDestinationCfg,\n): StreamingDestination {\n if (!isCloudConnected(cfg.runtime)) {\n throw new Error(\n `Cloud relay requested for ${cfg.platformName} but Eliza Cloud is not connected ` +\n `(ELIZAOS_CLOUD_API_KEY missing or ELIZAOS_CLOUD_ENABLED falsy)`,\n );\n }\n\n let activeSessionId: string | null = null;\n\n return {\n id: cfg.platformId,\n name: cfg.platformName,\n defaultOverlayLayout: cfg.defaultOverlayLayout,\n\n async getCredentials(): Promise<{ rtmpUrl: string; rtmpKey: string }> {\n const baseUrl = getCloudBaseUrl(cfg.runtime);\n const apiKey = getCloudApiKey(cfg.runtime);\n\n const res = await fetch(`${baseUrl}/apis/streaming/sessions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({ destinations: [cfg.platformId] }),\n signal: AbortSignal.timeout(20_000),\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\n `Cloud relay session create failed: ${res.status} ${text}`,\n );\n }\n\n const body = (await res.json()) as CreateRelaySessionResponse;\n if (!body.sessionId || !body.streamKey || !body.ingestUrl) {\n throw new Error(\n \"Cloud relay session create returned malformed response\",\n );\n }\n\n activeSessionId = body.sessionId;\n return { rtmpUrl: body.ingestUrl, rtmpKey: body.streamKey };\n },\n\n async onStreamStop(): Promise<void> {\n if (!activeSessionId) return;\n const baseUrl = getCloudBaseUrl(cfg.runtime);\n const apiKey = getCloudApiKey(cfg.runtime);\n const sessionId = activeSessionId;\n activeSessionId = null;\n\n const res = await fetch(\n `${baseUrl}/apis/streaming/sessions/${encodeURIComponent(sessionId)}`,\n {\n method: \"DELETE\",\n headers: { Authorization: `Bearer ${apiKey}` },\n signal: AbortSignal.timeout(15_000),\n },\n );\n if (!res.ok && res.status !== 404) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\n `Cloud relay session close failed: ${res.status} ${text}`,\n );\n }\n },\n };\n}\n\n// ── Backend selection ──────────────────────────────────────────────────────\n\nexport type StreamingBackend = \"direct\" | \"cloud\" | \"auto\";\n\nfunction readBackendSetting(\n runtime: IAgentRuntime,\n envVar: string,\n): StreamingBackend {\n const raw = readSetting(runtime, envVar);\n if (raw === null) return \"auto\";\n const lower = raw.toLowerCase();\n if (lower === \"direct\" || lower === \"cloud\" || lower === \"auto\") return lower;\n throw new Error(\n `Invalid ${envVar}=\"${raw}\" (expected \"direct\" | \"cloud\" | \"auto\")`,\n );\n}\n\n/**\n * Resolve which streaming backend to use for a given platform at runtime.\n *\n * Reads `<UPPER>_STREAMING_BACKEND` (e.g. `TWITCH_STREAMING_BACKEND`) — one\n * of `direct`, `cloud`, or `auto` (default `auto`).\n *\n * `auto` picks `cloud` iff Eliza Cloud is connected AND no local stream key\n * is set in `cfg.streamKeyEnvVar`. Otherwise it picks `direct`.\n */\nexport function resolveStreamingBackend(\n runtime: IAgentRuntime,\n cfg: StreamingPluginConfig,\n): \"direct\" | \"cloud\" {\n const upper = cfg.platformId.toUpperCase().replace(/[^A-Z0-9]/g, \"_\");\n const setting = readBackendSetting(runtime, `${upper}_STREAMING_BACKEND`);\n if (setting === \"direct\" || setting === \"cloud\") return setting;\n\n const localKey = readSetting(runtime, cfg.streamKeyEnvVar);\n if (localKey !== null) return \"direct\";\n return isCloudConnected(runtime) ? \"cloud\" : \"direct\";\n}\n\n// ── Plugin factory ──────────────────────────────────────────────────────────\n\nexport function streamingPipelineLocalPort(): number {\n return Number(process.env.SERVER_PORT || process.env.PORT || \"2138\");\n}\n\n// ── Unified STREAM_OP router action + streamStatus provider ────────────────\n\nexport const STREAMING_PLATFORMS = [\n \"twitch\",\n \"youtube\",\n \"x\",\n \"pumpfun\",\n] as const;\n\nexport type StreamingPlatform = (typeof STREAMING_PLATFORMS)[number];\n\nexport type StreamingOp = \"start\" | \"stop\" | \"status\";\n\nconst PLATFORM_LABELS: Record<StreamingPlatform, string> = {\n twitch: \"Twitch\",\n youtube: \"YouTube\",\n x: \"X (Twitter)\",\n pumpfun: \"pump.fun\",\n};\n\ninterface StreamStatusSnapshot {\n platform: StreamingPlatform;\n running: boolean;\n uptimeSeconds: number | null;\n frames: number | null;\n destination: string;\n}\n\nfunction isStreamingPlatform(value: unknown): value is StreamingPlatform {\n return (\n typeof value === \"string\" &&\n (STREAMING_PLATFORMS as readonly string[]).includes(value)\n );\n}\n\nfunction isStreamingOp(value: unknown): value is StreamingOp {\n return value === \"start\" || value === \"stop\" || value === \"status\";\n}\n\nfunction readParam(\n options: unknown,\n key: string,\n): string | number | boolean | null | undefined {\n if (!options || typeof options !== \"object\" || Array.isArray(options)) {\n return undefined;\n }\n const handler = options as HandlerOptions;\n const params = handler.parameters as Record<string, JsonValue> | undefined;\n if (params && key in params) {\n const v = params[key];\n if (\n typeof v === \"string\" ||\n typeof v === \"number\" ||\n typeof v === \"boolean\" ||\n v === null\n ) {\n return v as string | number | boolean | null;\n }\n }\n const flat = options as Record<string, unknown>;\n const v = flat[key];\n if (\n typeof v === \"string\" ||\n typeof v === \"number\" ||\n typeof v === \"boolean\" ||\n v === null\n ) {\n return v as string | number | boolean | null;\n }\n return undefined;\n}\n\nasync function fetchStreamStatus(\n platform: StreamingPlatform,\n): Promise<StreamStatusSnapshot> {\n const port = streamingPipelineLocalPort();\n const res = await fetch(`http://127.0.0.1:${port}/api/stream/status`, {\n signal: AbortSignal.timeout(10_000),\n });\n const data = (await res.json()) as Record<string, unknown>;\n const uptime = typeof data.uptime === \"number\" ? Number(data.uptime) : null;\n const frames = typeof data.frames === \"number\" ? Number(data.frames) : null;\n return {\n platform,\n running: !!data.running,\n uptimeSeconds: uptime,\n frames,\n destination: PLATFORM_LABELS[platform],\n };\n}\n\nexport interface BuildStreamOpActionParams {\n validate?: () => Promise<boolean>;\n}\n\nexport function buildStreamOpAction(\n params: BuildStreamOpActionParams = {},\n): Action {\n const validate = params.validate ?? (async () => true);\n\n const platformParam: ActionParameter = {\n name: \"platform\",\n description:\n \"Streaming destination platform: twitch, youtube, x, or pumpfun.\",\n descriptionCompressed: \"Platform: twitch|youtube|x|pumpfun.\",\n required: true,\n schema: {\n type: \"string\",\n enum: [...STREAMING_PLATFORMS],\n },\n };\n\n const opParam: ActionParameter = {\n name: \"subaction\",\n description:\n \"Operation to perform: start (go live), stop (go offline), or status.\",\n descriptionCompressed: \"Op: start|stop|status.\",\n required: true,\n schema: {\n type: \"string\",\n enum: [\"start\", \"stop\", \"status\"],\n },\n };\n\n return {\n name: \"STREAM\",\n contexts: [\"media\", \"automation\", \"connectors\"],\n contextGate: { anyOf: [\"media\", \"automation\", \"connectors\"] },\n roleGate: { minRole: \"ADMIN\" },\n description:\n \"Control the local RTMP streaming pipeline for a target platform. Dispatches start, stop, and status calls to the dashboard stream API for twitch, youtube, x, or pumpfun.\",\n descriptionCompressed:\n \"Stream ops: start, stop, status; platforms: twitch, youtube, x, pumpfun.\",\n similes: [\n \"START_STREAM\",\n \"STOP_STREAM\",\n \"GET_STREAM_STATUS\",\n \"GO_LIVE\",\n \"GO_OFFLINE\",\n \"STREAM_STATUS\",\n \"IS_LIVE\",\n ],\n parameters: [platformParam, opParam],\n validate,\n handler: async (\n _runtime: IAgentRuntime,\n _message: Memory,\n _state: State | undefined,\n options:\n | HandlerOptions\n | Record<string, JsonValue | undefined>\n | undefined,\n callback?: HandlerCallback,\n ): Promise<ActionResult> => {\n const platformRaw = readParam(options, \"platform\");\n const opRaw = readParam(options, \"op\");\n if (!isStreamingPlatform(platformRaw)) {\n const text = `STREAM_OP requires platform in {${STREAMING_PLATFORMS.join(\", \")}}, got ${String(platformRaw)}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: false, error: text };\n }\n if (!isStreamingOp(opRaw)) {\n const text = `STREAM_OP requires op in {start, stop, status}, got ${String(opRaw)}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: false, error: text };\n }\n\n const platform: StreamingPlatform = platformRaw;\n const op: StreamingOp = opRaw;\n const label = PLATFORM_LABELS[platform];\n const port = streamingPipelineLocalPort();\n\n try {\n if (op === \"start\") {\n const res = await fetch(`http://127.0.0.1:${port}/api/stream/live`, {\n method: \"POST\",\n signal: AbortSignal.timeout(30_000),\n });\n const data = (await res.json()) as Record<string, unknown>;\n const ok = !!data.ok;\n const text = ok\n ? `${label} stream started successfully! We're live.`\n : `Failed to start ${label} stream: ${data.error ?? \"unknown error\"}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: ok, text };\n }\n\n if (op === \"stop\") {\n const res = await fetch(\n `http://127.0.0.1:${port}/api/stream/offline`,\n {\n method: \"POST\",\n signal: AbortSignal.timeout(15_000),\n },\n );\n const data = (await res.json()) as Record<string, unknown>;\n const ok = !!data.ok;\n const text = ok\n ? `${label} stream stopped. We're offline now.`\n : `Failed to stop ${label} stream: ${data.error ?? \"unknown error\"}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: ok, text };\n }\n\n const snapshot = await fetchStreamStatus(platform);\n const status = snapshot.running ? \"LIVE\" : \"OFFLINE\";\n const uptime =\n snapshot.uptimeSeconds === null\n ? \"n/a\"\n : `${Math.floor(snapshot.uptimeSeconds / 60)}m`;\n const text = `${label} stream status: ${status} | Uptime: ${uptime} | Destination: ${label}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: true, text, data: { snapshot } };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const text = `Error running STREAM_OP ${op} for ${label}: ${msg}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: false, error: msg, text };\n }\n },\n examples: [\n [\n {\n name: \"{{user1}}\",\n content: { text: \"Go live on Twitch\" },\n },\n {\n name: \"{{agent}}\",\n content: {\n text: \"Starting the Twitch stream now.\",\n actions: [\"STREAM\"],\n },\n },\n ],\n [\n {\n name: \"{{user1}}\",\n content: { text: \"Stop the YouTube stream\" },\n },\n {\n name: \"{{agent}}\",\n content: {\n text: \"Stopping the stream now.\",\n actions: [\"STREAM\"],\n },\n },\n ],\n [\n {\n name: \"{{user1}}\",\n content: { text: \"Is the X stream live?\" },\n },\n {\n name: \"{{agent}}\",\n content: {\n text: \"Let me check the stream status.\",\n actions: [\"STREAM\"],\n },\n },\n ],\n ],\n };\n}\n\n/**\n * Provider that renders the live status of every supported streaming platform\n * as JSON context. The pipeline currently exposes a single shared\n * `/api/stream/status` endpoint, so each platform row reflects that same\n * snapshot tagged with its destination label.\n */\nexport const streamStatusProvider: Provider = {\n name: \"streamStatus\",\n description:\n \"Live RTMP pipeline status per supported platform (twitch, youtube, x, pumpfun) rendered as JSON.\",\n descriptionCompressed: \"RTMP status per platform.\",\n dynamic: true,\n contexts: [\"media\", \"automation\"],\n contextGate: { anyOf: [\"media\", \"automation\"] },\n cacheStable: false,\n cacheScope: \"turn\",\n get: async (\n _runtime: IAgentRuntime,\n _message: Memory,\n _state: State,\n ): Promise<ProviderResult> => {\n const rows = await Promise.all(\n STREAMING_PLATFORMS.map(async (platform) => {\n try {\n const snap = await fetchStreamStatus(platform);\n return {\n platform: snap.platform,\n running: snap.running,\n uptimeSeconds: snap.uptimeSeconds ?? 0,\n frames: snap.frames ?? 0,\n destination: snap.destination,\n error: null,\n };\n } catch (err) {\n return {\n platform,\n running: false,\n uptimeSeconds: 0,\n frames: 0,\n destination: PLATFORM_LABELS[platform],\n error: err instanceof Error ? err.message : String(err),\n };\n }\n }),\n );\n\n return {\n text: JSON.stringify({\n stream_status: {\n count: rows.length,\n platforms: rows,\n },\n }),\n data: { stream_status: rows },\n };\n },\n};\n\n/**\n * Build a complete elizaOS Plugin for a streaming destination.\n *\n * Returns:\n * - `plugin` -- the Plugin object to register with elizaOS\n * - `createDestination` -- the destination factory (for the streaming pipeline)\n */\n/** Result of {@link createStreamingPlugin} — plugin + a backend-aware destination factory. */\nexport interface CreatedStreamingPlugin {\n plugin: Plugin;\n createDestination: (\n runtime?: IAgentRuntime,\n overrides?: { streamKey?: string; rtmpUrl?: string },\n ) => StreamingDestination;\n}\n\nexport function createStreamingPlugin(\n cfg: StreamingPluginConfig,\n): CreatedStreamingPlugin {\n const NAME = cfg.platformName;\n\n const configEntries: Record<string, string | null> = {\n [cfg.streamKeyEnvVar]: process.env[cfg.streamKeyEnvVar] ?? null,\n };\n if (cfg.rtmpUrlEnvVar) {\n configEntries[cfg.rtmpUrlEnvVar] = process.env[cfg.rtmpUrlEnvVar] ?? null;\n }\n\n const plugin: Plugin = {\n name: cfg.pluginName ?? `${cfg.platformId}-streaming`,\n description: `${NAME} RTMP streaming destination — credentials and overlay layout. Stream control actions live on the unified streaming plugin.`,\n get config() {\n return configEntries;\n },\n actions: [],\n async init(_config: Record<string, string>, _runtime: IAgentRuntime) {\n const streamKey = (\n _config[cfg.streamKeyEnvVar] ??\n process.env[cfg.streamKeyEnvVar] ??\n \"\"\n ).trim();\n if (!streamKey) {\n return;\n }\n },\n };\n\n const createDestination = (\n runtime?: IAgentRuntime,\n overrides?: { streamKey?: string; rtmpUrl?: string },\n ): StreamingDestination => {\n if (cfg.cloudRelay && runtime) {\n const backend = resolveStreamingBackend(runtime, cfg);\n if (backend === \"cloud\") {\n return createCloudRelayDestination({\n platformId: cfg.platformId,\n platformName: cfg.platformName,\n runtime,\n defaultOverlayLayout: cfg.defaultOverlayLayout,\n });\n }\n }\n return createStreamingDestination(cfg, overrides);\n };\n\n return { plugin, createDestination };\n}\n"],"mappings":"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","names":["v","text"]}
|
|
1
|
+
{"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * Shared RTMP streaming utilities: destinations, cloud relay, overlay presets,\n * and pipeline control actions (local FFmpeg via dashboard API).\n */\n\nimport { isCloudConnected } from \"@elizaos/cloud-routing\";\nimport type {\n Action,\n ActionResult,\n Content,\n HandlerCallback,\n HandlerOptions,\n IAgentRuntime,\n JsonValue,\n Memory,\n Plugin,\n Provider,\n ProviderResult,\n State,\n} from \"@elizaos/core\";\n\n// ── Overlay layout data (JSON-serializable, no React refs) ──────────────────\n\nexport interface OverlayWidgetInstance {\n id: string;\n type: string;\n enabled: boolean;\n position: { x: number; y: number; width: number; height: number };\n zIndex: number;\n config: Record<string, unknown>;\n}\n\nexport interface OverlayLayoutData {\n version: 1;\n name: string;\n widgets: OverlayWidgetInstance[];\n}\n\n// ── Shared types ────────────────────────────────────────────────────────────\n// Canonical definition — stream-routes.ts re-exports this interface.\n\nexport interface StreamingDestination {\n id: string;\n name: string;\n getCredentials(): Promise<{ rtmpUrl: string; rtmpKey: string }>;\n onStreamStart?(): Promise<void>;\n onStreamStop?(): Promise<void>;\n /** Per-destination default overlay layout, seeded on first stream start. */\n defaultOverlayLayout?: OverlayLayoutData;\n}\n\nexport interface StreamingPluginConfig {\n /** Short lowercase identifier, e.g. \"twitch\" or \"youtube\" */\n platformId: string;\n /** Display name, e.g. \"Twitch\" or \"YouTube\" */\n platformName: string;\n /** Env var that holds the stream key, e.g. \"TWITCH_STREAM_KEY\" */\n streamKeyEnvVar: string;\n /** Default RTMP ingest URL for this platform */\n defaultRtmpUrl: string;\n /** Optional env var for a custom RTMP URL (YouTube supports this) */\n rtmpUrlEnvVar?: string;\n /** Override the elizaOS plugin name (defaults to `${platformId}-streaming`) */\n pluginName?: string;\n /** Per-destination default overlay layout, seeded on first stream start. */\n defaultOverlayLayout?: OverlayLayoutData;\n /**\n * When true, the plugin auto-selects between direct RTMP push and the\n * Eliza Cloud RTMP relay backend based on `<UPPER>_STREAMING_BACKEND`\n * (`direct` | `cloud` | `auto`, default `auto`).\n *\n * - `direct` — push to platform RTMP ingest using a local stream key (Mode A).\n * - `cloud` — request a per-session relay from Eliza Cloud (Mode B).\n * The cloud fans the inbound stream out to N destinations.\n * - `auto` — pick `cloud` when Eliza Cloud is connected AND no local\n * stream key is set; otherwise pick `direct`.\n *\n * Existing users with a local `<PLATFORM>_STREAM_KEY` keep the direct path\n * unchanged; cloud relay only activates when they enable cloud and have no\n * local key.\n */\n cloudRelay?: boolean;\n}\n\n// ── Preset layout builder ───────────────────────────────────────────────────\n\n/** All known built-in widget types. */\nconst WIDGET_DEFAULTS: Record<\n string,\n { position: OverlayWidgetInstance[\"position\"]; zIndex: number }\n> = {\n \"thought-bubble\": {\n position: { x: 2, y: 2, width: 30, height: 20 },\n zIndex: 10,\n },\n \"action-ticker\": {\n position: { x: 0, y: 85, width: 100, height: 15 },\n zIndex: 5,\n },\n \"alert-popup\": {\n position: { x: 30, y: 10, width: 40, height: 20 },\n zIndex: 20,\n },\n \"viewer-count\": {\n position: { x: 88, y: 2, width: 10, height: 6 },\n zIndex: 15,\n },\n branding: { position: { x: 2, y: 90, width: 20, height: 8 }, zIndex: 2 },\n \"custom-html\": {\n position: { x: 50, y: 50, width: 30, height: 20 },\n zIndex: 1,\n },\n \"peon-hud\": {\n position: { x: 82, y: 10, width: 16, height: 30 },\n zIndex: 12,\n },\n \"peon-glass\": {\n position: { x: 2, y: 2, width: 32, height: 40 },\n zIndex: 16,\n },\n \"peon-sakura\": {\n position: { x: 0, y: 0, width: 25, height: 50 },\n zIndex: 3,\n },\n};\n\nlet _presetCounter = 0;\n\n/**\n * Build a preset overlay layout with the given widget types enabled.\n * Widget types not listed in `enabledTypes` are included but disabled.\n */\nexport function buildPresetLayout(\n name: string,\n enabledTypes: string[],\n): OverlayLayoutData {\n const enabledSet = new Set(enabledTypes);\n const widgets: OverlayWidgetInstance[] = Object.entries(WIDGET_DEFAULTS).map(\n ([type, defaults]) => {\n _presetCounter += 1;\n return {\n id: `preset${_presetCounter.toString(36)}`,\n type,\n enabled: enabledSet.has(type),\n position: { ...defaults.position },\n zIndex: defaults.zIndex,\n config: {},\n };\n },\n );\n return { version: 1, name, widgets };\n}\n\n// ── Named / custom RTMP (config-driven ingest) ───────────────────────────────\n\nexport function createNamedRtmpDestination(params: {\n id: string;\n name?: string;\n rtmpUrl: string;\n rtmpKey: string;\n}): StreamingDestination {\n const trimmedId = params.id.trim();\n const label = (params.name ?? trimmedId).trim() || trimmedId;\n return {\n id: trimmedId,\n name: label,\n async getCredentials() {\n const rtmpUrl = params.rtmpUrl.trim();\n const rtmpKey = params.rtmpKey.trim();\n if (!rtmpUrl || !rtmpKey) {\n throw new Error(`${label}: RTMP URL and stream key are required`);\n }\n return { rtmpUrl, rtmpKey };\n },\n };\n}\n\nexport function createCustomRtmpDestination(config?: {\n rtmpUrl?: string;\n rtmpKey?: string;\n}): StreamingDestination {\n return {\n id: \"custom-rtmp\",\n name: \"Custom RTMP\",\n async getCredentials() {\n const rtmpUrl = (\n config?.rtmpUrl ??\n process.env.CUSTOM_RTMP_URL ??\n \"\"\n ).trim();\n const rtmpKey = (\n config?.rtmpKey ??\n process.env.CUSTOM_RTMP_KEY ??\n \"\"\n ).trim();\n if (!rtmpUrl || !rtmpKey) {\n throw new Error(\n \"Custom RTMP requires rtmpUrl and rtmpKey in streaming.customRtmp config or CUSTOM_RTMP_* env\",\n );\n }\n return { rtmpUrl, rtmpKey };\n },\n };\n}\n\n// ── Destination factory ─────────────────────────────────────────────────────\n\nexport function createStreamingDestination(\n cfg: StreamingPluginConfig,\n overrides?: { streamKey?: string; rtmpUrl?: string },\n): StreamingDestination {\n return {\n id: cfg.platformId,\n name: cfg.platformName,\n defaultOverlayLayout: cfg.defaultOverlayLayout,\n\n async getCredentials() {\n const streamKey = (\n overrides?.streamKey ??\n process.env[cfg.streamKeyEnvVar] ??\n \"\"\n ).trim();\n if (!streamKey) {\n throw new Error(`${cfg.platformName} stream key not configured`);\n }\n\n const rtmpUrl = (\n overrides?.rtmpUrl ??\n (cfg.rtmpUrlEnvVar ? process.env[cfg.rtmpUrlEnvVar] : undefined) ??\n cfg.defaultRtmpUrl\n ).trim();\n if (!rtmpUrl) {\n throw new Error(`${cfg.platformName} RTMP URL not configured`);\n }\n\n return { rtmpUrl, rtmpKey: streamKey };\n },\n // Platforms detect stream automatically via RTMP ingest -- no API calls needed\n };\n}\n\n// ── Cloud relay destination ────────────────────────────────────────────────\n\nconst CLOUD_BASE_FALLBACK = \"https://www.elizacloud.ai/api/v1\";\n\nfunction readSetting(runtime: IAgentRuntime, key: string): string | null {\n const raw = runtime.getSetting(key);\n if (raw === null || raw === undefined) return null;\n const str = String(raw).trim();\n return str.length > 0 ? str : null;\n}\n\nfunction getCloudBaseUrl(runtime: IAgentRuntime): string {\n const override = readSetting(runtime, \"ELIZAOS_CLOUD_BASE_URL\");\n return (override ?? CLOUD_BASE_FALLBACK).replace(/\\/+$/, \"\");\n}\n\nfunction getCloudApiKey(runtime: IAgentRuntime): string {\n const apiKey = readSetting(runtime, \"ELIZAOS_CLOUD_API_KEY\");\n if (apiKey === null) {\n throw new Error(\n \"Eliza Cloud relay requested but ELIZAOS_CLOUD_API_KEY is not set\",\n );\n }\n return apiKey;\n}\n\ninterface CreateRelaySessionResponse {\n sessionId: string;\n streamKey: string;\n ingestUrl: string;\n wsUrl?: string;\n}\n\n/**\n * Configuration for the Eliza Cloud relay-backed streaming destination.\n *\n * The destination POSTs to `/v1/apis/streaming/sessions` to acquire a\n * per-session ingest URL + stream key. The cloud forwards the inbound\n * stream to the user's stored destinations for `platformId`.\n */\nexport interface CloudRelayDestinationCfg {\n /** Short lowercase platform identifier — e.g. \"twitch\", \"youtube\". */\n platformId: string;\n /** Display name — e.g. \"Twitch\", \"YouTube\". */\n platformName: string;\n /** Active runtime — used to read ELIZAOS_CLOUD_* settings. */\n runtime: IAgentRuntime;\n /** Optional per-destination default overlay layout. */\n defaultOverlayLayout?: OverlayLayoutData;\n}\n\n/**\n * Build a `StreamingDestination` whose RTMP credentials come from the\n * Eliza Cloud relay (Mode B). The cloud-issued credentials point at the\n * SRS ingest, NOT at the platform's RTMP endpoint — the cloud relays the\n * inbound stream to platform RTMP servers using stored per-org credentials.\n *\n * Lifecycle:\n * - `getCredentials()` — POST `/v1/apis/streaming/sessions` →\n * `{ sessionId, ingestUrl, streamKey }`, returned to the caller as\n * `{ rtmpUrl: ingestUrl, rtmpKey: streamKey }`.\n * - `onStreamStop()` — DELETE `/v1/apis/streaming/sessions/{id}`.\n *\n * Throws if Eliza Cloud is not connected.\n */\nexport function createCloudRelayDestination(\n cfg: CloudRelayDestinationCfg,\n): StreamingDestination {\n if (!isCloudConnected(cfg.runtime)) {\n throw new Error(\n `Cloud relay requested for ${cfg.platformName} but Eliza Cloud is not connected ` +\n `(ELIZAOS_CLOUD_API_KEY missing or ELIZAOS_CLOUD_ENABLED falsy)`,\n );\n }\n\n let activeSessionId: string | null = null;\n\n return {\n id: cfg.platformId,\n name: cfg.platformName,\n defaultOverlayLayout: cfg.defaultOverlayLayout,\n\n async getCredentials(): Promise<{ rtmpUrl: string; rtmpKey: string }> {\n const baseUrl = getCloudBaseUrl(cfg.runtime);\n const apiKey = getCloudApiKey(cfg.runtime);\n\n const res = await fetch(`${baseUrl}/apis/streaming/sessions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({ destinations: [cfg.platformId] }),\n signal: AbortSignal.timeout(20_000),\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\n `Cloud relay session create failed: ${res.status} ${text}`,\n );\n }\n\n const body = (await res.json()) as CreateRelaySessionResponse;\n if (!body.sessionId || !body.streamKey || !body.ingestUrl) {\n throw new Error(\n \"Cloud relay session create returned malformed response\",\n );\n }\n\n activeSessionId = body.sessionId;\n return { rtmpUrl: body.ingestUrl, rtmpKey: body.streamKey };\n },\n\n async onStreamStop(): Promise<void> {\n if (!activeSessionId) return;\n const baseUrl = getCloudBaseUrl(cfg.runtime);\n const apiKey = getCloudApiKey(cfg.runtime);\n const sessionId = activeSessionId;\n activeSessionId = null;\n\n const res = await fetch(\n `${baseUrl}/apis/streaming/sessions/${encodeURIComponent(sessionId)}`,\n {\n method: \"DELETE\",\n headers: { Authorization: `Bearer ${apiKey}` },\n signal: AbortSignal.timeout(15_000),\n },\n );\n if (!res.ok && res.status !== 404) {\n const text = await res.text().catch(() => \"\");\n throw new Error(\n `Cloud relay session close failed: ${res.status} ${text}`,\n );\n }\n },\n };\n}\n\n// ── Backend selection ──────────────────────────────────────────────────────\n\nexport type StreamingBackend = \"direct\" | \"cloud\" | \"auto\";\n\nfunction readBackendSetting(\n runtime: IAgentRuntime,\n envVar: string,\n): StreamingBackend {\n const raw = readSetting(runtime, envVar);\n if (raw === null) return \"auto\";\n const lower = raw.toLowerCase();\n if (lower === \"direct\" || lower === \"cloud\" || lower === \"auto\") return lower;\n throw new Error(\n `Invalid ${envVar}=\"${raw}\" (expected \"direct\" | \"cloud\" | \"auto\")`,\n );\n}\n\n/**\n * Resolve which streaming backend to use for a given platform at runtime.\n *\n * Reads `<UPPER>_STREAMING_BACKEND` (e.g. `TWITCH_STREAMING_BACKEND`) — one\n * of `direct`, `cloud`, or `auto` (default `auto`).\n *\n * `auto` picks `cloud` iff Eliza Cloud is connected AND no local stream key\n * is set in `cfg.streamKeyEnvVar`. Otherwise it picks `direct`.\n */\nexport function resolveStreamingBackend(\n runtime: IAgentRuntime,\n cfg: StreamingPluginConfig,\n): \"direct\" | \"cloud\" {\n const upper = cfg.platformId.toUpperCase().replace(/[^A-Z0-9]/g, \"_\");\n const setting = readBackendSetting(runtime, `${upper}_STREAMING_BACKEND`);\n if (setting === \"direct\" || setting === \"cloud\") return setting;\n\n const localKey = readSetting(runtime, cfg.streamKeyEnvVar);\n if (localKey !== null) return \"direct\";\n return isCloudConnected(runtime) ? \"cloud\" : \"direct\";\n}\n\n// ── Plugin factory ──────────────────────────────────────────────────────────\n\nexport function streamingPipelineLocalPort(): number {\n return Number(process.env.SERVER_PORT || process.env.PORT || \"2138\");\n}\n\n// ── Unified STREAM_OP router action + streamStatus provider ────────────────\n\nexport const STREAMING_PLATFORMS = [\n \"twitch\",\n \"youtube\",\n \"x\",\n \"pumpfun\",\n] as const;\n\nexport type StreamingPlatform = (typeof STREAMING_PLATFORMS)[number];\n\nexport type StreamingOp = \"start\" | \"stop\" | \"status\";\n\nconst PLATFORM_LABELS: Record<StreamingPlatform, string> = {\n twitch: \"Twitch\",\n youtube: \"YouTube\",\n x: \"X (Twitter)\",\n pumpfun: \"pump.fun\",\n};\n\ninterface StreamStatusSnapshot {\n platform: StreamingPlatform;\n running: boolean;\n uptimeSeconds: number | null;\n frames: number | null;\n destination: string;\n}\n\nfunction isStreamingPlatform(value: unknown): value is StreamingPlatform {\n return (\n typeof value === \"string\" &&\n (STREAMING_PLATFORMS as readonly string[]).includes(value)\n );\n}\n\nfunction isStreamingOp(value: unknown): value is StreamingOp {\n return value === \"start\" || value === \"stop\" || value === \"status\";\n}\n\nfunction readParam(\n options: unknown,\n key: string,\n): string | number | boolean | null | undefined {\n if (!options || typeof options !== \"object\" || Array.isArray(options)) {\n return undefined;\n }\n const handler = options as HandlerOptions;\n const params = handler.parameters as Record<string, JsonValue> | undefined;\n if (params && key in params) {\n const v = params[key];\n if (\n typeof v === \"string\" ||\n typeof v === \"number\" ||\n typeof v === \"boolean\" ||\n v === null\n ) {\n return v as string | number | boolean | null;\n }\n }\n const flat = options as Record<string, unknown>;\n const v = flat[key];\n if (\n typeof v === \"string\" ||\n typeof v === \"number\" ||\n typeof v === \"boolean\" ||\n v === null\n ) {\n return v as string | number | boolean | null;\n }\n return undefined;\n}\n\nasync function fetchStreamStatus(\n platform: StreamingPlatform,\n): Promise<StreamStatusSnapshot> {\n const port = streamingPipelineLocalPort();\n const res = await fetch(`http://127.0.0.1:${port}/api/stream/status`, {\n signal: AbortSignal.timeout(10_000),\n });\n const data = (await res.json()) as Record<string, unknown>;\n const uptime = typeof data.uptime === \"number\" ? Number(data.uptime) : null;\n const frames = typeof data.frames === \"number\" ? Number(data.frames) : null;\n return {\n platform,\n running: !!data.running,\n uptimeSeconds: uptime,\n frames,\n destination: PLATFORM_LABELS[platform],\n };\n}\n\nexport interface BuildStreamOpActionParams {\n validate?: () => Promise<boolean>;\n}\n\nexport function buildStreamOpAction(\n params: BuildStreamOpActionParams = {},\n): Action {\n const validate = params.validate ?? (async () => true);\n\n return {\n name: \"STREAM\",\n contexts: [\"media\", \"automation\", \"connectors\"],\n contextGate: { anyOf: [\"media\", \"automation\", \"connectors\"] },\n roleGate: { minRole: \"ADMIN\" },\n description:\n \"Control the local RTMP streaming pipeline for a target platform. Dispatches start, stop, and status calls to the dashboard stream API for twitch, youtube, x, or pumpfun.\",\n descriptionCompressed:\n \"Stream ops: start, stop, status; platforms: twitch, youtube, x, pumpfun.\",\n similes: [\n \"START_STREAM\",\n \"STOP_STREAM\",\n \"GET_STREAM_STATUS\",\n \"GO_LIVE\",\n \"GO_OFFLINE\",\n \"STREAM_STATUS\",\n \"IS_LIVE\",\n ],\n parameters: [\n {\n name: \"platform\",\n description:\n \"Streaming destination platform: twitch, youtube, x, or pumpfun.\",\n descriptionCompressed: \"Platform: twitch|youtube|x|pumpfun.\",\n required: true,\n schema: {\n type: \"string\",\n enum: [...STREAMING_PLATFORMS],\n },\n },\n {\n name: \"action\",\n description:\n \"Operation to perform: start (go live), stop (go offline), or status.\",\n descriptionCompressed: \"Op: start|stop|status.\",\n required: true,\n schema: {\n type: \"string\",\n enum: [\"start\", \"stop\", \"status\"],\n },\n },\n {\n name: \"subaction\",\n description: \"Legacy alias for action.\",\n descriptionCompressed: \"Legacy action alias.\",\n required: false,\n schema: {\n type: \"string\",\n enum: [\"start\", \"stop\", \"status\"],\n },\n },\n ],\n validate,\n handler: async (\n _runtime: IAgentRuntime,\n _message: Memory,\n _state: State | undefined,\n options:\n | HandlerOptions\n | Record<string, JsonValue | undefined>\n | undefined,\n callback?: HandlerCallback,\n ): Promise<ActionResult> => {\n const platformRaw = readParam(options, \"platform\");\n const opRaw =\n readParam(options, \"action\") ??\n readParam(options, \"subaction\") ??\n readParam(options, \"op\") ??\n readParam(options, \"operation\");\n if (!isStreamingPlatform(platformRaw)) {\n const text = `STREAM_OP requires platform in {${STREAMING_PLATFORMS.join(\", \")}}, got ${String(platformRaw)}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: false, error: text };\n }\n if (!isStreamingOp(opRaw)) {\n const text = `STREAM requires action in {start, stop, status}, got ${String(opRaw)}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: false, error: text };\n }\n\n const platform: StreamingPlatform = platformRaw;\n const op: StreamingOp = opRaw;\n const label = PLATFORM_LABELS[platform];\n const port = streamingPipelineLocalPort();\n\n try {\n if (op === \"start\") {\n const res = await fetch(`http://127.0.0.1:${port}/api/stream/live`, {\n method: \"POST\",\n signal: AbortSignal.timeout(30_000),\n });\n const data = (await res.json()) as Record<string, unknown>;\n const ok = !!data.ok;\n const text = ok\n ? `${label} stream started successfully! We're live.`\n : `Failed to start ${label} stream: ${data.error ?? \"unknown error\"}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: ok, text };\n }\n\n if (op === \"stop\") {\n const res = await fetch(\n `http://127.0.0.1:${port}/api/stream/offline`,\n {\n method: \"POST\",\n signal: AbortSignal.timeout(15_000),\n },\n );\n const data = (await res.json()) as Record<string, unknown>;\n const ok = !!data.ok;\n const text = ok\n ? `${label} stream stopped. We're offline now.`\n : `Failed to stop ${label} stream: ${data.error ?? \"unknown error\"}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: ok, text };\n }\n\n const snapshot = await fetchStreamStatus(platform);\n const status = snapshot.running ? \"LIVE\" : \"OFFLINE\";\n const uptime =\n snapshot.uptimeSeconds === null\n ? \"n/a\"\n : `${Math.floor(snapshot.uptimeSeconds / 60)}m`;\n const text = `${label} stream status: ${status} | Uptime: ${uptime} | Destination: ${label}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: true, text, data: { snapshot } };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const text = `Error running STREAM_OP ${op} for ${label}: ${msg}`;\n if (callback) await callback({ text, actions: [] } as Content);\n return { success: false, error: msg, text };\n }\n },\n examples: [\n [\n {\n name: \"{{user1}}\",\n content: { text: \"Go live on Twitch\" },\n },\n {\n name: \"{{agent}}\",\n content: {\n text: \"Starting the Twitch stream now.\",\n actions: [\"STREAM\"],\n },\n },\n ],\n [\n {\n name: \"{{user1}}\",\n content: { text: \"Stop the YouTube stream\" },\n },\n {\n name: \"{{agent}}\",\n content: {\n text: \"Stopping the stream now.\",\n actions: [\"STREAM\"],\n },\n },\n ],\n [\n {\n name: \"{{user1}}\",\n content: { text: \"Is the X stream live?\" },\n },\n {\n name: \"{{agent}}\",\n content: {\n text: \"Let me check the stream status.\",\n actions: [\"STREAM\"],\n },\n },\n ],\n ],\n };\n}\n\n/**\n * Provider that renders the live status of every supported streaming platform\n * as JSON context. The pipeline currently exposes a single shared\n * `/api/stream/status` endpoint, so each platform row reflects that same\n * snapshot tagged with its destination label.\n */\nexport const streamStatusProvider: Provider = {\n name: \"streamStatus\",\n description:\n \"Live RTMP pipeline status per supported platform (twitch, youtube, x, pumpfun) rendered as JSON.\",\n descriptionCompressed: \"RTMP status per platform.\",\n dynamic: true,\n contexts: [\"media\", \"automation\"],\n contextGate: { anyOf: [\"media\", \"automation\"] },\n cacheStable: false,\n cacheScope: \"turn\",\n get: async (\n _runtime: IAgentRuntime,\n _message: Memory,\n _state: State,\n ): Promise<ProviderResult> => {\n const rows = await Promise.all(\n STREAMING_PLATFORMS.map(async (platform) => {\n try {\n const snap = await fetchStreamStatus(platform);\n return {\n platform: snap.platform,\n running: snap.running,\n uptimeSeconds: snap.uptimeSeconds ?? 0,\n frames: snap.frames ?? 0,\n destination: snap.destination,\n error: null,\n };\n } catch (err) {\n return {\n platform,\n running: false,\n uptimeSeconds: 0,\n frames: 0,\n destination: PLATFORM_LABELS[platform],\n error: err instanceof Error ? err.message : String(err),\n };\n }\n }),\n );\n\n return {\n text: JSON.stringify({\n stream_status: {\n count: rows.length,\n platforms: rows,\n },\n }),\n data: { stream_status: rows },\n };\n },\n};\n\n/**\n * Build a complete elizaOS Plugin for a streaming destination.\n *\n * Returns:\n * - `plugin` -- the Plugin object to register with elizaOS\n * - `createDestination` -- the destination factory (for the streaming pipeline)\n */\n/** Result of {@link createStreamingPlugin} — plugin + a backend-aware destination factory. */\nexport interface CreatedStreamingPlugin {\n plugin: Plugin;\n createDestination: (\n runtime?: IAgentRuntime,\n overrides?: { streamKey?: string; rtmpUrl?: string },\n ) => StreamingDestination;\n}\n\nexport function createStreamingPlugin(\n cfg: StreamingPluginConfig,\n): CreatedStreamingPlugin {\n const NAME = cfg.platformName;\n\n const configEntries: Record<string, string | null> = {\n [cfg.streamKeyEnvVar]: process.env[cfg.streamKeyEnvVar] ?? null,\n };\n if (cfg.rtmpUrlEnvVar) {\n configEntries[cfg.rtmpUrlEnvVar] = process.env[cfg.rtmpUrlEnvVar] ?? null;\n }\n\n const plugin: Plugin = {\n name: cfg.pluginName ?? `${cfg.platformId}-streaming`,\n description: `${NAME} RTMP streaming destination — credentials and overlay layout. Stream control actions live on the unified streaming plugin.`,\n get config() {\n return configEntries;\n },\n actions: [],\n async init(_config: Record<string, string>, _runtime: IAgentRuntime) {\n const streamKey = (\n _config[cfg.streamKeyEnvVar] ??\n process.env[cfg.streamKeyEnvVar] ??\n \"\"\n ).trim();\n if (!streamKey) {\n return;\n }\n },\n };\n\n const createDestination = (\n runtime?: IAgentRuntime,\n overrides?: { streamKey?: string; rtmpUrl?: string },\n ): StreamingDestination => {\n if (cfg.cloudRelay && runtime) {\n const backend = resolveStreamingBackend(runtime, cfg);\n if (backend === \"cloud\") {\n return createCloudRelayDestination({\n platformId: cfg.platformId,\n platformName: cfg.platformName,\n runtime,\n defaultOverlayLayout: cfg.defaultOverlayLayout,\n });\n }\n }\n return createStreamingDestination(cfg, overrides);\n };\n\n return { plugin, createDestination };\n}\n"],"mappings":"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","names":["v","text"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,35 +1,29 @@
|
|
|
1
|
-
export { StreamVisualSettings, StreamVoiceSettings, getHeadlessCaptureConfig, parseDestinationQuery, readOverlayLayout, readStreamSettings, safeDestId, seedOverlayDefaults, validateStreamSettings, writeOverlayLayout, writeStreamSettings } from './api/stream-persistence.js';
|
|
2
|
-
export { StreamRouteState } from './api/stream-route-state.js';
|
|
3
|
-
export { detectCaptureMode, ensureXvfb, getActiveDestination, handleStreamRoute } from './api/stream-routes.js';
|
|
4
|
-
export { StreamingUpdate, StreamingUpdateKind, mergeStreamingText, resolveStreamingUpdate } from './api/streaming-text.js';
|
|
5
|
-
export { TtsRouteContext, handleTtsRoutes } from './api/tts-routes.js';
|
|
6
|
-
import { StreamingDestination } from './core.js';
|
|
7
|
-
export { BuildStreamOpActionParams, CloudRelayDestinationCfg, CreatedStreamingPlugin, OverlayLayoutData, OverlayWidgetInstance, STREAMING_PLATFORMS, StreamingBackend, StreamingOp, StreamingPlatform, StreamingPluginConfig, buildPresetLayout, buildStreamOpAction, createCloudRelayDestination, createCustomRtmpDestination, createNamedRtmpDestination, createStreamingDestination, createStreamingPlugin, resolveStreamingBackend, streamStatusProvider, streamingPipelineLocalPort } from './core.js';
|
|
8
|
-
export { AudioSource, StreamConfig, streamManager } from './services/stream-manager.js';
|
|
9
|
-
import { IAgentRuntime, Plugin } from '@elizaos/core';
|
|
10
|
-
import 'node:http';
|
|
11
|
-
import './services/tts-stream-bridge.js';
|
|
12
|
-
import 'node:stream';
|
|
13
|
-
|
|
14
1
|
/**
|
|
15
2
|
* @elizaos/plugin-streaming — RTMP destinations (Twitch, YouTube, X, pump.fun, custom/named ingest).
|
|
16
3
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
4
|
+
export { getHeadlessCaptureConfig, parseDestinationQuery, readOverlayLayout, readStreamSettings, type StreamVisualSettings, type StreamVoiceSettings, safeDestId, seedOverlayDefaults, validateStreamSettings, writeOverlayLayout, writeStreamSettings, } from "./api/stream-persistence.ts";
|
|
5
|
+
export type { StreamRouteState } from "./api/stream-route-state.ts";
|
|
6
|
+
export { detectCaptureMode, ensureXvfb, getActiveDestination, handleStreamRoute, } from "./api/stream-routes.ts";
|
|
7
|
+
export { mergeStreamingText, resolveStreamingUpdate, type StreamingUpdate, type StreamingUpdateKind, } from "./api/streaming-text.ts";
|
|
8
|
+
export { handleTtsRoutes, type TtsRouteContext } from "./api/tts-routes.ts";
|
|
9
|
+
export * from "./core.ts";
|
|
10
|
+
export { type AudioSource, type StreamConfig, streamManager, } from "./services/stream-manager.ts";
|
|
11
|
+
import type { IAgentRuntime, Plugin } from "@elizaos/core";
|
|
12
|
+
import { type StreamingDestination } from "./core.ts";
|
|
13
|
+
export declare function createTwitchDestination(runtime?: IAgentRuntime, config?: {
|
|
19
14
|
streamKey?: string;
|
|
20
15
|
}): StreamingDestination;
|
|
21
|
-
declare function createYoutubeDestination(runtime?: IAgentRuntime, config?: {
|
|
16
|
+
export declare function createYoutubeDestination(runtime?: IAgentRuntime, config?: {
|
|
22
17
|
streamKey?: string;
|
|
23
18
|
rtmpUrl?: string;
|
|
24
19
|
}): StreamingDestination;
|
|
25
|
-
declare function createXStreamDestination(runtime?: IAgentRuntime, config?: {
|
|
20
|
+
export declare function createXStreamDestination(runtime?: IAgentRuntime, config?: {
|
|
26
21
|
streamKey?: string;
|
|
27
22
|
rtmpUrl?: string;
|
|
28
23
|
}): StreamingDestination;
|
|
29
|
-
declare function createPumpfunDestination(runtime?: IAgentRuntime, config?: {
|
|
24
|
+
export declare function createPumpfunDestination(runtime?: IAgentRuntime, config?: {
|
|
30
25
|
streamKey?: string;
|
|
31
26
|
rtmpUrl?: string;
|
|
32
27
|
}): StreamingDestination;
|
|
33
|
-
declare const streamingPlugin: Plugin;
|
|
34
|
-
|
|
35
|
-
export { StreamingDestination, createPumpfunDestination, createTwitchDestination, createXStreamDestination, createYoutubeDestination, streamingPlugin as default, streamingPlugin };
|
|
28
|
+
export declare const streamingPlugin: Plugin;
|
|
29
|
+
export default streamingPlugin;
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { ITtsStreamBridge } from './tts-stream-bridge.js';
|
|
2
|
-
import 'node:stream';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* Stream Manager — cross-platform RTMP streaming via FFmpeg.
|
|
6
3
|
*
|
|
@@ -30,9 +27,9 @@ import 'node:stream';
|
|
|
30
27
|
*
|
|
31
28
|
* @module services/stream-manager
|
|
32
29
|
*/
|
|
33
|
-
|
|
34
|
-
type AudioSource = "silent" | "system" | "microphone" | "tts";
|
|
35
|
-
interface StreamConfig {
|
|
30
|
+
import { type ITtsStreamBridge } from "./tts-stream-bridge.js";
|
|
31
|
+
export type AudioSource = "silent" | "system" | "microphone" | "tts";
|
|
32
|
+
export interface StreamConfig {
|
|
36
33
|
rtmpUrl: string;
|
|
37
34
|
rtmpKey: string;
|
|
38
35
|
/** FFmpeg video input source. Defaults to "testsrc" (test pattern). */
|
|
@@ -119,6 +116,5 @@ declare class StreamManager {
|
|
|
119
116
|
/** Get the TTS stream bridge for external speak triggers. */
|
|
120
117
|
getTtsBridge(): ITtsStreamBridge;
|
|
121
118
|
}
|
|
122
|
-
declare const streamManager: StreamManager;
|
|
123
|
-
|
|
124
|
-
export { type AudioSource, type StreamConfig, streamManager };
|
|
119
|
+
export declare const streamManager: StreamManager;
|
|
120
|
+
export {};
|
|
@@ -510,12 +510,9 @@ ${installHint}`
|
|
|
510
510
|
if (source.startsWith("/") || source.startsWith("./")) {
|
|
511
511
|
return ["-stream_loop", "-1", "-i", source];
|
|
512
512
|
}
|
|
513
|
-
|
|
514
|
-
"
|
|
515
|
-
|
|
516
|
-
"-i",
|
|
517
|
-
"anullsrc=channel_layout=stereo:sample_rate=44100"
|
|
518
|
-
];
|
|
513
|
+
throw new Error(
|
|
514
|
+
`${TAG} Unsupported audio source ${JSON.stringify(source)}. Use "silent" explicitly for a silent track, "tts" for model audio, "system", "microphone", or an audio file path.`
|
|
515
|
+
);
|
|
519
516
|
}
|
|
520
517
|
}
|
|
521
518
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/stream-manager.ts"],"sourcesContent":["/**\n * Stream Manager — cross-platform RTMP streaming via FFmpeg.\n *\n * Supports multiple input modes:\n * - \"pipe\": Receives JPEG frames via writeFrame() → FFmpeg stdin (image2pipe).\n * Used for streaming desktop window contents captured by the host bridge.\n * - \"avfoundation\" / \"screen\": macOS native screen capture.\n * - \"x11grab\": Linux virtual display capture (Xvfb). Used for GPU-backed game streams.\n * - \"file\": Reads a continuously-updated JPEG file (browser-capture).\n * - \"testsrc\": Solid color test pattern (default fallback).\n *\n * Audio support:\n * - \"silent\": Synthetic silent audio (anullsrc) — default.\n * - \"system\": System/desktop audio capture.\n * - \"microphone\": Microphone input.\n * - File path: Play an audio file as stream audio.\n *\n * Volume control:\n * - setVolume(0-100), mute(), unmute() — restarts FFmpeg to apply.\n *\n * Usage:\n * import { streamManager } from \"./services/stream-manager\";\n * await streamManager.start({ rtmpUrl, rtmpKey, inputMode: \"pipe\" });\n * streamManager.writeFrame(jpegBuffer); // called from frame capture\n * streamManager.setVolume(50); // adjust volume mid-stream\n * await streamManager.stop();\n *\n * @module services/stream-manager\n */\n\nimport { type ChildProcess, execSync, spawn } from \"node:child_process\";\nimport { logger } from \"@elizaos/core\";\nimport { type ITtsStreamBridge, ttsStreamBridge } from \"./tts-stream-bridge.js\";\n\nconst TAG = \"[StreamManager]\";\n\nexport type AudioSource = \"silent\" | \"system\" | \"microphone\" | \"tts\";\n\nexport interface StreamConfig {\n rtmpUrl: string;\n rtmpKey: string;\n /** FFmpeg video input source. Defaults to \"testsrc\" (test pattern). */\n inputMode?:\n | \"testsrc\"\n | \"avfoundation\"\n | \"screen\"\n | \"pipe\"\n | \"file\"\n | \"x11grab\";\n /** avfoundation video device index (default \"3\" = Capture screen 0 on macOS) */\n videoDevice?: string;\n /** Path to JPEG frame file (for \"file\" input mode) */\n frameFile?: string;\n /** Resolution (default \"1280x720\") */\n resolution?: string;\n /** Video bitrate (default \"2500k\") */\n bitrate?: string;\n /** Frame rate (default 15) */\n framerate?: number;\n /** X11 display for x11grab mode (e.g., \":99\"). Default \":99\". */\n display?: string;\n /** Audio source. Default \"silent\" (anullsrc). Can also be an absolute file path. */\n audioSource?: AudioSource | string;\n /** Audio device identifier (platform-specific). For macOS avfoundation: device index. For Linux: pulse/alsa device name. */\n audioDevice?: string;\n /** Volume level 0–100. Default 80. Applied as FFmpeg audio filter. */\n volume?: number;\n /** Whether audio is muted. Default false. Overrides volume to 0 when true. */\n muted?: boolean;\n}\n\nclass StreamManager {\n private ffmpeg: ChildProcess | null = null;\n private _running = false;\n private startedAt: number | null = null;\n private _frameCount = 0;\n /** Current stream config — stored for restart on volume/audio changes. */\n private _config: StreamConfig | null = null;\n /** Current volume level (0–100). */\n private _volume = 80;\n /** Whether audio is muted. */\n private _muted = false;\n /** Auto-restart state. */\n private _restartAttempts = 0;\n private _maxRestartAttempts = 5;\n private _restartDecayTimer: ReturnType<typeof setInterval> | null = null;\n private _intentionalStop = false;\n /** Pending auto-restart timer — cleared in stop() to prevent races. */\n private _restartTimer: ReturnType<typeof setTimeout> | null = null;\n /** Guard: prevents concurrent start() calls from orphaning FFmpeg. */\n private _starting = false;\n\n isRunning(): boolean {\n return this._running;\n }\n\n getUptime(): number {\n if (!this.startedAt) return 0;\n return Math.floor((Date.now() - this.startedAt) / 1000);\n }\n\n getHealth() {\n return {\n running: this._running,\n ffmpegAlive:\n this.ffmpeg !== null &&\n this.ffmpeg.exitCode === null &&\n !this.ffmpeg.killed,\n uptime: this.getUptime(),\n frameCount: this._frameCount,\n volume: this._volume,\n muted: this._muted,\n audioSource: this._config?.audioSource || \"silent\",\n inputMode: this._config?.inputMode || null,\n };\n }\n\n getVolume(): number {\n return this._muted ? 0 : this._volume;\n }\n\n isMuted(): boolean {\n return this._muted;\n }\n\n /**\n * Set volume (0–100). Restarts FFmpeg if currently streaming to apply the change.\n */\n async setVolume(level: number): Promise<void> {\n this._volume = Math.max(0, Math.min(100, Math.round(level)));\n logger.info(`${TAG} Volume set to ${this._volume}`);\n if (this._running && this._config) {\n await this.restart();\n }\n }\n\n /** Mute audio. Restarts FFmpeg if currently streaming. */\n async mute(): Promise<void> {\n if (this._muted) return;\n this._muted = true;\n logger.info(`${TAG} Audio muted`);\n if (this._running && this._config) {\n await this.restart();\n }\n }\n\n /** Unmute audio. Restarts FFmpeg if currently streaming. */\n async unmute(): Promise<void> {\n if (!this._muted) return;\n this._muted = false;\n logger.info(`${TAG} Audio unmuted (volume: ${this._volume})`);\n if (this._running && this._config) {\n await this.restart();\n }\n }\n\n /** Restart the stream with updated config (preserves uptime tracking). */\n private async restart(): Promise<void> {\n if (!this._config) return;\n const savedStartedAt = this.startedAt;\n const savedFrameCount = this._frameCount;\n\n // Mark as intentional so the exit handler doesn't trigger autoRestart()\n // concurrently with our manual restart below.\n this._intentionalStop = true;\n\n // Detach TTS bridge before stopping FFmpeg\n ttsStreamBridge.detach();\n\n // Stop FFmpeg without resetting tracking\n if (this.ffmpeg && !this.ffmpeg.killed && this.ffmpeg.exitCode === null) {\n if (this.ffmpeg.stdin) {\n try {\n this.ffmpeg.stdin.end();\n } catch {\n /* ignore */\n }\n }\n this.ffmpeg.kill(\"SIGTERM\");\n await Promise.race([\n new Promise((resolve) => this.ffmpeg?.on(\"exit\", resolve)),\n new Promise((resolve) => setTimeout(resolve, 2000)),\n ]);\n if (this.ffmpeg?.exitCode === null) {\n this.ffmpeg.kill(\"SIGKILL\");\n }\n }\n this.ffmpeg = null;\n this._running = false;\n\n // Restart with current volume/mute applied\n const config = {\n ...this._config,\n volume: this._volume,\n muted: this._muted,\n };\n this._intentionalStop = false;\n await this.start(config);\n\n // Restore tracking\n this.startedAt = savedStartedAt;\n this._frameCount = savedFrameCount;\n logger.info(\n `${TAG} Stream restarted (volume=${this._volume}, muted=${this._muted})`,\n );\n }\n\n /**\n * Write a JPEG frame to FFmpeg's stdin (only works in \"pipe\" mode).\n * Returns true if the frame was accepted.\n */\n writeFrame(jpegData: Buffer): boolean {\n if (!this._running || !this.ffmpeg?.stdin) return false;\n if (this.ffmpeg.killed || this.ffmpeg.exitCode !== null) return false;\n\n try {\n this.ffmpeg.stdin.write(jpegData);\n this._frameCount++;\n if (this._frameCount % 150 === 0) {\n logger.info(`${TAG} Piped ${this._frameCount} frames to FFmpeg`);\n }\n return true;\n } catch {\n return false;\n }\n }\n\n async start(config: StreamConfig): Promise<void> {\n if (this._running || this._starting) {\n logger.warn(`${TAG} Already running or starting — stop first`);\n return;\n }\n this._starting = true;\n try {\n await this._startInner(config);\n } finally {\n this._starting = false;\n }\n }\n\n private async _startInner(config: StreamConfig): Promise<void> {\n // Pre-flight: ensure FFmpeg is installed\n try {\n execSync(\"ffmpeg -version\", { stdio: \"ignore\", timeout: 5000 });\n } catch {\n const installHint =\n process.platform === \"darwin\"\n ? \"Install with: brew install ffmpeg\"\n : process.platform === \"linux\"\n ? \"Install with: sudo apt install ffmpeg (or your distro's package manager)\"\n : \"Download from https://ffmpeg.org/download.html\";\n throw new Error(\n `FFmpeg not found. Streaming requires FFmpeg to be installed.\\n${installHint}`,\n );\n }\n\n this._config = config;\n this._frameCount = 0;\n this._volume = config.volume ?? this._volume;\n this._muted = config.muted ?? this._muted;\n\n const resolution = config.resolution || \"1280x720\";\n const bitrate = config.bitrate || \"2500k\";\n const framerate = config.framerate || 15;\n const rtmpTarget = `${config.rtmpUrl}/${config.rtmpKey}`;\n const bufsize = `${parseInt(bitrate, 10) * 2}k`;\n const mode = config.inputMode || \"testsrc\";\n\n // Build FFmpeg args based on input mode\n const videoInputArgs = this.buildVideoInputArgs(\n config,\n resolution,\n framerate,\n );\n const audioInputArgs = this.buildAudioInputArgs(config);\n const isPipe = mode === \"pipe\";\n const isScreenCapture =\n mode === \"avfoundation\" || mode === \"screen\" || mode === \"x11grab\";\n\n // Effective volume: 0 when muted, otherwise 0–1.0 scale\n const effectiveVolume = this._muted ? 0 : this._volume / 100;\n\n // FFmpeg arg order: all inputs first, then filters, then encoding/output\n const ffmpegArgs = [\n \"-thread_queue_size\",\n \"512\",\n // Video input\n ...videoInputArgs,\n // Audio input\n ...audioInputArgs,\n // Video filter: scale for screen capture modes\n ...(isScreenCapture\n ? [\"-vf\", `scale=${resolution.replace(\"x\", \":\")}:flags=fast_bilinear`]\n : []),\n // Audio filter: volume control\n \"-af\",\n `volume=${effectiveVolume.toFixed(2)}`,\n // Video encoding (platform-specific)\n ...(process.platform === \"darwin\"\n ? [\n \"-c:v\",\n \"h264_videotoolbox\",\n \"-realtime\",\n \"1\",\n \"-b:v\",\n bitrate,\n \"-maxrate\",\n bitrate,\n \"-bufsize\",\n bufsize,\n ]\n : [\n \"-c:v\",\n \"libx264\",\n \"-preset\",\n \"veryfast\",\n \"-tune\",\n \"zerolatency\",\n \"-b:v\",\n bitrate,\n \"-maxrate\",\n bitrate,\n \"-bufsize\",\n bufsize,\n ]),\n \"-s\",\n resolution,\n \"-pix_fmt\",\n \"yuv420p\",\n \"-g\",\n \"60\",\n // Audio encoding\n \"-c:a\",\n \"aac\",\n \"-b:a\",\n \"128k\",\n // Output\n \"-f\",\n \"flv\",\n rtmpTarget,\n ];\n\n const audioSrc = config.audioSource || \"silent\";\n logger.info(\n `${TAG} Starting FFmpeg RTMP stream (video=${mode}, audio=${audioSrc}, vol=${this._volume}${this._muted ? \" MUTED\" : \"\"}) → ${config.rtmpUrl}`,\n );\n logger.info(\n `${TAG} Resolution: ${resolution}, Bitrate: ${bitrate}, FPS: ${framerate}`,\n );\n\n const isTts = (config.audioSource || \"silent\") === \"tts\";\n\n // In pipe mode, FFmpeg reads from stdin; otherwise stdin is ignored.\n // TTS mode adds a 4th stdio fd (pipe:3) for raw PCM audio input.\n this.ffmpeg = spawn(\"ffmpeg\", [\"-y\", ...ffmpegArgs], {\n stdio: [\n isPipe ? \"pipe\" : \"ignore\",\n \"pipe\",\n \"pipe\",\n ...(isTts ? ([\"pipe\"] as const) : []),\n ],\n });\n\n // Log all FFmpeg stderr for debugging\n this.ffmpeg.stderr?.on(\"data\", (chunk: Buffer) => {\n const line = chunk.toString().trim();\n if (line) {\n logger.debug(`[FFmpeg] ${line}`);\n }\n });\n\n this.ffmpeg.on(\"exit\", (code, signal) => {\n if (this._running) {\n logger.warn(\n `${TAG} FFmpeg exited unexpectedly (code=${code}, signal=${signal})`,\n );\n this._running = false;\n if (!this._intentionalStop && this._config) {\n this.autoRestart();\n } else {\n this.startedAt = null;\n }\n }\n });\n\n // Handle stdin errors gracefully in pipe mode\n if (isPipe && this.ffmpeg.stdin) {\n this.ffmpeg.stdin.on(\"error\", (err) => {\n logger.warn(`${TAG} FFmpeg stdin error: ${err.message}`);\n });\n }\n\n // Attach TTS bridge to pipe:3 for PCM audio\n if (isTts && this.ffmpeg.stdio[3]) {\n const pipe3 = this.ffmpeg.stdio[3] as import(\"node:stream\").Writable;\n ttsStreamBridge.attach(pipe3);\n logger.info(`${TAG} TTS bridge attached to pipe:3`);\n }\n\n // Wait a moment to confirm it started\n await new Promise((r) => setTimeout(r, 1500));\n\n if (this.ffmpeg.exitCode !== null) {\n const exitCode = this.ffmpeg.exitCode;\n this.ffmpeg = null;\n throw new Error(`${TAG} FFmpeg exited immediately with code ${exitCode}`);\n }\n\n this._running = true;\n this.startedAt = Date.now();\n this._intentionalStop = false;\n // Decay restart counter every 30s of healthy running\n if (this._restartDecayTimer) clearInterval(this._restartDecayTimer);\n this._restartDecayTimer = setInterval(() => {\n if (this._restartAttempts > 0) {\n this._restartAttempts = Math.max(0, this._restartAttempts - 1);\n logger.info(\n `${TAG} Restart counter decayed to ${this._restartAttempts}`,\n );\n }\n }, 30_000);\n logger.info(`${TAG} FFmpeg streaming to RTMP — stream should be live`);\n }\n\n async stop(): Promise<{ uptime: number }> {\n const uptime = this.getUptime();\n const frames = this._frameCount;\n\n // Detach TTS bridge before killing FFmpeg\n ttsStreamBridge.detach();\n\n if (this.ffmpeg && !this.ffmpeg.killed && this.ffmpeg.exitCode === null) {\n const ffmpegProc = this.ffmpeg;\n // Close stdin first in pipe mode to signal EOF\n if (ffmpegProc.stdin) {\n try {\n ffmpegProc.stdin.end();\n } catch {\n /* ignore */\n }\n }\n ffmpegProc.kill(\"SIGTERM\");\n await Promise.race([\n new Promise((resolve) => ffmpegProc.on(\"exit\", resolve)),\n new Promise((resolve) => setTimeout(resolve, 3000)),\n ]);\n if (ffmpegProc.exitCode === null) {\n ffmpegProc.kill(\"SIGKILL\");\n }\n }\n\n this._intentionalStop = true;\n if (this._restartTimer) {\n clearTimeout(this._restartTimer);\n this._restartTimer = null;\n }\n if (this._restartDecayTimer) {\n clearInterval(this._restartDecayTimer);\n this._restartDecayTimer = null;\n }\n this.ffmpeg = null;\n this._running = false;\n this.startedAt = null;\n this._frameCount = 0;\n this._restartAttempts = 0;\n this._config = null;\n logger.info(\n `${TAG} Stream stopped (uptime: ${uptime}s, frames: ${frames})`,\n );\n return { uptime };\n }\n\n /** Attempt to restart FFmpeg after unexpected exit with exponential backoff. */\n private autoRestart(): void {\n if (this._restartAttempts >= this._maxRestartAttempts) {\n logger.error(\n `${TAG} Max restart attempts (${this._maxRestartAttempts}) reached — giving up`,\n );\n this.startedAt = null;\n if (this._restartDecayTimer) {\n clearInterval(this._restartDecayTimer);\n this._restartDecayTimer = null;\n }\n return;\n }\n\n this._restartAttempts++;\n const delay = Math.min(1000 * 2 ** (this._restartAttempts - 1), 60_000);\n logger.info(\n `${TAG} Auto-restart attempt ${this._restartAttempts}/${this._maxRestartAttempts} in ${delay}ms`,\n );\n\n this._restartTimer = setTimeout(async () => {\n this._restartTimer = null;\n if (this._intentionalStop || !this._config) return;\n\n const savedStartedAt = this.startedAt;\n const savedFrameCount = this._frameCount;\n\n try {\n this.ffmpeg = null;\n await this.start({\n ...this._config,\n volume: this._volume,\n muted: this._muted,\n });\n // Restore tracking so uptime is continuous\n this.startedAt = savedStartedAt;\n this._frameCount = savedFrameCount;\n logger.info(`${TAG} Auto-restart successful`);\n } catch (err) {\n this._running = false;\n logger.error(`${TAG} Auto-restart failed: ${String(err)}`);\n // start() failed before spawning FFmpeg — no exit event will fire,\n // so manually chain the next restart attempt if retries remain.\n if (!this._intentionalStop && this._config) {\n this.autoRestart();\n }\n }\n }, delay);\n }\n\n // ---------------------------------------------------------------------------\n // Video input args\n // ---------------------------------------------------------------------------\n\n private buildVideoInputArgs(\n config: StreamConfig,\n resolution: string,\n framerate: number,\n ): string[] {\n const mode = config.inputMode || \"testsrc\";\n\n switch (mode) {\n case \"pipe\": {\n // Read JPEG frames from stdin via image2pipe.\n // -c:v mjpeg is mandatory: image2pipe cannot auto-detect JPEG from piped data.\n // -probesize/-analyzeduration eliminate the default 5MB probe buffer that\n // causes FFmpeg to stall for ~100 frames before decoding starts.\n return [\n \"-probesize\",\n \"32\",\n \"-analyzeduration\",\n \"0\",\n \"-f\",\n \"image2pipe\",\n \"-c:v\",\n \"mjpeg\",\n \"-framerate\",\n String(framerate),\n \"-i\",\n \"pipe:0\",\n ];\n }\n case \"avfoundation\":\n case \"screen\": {\n // macOS native screen capture via avfoundation.\n // videoDevice \"3\" = Capture screen 0; \":none\" = no audio from avfoundation.\n const videoDevice = config.videoDevice || \"3\";\n return [\n \"-f\",\n \"avfoundation\",\n \"-framerate\",\n String(framerate),\n \"-pixel_format\",\n \"nv12\",\n \"-capture_cursor\",\n \"1\",\n \"-i\",\n `${videoDevice}:none`,\n ];\n }\n case \"x11grab\": {\n // Linux virtual display capture (Xvfb) for GPU-backed game streams.\n // Requires: Xvfb :99 -screen 0 1280x720x24 -ac &\n // Then run a browser/TUI on display :99.\n const display = config.display || \":99\";\n return [\n \"-f\",\n \"x11grab\",\n \"-video_size\",\n resolution,\n \"-framerate\",\n String(framerate),\n \"-draw_mouse\",\n \"0\",\n \"-i\",\n display,\n ];\n }\n case \"file\": {\n // Read from a continuously-updated JPEG file (written by browser-capture).\n const framePath = config.frameFile || \"/tmp/eliza-stream-frame.jpg\";\n return [\n \"-probesize\",\n \"32\",\n \"-analyzeduration\",\n \"0\",\n \"-loop\",\n \"1\",\n \"-f\",\n \"image2\",\n \"-c:v\",\n \"mjpeg\",\n \"-framerate\",\n String(framerate),\n \"-i\",\n framePath,\n ];\n }\n default: {\n // Solid color test pattern (dark navy)\n return [\n \"-f\",\n \"lavfi\",\n \"-i\",\n `color=c=0x1a1a2e:s=${resolution}:r=${framerate}`,\n ];\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Audio input args\n // ---------------------------------------------------------------------------\n\n private buildAudioInputArgs(config: StreamConfig): string[] {\n const source = config.audioSource || \"silent\";\n\n switch (source) {\n case \"tts\": {\n // Raw PCM from TTS bridge via pipe:3 (4th stdio fd).\n // Format must match tts-stream-bridge output: s16le, 24kHz, mono.\n // -use_wallclock_as_timestamps 1: raw PCM has no timestamps, so FFmpeg\n // uses wall-clock time to sync with the video stream.\n // -probesize/-analyzeduration: eliminate probe buffering for immediate start.\n // -thread_queue_size: prevent queue overflow from high-frequency tick writes.\n return [\n \"-use_wallclock_as_timestamps\",\n \"1\",\n \"-probesize\",\n \"32\",\n \"-analyzeduration\",\n \"0\",\n \"-thread_queue_size\",\n \"512\",\n \"-f\",\n \"s16le\",\n \"-ar\",\n \"24000\",\n \"-ac\",\n \"1\",\n \"-i\",\n \"pipe:3\",\n ];\n }\n case \"silent\": {\n // Synthetic silent audio — always works, no hardware required.\n return [\n \"-f\",\n \"lavfi\",\n \"-i\",\n \"anullsrc=channel_layout=stereo:sample_rate=44100\",\n ];\n }\n case \"system\": {\n // System/desktop audio capture.\n if (process.platform === \"darwin\") {\n // macOS: requires BlackHole or similar virtual audio device.\n // audioDevice is the avfoundation audio device index (e.g., \"2\").\n const device = config.audioDevice || \"0\";\n return [\"-f\", \"avfoundation\", \"-i\", `none:${device}`];\n }\n // Linux: PulseAudio monitor source captures desktop audio.\n const device = config.audioDevice || \"default\";\n return [\"-f\", \"pulse\", \"-i\", device];\n }\n case \"microphone\": {\n // Microphone input.\n if (process.platform === \"darwin\") {\n const device = config.audioDevice || \"0\";\n return [\"-f\", \"avfoundation\", \"-i\", `none:${device}`];\n }\n const device = config.audioDevice || \"default\";\n return [\"-f\", \"pulse\", \"-i\", device];\n }\n default: {\n // Treat as a file path — play audio file as stream audio.\n // Supports mp3, wav, ogg, flac, etc.\n if (source.startsWith(\"/\") || source.startsWith(\"./\")) {\n return [\"-stream_loop\", \"-1\", \"-i\", source];\n }\n // Fallback to silent if source is unrecognized.\n return [\n \"-f\",\n \"lavfi\",\n \"-i\",\n \"anullsrc=channel_layout=stereo:sample_rate=44100\",\n ];\n }\n }\n }\n\n /** Get the TTS stream bridge for external speak triggers. */\n getTtsBridge(): ITtsStreamBridge {\n return ttsStreamBridge;\n }\n}\n\n// Module singleton\nexport const streamManager = new StreamManager();\n"],"mappings":"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","names":["device"]}
|
|
1
|
+
{"version":3,"sources":["../../src/services/stream-manager.ts"],"sourcesContent":["/**\n * Stream Manager — cross-platform RTMP streaming via FFmpeg.\n *\n * Supports multiple input modes:\n * - \"pipe\": Receives JPEG frames via writeFrame() → FFmpeg stdin (image2pipe).\n * Used for streaming desktop window contents captured by the host bridge.\n * - \"avfoundation\" / \"screen\": macOS native screen capture.\n * - \"x11grab\": Linux virtual display capture (Xvfb). Used for GPU-backed game streams.\n * - \"file\": Reads a continuously-updated JPEG file (browser-capture).\n * - \"testsrc\": Solid color test pattern (default fallback).\n *\n * Audio support:\n * - \"silent\": Synthetic silent audio (anullsrc) — default.\n * - \"system\": System/desktop audio capture.\n * - \"microphone\": Microphone input.\n * - File path: Play an audio file as stream audio.\n *\n * Volume control:\n * - setVolume(0-100), mute(), unmute() — restarts FFmpeg to apply.\n *\n * Usage:\n * import { streamManager } from \"./services/stream-manager\";\n * await streamManager.start({ rtmpUrl, rtmpKey, inputMode: \"pipe\" });\n * streamManager.writeFrame(jpegBuffer); // called from frame capture\n * streamManager.setVolume(50); // adjust volume mid-stream\n * await streamManager.stop();\n *\n * @module services/stream-manager\n */\n\nimport { type ChildProcess, execSync, spawn } from \"node:child_process\";\nimport { logger } from \"@elizaos/core\";\nimport { type ITtsStreamBridge, ttsStreamBridge } from \"./tts-stream-bridge.js\";\n\nconst TAG = \"[StreamManager]\";\n\nexport type AudioSource = \"silent\" | \"system\" | \"microphone\" | \"tts\";\n\nexport interface StreamConfig {\n rtmpUrl: string;\n rtmpKey: string;\n /** FFmpeg video input source. Defaults to \"testsrc\" (test pattern). */\n inputMode?:\n | \"testsrc\"\n | \"avfoundation\"\n | \"screen\"\n | \"pipe\"\n | \"file\"\n | \"x11grab\";\n /** avfoundation video device index (default \"3\" = Capture screen 0 on macOS) */\n videoDevice?: string;\n /** Path to JPEG frame file (for \"file\" input mode) */\n frameFile?: string;\n /** Resolution (default \"1280x720\") */\n resolution?: string;\n /** Video bitrate (default \"2500k\") */\n bitrate?: string;\n /** Frame rate (default 15) */\n framerate?: number;\n /** X11 display for x11grab mode (e.g., \":99\"). Default \":99\". */\n display?: string;\n /** Audio source. Default \"silent\" (anullsrc). Can also be an absolute file path. */\n audioSource?: AudioSource | string;\n /** Audio device identifier (platform-specific). For macOS avfoundation: device index. For Linux: pulse/alsa device name. */\n audioDevice?: string;\n /** Volume level 0–100. Default 80. Applied as FFmpeg audio filter. */\n volume?: number;\n /** Whether audio is muted. Default false. Overrides volume to 0 when true. */\n muted?: boolean;\n}\n\nclass StreamManager {\n private ffmpeg: ChildProcess | null = null;\n private _running = false;\n private startedAt: number | null = null;\n private _frameCount = 0;\n /** Current stream config — stored for restart on volume/audio changes. */\n private _config: StreamConfig | null = null;\n /** Current volume level (0–100). */\n private _volume = 80;\n /** Whether audio is muted. */\n private _muted = false;\n /** Auto-restart state. */\n private _restartAttempts = 0;\n private _maxRestartAttempts = 5;\n private _restartDecayTimer: ReturnType<typeof setInterval> | null = null;\n private _intentionalStop = false;\n /** Pending auto-restart timer — cleared in stop() to prevent races. */\n private _restartTimer: ReturnType<typeof setTimeout> | null = null;\n /** Guard: prevents concurrent start() calls from orphaning FFmpeg. */\n private _starting = false;\n\n isRunning(): boolean {\n return this._running;\n }\n\n getUptime(): number {\n if (!this.startedAt) return 0;\n return Math.floor((Date.now() - this.startedAt) / 1000);\n }\n\n getHealth() {\n return {\n running: this._running,\n ffmpegAlive:\n this.ffmpeg !== null &&\n this.ffmpeg.exitCode === null &&\n !this.ffmpeg.killed,\n uptime: this.getUptime(),\n frameCount: this._frameCount,\n volume: this._volume,\n muted: this._muted,\n audioSource: this._config?.audioSource || \"silent\",\n inputMode: this._config?.inputMode || null,\n };\n }\n\n getVolume(): number {\n return this._muted ? 0 : this._volume;\n }\n\n isMuted(): boolean {\n return this._muted;\n }\n\n /**\n * Set volume (0–100). Restarts FFmpeg if currently streaming to apply the change.\n */\n async setVolume(level: number): Promise<void> {\n this._volume = Math.max(0, Math.min(100, Math.round(level)));\n logger.info(`${TAG} Volume set to ${this._volume}`);\n if (this._running && this._config) {\n await this.restart();\n }\n }\n\n /** Mute audio. Restarts FFmpeg if currently streaming. */\n async mute(): Promise<void> {\n if (this._muted) return;\n this._muted = true;\n logger.info(`${TAG} Audio muted`);\n if (this._running && this._config) {\n await this.restart();\n }\n }\n\n /** Unmute audio. Restarts FFmpeg if currently streaming. */\n async unmute(): Promise<void> {\n if (!this._muted) return;\n this._muted = false;\n logger.info(`${TAG} Audio unmuted (volume: ${this._volume})`);\n if (this._running && this._config) {\n await this.restart();\n }\n }\n\n /** Restart the stream with updated config (preserves uptime tracking). */\n private async restart(): Promise<void> {\n if (!this._config) return;\n const savedStartedAt = this.startedAt;\n const savedFrameCount = this._frameCount;\n\n // Mark as intentional so the exit handler doesn't trigger autoRestart()\n // concurrently with our manual restart below.\n this._intentionalStop = true;\n\n // Detach TTS bridge before stopping FFmpeg\n ttsStreamBridge.detach();\n\n // Stop FFmpeg without resetting tracking\n if (this.ffmpeg && !this.ffmpeg.killed && this.ffmpeg.exitCode === null) {\n if (this.ffmpeg.stdin) {\n try {\n this.ffmpeg.stdin.end();\n } catch {\n /* ignore */\n }\n }\n this.ffmpeg.kill(\"SIGTERM\");\n await Promise.race([\n new Promise((resolve) => this.ffmpeg?.on(\"exit\", resolve)),\n new Promise((resolve) => setTimeout(resolve, 2000)),\n ]);\n if (this.ffmpeg?.exitCode === null) {\n this.ffmpeg.kill(\"SIGKILL\");\n }\n }\n this.ffmpeg = null;\n this._running = false;\n\n // Restart with current volume/mute applied\n const config = {\n ...this._config,\n volume: this._volume,\n muted: this._muted,\n };\n this._intentionalStop = false;\n await this.start(config);\n\n // Restore tracking\n this.startedAt = savedStartedAt;\n this._frameCount = savedFrameCount;\n logger.info(\n `${TAG} Stream restarted (volume=${this._volume}, muted=${this._muted})`,\n );\n }\n\n /**\n * Write a JPEG frame to FFmpeg's stdin (only works in \"pipe\" mode).\n * Returns true if the frame was accepted.\n */\n writeFrame(jpegData: Buffer): boolean {\n if (!this._running || !this.ffmpeg?.stdin) return false;\n if (this.ffmpeg.killed || this.ffmpeg.exitCode !== null) return false;\n\n try {\n this.ffmpeg.stdin.write(jpegData);\n this._frameCount++;\n if (this._frameCount % 150 === 0) {\n logger.info(`${TAG} Piped ${this._frameCount} frames to FFmpeg`);\n }\n return true;\n } catch {\n return false;\n }\n }\n\n async start(config: StreamConfig): Promise<void> {\n if (this._running || this._starting) {\n logger.warn(`${TAG} Already running or starting — stop first`);\n return;\n }\n this._starting = true;\n try {\n await this._startInner(config);\n } finally {\n this._starting = false;\n }\n }\n\n private async _startInner(config: StreamConfig): Promise<void> {\n // Pre-flight: ensure FFmpeg is installed\n try {\n execSync(\"ffmpeg -version\", { stdio: \"ignore\", timeout: 5000 });\n } catch {\n const installHint =\n process.platform === \"darwin\"\n ? \"Install with: brew install ffmpeg\"\n : process.platform === \"linux\"\n ? \"Install with: sudo apt install ffmpeg (or your distro's package manager)\"\n : \"Download from https://ffmpeg.org/download.html\";\n throw new Error(\n `FFmpeg not found. Streaming requires FFmpeg to be installed.\\n${installHint}`,\n );\n }\n\n this._config = config;\n this._frameCount = 0;\n this._volume = config.volume ?? this._volume;\n this._muted = config.muted ?? this._muted;\n\n const resolution = config.resolution || \"1280x720\";\n const bitrate = config.bitrate || \"2500k\";\n const framerate = config.framerate || 15;\n const rtmpTarget = `${config.rtmpUrl}/${config.rtmpKey}`;\n const bufsize = `${parseInt(bitrate, 10) * 2}k`;\n const mode = config.inputMode || \"testsrc\";\n\n // Build FFmpeg args based on input mode\n const videoInputArgs = this.buildVideoInputArgs(\n config,\n resolution,\n framerate,\n );\n const audioInputArgs = this.buildAudioInputArgs(config);\n const isPipe = mode === \"pipe\";\n const isScreenCapture =\n mode === \"avfoundation\" || mode === \"screen\" || mode === \"x11grab\";\n\n // Effective volume: 0 when muted, otherwise 0–1.0 scale\n const effectiveVolume = this._muted ? 0 : this._volume / 100;\n\n // FFmpeg arg order: all inputs first, then filters, then encoding/output\n const ffmpegArgs = [\n \"-thread_queue_size\",\n \"512\",\n // Video input\n ...videoInputArgs,\n // Audio input\n ...audioInputArgs,\n // Video filter: scale for screen capture modes\n ...(isScreenCapture\n ? [\"-vf\", `scale=${resolution.replace(\"x\", \":\")}:flags=fast_bilinear`]\n : []),\n // Audio filter: volume control\n \"-af\",\n `volume=${effectiveVolume.toFixed(2)}`,\n // Video encoding (platform-specific)\n ...(process.platform === \"darwin\"\n ? [\n \"-c:v\",\n \"h264_videotoolbox\",\n \"-realtime\",\n \"1\",\n \"-b:v\",\n bitrate,\n \"-maxrate\",\n bitrate,\n \"-bufsize\",\n bufsize,\n ]\n : [\n \"-c:v\",\n \"libx264\",\n \"-preset\",\n \"veryfast\",\n \"-tune\",\n \"zerolatency\",\n \"-b:v\",\n bitrate,\n \"-maxrate\",\n bitrate,\n \"-bufsize\",\n bufsize,\n ]),\n \"-s\",\n resolution,\n \"-pix_fmt\",\n \"yuv420p\",\n \"-g\",\n \"60\",\n // Audio encoding\n \"-c:a\",\n \"aac\",\n \"-b:a\",\n \"128k\",\n // Output\n \"-f\",\n \"flv\",\n rtmpTarget,\n ];\n\n const audioSrc = config.audioSource || \"silent\";\n logger.info(\n `${TAG} Starting FFmpeg RTMP stream (video=${mode}, audio=${audioSrc}, vol=${this._volume}${this._muted ? \" MUTED\" : \"\"}) → ${config.rtmpUrl}`,\n );\n logger.info(\n `${TAG} Resolution: ${resolution}, Bitrate: ${bitrate}, FPS: ${framerate}`,\n );\n\n const isTts = (config.audioSource || \"silent\") === \"tts\";\n\n // In pipe mode, FFmpeg reads from stdin; otherwise stdin is ignored.\n // TTS mode adds a 4th stdio fd (pipe:3) for raw PCM audio input.\n this.ffmpeg = spawn(\"ffmpeg\", [\"-y\", ...ffmpegArgs], {\n stdio: [\n isPipe ? \"pipe\" : \"ignore\",\n \"pipe\",\n \"pipe\",\n ...(isTts ? ([\"pipe\"] as const) : []),\n ],\n });\n\n // Log all FFmpeg stderr for debugging\n this.ffmpeg.stderr?.on(\"data\", (chunk: Buffer) => {\n const line = chunk.toString().trim();\n if (line) {\n logger.debug(`[FFmpeg] ${line}`);\n }\n });\n\n this.ffmpeg.on(\"exit\", (code, signal) => {\n if (this._running) {\n logger.warn(\n `${TAG} FFmpeg exited unexpectedly (code=${code}, signal=${signal})`,\n );\n this._running = false;\n if (!this._intentionalStop && this._config) {\n this.autoRestart();\n } else {\n this.startedAt = null;\n }\n }\n });\n\n // Handle stdin errors gracefully in pipe mode\n if (isPipe && this.ffmpeg.stdin) {\n this.ffmpeg.stdin.on(\"error\", (err) => {\n logger.warn(`${TAG} FFmpeg stdin error: ${err.message}`);\n });\n }\n\n // Attach TTS bridge to pipe:3 for PCM audio\n if (isTts && this.ffmpeg.stdio[3]) {\n const pipe3 = this.ffmpeg.stdio[3] as import(\"node:stream\").Writable;\n ttsStreamBridge.attach(pipe3);\n logger.info(`${TAG} TTS bridge attached to pipe:3`);\n }\n\n // Wait a moment to confirm it started\n await new Promise((r) => setTimeout(r, 1500));\n\n if (this.ffmpeg.exitCode !== null) {\n const exitCode = this.ffmpeg.exitCode;\n this.ffmpeg = null;\n throw new Error(`${TAG} FFmpeg exited immediately with code ${exitCode}`);\n }\n\n this._running = true;\n this.startedAt = Date.now();\n this._intentionalStop = false;\n // Decay restart counter every 30s of healthy running\n if (this._restartDecayTimer) clearInterval(this._restartDecayTimer);\n this._restartDecayTimer = setInterval(() => {\n if (this._restartAttempts > 0) {\n this._restartAttempts = Math.max(0, this._restartAttempts - 1);\n logger.info(\n `${TAG} Restart counter decayed to ${this._restartAttempts}`,\n );\n }\n }, 30_000);\n logger.info(`${TAG} FFmpeg streaming to RTMP — stream should be live`);\n }\n\n async stop(): Promise<{ uptime: number }> {\n const uptime = this.getUptime();\n const frames = this._frameCount;\n\n // Detach TTS bridge before killing FFmpeg\n ttsStreamBridge.detach();\n\n if (this.ffmpeg && !this.ffmpeg.killed && this.ffmpeg.exitCode === null) {\n const ffmpegProc = this.ffmpeg;\n // Close stdin first in pipe mode to signal EOF\n if (ffmpegProc.stdin) {\n try {\n ffmpegProc.stdin.end();\n } catch {\n /* ignore */\n }\n }\n ffmpegProc.kill(\"SIGTERM\");\n await Promise.race([\n new Promise((resolve) => ffmpegProc.on(\"exit\", resolve)),\n new Promise((resolve) => setTimeout(resolve, 3000)),\n ]);\n if (ffmpegProc.exitCode === null) {\n ffmpegProc.kill(\"SIGKILL\");\n }\n }\n\n this._intentionalStop = true;\n if (this._restartTimer) {\n clearTimeout(this._restartTimer);\n this._restartTimer = null;\n }\n if (this._restartDecayTimer) {\n clearInterval(this._restartDecayTimer);\n this._restartDecayTimer = null;\n }\n this.ffmpeg = null;\n this._running = false;\n this.startedAt = null;\n this._frameCount = 0;\n this._restartAttempts = 0;\n this._config = null;\n logger.info(\n `${TAG} Stream stopped (uptime: ${uptime}s, frames: ${frames})`,\n );\n return { uptime };\n }\n\n /** Attempt to restart FFmpeg after unexpected exit with exponential backoff. */\n private autoRestart(): void {\n if (this._restartAttempts >= this._maxRestartAttempts) {\n logger.error(\n `${TAG} Max restart attempts (${this._maxRestartAttempts}) reached — giving up`,\n );\n this.startedAt = null;\n if (this._restartDecayTimer) {\n clearInterval(this._restartDecayTimer);\n this._restartDecayTimer = null;\n }\n return;\n }\n\n this._restartAttempts++;\n const delay = Math.min(1000 * 2 ** (this._restartAttempts - 1), 60_000);\n logger.info(\n `${TAG} Auto-restart attempt ${this._restartAttempts}/${this._maxRestartAttempts} in ${delay}ms`,\n );\n\n this._restartTimer = setTimeout(async () => {\n this._restartTimer = null;\n if (this._intentionalStop || !this._config) return;\n\n const savedStartedAt = this.startedAt;\n const savedFrameCount = this._frameCount;\n\n try {\n this.ffmpeg = null;\n await this.start({\n ...this._config,\n volume: this._volume,\n muted: this._muted,\n });\n // Restore tracking so uptime is continuous\n this.startedAt = savedStartedAt;\n this._frameCount = savedFrameCount;\n logger.info(`${TAG} Auto-restart successful`);\n } catch (err) {\n this._running = false;\n logger.error(`${TAG} Auto-restart failed: ${String(err)}`);\n // start() failed before spawning FFmpeg — no exit event will fire,\n // so manually chain the next restart attempt if retries remain.\n if (!this._intentionalStop && this._config) {\n this.autoRestart();\n }\n }\n }, delay);\n }\n\n // ---------------------------------------------------------------------------\n // Video input args\n // ---------------------------------------------------------------------------\n\n private buildVideoInputArgs(\n config: StreamConfig,\n resolution: string,\n framerate: number,\n ): string[] {\n const mode = config.inputMode || \"testsrc\";\n\n switch (mode) {\n case \"pipe\": {\n // Read JPEG frames from stdin via image2pipe.\n // -c:v mjpeg is mandatory: image2pipe cannot auto-detect JPEG from piped data.\n // -probesize/-analyzeduration eliminate the default 5MB probe buffer that\n // causes FFmpeg to stall for ~100 frames before decoding starts.\n return [\n \"-probesize\",\n \"32\",\n \"-analyzeduration\",\n \"0\",\n \"-f\",\n \"image2pipe\",\n \"-c:v\",\n \"mjpeg\",\n \"-framerate\",\n String(framerate),\n \"-i\",\n \"pipe:0\",\n ];\n }\n case \"avfoundation\":\n case \"screen\": {\n // macOS native screen capture via avfoundation.\n // videoDevice \"3\" = Capture screen 0; \":none\" = no audio from avfoundation.\n const videoDevice = config.videoDevice || \"3\";\n return [\n \"-f\",\n \"avfoundation\",\n \"-framerate\",\n String(framerate),\n \"-pixel_format\",\n \"nv12\",\n \"-capture_cursor\",\n \"1\",\n \"-i\",\n `${videoDevice}:none`,\n ];\n }\n case \"x11grab\": {\n // Linux virtual display capture (Xvfb) for GPU-backed game streams.\n // Requires: Xvfb :99 -screen 0 1280x720x24 -ac &\n // Then run a browser/TUI on display :99.\n const display = config.display || \":99\";\n return [\n \"-f\",\n \"x11grab\",\n \"-video_size\",\n resolution,\n \"-framerate\",\n String(framerate),\n \"-draw_mouse\",\n \"0\",\n \"-i\",\n display,\n ];\n }\n case \"file\": {\n // Read from a continuously-updated JPEG file (written by browser-capture).\n const framePath = config.frameFile || \"/tmp/eliza-stream-frame.jpg\";\n return [\n \"-probesize\",\n \"32\",\n \"-analyzeduration\",\n \"0\",\n \"-loop\",\n \"1\",\n \"-f\",\n \"image2\",\n \"-c:v\",\n \"mjpeg\",\n \"-framerate\",\n String(framerate),\n \"-i\",\n framePath,\n ];\n }\n default: {\n // Solid color test pattern (dark navy)\n return [\n \"-f\",\n \"lavfi\",\n \"-i\",\n `color=c=0x1a1a2e:s=${resolution}:r=${framerate}`,\n ];\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Audio input args\n // ---------------------------------------------------------------------------\n\n private buildAudioInputArgs(config: StreamConfig): string[] {\n const source = config.audioSource || \"silent\";\n\n switch (source) {\n case \"tts\": {\n // Raw PCM from TTS bridge via pipe:3 (4th stdio fd).\n // Format must match tts-stream-bridge output: s16le, 24kHz, mono.\n // -use_wallclock_as_timestamps 1: raw PCM has no timestamps, so FFmpeg\n // uses wall-clock time to sync with the video stream.\n // -probesize/-analyzeduration: eliminate probe buffering for immediate start.\n // -thread_queue_size: prevent queue overflow from high-frequency tick writes.\n return [\n \"-use_wallclock_as_timestamps\",\n \"1\",\n \"-probesize\",\n \"32\",\n \"-analyzeduration\",\n \"0\",\n \"-thread_queue_size\",\n \"512\",\n \"-f\",\n \"s16le\",\n \"-ar\",\n \"24000\",\n \"-ac\",\n \"1\",\n \"-i\",\n \"pipe:3\",\n ];\n }\n case \"silent\": {\n // Synthetic silent audio — always works, no hardware required.\n return [\n \"-f\",\n \"lavfi\",\n \"-i\",\n \"anullsrc=channel_layout=stereo:sample_rate=44100\",\n ];\n }\n case \"system\": {\n // System/desktop audio capture.\n if (process.platform === \"darwin\") {\n // macOS: requires BlackHole or similar virtual audio device.\n // audioDevice is the avfoundation audio device index (e.g., \"2\").\n const device = config.audioDevice || \"0\";\n return [\"-f\", \"avfoundation\", \"-i\", `none:${device}`];\n }\n // Linux: PulseAudio monitor source captures desktop audio.\n const device = config.audioDevice || \"default\";\n return [\"-f\", \"pulse\", \"-i\", device];\n }\n case \"microphone\": {\n // Microphone input.\n if (process.platform === \"darwin\") {\n const device = config.audioDevice || \"0\";\n return [\"-f\", \"avfoundation\", \"-i\", `none:${device}`];\n }\n const device = config.audioDevice || \"default\";\n return [\"-f\", \"pulse\", \"-i\", device];\n }\n default: {\n // Treat as a file path — play audio file as stream audio.\n // Supports mp3, wav, ogg, flac, etc.\n if (source.startsWith(\"/\") || source.startsWith(\"./\")) {\n return [\"-stream_loop\", \"-1\", \"-i\", source];\n }\n throw new Error(\n `${TAG} Unsupported audio source ${JSON.stringify(source)}. Use \"silent\" explicitly for a silent track, \"tts\" for model audio, \"system\", \"microphone\", or an audio file path.`,\n );\n }\n }\n }\n\n /** Get the TTS stream bridge for external speak triggers. */\n getTtsBridge(): ITtsStreamBridge {\n return ttsStreamBridge;\n }\n}\n\n// Module singleton\nexport const streamManager = new StreamManager();\n"],"mappings":"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","names":["device"]}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Writable } from 'node:stream';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* TTS Stream Bridge — generates TTS audio server-side and pipes PCM data
|
|
5
3
|
* into FFmpeg's audio track for RTMP streaming.
|
|
@@ -10,9 +8,10 @@ import { Writable } from 'node:stream';
|
|
|
10
8
|
*
|
|
11
9
|
* @module services/tts-stream-bridge
|
|
12
10
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type
|
|
11
|
+
import type { Writable } from "node:stream";
|
|
12
|
+
import { type IAgentRuntime } from "@elizaos/core";
|
|
13
|
+
export type TtsProvider = "local-inference" | "elevenlabs" | "openai" | "edge" | (string & {});
|
|
14
|
+
export type TtsConfig = {
|
|
16
15
|
enabled?: boolean;
|
|
17
16
|
provider?: TtsProvider;
|
|
18
17
|
elevenlabs?: {
|
|
@@ -31,8 +30,9 @@ type TtsConfig = {
|
|
|
31
30
|
};
|
|
32
31
|
};
|
|
33
32
|
/** Resolved TTS configuration for a speak() call. */
|
|
34
|
-
interface ResolvedTtsConfig {
|
|
33
|
+
export interface ResolvedTtsConfig {
|
|
35
34
|
provider: TtsProvider;
|
|
35
|
+
runtime?: IAgentRuntime;
|
|
36
36
|
elevenlabs?: {
|
|
37
37
|
apiKey: string;
|
|
38
38
|
voiceId: string;
|
|
@@ -49,7 +49,7 @@ interface ResolvedTtsConfig {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
/** Public interface for the TTS stream bridge. */
|
|
52
|
-
interface ITtsStreamBridge {
|
|
52
|
+
export interface ITtsStreamBridge {
|
|
53
53
|
attach(stream: Writable): void;
|
|
54
54
|
detach(): void;
|
|
55
55
|
isAttached(): boolean;
|
|
@@ -79,25 +79,25 @@ declare class TtsStreamBridge implements ITtsStreamBridge {
|
|
|
79
79
|
/** Called every CHUNK_MS — writes next PCM chunk (TTS or silence) to FFmpeg. */
|
|
80
80
|
private tick;
|
|
81
81
|
private generateTts;
|
|
82
|
+
private generateLocalInference;
|
|
82
83
|
private generateElevenlabs;
|
|
83
84
|
private generateOpenai;
|
|
84
85
|
private generateEdge;
|
|
85
|
-
/** Decode
|
|
86
|
-
private
|
|
86
|
+
/** Decode provider audio (WAV, MP3, etc.) to raw s16le PCM using FFmpeg. */
|
|
87
|
+
private decodeAudioToPcm;
|
|
87
88
|
}
|
|
88
89
|
/**
|
|
89
90
|
* Resolve TTS configuration from eliza config, finding the best available
|
|
90
91
|
* provider with valid API keys.
|
|
91
92
|
*/
|
|
92
|
-
declare function resolveTtsConfig(ttsConfig: TtsConfig | undefined): ResolvedTtsConfig | null;
|
|
93
|
+
export declare function resolveTtsConfig(ttsConfig: TtsConfig | undefined, runtime?: IAgentRuntime): ResolvedTtsConfig | null;
|
|
93
94
|
/**
|
|
94
95
|
* Get a summary of available TTS providers and their status.
|
|
95
96
|
*/
|
|
96
|
-
declare function getTtsProviderStatus(ttsConfig: TtsConfig | undefined): {
|
|
97
|
+
export declare function getTtsProviderStatus(ttsConfig: TtsConfig | undefined, runtime?: IAgentRuntime): {
|
|
97
98
|
configuredProvider: string | null;
|
|
98
99
|
hasApiKey: boolean;
|
|
99
100
|
resolvedProvider: string | null;
|
|
100
101
|
};
|
|
101
|
-
declare const ttsStreamBridge: TtsStreamBridge;
|
|
102
|
-
|
|
103
|
-
export { type ITtsStreamBridge, type ResolvedTtsConfig, type TtsConfig, type TtsProvider, getTtsProviderStatus, resolveTtsConfig, ttsStreamBridge };
|
|
102
|
+
export declare const ttsStreamBridge: TtsStreamBridge;
|
|
103
|
+
export {};
|