@lox-audioserver/node-libraop
v0.3.3
Published
Node.js bindings for the libraop RAOP (AirPlay 1) receiver with prebuilt binaries.
Maintainers
Readme
Node bindings for the libraop RAOP (AirPlay 1) receiver. The module bundles the native libraop sources and ships prebuilt binaries so consumers do not need a compiler on the target machine.
Features
- Native RAOP receiver backed by libraop, exposed through a minimal Node API.
- Thread-safe event delivery into JavaScript for stream lifecycle, metadata, artwork, PCM frames, and volume.
- Prebuilt binaries for Linux (x64, arm64) and macOS (x64, arm64) produced by GitHub Actions.
- Bundled vendor sources for reproducible local builds when needed.
Installation
npm install @lox-audioserver/node-libraopPrebuilt .node binaries are downloaded via node-gyp-build at runtime. If a prebuild is not available for your platform, the package will fall back to building from the vendored sources.
Usage
import { startReceiver, stopReceiver, RaopEvent } from '@lox-audioserver/node-libraop';
const handle = startReceiver(
{
name: 'My AirPlay Target',
model: 'Node-Libraop',
metadata: true,
portBase: 6000,
portRange: 100,
},
(event: RaopEvent) => {
switch (event.type) {
case 'stream':
console.log(`Incoming RAOP stream on port ${event.port}`);
break;
case 'metadata':
console.log(`Now playing: ${event.artist} - ${event.title} (${event.album})`);
break;
case 'pcm':
// event.data is a Buffer with raw PCM samples
break;
case 'stop':
console.log('Playback stopped');
break;
}
}
);
process.on('SIGINT', () => {
stopReceiver(handle);
process.exit(0);
});Sending to an AirPlay target (PCM)
import { startSender, sendChunk, stopSender } from '@lox-audioserver/node-libraop';
import fs from 'node:fs';
const sender = startSender({ target: '192.168.1.50', port: 5000, sampleRate: 44100, channels: 2 });
const pcmStream = fs.createReadStream('audio.pcm'); // 16-bit, little endian, stereo
pcmStream.on('data', (chunk) => {
// Try to enqueue the chunk; if not ready yet the data is skipped
const result = sendChunk(sender, chunk);
if (!result.sent) {
console.warn('Sender not ready yet; waiting for queue to drain');
}
});
pcmStream.on('end', () => stopSender(sender));Sender pacing and health
Use getSenderState to check connectivity and buffer depth before pushing audio:
import { startSender, getSenderState, sendChunk } from '@lox-audioserver/node-libraop';
const sender = startSender({ target: '192.168.1.50', port: 5000 });
function maybeSend(pcm: Buffer) {
const state = getSenderState(sender);
if (!state.connected) {
console.warn('Not connected yet');
return;
}
const result = sendChunk(sender, pcm);
if (!result.sent && result.reason === 'not-ready') {
// consider waiting result.latencyFrames / sampleRate seconds before retrying
}
}Sender metadata, controls, and keepalive
import { senderControl, setSenderMetadata, setSenderProgress, setSenderArtwork, setSenderVolume, sendKeepAlive } from '@lox-audioserver/node-libraop';
import fs from 'node:fs';
senderControl(sender, 'play');
setSenderMetadata(sender, { title: 'Track', artist: 'Artist', album: 'Album' });
setSenderProgress(sender, 15_000, 180_000);
setSenderVolume(sender, 60);
setSenderArtwork(sender, 'image/jpeg', fs.readFileSync('cover.jpg'));
sendKeepAlive(sender);Apple TV pairing (interactive)
import { pairWithAppleTv } from '@lox-audioserver/node-libraop';
const result = pairWithAppleTv();
console.log(result);Apple TV pairing by IP (interactive)
import { pairWithAppleTvByIp } from '@lox-audioserver/node-libraop';
const result = pairWithAppleTvByIp('192.168.1.165', 7000);
console.log(result);API
startReceiver(options?, handler): number
Starts the RAOP receiver. Returns a handle that you should pass tostopReceiver. Thehandlercallback receivesRaopEventobjects.stopReceiver(handle): void
Stops the receiver associated with the provided handle.sendRemoteCommand(handle, command): boolean
Sends a transport command (play,pause,stop,next,prev/previous) to the active sender if available.startSender(options): number
Connects to an AirPlay (RAOP) target and returns a handle used bysendChunk/stopSender.sendChunk(handle, pcmBuffer): SendResult
Attempts to enqueue a PCM chunk (16-bit, little endian). Returns whether it was sent, queue details, latency frames, and optionalreason(not-readyordisconnected).stopSender(handle): void
Disconnects from the AirPlay target and frees resources.getSenderState(handle): SenderState
Returns connection status plus queue/latency stats without sending audio.senderControl(handle, command): boolean
Controls playback state for the sender (play,pause,stop).setSenderVolume(handle, volume): boolean
Sets the target volume (0-100).setSenderProgress(handle, elapsedMs, durationMs): boolean
Sends playback progress in milliseconds.setSenderMetadata(handle, metadata): boolean
Sends track metadata (title,artist,album).setSenderArtwork(handle, contentType, data): boolean
Sends artwork bytes with a content type (e.g.image/jpeg).sendKeepAlive(handle): boolean
Sends a keepalive to reduce playback dropouts on some devices.pairWithAppleTv(): { ok, udn?, secret? }
Starts interactive Apple TV pairing via mDNS discovery (stdin/stdout prompts).pairWithAppleTvByIp(targetIp, port?): { ok, secret? }
Starts interactive Apple TV pairing via explicit IP/port (default port7000).setLogHandler(handler?, level?): void
Forward libraop native logs into JavaScript. Passnullto disable. Levels:error,warn(default),info,debug,sdebug. Optional per-channel override:setLogHandler(fn, 'info', 'debug', 'warn')sets defaultinfo, RAOP todebug, util towarn. Callback receives{ level, source, timestamp, line }.
Options
All fields are optional; libraop defaults are applied when omitted.
Receiver options
| Option | Type | Default | Description |
| ----------- | ------- | ------------------ | -------------------------------------------------------- |
| name | string | LoxAirplay | Friendly name advertised over RAOP. |
| model | string | Lox-RAOP | Model identifier included in mDNS advertisements. |
| mac | string | 00:11:22:33:44:55| MAC-like identifier used for the hostname. |
| latencies | string | 1000:0 | Latency configuration string passed to libraop. |
| metadata | boolean | true | Whether to emit metadata/artwork events. |
| portBase | number | 6000 | Base port for RAOP listener sockets. |
| portRange | number | 100 | Number of ports available for the listener pool. |
| host | string | 0.0.0.0 | Optional host override for binding and mDNS. |
Sender options
| Option | Type | Default | Description |
| --------------- | ------- | ------------- | ----------------------------------------------------- |
| target | string | required | Target IPv4 address. |
| port | number | 5000 | Target RTSP port. |
| sampleRate | number | 44100 | PCM sample rate in Hz. |
| channels | number | 2 | PCM channel count. |
| sampleSize | number | 2 | PCM bytes per sample. |
| frameLength | number | 352 | Frames per chunk (bounded by libraop limits). |
| latencyFrames | number | 11025 | Requested playback latency in frames. |
| volume | number | 50 | Initial volume (0-100). |
| dacpId | string | empty | DACP-ID header value for remote control integration. |
| activeRemote | string | empty | Active-Remote header value for remote control. |
| et | string | empty | mDNS TXT et value for RTSP auth setup. |
| md | string | empty | mDNS TXT md value for metadata capability flags. |
| auth | boolean | false | Whether RTSP auth is enabled. |
| secret | string | empty | Pairing secret (mDNS TXT pk-derived). |
| passwd | string | empty | AirPlay password (mDNS TXT pw). |
| local | string | 0.0.0.0 | Local bind IPv4 address. |
Events
stream—{ port }: Emitted when a new stream announces the data port.play,pause,flush,stop: Playback lifecycle events.volume—{ value }: AirPlay volume updates.metadata—{ title?, artist?, album?, durationMs?, elapsedMs? }: Track metadata (duration/elapsed in milliseconds when available).artwork—{ data, title?, artist?, album? }: Artwork bytes.pcm—{ sampleRate, channels, data }: Raw PCM frames (16-bit signed).
Prebuilt binaries
Prebuilds are produced by .github/workflows/prebuild.yml on workflow_dispatch or when a GitHub release is published. The matrix covers:
- Linux: x64, arm64
- macOS: x64 (Intel), arm64 (Apple Silicon)
- Windows: not prebuilt yet (source build required)
Artifacts are uploaded to the workflow run for download, and attached to releases when triggered from a tag. To build a missing prebuild locally:
npm ci
npm run build
npm run build:prebuilds -- --arch <arch> --platform <linux|darwin>Building from source
You only need this if you are on an unsupported platform or hacking on the addon.
Prerequisites:
- Node.js 18+
- Python 3, make, C/C++ toolchain
- OpenSSL headers (
libssl-devon Debian/Ubuntu,opensslvia Homebrew on macOS,choco install opensslon Windows) git(the build script fetches libraop from GitHub ifvendor/libraopis missing) Notes:- Windows builds require the MSVC toolchain and currently must be built from source; prebuilds are not shipped yet.
- Vendored libraop contains
#warningdirectives; for MSVC these are mapped to#pragma messageto keep builds green. - Before publishing,
npm run prune:vendorstrips unused vendor assets (codecs, binaries, fuzz corpora) to keep the tarball small.
Commands:
npm ci
npm run build:native # compiles the .node binding from vendored sources
npm run build # builds the TypeScript wrapper
npm run build:prebuilds -- --arch $(node -p "process.arch") --platform $(node -p "process.platform") # optionalThe build uses a pinned libraop commit (81c2182649da8645ac2a58b78e9f370c79a4165b) and will clone it automatically if vendor/libraop is absent.
Development
- Native sources live in
native/and are built viabinding.gyp. - Libraop sources are vendored under
vendor/;scripts/prepare-libraop.shvalidates their presence before compiling. - TypeScript wrapper lives in
src/and compiles todist/. - Clean the workspace with
npm run clean.
