Skip to content

fix: multi-sink routing, cloud hardware-source relay, and per-node recording#854

Merged
leszko merged 11 commits into
mainfrom
rafal/9-multi-bug-fixes
Apr 7, 2026
Merged

fix: multi-sink routing, cloud hardware-source relay, and per-node recording#854
leszko merged 11 commits into
mainfrom
rafal/9-multi-bug-fixes

Conversation

@leszko
Copy link
Copy Markdown
Collaborator

@leszko leszko commented Apr 7, 2026

Summary

Fixes multiple bugs in multi-sink graph execution and cloud relay that caused broken output, dropped frames, and recording failures when using graphs with multiple sinks, record nodes, or hardware sources (Syphon/NDI/Spout).

Also adds MCP server support for multi-source/multi-sink graph workflows (headless cloud mode).

Bugs Fixed

Per-sink frame routing returned identical frames for all sinks

HeadlessSession did not maintain per-sink frame buffers — every sink returned the same (primary) frame. Added _last_frames_by_sink so each sink captures its own output independently.

Pipeline aliasing collision caused wrong output on multi-node graphs

alias_pipeline was overwriting per-node pipeline instances with a stale registry-key singleton. For example, a graph with both passthrough and split-screen nodes would show split-screen output on both. Restored the guard that skips aliasing when the node ID is already registered.

Record nodes were silently dropped from graph execution

Record nodes were treated as UI-only (ui_state) and never loaded into the execution graph. Fixed graphConfigToFlow to include record nodes as execution nodes, and wired WebRTC transceivers so extra sinks and record tracks receive output.

Recording download failed after stopping

The recording file lookup had no fallback after stop, causing download requests to 404. Added fallback path resolution in the recording coordinator.

Cloud relay dropped Syphon/NDI/Spout source frames when input_mode was omitted

CloudRelay.video_mode stayed False when input_mode: "video" was not explicitly set, even when the graph contained hardware source nodes (Syphon, NDI, Spout, video file). Added compute_relay_video_mode() that infers video mode from graph source nodes.

Cloud ignored hardware-source tracks (Syphon/NDI/Spout)

The cloud's handle_offer only routed incoming WebRTC tracks matching webrtc_source_node_ids, which excludes hardware sources relayed from the local instance. Syphon/NDI/Spout tracks were silently dropped and their pipeline source queues starved. Fixed _parse_graph_node_ids to return all source node IDs so every incoming track is routed correctly.

VP8 keyframe request only sent for primary video track

The PLI (Picture Loss Indication) keyframe request was only sent for video track index 0. Extra sink and record tracks (index 1+) never received a keyframe, so their VP8 decoders started on P-frames — producing blocky/glitchy artifacts. Now sends PLI for every video receiver after connection.

Wrong attribute name in MCP cloud output wiring

_wire_cloud_outputs referenced extra_output_handlers which does not exist on CloudWebRTCClient. Fixed to use the correct output_handlers attribute.

Files Changed

Area Files What changed
Cloud relay cloud_relay.py, cloud_webrtc_client.py, frame_processor.py Hardware-source relay, video mode inference, keyframe requests for all tracks
Graph/WebRTC webrtc.py, pipeline_manager.py Source track routing, alias guard, per-node pipeline instances
Headless/Recording headless.py, sink_manager.py, recording_coordinator.py Per-sink frame buffers, per-node recording endpoints, download fallback
MCP server mcp_router.py, mcp_server.py, app.py Graph-mode session start, per-sink capture, cloud output wiring
Frontend graphUtils.ts, StreamPage.tsx Record nodes in graph execution, WebRTC transceiver wiring
Docs CLAUDE.md Testing instructions for multi-source/sink and local cloud dev

Test plan

  • Load a multi-pipeline graph (e.g., passthrough + split-screen) and verify each sink shows the correct pipeline's output
  • Start a graph session with record nodes, start/stop recording per node, and download the recordings
  • Test cloud relay with Syphon/NDI/Spout sources — verify frames flow through to all sinks
  • Verify VP8 output on extra sink/record tracks is clean (no blocky artifacts on first frames)
  • Run headless MCP workflow: resolve → load → start session (graph mode) → capture per-sink frames

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d27dc32c-d3bb-40ff-9b32-c2e30a954f09

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rafal/9-multi-bug-fixes

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.

