Skip to content

Add live RTMP streaming (e.g. Youtube Live)#662

Closed
marcusschiesser wants to merge 3 commits into
siddharthvaddem:mainfrom
marcusschiesser:codex/live-rtmp-setup
Closed

Add live RTMP streaming (e.g. Youtube Live)#662
marcusschiesser wants to merge 3 commits into
siddharthvaddem:mainfrom
marcusschiesser:codex/live-rtmp-setup

Conversation

@marcusschiesser
Copy link
Copy Markdown
Contributor

@marcusschiesser marcusschiesser commented May 28, 2026

Summary

  • add a Stream Setup dialog for live RTMP/RTMPS broadcasting
  • support output aspect presets, webcam layout preview, and RTMP URL validation
  • pipe canvas recordings through FFmpeg to RTMP with YouTube-compatible audio/keyframe settings

Verification

  • npm run lint
  • npm run test -- src/lib/liveStream.test.ts
  • npm run build-vite
  • manually verified YouTube Studio ingest reports live stream health as Excellent

Summary by CodeRabbit

  • New Features

    • Live streaming to RTMP/RTMPS with start/stop and chunked streaming; HUD Live button shows elapsed time
    • Live stream setup dialog (server URL, stream key, output presets) with webcam picture‑in‑picture and drag-to-reposition
    • Multiple 1080p output presets (16:9, 9:16, 1:1)
    • Refreshable source preview and HUD expand/collapse control
    • UI disables conflicting controls while streaming
  • Tests

    • Unit tests for presets, URL validation, and composite layout bounds

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 13e6d4a6-a1ef-4d8d-8933-aa369deba700

📥 Commits

Reviewing files that changed from the base of the PR and between b7cff99 and 965361b.

📒 Files selected for processing (2)
  • electron/ipc/handlers.ts
  • src/hooks/useLiveStreamer.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • electron/ipc/handlers.ts
  • src/hooks/useLiveStreamer.ts

📝 Walkthrough

Walkthrough

Adds end-to-end live streaming: domain presets and validation, renderer capture/composite/recorder hook, Electron IPC that spawns a bundled FFmpeg process and accepts binary chunks, HUD expansion control and preview refresh, and UI to configure/start/stop streams.

Changes

Live Streaming Feature

Layer / File(s) Summary
Live stream domain model and validation
src/lib/liveStream.ts, src/lib/liveStream.test.ts
Defines output presets, layout/start types, RTMP(S) URL utilities, and destination validation. Tests cover presets, URL joining, and validation error messages.
Electron backend: FFmpeg IPC, preview refresh, preload & typings, HUD
electron/ipc/handlers.ts, electron/preload.ts, electron/electron-env.d.ts, electron/windows.ts, package.json
Adds renderer typings for capture/stream APIs, preload IPC wiring, IPC handlers to start/write/stop a bundled FFmpeg process (stdin piping, stderr buffering, coordinated shutdown), desktop source serialization and preview refresh, HUD expand/collapse handler, and ffmpeg-static dependency.
Renderer hook: media capture, audio mixing, canvas composition, recorder
src/hooks/useLiveStreamer.ts
Captures screen/mic/webcam, composes frames using layout presets onto a canvas, mixes audio tracks (with gain or silent fallback), records via MediaRecorder, forwards chunks to backend, and provides streaming state and cleanup.
UI: LiveStreamSetupDialog and LaunchWindow integration
src/components/launch/LiveStreamSetupDialog.tsx, src/components/launch/LaunchWindow.tsx
Adds dialog for server URL/stream key, output preset selection, webcam overlay/mask and drag positioning, and Go Live flow. LaunchWindow integrates the hook, adds live HUD button, refreshes selected source preview, and disables conflicting controls while streaming.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • siddharthvaddem

Poem

