Skip to content

Repository files navigation

Sync Browser to native video bridge

Open source from Noise Factor · more projects

Sync

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.

Project status

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 Camera output;
  • 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.

Known issues

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 logs net::ERR_BLOCKED_BY_CLIENT and 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.1 behind the loopback-network permission. 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. MFCreateVirtualCamera arrived 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-camera as 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.

Providers

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.

Building the native daemon

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-failure

The 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-failure

Run 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-failure

The 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.example

Naming 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.dll

See the provider table above for each runtime and license boundary.

Ubuntu 24.04 daemon

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 doctor

If 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/video12

Safe 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-sync

Locally modified module configuration is retained and named on stderr. Removal does not unload a live module or remove the noisedeck-sync group.

Packaging the desktop previews

macOS

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.

Windows

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.

Browser SDK

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.tgz

You 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()

Tests

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-failure

test: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:soak

This standalone soak still sends at unlimited speed by default. Set SYNC_SOAK_FPS to a positive frame-rate ceiling for a paced workload.

Security

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.

License

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

About

Noisedeck Sync: Low-latency bridge between browser renderers and native video ecosystems

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Contributors

Languages