From b2b5a0bdf59d4b73b0ed56e9a91a06cfb1e1f201 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sun, 22 Feb 2026 16:54:05 -0800 Subject: [PATCH] fix outputs --- scripts/stream-browser.ts | 58 +++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/scripts/stream-browser.ts b/scripts/stream-browser.ts index 4b1ebba..325a2c0 100644 --- a/scripts/stream-browser.ts +++ b/scripts/stream-browser.ts @@ -39,6 +39,21 @@ const audioBitrate = process.env.STREAM_AUDIO_BITRATE ?? "160k"; const streamKey = process.env.TWITCH_STREAM_KEY; const serverPort = process.env.STREAM_APP_PORT ?? "5109"; const broadcastUrl = process.env.BROADCAST_URL ?? `http://127.0.0.1:${serverPort}/broadcast`; +const redactionTokens = [ + broadcastUrl, + streamKey, + streamKey ? encodeURIComponent(streamKey) : undefined, + streamKey ? `rtmp://live.twitch.tv/app/${streamKey}` : undefined, +].filter((token): token is string => Boolean(token)); +const redactionWindow = Math.max(1, ...redactionTokens.map((token) => token.length)); + +function redactSensitive(value: string): string { + let output = value; + for (const token of redactionTokens) { + output = output.split(token).join("[REDACTED]"); + } + return output; +} if (mode === "live" && !streamKey) { console.error("TWITCH_STREAM_KEY is not set."); @@ -57,7 +72,7 @@ async function assertBroadcastReachable(url: string) { } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw new Error( - `Cannot reach broadcast page at ${url} (${detail}). Start the app server first (bun run start or bun run start:web).`, + `Cannot reach broadcast page (${redactSensitive(detail)}). Start the app server first (bun run start or bun run start:web).`, ); } finally { clearTimeout(timeout); @@ -140,6 +155,36 @@ async function pipeReadableToSink( } } +async function pipeReadableToRedactedStderr(readable: ReadableStream) { + const reader = readable.getReader(); + const decoder = new TextDecoder(); + let carry = ""; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + const chunk = decoder.decode(value, { stream: true }); + const combined = carry + chunk; + if (combined.length <= redactionWindow) { + carry = combined; + continue; + } + const flushUntil = combined.length - (redactionWindow - 1); + const safeOutput = combined.slice(0, flushUntil); + carry = combined.slice(flushUntil); + if (safeOutput.length > 0) { + process.stderr.write(redactSensitive(safeOutput)); + } + } + const trailing = carry + decoder.decode(); + if (trailing.length > 0) { + process.stderr.write(redactSensitive(trailing)); + } + } finally { + reader.releaseLock(); + } +} + async function main() { await assertBroadcastReachable(broadcastUrl); @@ -147,8 +192,11 @@ async function main() { const ffmpeg = Bun.spawn(["ffmpeg", ...ffmpegArgs], { stdin: "pipe", stdout: mode === "dryrun" ? "pipe" : "inherit", - stderr: "inherit", + stderr: "pipe", }); + if (ffmpeg.stderr) { + void pipeReadableToRedactedStderr(ffmpeg.stderr); + } let ffmpegWritable = true; let ffplay: Bun.Subprocess | null = null; @@ -251,7 +299,7 @@ async function main() { await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 }); page.on("console", (msg) => { if (process.env.STREAM_DEBUG === "1") { - console.log(`[broadcast] ${msg.type()}: ${msg.text()}`); + console.log(`[broadcast] ${msg.type()}: ${redactSensitive(msg.text())}`); } }); @@ -270,7 +318,7 @@ async function main() { }, 10_000); await firstChunk.finally(() => clearTimeout(firstChunkTimer)); - console.log(`Streaming from ${broadcastUrl} in ${mode} mode`); + console.log(`Streaming broadcast in ${mode} mode`); let shuttingDown = false; shutdown = async () => { @@ -332,6 +380,6 @@ async function main() { main().catch((error) => { const detail = error instanceof Error ? error.message : String(error); - console.error(detail); + console.error(redactSensitive(detail)); process.exit(1); });