midnight ffmpeg hums, stdin fed in tidy chunks,
canvas pastes screens and rounded cams in clumsy funks.
hud grows wide, a radio tower blinks its tiny light,
drag the selfie, type the key, go live without a fright. 📡

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the main changes and verification steps, but doesn't follow the provided template structure (missing explicit sections like Description, Motivation, Type of Change, etc.). Consider using the standard PR template with sections for Description, Motivation, Type of Change, and Related Issues to improve clarity and consistency.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature: live RTMP streaming support with a relevant example (YouTube Live).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 853a6553c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/useLiveStreamer.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/components/launch/LaunchWindow.tsx (1)

330-336: 💤 Low value

cleanup always resets expanded to false even when dialog is still open

this effect's cleanup runs on every re-render when deps change, not just unmount. if isLanguageMenuOpen changes while liveSetupOpen is still true, the cleanup will fire and set expanded to false before the new effect runs. this could cause a brief flicker where the HUD collapses and re-expands. lowkey might not be noticeable in practice, but a guard in cleanup would be cleaner:

 	useEffect(() => {
 		window.electronAPI?.setHudOverlayExpanded?.(liveSetupOpen);
 		setHudMouseEventsEnabled(liveSetupOpen || isLanguageMenuOpen);
-		return () => {
-			window.electronAPI?.setHudOverlayExpanded?.(false);
-		};
 	}, [isLanguageMenuOpen, liveSetupOpen, setHudMouseEventsEnabled]);
+
+	useEffect(() => {
+		return () => {
+			window.electronAPI?.setHudOverlayExpanded?.(false);
+		};
+	}, []);