@leszko leszko changed the base branch from rafal/7-mcp-multi to main April 7, 2026 09:01
@leszko leszko changed the title Rafal/9 multi bug fixes fix: multi-sink routing, cloud hardware-source relay, and per-node recording Apr 7, 2026
leszko and others added 5 commits April 7, 2026 11:10
…loud

When the local instance relays hardware-source frames (Syphon/NDI/Spout) to
the cloud via WebRTC, the cloud receives them as plain video tracks. Previously
the cloud's handle_offer only routed tracks matching webrtc_source_node_ids
(which excludes hardware sources), so Syphon tracks were silently dropped and
their pipeline source queues starved — producing no output.

Changes:
- _parse_graph_node_ids now returns all_source_node_ids alongside the filtered
  webrtc_source_node_ids. The cloud's handle_offer uses the full list so every
  incoming track is routed to the correct SourceInputHandler.
- handle_offer_with_relay routes browser tracks through CloudSourceInputHandler
  (per-track cloud input) instead of _input_loop → FrameProcessor.put() which
  would always send to generic track 0, colliding with hardware-source frames.
- When all sources are hardware, the browser video track is ignored entirely.
- Added diagnostic logging to CloudRelay and FrameProcessor for first-frame
  send/receive and video_mode detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Rafał Leszko <rafal@livepeer.org>
Add compute_relay_video_mode() in cloud_relay so CloudRelay forwards frames
when WebRTC params lack input_mode: video but the graph has source nodes or
server-side capture (Syphon, NDI, Spout, video file). FrameProcessor uses it
when building the relay.

Signed-off-by: Rafał Leszko <rafal@livepeer.org>
Made-with: Cursor
Re-skip aliasing when the node id is already registered so WebRTC does not
overwrite per-node pipeline instances with a stale registry-key singleton.
Fixes wrong pipeline output on multi-node graphs (e.g. passthrough showing
split-screen).

Signed-off-by: Rafal Leszko <rafal@livepeer.org>
Made-with: Cursor
…cture

Add per-sink frame capture in HeadlessSession, fix attribute paths in
cloud output wiring, fix alias_pipeline collision, add recording file
fallback after stop, and restructure CLAUDE.md to separate single-instance
and local cloud testing docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Rafal Leszko <rafal@livepeer.org>
- Treat record nodes as graph execution nodes (not ui-only); load them from
  graph.nodes in graphConfigToFlow and skip duplicate record entries from ui_state.
- Pass sink + record node IDs to WebRTC so recvonly transceivers match backend
  extra outputs (sinks then record tracks).
- Add ?node_id= to recording download/start/stop; route graph sessions to
  RecordingCoordinator; skip session-wide auto-recording when record nodes exist.
- Clarify stripUIFields: topology including record stays in nodes/edges.

Signed-off-by: Rafal Leszko <rafal@livepeer.org>
Made-with: Cursor
@leszko leszko force-pushed the rafal/9-multi-bug-fixes branch from 0738358 to 01cdf79 Compare April 7, 2026 09:14
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 7, 2026

🚀 fal.ai Preview Deployment

App ID daydream/scope-pr-854--preview
WebSocket wss://fal.run/daydream/scope-pr-854--preview/ws
Commit 3197680

Livepeer Runner

App ID daydream/scope-livepeer-pr-854--preview
WebSocket wss://fal.run/daydream/scope-livepeer-pr-854--preview/ws
Auth private

Testing Livepeer Mode

SCOPE_CLOUD_MODE=livepeer SCOPE_CLOUD_APP_ID="daydream/scope-livepeer-pr-854--preview/ws" uv run daydream-scope

leszko added 4 commits April 7, 2026 11:46
…ources

Graph Source nodes call onSourceModeChange with a node id, so switchMode
never ran for Syphon/NDI/Spout and the default file stream was still sent
via updateVideoTrack. Sync global useVideoSource on those modes and skip
the mid-stream track replace when the graph is server-side only.

Signed-off-by: Rafał Leszko <rafal@livepeer.org>
Made-with: Cursor
@leszko leszko marked this pull request as ready for review April 7, 2026 10:32
@leszko leszko merged commit af49de0 into main Apr 7, 2026
9 of 10 checks passed
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