stagelinq-js
v0.3.1
Published
A zero-dependency, symmetric-peer implementation of the Denon StageLinq protocol for Node.js
Maintainers
Readme
stagelinq.js
A zero-dependency Node.js / TypeScript implementation of Denon/inMusic's StageLinq protocol, used by Prime-series DJ hardware (SC5000/SC6000, Prime 2/4/Go, X1800/X1850) to share live performance data with lighting, streaming, and production software.
This is a from-scratch implementation, not a fork or translation of any existing library. Design is informed by the best current understanding of the StageLinq protocol. Regardless, this project leans heavily on the hard work of many in the community:
- icedream (@icedream) - The OG! Did the first, ground-breaking reverse-engineering work on Stagelinq. Maintainer of the go-stagelinq library
- MarByteBeep (@MarByteBeep) - Author of the original TypeScript implementation.
- Chris Le / Triode (@chrisle) - Maintainer of the StageLinq TypeScript library and creator of the Now Playing app
- Matt Hite (@mhite) - go-stagelinq contributor and collaborator in the reverse-engineering of many services
- Domas Zelionis (@dzelionis) - Collaborator in the reverse-engineering of the BeatInfo service, and author of it's Python implementation
- DJ LiQuiD iCe (@djliquidice) - Active Denon DJ forum member, Radio host, whose work and knowledge of the database format have been an invaluable resource
Design Goals
- Protocol correctness. Strive to replicate the behaviour of devices and software which use the official StageLinq SDK - On-spec, if we had access to the official spec, that is... 😉
- Lean core. The core sticks to discovery, the wire format, and connection
negotiation — it doesn't track application state (what's playing, what's
synced) or touch the filesystem. Loading and persisting a device identity is
left to the application (see
examples/config.ts); none of that belongs down here. - Self-contained services. Each service (StateMap, BeatInfo, TimeSync,
Syncing, Broadcast, FileTransfer) lives in its own folder under
src/services/and only depends onsrc/core, never on another service. This is enforced by lint rule, not just convention (seeeslint.config.js). - Symmetric peers. A StageLinq network-participant isn't a client that dials into services, or a server that waits for clients - it's a peer that can initiate or accept connections; behaviour which is dictated by the numerical relationship between their deviceIds.
Novel Functionality - Limitless Usability
What cool, new functionality can you accomplish with this library? Here's a short list of what we've managed to demonstrate so far:
- Browse remote filesystems. (examples/filetransfer-browse.ts) - Inspired by @mhite's demonstration, you can traverse a remote source's files and directories via the command line.
- Keep downloaded files synced with remote. (examples/filetransfer-watch.ts) - When a remote player makes changes to a database you've downloaded, it sends a 0x6 DataUpdate message indicating which chunks of the file have changed. This example downloads these chunks, and patches them into the local copy of the file.
- Serve your own StateMap states. (examples/statemap.ts) - If a peer asks you for a state, you can
respond()with real data orreject()it. The example demonstrates accepting connections and answering requests, but rejects every one since it has no real deck state to draw from — wiring in actual state is left to your application.
Have something else in mind? You aren't limited by what we've thought of. The application has freedom to handle responses and craft it's own replies.
Status: pre-release
The protocol layer is fairly solid — confirmed against real SC6000 players and their OfflineAnalyzer companions. The application-facing API is still finding its shape and may change before 1.0.
Discovery, Directory, and every listed service are implemented and have been
confirmed working against real hardware (Denon SC6000 players and their
OfflineAnalyzer companion processes) — see examples/ for the live-testing
scripts this was verified with, and docs/PROTOCOL.md /
docs/FILETRANSFER_FINDINGS.md for exactly what's confirmed on the wire
versus still an open question. Some corners of the protocol are
deliberately left unconfirmed rather than guessed at — e.g. FileTransfer's
push/write-back flow has no confirmed "the write succeeded" signal yet —
those gaps are called out explicitly in the docs.
Project layout
src/
core/ shared wire codec, discovery, connection/role negotiation
services/
directory/ per-peer service registry + discovery handshake — every
other service's availability is announced through this
statemap/ deck/mixer parameter subscribe + serve
beatinfo/ real-time beat/bpm stream
timesync/ clock/epoch ping-pong between peers
syncing/ master-deck beat/tempo broadcast + handoff
broadcast/ arbitrary key/value database-change push, no request/reply
filetransfer/ Engine Library database + track file browsing, reads,
and live update watching
peer.ts optional orchestration layer over Discovery + Directory +
however many service servers an application registers
index.ts public entry point
examples/ manual, live-testing scripts run against real hardware
docs/
PROTOCOL.md confirmed, cross-service protocol facts
FILETRANSFER_FINDINGS.md detailed FileTransfer evidence trail, capture by capture
DEVICEID_FINDINGS.md DeviceId structure/generation analysis, capture by capture
KNOWN_STATES.md StateMap paths seen on real hardware, copy-pasteable
into example:statemap
test/
core/
services/
examples/
peer.test.tsExamples
Every service has a manual test script under examples/, meant to run
against real hardware on the same network — these aren't automated tests
(see test/ for those). Each one loads a stable device identity via .env
on first run, using examples/config.ts — see that file's doc comment for
why that's example-level policy rather than something Peer decides for
you.
pnpm example:discover # Discovery only — see what's on the network
pnpm example:directory # Discovery + Directory service negotiation
pnpm example:statemap # Subscribe to a handful of deck/mixer states
pnpm example:beatinfo # Stream a peer's real-time beat clock
pnpm example:timesync # Exchange clock/epoch pings
pnpm example:syncing # Watch master-deck beat/tempo + handoff traffic
pnpm example:broadcast # Watch arbitrary database-change broadcasts
pnpm example:filetransfer-browse # Interactively browse a peer's Engine Library
pnpm example:filetransfer-download # Download a specific file by path
pnpm example:filetransfer-watch # Download a file once, then mirror live updates to itSome real hardware (e.g. a companion OfflineAnalyzer process) is configured
with a DeviceId that makes this software always the side being dialed,
never the one dialing out — the three filetransfer-* scripts handle this
with an interactive picker over every peer seen either way; see
docs/PROTOCOL.md for the underlying DeviceId dial convention.
Protocol notes
Wire-format facts shared across more than one service — the DeviceId dial
convention, the self-announcement frame shape every service's connect
handshake reuses, TimeAlive semantics — live in
docs/PROTOCOL.md rather than getting repeated in each
service's code. FileTransfer also has
docs/FILETRANSFER_FINDINGS.md, the
detailed, capture-by-capture evidence trail behind everything
PROTOCOL.md calls confirmed for that service. DeviceId structure and
generation has its own equivalent write-up in
docs/DEVICEID_FINDINGS.md. StateMap paths
observed on real hardware are collected in
docs/KNOWN_STATES.md — a good starting point if
you want to subscribe to something examples/statemap.ts doesn't already.
Development
pnpm install
pnpm build # compiles src/ to dist/
pnpm typecheck # type-checks src/ and examples/, no emit
pnpm test # runs the automated test suite (vitest)
pnpm lint # eslint, including the core/service dependency boundary rule