or just remove the cleanup entirely if the expanded state is always explicitly set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/launch/LaunchWindow.tsx` around lines 330 - 336, The cleanup
in the useEffect always calls window.electronAPI?.setHudOverlayExpanded?.(false)
which can run on dependency changes and briefly collapse the HUD; update the
cleanup to only call setHudOverlayExpanded(false) when both liveSetupOpen and
isLanguageMenuOpen are false (i.e., guard against collapsing when a dialog is
still open), or remove the cleanup entirely if expanded is always explicitly set
elsewhere; locate the effect using useEffect and the call to
window.electronAPI?.setHudOverlayExpanded and adjust the cleanup logic around
liveSetupOpen and isLanguageMenuOpen.
src/components/launch/LiveStreamSetupDialog.tsx (2)

138-191: 💤 Low value

webcam cleanup looks solid but video.play() error is swallowed

the cleanup logic for stopping tracks and clearing srcObject is good. one small thing - line 174 catches play errors with () => undefined which is fine for autoplay policy failures, but you might want to at least log them during dev for debugging:

-					video.play().catch(() => undefined);
+					video.play().catch((err) => {
+						if (import.meta.env.DEV) {
+							console.debug("Webcam preview autoplay blocked:", err);
+						}
+					});

not a big deal since it's just a preview, but could help diagnose issues.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/launch/LiveStreamSetupDialog.tsx` around lines 138 - 191, The
video.play() catch currently swallows errors in the useEffect acquire flow (see
acquire, webcamVideoRef, and video.play), so change the noop catch (() =>
undefined) to log the error in non-production/dev builds—e.g., catch((err) => {
if (process.env.NODE_ENV !== 'production') console.debug('webcam preview play
failed:', err); })—so autoplay-related failures are visible during development
while remaining silent in production; ensure this logging is only added where
video.play() is called (inside the onloadedmetadata handler).

222-238: ⚖️ Poor tradeoff

closing dialog before stream starts could feel janky on slow networks

the flow here is: validate → close dialog → wait 220ms → call onStart → if failed, reopen dialog. on a slow FFmpeg spawn or network hiccup, the user sees the dialog close, then potentially reopen with an error. the UX might feel more polished if you kept the dialog open with a "starting" state until confirmed, then closed on success.

that said, i understand the 220ms delay is probably to let the dialog animation complete before capturing. if that's intentional for visual reasons, this is fine as-is.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/launch/LiveStreamSetupDialog.tsx` around lines 222 - 238, The
current handleStart closes the dialog (onOpenChange(false)) before awaiting
delay/ onStart which can feel janky on slow networks; change handleStart so it
validates, setStarting(true) (to disable inputs/show spinner), await
delay(START_CAPTURE_DELAY_MS) and then call onStart({ serverUrl, streamKey,
layout}) and only call onOpenChange(false) if onStart returns true; if onStart
fails, leave the dialog open and setError appropriately and setStarting(false).
Keep validateLiveStreamDestination, setError, setStarting, delay,
START_CAPTURE_DELAY_MS and onStart usage but move the onOpenChange(false) to run
only after a successful start and ensure the UI shows the "starting" state while
awaiting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/ipc/handlers.ts`:
- Around line 200-224: The current timeoutPromise sends SIGTERM and immediately
causes the handler to return success when Promise.race picks the timeout path;
instead, after sending SIGTERM in timeoutPromise (or when result.signal ===
"SIGTERM"), wait for the actual process exit by awaiting the existing
exitPromise (or chain a short secondary timeout) before considering the stop
successful. Update the logic around timeoutPromise/exitPromise and the result
check in the handler so that proc.kill("SIGTERM") is issued but the function
only returns { success: true } once exitPromise has resolved with code 0 or a
terminating signal; keep existing cleanup of liveStreamProcess,
liveStreamStopping, and liveStreamStderr intact and use those symbols
(timeoutPromise, exitPromise, proc, liveStreamProcess, liveStreamStopping,
liveStreamStderr) to locate and modify the code.

In `@electron/windows.ts`:
- Line 58: The code currently uses screen.getPrimaryDisplay() (const { workArea
} = screen.getPrimaryDisplay()) which forces the expanded HUD to recenter on the
primary monitor; instead retrieve the display that actually contains the HUD
window by getting the HUD BrowserWindow's bounds (e.g., hudWindow or
mainWindow). Call screen.getDisplayMatching(hudWindow.getBounds()) (or
screen.getDisplayNearestPoint) and use that display.workArea for positioning;
update both the occurrence at the shown line and the similar block around lines
69–73 so the HUD stays on the current monitor when expanded.

In `@src/components/launch/LaunchWindow.tsx`:
- Around line 381-407: The call to startLiveStream can throw and is currently
unhandled; update handleStartLiveStream to catch any exceptions from
startLiveStream (e.g., wrap the await startLiveStream(config) in try/catch), log
the error (console.warn/processLogger) and return a safe failure value (false or
an error object) so it never throws; also update the caller(s) that invoke
handleStartLiveStream (the async fire-and-forget invocation in the UI) to either
await the returned result and handle failure or add its own try/catch to
surface/log errors to the user. Ensure you reference the functions
handleStartLiveStream and startLiveStream when making these changes.

In `@src/components/launch/LiveStreamSetupDialog.tsx`:
- Around line 371-395: The onPointerLeave handler on the video element can
prematurely call handleWebcamPointerUp and end drags when the pointer briefly
exits the element; update the event bindings on the video (where webcamVideoRef
is used and handlers
handleWebcamPointerDown/handleWebcamPointerMove/handleWebcamPointerUp are
referenced) to stop using onPointerLeave and instead rely on onPointerUp and
onPointerCancel (or add an onPointerCancel that calls handleWebcamPointerUp) so
dragging only ends on pointerup/pointercancel while keeping pointer capture in
handleWebcamPointerDown.

In `@src/hooks/useLiveStreamer.ts`:
- Around line 474-485: recorder.ondataavailable handler can throw rejected
promises from event.data.arrayBuffer() or
window.electronAPI.writeLiveStreamChunk causing unhandled rejections; wrap the
async work in a try/catch (or append .catch) inside the handler used by
recorder.ondataavailable to catch errors, log/show them via toast.error (similar
messaging as existing failure path), set/inspect stopping.current and call await
stopLiveStream() on error, and ensure any thrown error from arrayBuffer() or
writeLiveStreamChunk is handled to avoid leaving the stream running.
- Around line 378-381: The screen video track created in useLiveStreamer (the
screenTrack variable obtained from screenStream.getVideoTracks()[0]) can end
externally; attach an onended/ended event handler to that track to trigger the
same shutdown/cleanup used when the user stops streaming (call the hook's
existing stop/cleanup function such as stopLiveStream or whatever cleans up
streams, state and timers), remove the handler when replacing tracks, and ensure
you also handle cases where screenTrack is null by early-returning; this ensures
the hook auto-stops and clears state when the browser or user revokes screen
share.

---

Nitpick comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 330-336: The cleanup in the useEffect always calls
window.electronAPI?.setHudOverlayExpanded?.(false) which can run on dependency
changes and briefly collapse the HUD; update the cleanup to only call
setHudOverlayExpanded(false) when both liveSetupOpen and isLanguageMenuOpen are
false (i.e., guard against collapsing when a dialog is still open), or remove
the cleanup entirely if expanded is always explicitly set elsewhere; locate the
effect using useEffect and the call to window.electronAPI?.setHudOverlayExpanded
and adjust the cleanup logic around liveSetupOpen and isLanguageMenuOpen.

In `@src/components/launch/LiveStreamSetupDialog.tsx`:
- Around line 138-191: The video.play() catch currently swallows errors in the
useEffect acquire flow (see acquire, webcamVideoRef, and video.play), so change
the noop catch (() => undefined) to log the error in non-production/dev
builds—e.g., catch((err) => { if (process.env.NODE_ENV !== 'production')
console.debug('webcam preview play failed:', err); })—so autoplay-related
failures are visible during development while remaining silent in production;
ensure this logging is only added where video.play() is called (inside the
onloadedmetadata handler).
- Around line 222-238: The current handleStart closes the dialog
(onOpenChange(false)) before awaiting delay/ onStart which can feel janky on
slow networks; change handleStart so it validates, setStarting(true) (to disable
inputs/show spinner), await delay(START_CAPTURE_DELAY_MS) and then call
onStart({ serverUrl, streamKey, layout}) and only call onOpenChange(false) if
onStart returns true; if onStart fails, leave the dialog open and setError
appropriately and setStarting(false). Keep validateLiveStreamDestination,
setError, setStarting, delay, START_CAPTURE_DELAY_MS and onStart usage but move
the onOpenChange(false) to run only after a successful start and ensure the UI
shows the "starting" state while awaiting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 26b2c23d-a123-4ad1-85b3-a82cd60c5774

📥 Commits

Reviewing files that changed from the base of the PR and between 8117d48 and 853a655.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/preload.ts
  • electron/windows.ts
  • package.json
  • src/components/launch/LaunchWindow.tsx
  • src/components/launch/LiveStreamSetupDialog.tsx
  • src/hooks/useLiveStreamer.ts
  • src/lib/liveStream.test.ts
  • src/lib/liveStream.ts

Comment thread electron/ipc/handlers.ts
Comment thread electron/windows.ts Outdated
Comment thread src/components/launch/LaunchWindow.tsx
Comment thread src/components/launch/LiveStreamSetupDialog.tsx
Comment thread src/hooks/useLiveStreamer.ts
Comment thread src/hooks/useLiveStreamer.ts
@marcusschiesser marcusschiesser force-pushed the codex/live-rtmp-setup branch from 853a655 to 05c4154 Compare May 28, 2026 09:43
@marcusschiesser
Copy link
Copy Markdown
Contributor Author

@siddharthvaddem here's a screenshot how this looks like:
image
I tested it with youtube live - let me know if the change works for you

@marcusschiesser marcusschiesser changed the title Add live RTMP stream setup Add live RTMP streaming (e.g. Youtube Live) May 28, 2026
@marcusschiesser
Copy link
Copy Markdown
Contributor Author

closing - made a fork now for streaming: https://github.com/marcusschiesser/openstream

@marcusschiesser marcusschiesser deleted the codex/live-rtmp-setup branch June 2, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant