Add Syphon output sink (macOS, opt-in)#97
Open
ktamas77 wants to merge 2 commits into
Open
Conversation
Adds a third output sink for macOS users: each captured Web-contents frame is published as a Syphon source via node-syphon, so other apps on the same machine (Resolume, OBS, VDMX, TouchDesigner, MadMapper) can consume the video as a zero-copy GPU texture rather than going through the NDI encoder + network stack. How it works ------------ - A new optional dependency on `node-syphon` (1.5.0). Its prebuilt arm64 binary and bundled Syphon.framework are shipped via asarUnpack. Install is a no-op on Linux/Windows because of optionalDependencies. - A new browser config field `s` / `Output2SinkSyphonEnabled`, parallel to the existing `n` (NDI) and `m` (FFmpeg) sub-sinks under the `N` (Output 2) master toggle. - A `support.syphon` runtime flag, true only when running on macOS with the optional dep installed; the control UI's Syphon row is rendered only when that flag is set. - The worker creates a `SyphonMetalServer` named after the browser title, publishes each frame's BGRA buffer via `publishImageData` (wrapped as a `Uint8ClampedArray`), and disposes the server in stop(). Publishing happens BEFORE the existing NDI BGRA->BGRX in-place mutation so Syphon receives the original alpha-preserving frame. - The validity check (`browser.valid()` and the renderer-side equivalent in vingester-control.js) now treats Syphon as a recognized sub-sink, so users can run an instance with only Syphon enabled. Verified -------- - macOS 26.4 / Apple Silicon: `cfg.s=true` flows to the worker, node-syphon loads in the renderer, `SyphonMetalServer` constructs with the correct name and a real `info.v002.Syphon.<UUID>` identifier, frames publish without errors, no SIGSEGV. - npm install on a non-macOS host skips node-syphon (optionalDep) and `support.syphon` evaluates false; the Syphon UI row stays hidden. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without this, a config imported on Linux/Windows (or a macOS host without node-syphon installed) with `Output2SinkSyphonEnabled: true` and both NDI and FFmpeg disabled would pass valid() and start capture, but the worker would silently drop every frame because SyphonMetalServer is null. The renderer-side check in vingester-control.js had the same bug — the UI would happily show the instance as ready to start. Also: short comment in the worker clarifying that FFmpeg is intentionally absent from the BGRA endianness normalization (it gets a re-encoded JPEG from nativeImage and handles pixel format itself). Addresses codemouseai PR #1 review comments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a third Output 2 sink alongside NDI and FFmpeg: a macOS-only Syphon server that publishes captured Web frames as a Syphon source via
node-syphon. Other macOS apps on the same machine — Resolume, OBS (with the Syphon plugin), VDMX, TouchDesigner, MadMapper — can then consume the video locally without going through the NDI encoder + network stack.Why add a Syphon sink
For local same-Mac pipelines into VJ/streaming software, Syphon is the right primitive:
NDI keeps doing what it does best (cross-machine routing). This just gives macOS users a much faster local option.
Cross-platform philosophy: opt-in only, no impact elsewhere
I'm aware the project values tri-platform parity. This PR is structured so it cleanly degrades on Windows and Linux:
node-syphonis inoptionalDependencies, notdependencies—npm installskips it cleanly on Win/Linux.support.syphonflag istrueonly on macOS and only when the dep loaded — gated byprocess.platform === "darwin"plus a try/require.support.syphonis true (v-show="… && support.syphon"), so the control window on Win/Linux is byte-identical to before.valid()(main process) and the renderer-side mirror only countcfg.sas a valid Output 2 sub-sink when Syphon is actually available — so a YAML config imported on an unsupported host withOutput2SinkSyphonEnabled: trueis correctly flagged invalid rather than silently dropping every frame.A natural counterpart on Windows is Spout, which has the same role and is also wrapped by some Node bindings. Happy to look at adding Spout in a follow-up PR if you'd like; I kept this PR macOS-only to keep the surface small and reviewable, and because Syphon is what I have a working setup for.
Implementation details
s/Output2SinkSyphonEnabled, parallel ton(NDI) andm(FFmpeg).vingester-browser-worker.js: try-requiresSyphonMetalServeronce at module load on darwin; creates one server per worker named after the browser title; publishes each captured frame's BGRA buffer viapublishImageData(wrapped asUint8ClampedArray— that's whatnode-syphonrequires); disposes instop(). The publish happens before the existing NDI in-placeBGRA→BGRXmutation so Syphon receives the original alpha-preserving frame.vingester-main.js: detectsnode-syphonavailability and exposessupport.syphonto the renderer via the existingsupportIPC handle; logs Syphon availability at startup alongside NDI/FFmpeg.vingester-control.html: new SINK row with a YES/NO toggle, gated onsupport.syphon.cfg.Nmaster toggle still gates the entire Output 2 block; Syphon is just one of three checkbox-level sub-sinks underneath it.nativeImage.toJPEG()and handles pixel format itself).Diff size
package.jsonvingester-main.jsvingester-browser.jsvingester-browser-worker.jsvingester-control.jsvingester-control.htmlLicense
node-syphonis GPL-3.0+, compatible with Vingester's GPL-3.0-only.Verified locally
cfg.s=trueflows from YAML → main → worker;node-syphonloads inside the Electron 18 renderer;SyphonMetalServerconstructs with the correct name and a realinfo.v002.Syphon.<UUID>identifier; frames publish at the configured FPS without errors; clean shutdown on stop.node_modules/node-syphon/**isasarUnpacked sosyphon.nodeandSyphon.framework(universal2) are both extracted next to the runtime.Known follow-ups (not in this PR)
log.errorper failed frame, which can flood at 30+ fps if a sink gets stuck). This is a pre-existing pattern; happy to send a separate PR if you'd like it.