Open source from Noise Factor · more projects
Sync is Noise Factor's low-latency bridge between browser renderers and native video ecosystems. It carries GPU-rendered RGBA frames over authenticated loopback WebSockets and republishes them through platform-native video-sharing providers, allowing applications such as Noisedeck to appear in existing source pickers without a custom plugin in every downstream host.
Sync is under active development. This source tree currently includes:
- an independently versioned frame and control protocol;
- a browser SDK with bounded discovery, explicit pairing, and separate control and sender-data sockets;
- a native C++20 loopback daemon with per-origin, revocable authorization;
- bounded sender and connection ownership with non-blocking browser submission;
- a macOS Metal publisher and dynamically discovered Syphon integration;
- a Windows Spout publisher and a cross-platform NDI publisher, both dynamically discovered;
- a generic Ubuntu 24.04 user daemon with an owner-only control socket and a
stock-v4l2loopback
Sync Cameraoutput; - an Apple Silicon menu-bar companion and a Windows tray companion, each with bounded helper supervision; and
- native, browser, protocol, security-boundary, and real-loopback tests.
The SDK 0.3.0 and native companion source at
1972af1ce3f0d14054f3693e250c668aff536884
include audio input for 1–32 channels per source. Native interfaces feed browser
audio controls over the same authenticated loopback service used for video.
That exact source passed the
cross-platform CI matrix
and the separate
Windows camera end-to-end workflow.
Concurrent 32-channel audio capture and 1080p60 video delivery were qualified in the
combined audio and video load qualification report
(30-minute soak: 0 audio drops, 0 cursor discontinuities, nominal 59.8+ delivered video FPS).
The Windows platform parity report
qualified the Windows Media Foundation virtual camera (SyncCamera.dll), DirectShow bridging
via Kernel Streaming proxy (ksproxy.ax), fixed-point BT.709 NV12 conversion, and WASAPI
audio capture architecture on Windows 11 Build 26200.
Native preview 0.2.68
and SDK 0.3.0
are published from that source. Current evidence is not a general hardware
compatibility claim. See the
audio research, design, and qualification matrix.
Both companions are previews and are not ready for general use. Reverse-direction native video sources and automatic updates are not part of the current public implementation.
These issues stop Sync from working today. Each entry includes a workaround where one exists. Keep this list current. Add an entry when a report is diagnosed. Remove it when the fix ships.
- Browser consumers of the Sync camera receive about 55 distinct frames per second at 1080p, with repeats. Native consumers receive all 60. On 2026-09-09 a native AVFoundation reader on an Apple Silicon Mac received every frame of a 60 fps sender for 10 minutes: 60 per second, no repeats, no second below 50 at 1024 × 1024. On 2026-09-18, the 1080p paired soak was completed (see the 1080p60 native AVFoundation versus Chromium report): the native AVFoundation consumer received 23,664 frames with 0 duplicate frames, 0 drops, and median relay latency of 3.22 ms, proving that the CoreMediaIO system extension and daemon relay deliver lossless 1080p60. Under identical conditions, Chromium delivered 6,277 repeated frames (20.9%) and dropped 332 frames in its internal capture queue. The repeats and capture ceiling originate in Chromium's video capture service and uncompressed socket backpressure, not in the CoreMediaIO system extension. Earlier reports of a slowdown over hours came from the test harness, not from Sync. Its memory inspector suspended the sending browser, its second test client competed with the sender for the loopback, and its launch shell ran the daemon at a lower priority than the browsers. The harness no longer does any of these.
- Short delivery stalls from browser senders remain under investigation.
If Chrome rendering and delivery both settle at 30 FPS on battery power,
connect the computer to power and check Settings > Performance > Energy
Saver. Chrome can limit frame cadence when battery charge is low. This
check does not explain delivery stalls while rendering remains at 60 FPS.
The 30-minute direct-receiver report
records both symptoms. Its experimental path still has unresolved stalls;
it is not a released fix.
The allocation pooling report
records 13 short web and desktop tests. The unchanged receiver also slowed
on a later run. New pooling changes are therefore not required to reproduce
the loss. Sender backpressure increased while rendering continued near
60 FPS. The cause remains unresolved. These tests do not establish a GC fix.
The source byte-budget report
records a separate bounded queue experiment. Its two-packet source limit
averaged 59.730 FPS in web and 59.668 in desktop over three minutes.
Both still had seconds below 50. The sequential runs do not establish
a performance benefit or the effect on latency. The report retains one
rejected marker-check attempt. Its cause remains unproved.
The decoder and ACK report
records a separate allocation candidate with the same source limit. Web
averaged 59.863 FPS over three minutes, with no complete seconds below 50.
Desktop averaged 58.015 FPS and had five seconds below 50. Pixel checks
passed in both contexts. These short runs do not establish sustained
60 FPS or a general pooling benefit. The candidate has not shipped.
The native write report
records a 227.731 ms write with 4.019 ms of thread CPU time. These timings
do not distinguish scheduling delay from a system wait. Web and desktop
observations still had seconds below 50. One source renderer stopped
before cleanup. Its caller remains unknown. The report retains that
rejected attempt. These diagnostics do not establish a GC cause or fix.
A fixed source-encoder candidate completed fresh web and desktop pixel checks.
Three-minute observations averaged 58.181 unique pairs/s on web and
59.118 on desktop. Web had six complete seconds below 50, including one
with zero pairs. Desktop had two, with a minimum of 43. Source telemetry
retained gaps between pipeline calls. Their cause remains unknown.
GPU contention on the busy test machine is a working hypothesis.
These tests did not measure GPU scheduling or contention.
A warm test avoided explicit view constructors and BigInt calls. It does
not prove zero allocation or a GC fix. These sequential runs do not
establish a performance benefit. The candidate has not shipped.
The fixed source encoder report is live.
A separate native socket experiment passed its pixel, source and cleanup
checks but failed both three-minute delivery checkpoints. A fixed
262,144-byte HTTP send buffer averaged 18.313 unique pairs/s on web and
16.321 on desktop. Each run had 174 complete seconds below 50.
This setting fails release requirements and is not a released workaround.
Mean initial native write time decreased while mean callback time
increased. These native measurements include setup and cleanup. Lost event
records prevent a complete per-write reconstruction. The sequential tests
do not establish an exclusive cause or explain the earlier source-entry
gaps. The fixed socket buffer report is live.
A separate source packet copy candidate uses three reusable packets. It removes one
8,294,400-byte payload copy per fast-path encoding attempt and reduces
logical retained CPU packet storage by 8,294,272 bytes. Fresh web and desktop
pixel controls passed. Three-minute observations averaged 45.479 unique
pairs/s on web and 55.562 on desktop. A later unchanged encoder reference
averaged 53.978 on web. These sequential runs do not isolate the change's
effect. A repeated candidate measurement failed its diagnostic deadline
and has no accepted FPS. The prepared report retains both failed attempts. Publication is pending.
The controlled sender stage separation report
isolated GPU readback fence wait (11.7 ms quiet / 12.4 ms load), PBO buffer
readback with row flipping (3.3 ms), staging payload copy (0.6 ms), and
WebSocket transmission (0.8 ms), proving that the 20.7 ms stamp-to-send
latency reflects single-frame GPU pipeline depth and memory copies rather
than socket pressure.
The combined audio and video load qualification report
demonstrated zero audio dropouts, 0 low-rate intervals (<50 FPS), and flat
~12.5 MB resident memory during a continuous 30-minute concurrent 32-channel
audio and uncompressed 1080p60 video soak. Native daemon optimizations
(NEON/SSE2 SIMD unmasking, decoupled audio ring buffer lock hold, and sampled
payload hashing) eliminated event loop starvation.
The Windows platform parity report
qualified the Windows Media Foundation virtual camera (
SyncCamera.dll), DirectShow bridging via Kernel Streaming proxy (ksproxy.ax), fixed-point BT.709 NV12 conversion, and WASAPI audio capture architecture on Windows 11 Build 26200, confirming platform parity with macOS. The macOS camera fitter reduces conversion passes for all alpha modes on Apple Silicon. Two row ranges each use one pass per pixel. The fitter reuses its completion signals between frames. This shared native change applies to web and desktop senders. It reduces conversion work; it does not establish a delivery-stall fix. - Sync output stops when the Noisedeck window is fully covered. Chrome marks a fully covered page hidden, Noisedeck then pauses rendering, and Sync output stops until the window is visible again. Workaround: keep part of the Noisedeck window visible, or put it on a second screen.
- Content blockers block the loopback health request. uBlock Origin,
uBlock Origin Lite, and AdGuard ship EasyPrivacy and "block LAN" rules that
stop public pages from reaching
127.0.0.1. Chrome logsnet::ERR_BLOCKED_BY_CLIENTand Noisedeck reports the companion absent. Workaround: set the blocker to no filtering on the Noisedeck origin. - Browsers require a loopback permission first. Chrome 145+ and Firefox
150+ gate
127.0.0.1behind theloopback-networkpermission. The passive check stops at "Needs attention". Only Connect Sync can show the browser's prompt. Noisedeck Standalone (Electron) grants it by default. - The macOS approval row can lag a day behind the request. System Settings > Privacy & Security > Security shows "System software from application "Sync" was blocked from loading" only for a live request. If the row is missing, quit Sync. Relaunch it from Applications. Open Settings again.
- The native pairing prompt defaults to Deny. Pressing Return in the companion's pairing dialog denies the origin.
- The Windows installer is not code-signed and Windows warns about an unrecognised publisher. Each release publishes a SHA-256 instead.
- The Windows camera needs Windows 11.
MFCreateVirtualCameraarrived in build 22000, so on Windows 10 the provider reports "the camera needs Windows 11 (build 22000) or later" and the rest of Sync is unaffected. - Declining the uninstall prompt leaves the camera registered. Removing
the CLSID from HKLM needs the same elevation that added it, and a per-user
uninstall is not elevated. The stale key is harmless and the next install
reuses it. Run
syncd --unregister-cameraas an administrator to clear it. - Two users signed in at once share one Windows camera. The media source cannot be told which account to pair with without an administrator-only API, so it accepts frames from whoever is logged in interactively. With fast user switching the most recent sender wins.
Sync publishes through every provider that is available on the running platform, at once: a single named output appears simultaneously as a Spout sender and an NDI source on Windows. Receiving applications pick whichever they support.
| Provider | Platform | Runtime | Bundled |
|---|---|---|---|
| Syphon | macOS | Syphon.framework |
Yes — see docs/dependencies/syphon.md |
| Spout | Windows | SpoutLibrary.dll |
Yes — see docs/dependencies/spout.md |
| NDI | Windows, macOS, Linux | Operator-installed NDI Runtime (libndi.so.5 on Linux) |
No — the SDK licence forbids redistribution; see docs/dependencies/ndi.md |
| Camera | macOS | Sync Camera extension, bundled in Sync.app | Yes — activated by Sync.app on first launch, approved once in System Settings |
| Camera | Windows 11 | SyncCamera.dll, bundled with the installer |
Yes — enabled once from the tray menu, which asks for administrator rights |
| Camera | Ubuntu 24.04 x86_64 | Ubuntu v4l2loopback packages |
No kernel module is bundled; one-time setup configures the stock module |
The camera provider publishes a 1920×1080 stream, so any app that picks a camera can use it. While no sender is live, the camera shows a dark Sync waiting card instead of a black picture.
On macOS it is a CoreMediaIO system extension shipping inside Sync.app, appearing as "Sync Camera". macOS activates it only for an app under /Applications, and asks the user once. Sync.app restarts its helper when activation completes, which is when the camera first appears in Noisedeck's provider list.
On Windows it is a Media Foundation virtual camera, appearing as "Sync"
— the pipeline appends "Windows Virtual Camera" to the name itself. It needs
Windows 11 (build 22000), because MFCreateVirtualCamera does not exist
before it; on Windows 10 the provider reports itself unavailable and says so.
The media source is a COM server the frame server loads, so its CLSID has to
live in HKLM, which needs administrator rights once: choose Enable Sync
Camera… from the tray menu and approve the prompt. Sync restarts its helper
afterwards, for the same reason it does on macOS.
The Windows camera offers NV12 first and RGB32 second, and converts from the one BGRA canvas per consumer, so two applications can negotiate different formats against the same device.
On Ubuntu 24.04, syncd is an unprivileged systemd user service and the
camera appears as Sync Camera. The setup command installs a fixed
v4l2loopback configuration and a group-scoped udev rule; ordinary daemon
operation never needs root. Sync deliberately creates no /dev alias or udev
symlink: it discovers and validates the kernel-owned /dev/videoN each time it
opens the camera. PipeWire and WirePlumber are useful interoperability checks
reported by syncctl doctor, but are not in the frame path. The daemon writes
NV12 directly to V4L2.
No provider is ever linked at build time. Sync discovers each provider at
runtime through its documented public entry point. A provider whose runtime
is absent reports itself unavailable rather than failing the daemon.
An unavailable selected provider also prints one line to stderr that explains
why. Thus, available: false is never the whole diagnosis.
The ready record on stdout keeps its exact shape.
Sync requires CMake 3.21 or newer, a C++20 compiler, OpenSSL 3, and libuv. macOS builds also use the system Foundation and Metal frameworks and locate libuv through pkg-config. Windows builds use MSVC and locate libuv and OpenSSL through a CONFIG package such as vcpkg:
vcpkg install libuv:x64-windows openssl:x64-windows
cmake -S . -B build -A x64 `
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake"
cmake --build build --config Release --target syncd
ctest --test-dir build --build-config Release --output-on-failureThe camera's end-to-end test is labelled sync_camera_e2e and is the one test
that needs more than a build: Windows 11 and the media source registered under
HKLM. Run syncd --register-camera from an elevated prompt first, or exclude
it with ctest --label-exclude sync_camera_e2e. Everything else, including the
media source driven in process, runs on any Windows machine.
MSVC is what CI builds and what the installer ships. The tree also builds and passes its tests under MinGW-w64 (GCC), which needs no administrator rights and is a practical local setup:
pacman -S --needed mingw-w64-x86_64-{gcc,cmake,ninja,openssl,libuv,pkgconf}
cmake -S . -B build -G Ninja && cmake --build build
ctest --test-dir build --output-on-failureRun the tests from a shell with a Windows-shaped environment.
An MSYS2 login shell unsets LOCALAPPDATA and points TMP/TEMP at /tmp.
The pairing store resolves its default path from %LOCALAPPDATA%.
It refuses paths that are not drive-absolute.
Thus, several tests fail in an MSYS2 login shell for reasons unrelated to the code.
cmake -S . -B build
cmake --build build --target syncd -j4
ctest --test-dir build --output-on-failureThe daemon binds only to IPv4 and IPv6 loopback. Production mode uses port
53979 unless overridden:
./build/syncd
./build/syncd --port 54000
./build/syncd --list-pairings
./build/syncd --revoke-origin https://visuals.exampleNaming no publisher selects every provider the platform offers. Naming one or more restricts the daemon to exactly those, and each accepts an explicit runtime path for development builds:
./build/syncd --publisher spout --publisher ndi
./build/syncd --publisher ndi --ndi-runtime /opt/ndi/lib
./build/syncd --publisher spout --spout-library C:/Spout/SpoutLibrary.dllSee the provider table above for each runtime and license boundary.
The supported Linux package is x86_64. The same .deb can be downloaded
directly or indexed unchanged by a signed Noisefactor-hosted APT repository:
sudo apt install ./Sync-<version>-linux-amd64.deb
sudo syncctl camera setup --user "$USER"
# Log out and back in if setup added the group.
systemctl --user enable --now noisedeck-sync.service
syncctl pair
syncctl doctorIf another v4l2loopback configuration or loaded instance already exists,
setup refuses to merge or renumber it. Resolve that administrator-owned state
explicitly and rerun setup. To select one already validated device, add a user
unit drop-in with systemctl --user edit noisedeck-sync.service:
[Service]
ExecStart=
ExecStart=/usr/bin/syncd --camera-device /dev/video12Safe removal stops and disables the user service first, removes the package, then purges only setup files whose complete contents still match Sync's templates:
systemctl --user disable --now noisedeck-sync.service
sudo apt remove noisedeck-sync
sudo apt purge noisedeck-syncLocally modified module configuration is retained and named on stderr. Removal
does not unload a live module or remove the noisedeck-sync group.
Packaging requires macOS 13 or newer, an Apple Silicon build, dylibbundler,
librsvg, and a locally built Syphon.framework. The release workflow pins
Syphon source revision 71351d4b484cd2d1917867f7846a5cdca724552d; use that
same revision for local release-equivalent packages.
SYNC_PRODUCT_VERSION=X.Y.Z
cmake -S . -B build-package \
-DSYNC_PRODUCT_VERSION="$SYNC_PRODUCT_VERSION" \
-DSYNC_SYPHON_FRAMEWORK_PATH=/absolute/path/to/Syphon.framework
cmake --build build-package --target sync_macos_dmg -j4
SYNC_PACKAGE_DIR=build-package/package \
node --test test/packaging/macos-package.test.js
scripts/smoke-macos-app.sh "$PWD/build-package/package/Sync.app"The local target creates an unsigned app and DMG. Developer ID signing, notarization, stapling, and public publication belong to the Noise Factor release workflow so credentials never enter this public repository.
Packaging requires Windows 10 or newer, an x64 MSVC toolchain, Inno Setup 6
(ISCC on PATH), ImageMagick (magick on PATH), and a locally built
SpoutLibrary.dll.
$SyncProductVersion = "X.Y.Z"
cmake -S . -B build-package -A x64 `
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" `
-DSYNC_PRODUCT_VERSION="$SyncProductVersion" `
-DSYNC_SPOUT_LIBRARY_PATH=C:\absolute\path\to\SpoutLibrary.dll `
-DSYNC_WINDOWS_DEPENDENCY_PATH="$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows\bin"
cmake --build build-package --config Release --target sync_windows_installer --parallel 4
$env:SYNC_PACKAGE_DIR = "build-package/package"
node --test test/packaging/windows-package.test.js
./scripts/smoke-windows-app.ps1 -Bundle "$PWD/build-package/package/Sync"The local target creates an unsigned application directory and installer. Authenticode signing and public publication likewise belong to the Noise Factor release workflow.
The browser SDK can connect any web renderer to Sync. It includes direct RGBA,
Canvas 2D, WebGL2, and WebGPU export queues plus native audio-source discovery
and bounded PCM reads. The dependency-free source modules live in
browser/.
The Sync SDK 0.3.0 release page includes an installable tarball, browser modules, and SHA-256 checksums. Install the tarball in your application:
npm install https://github.com/noisefactorllc/sync/releases/download/sdk-v0.3.0/noisefactor-sync-0.3.0.tgzYou can then import from @noisefactor/sync.
See the developer guide for direct browser imports, local builds, and the complete API.
Passive discovery never starts pairing. A deliberate user action must call
pair(). The host application owns storage for the returned token. See the
browser client guide for permission and lifecycle details.
Runnable browser and Electron examples cover Canvas 2D, WebGL2, and WebGPU.
Audio integrations must check for a selected and available audio provider
with direction receive; the SDK and companion product versions are
independent. See the developer audio example.
Linux users who need JACK through PipeWire must start Sync through pw-jack;
see the explicit service setup.
import { SyncBridgeClient } from '@noisefactor/sync'
const pairingClient = new SyncBridgeClient()
const { token } = await pairingClient.pair('My visual app')
pairingClient.close()
const sync = new SyncBridgeClient({ token })
await sync.connect()npm run test:unit
npm run test:browser
npm run test:packaging
SYNC_DAEMON_PATH=build/syncd npm run test:integration
ctest --test-dir build --output-on-failuretest:unit checks protocol and harness behavior without a native build. The
integration command also runs test:integration:soak in a separate test process:
the regular short soak uses a 60 FPS ceiling and fixed memory readings, while an
idle real daemon verifies process inspection (including private memory on Windows).
Windows inspection has a 60-second command budget inside a 75-second test and a
180-second runner budget. A missing daemon fails integration instead of skipping it.
To repeat these checks after a harness change, run test:integration:soak on the
same build several times. Every run must pass without a retry that hides failure.
Fairness under unlimited writes is checked with immediately completed writes and a
queued stop, independently of native throughput or runner scheduling speed.
The memory soak streams 1080p frames through a test-receiver daemon while cycling senders and probing health, and fails on footprint growth. Run it against a Release build. A Debug daemon is too slow to be representative:
SYNC_DAEMON_PATH=build-release/syncd SYNC_SOAK_SECONDS=60 npm run test:soakThis standalone soak still sends at unlimited speed by default. Set
SYNC_SOAK_FPS to a positive frame-rate ceiling for a paced workload.
Unknown origins cannot silently publish. Pairing requires a browser-initiated request and a visible native approval prompt. Reusable credentials are scoped to an exact normalized origin and can be revoked. Please report suspected vulnerabilities privately using SECURITY.md.
Sync is released under the MIT License. See TRADEMARK.md for the branding boundary. Third-party runtime providers retain their own licenses and are not relicensed by this repository.
Copyright © 2026 Noise Factor LLC
