Skip to main content
Version: 0.11.x

Overview

We offer ODIN, a high-quality cross-platform immersive voice SDK that includes state-of-the-art noise suppression technologies.

The Node.js SDK provides native bindings for the ODIN Voice SDK, enabling you to build powerful server-side voice applications including recording bots, AI voice assistants, audio processing pipelines, and content moderation tools.

Features

  • 🎙️ Real-time Voice Chat - Low-latency voice communication
  • 🔐 End-to-End Encryption - Built-in E2EE with OdinCipher
  • 🤖 Bot Integration - Perfect for recording bots, AI assistants, and moderation tools
  • 📊 Raw Audio Access - Get PCM audio data for processing, recording, or transcription
  • 🌍 Proximity Chat - 3D positional audio support
  • High Performance - Native C++ bindings for maximum efficiency
  • 📈 Diagnostics - Real-time connection and audio quality monitoring

Web SDK vs Node.js SDK

Our Web SDK includes fallback code for WebRTC and is compatible with most browsers. While it can also be used in Node.js, it may not be as performant as our native Node.js SDK, which is designed for advanced use cases and bots.

Although we strive to keep the API as similar as possible between our SDKs, there are some differences. The main difference is that the Node.js version allows you to access raw audio data from the ODIN server, which enables you to record or stream audio to the room.

tip

The Node.js SDK is optimized for server-side use cases like:

  • 🎙️ Audio recording bots
  • 🤖 AI-powered voice assistants
  • 📝 Speech-to-text transcription
  • 🛡️ Content moderation
  • 🔊 Audio processing pipelines

Important: You cannot combine the WebSDK and the Node.js SDK in the same project. You either have @4players/odin-nodejs or @4players/odin in your package.json file, but not both.

Source Code

The Node.js SDK is developed in C/C++ using Node API and is open source. Check out the source in our public GitHub repository.

Installation

npm npm npm

Install the SDK via npm in your project:

npm install @4players/odin-nodejs

Platform Support

This SDK includes prebuilt binaries for:

  • macOS (x86_64 and ARM64)
  • Windows (x86_64)
  • Linux (x86_64)

If you deploy your application to a different platform, npm install will try to build the library from source. This requires a working C/C++ compiler and the necessary build tools. You can find more info in the node-gyp documentation.

Quick Start

1. Get Your Access Key

Sign up at 4Players ODIN to get your free access key.

2. Basic Connection Example

import odin from '@4players/odin-nodejs';
const { OdinClient } = odin;

// Configuration - replace with your credentials
const accessKey = "__YOUR_ACCESS_KEY__";
const roomId = "my-room";
const userId = "user-123";

async function main() {
// Create client and generate token locally
const client = new OdinClient();
const token = client.generateToken(accessKey, roomId, userId);

// Create room using factory pattern
const room = client.createRoom(token);

// Set up event handlers
room.onJoined((event) => {
console.log(`Joined room: ${event.roomId}`);
console.log(`My peer ID: ${event.ownPeerId}`);
console.log(`Available media IDs: ${event.mediaIds}`);
});

room.onPeerJoined((event) => {
console.log(`Peer joined: ${event.peerId}`);
});

room.onPeerLeft((event) => {
console.log(`Peer left: ${event.peerId}`);
});

// Join the room
room.join("https://gateway.odin.4players.io");

// Keep connection alive
process.on('SIGINT', () => {
room.close();
process.exit(0);
});
}

main();

Event Handling

The SDK provides typed event handlers for easy integration:

// Connection events
room.onConnectionStateChanged((event) => {
console.log(`State: ${event.state}`); // Connecting, Joined, Disconnected, etc.
});

room.onJoined((event) => {
// { roomId, ownPeerId, room, mediaIds }
});

room.onLeft((event) => {
// { reason }
});

// Peer events
room.onPeerJoined((event) => {
// { peerId, userId, userData, peer }
});

room.onPeerLeft((event) => {
// { peerId }
});

// Media events
room.onMediaStarted((event) => {
// { peerId, media }
});

room.onMediaStopped((event) => {
// { peerId, mediaId }
});

room.onMediaActivity((event) => {
// { peerId, mediaId, state } - Voice Activity Detection
});

// Messages
room.onMessageReceived((event) => {
// { senderPeerId, message }
});

// Audio data (for recording/processing)
room.onAudioDataReceived((data) => {
// { peerId, mediaId, samples16, samples32 }
});

Using addEventListener (Alternative)

You can also use the traditional addEventListener pattern:

room.addEventListener('PeerJoined', (event) => {
console.log("Received PeerJoined event", event);
console.log(JSON.parse(new TextDecoder().decode(event.userData)));
});

room.addEventListener('PeerLeft', (event) => {
console.log("Received PeerLeft event", event);
});

Available events include: Joined, Left, PeerJoined, PeerLeft, MessageReceived, PeerUserDataChanged, RoomUserDataChanged, MediaStarted, MediaStopped, MediaActivity, AudioDataReceived, PeerTagsChanged, ConnectionStateChanged.

Working with Audio

Receiving Audio Data

Once the script has joined a room, you receive audio data through the AudioDataReceived event or the onAudioDataReceived handler:

room.onAudioDataReceived((data) => {
const { peerId, mediaId, samples16, samples32 } = data;

// samples16 = 16-bit integer samples (for recording to WAV)
// samples32 = 32-bit float samples (for processing, range -1 to 1)

// Example: Log sample information
console.log(`Audio from peer ${peerId}, ${samples16.length} samples`);
});

The high-level API handles all the complexity automatically - media ID allocation, StartMedia RPC, and timing:

import odin from '@4players/odin-nodejs';
const { OdinClient } = odin;

async function main() {
const client = new OdinClient();
const token = client.generateToken(accessKey, roomId, userId);
const room = client.createRoom(token);

// Wait for room join
const joinPromise = new Promise(resolve => room.onJoined(resolve));
room.join("https://gateway.odin.4players.io");
await joinPromise;

// Create audio stream and send audio with one line!
const media = room.createAudioStream(44100, 2);

// Send an MP3 file (auto-decodes and streams with correct timing)
await media.sendMP3('./music.mp3');

// Or send a WAV file
await media.sendWAV('./audio.wav');

// Or send a decoded AudioBuffer
// await media.sendBuffer(audioBuffer);

// Close the stream when done - this sends StopMedia RPC automatically
media.close();
room.close();
}

main();

Sending Audio - Low-Level API

For full control over audio transmission, use the low-level API:

import odin from '@4players/odin-nodejs';
const { OdinClient } = odin;
import { encode } from '@msgpack/msgpack';

async function main() {
const client = new OdinClient();
const token = client.generateToken(accessKey, roomId, userId);
const room = client.createRoom(token);

room.onJoined(async (event) => {
// 1. Get media ID from the event
const mediaId = event.mediaIds[0];

// 2. Create audio stream
const media = room.createAudioStream(48000, 2);

// 3. Set the server-assigned media ID
media.setMediaId(mediaId);

// 4. Send StartMedia RPC to notify server
const rpc = encode([0, 1, "StartMedia", {
media_id: mediaId,
properties: { kind: "audio" }
}]);
room.sendRpc(new Uint8Array(rpc));

// 5. Send audio data in 20ms chunks
const chunkDurationMs = 20;
const samplesPerChunk = Math.floor(48000 * chunkDurationMs / 1000) * 2;

// Your audio data as Float32Array (interleaved stereo, range [-1, 1])
const audioChunk = new Float32Array(samplesPerChunk);
// ... fill with audio samples ...
media.sendAudioData(audioChunk);

// 6. When done, close the stream (sends StopMedia RPC automatically)
media.close();
});

room.join("https://gateway.odin.4players.io");
}

main();
tip

ODIN requires 20ms chunks of audio data (i.e., 50 times a second). Calculate the chunk length based on the sample rate: chunkLength = sampleRate / 50.

End-to-End Encryption (E2EE)

Enable encryption for secure voice communication using OdinCipher:

import odin from '@4players/odin-nodejs';
const { OdinClient, OdinCipher } = odin;

const client = new OdinClient();
const token = client.generateToken(accessKey, roomId, userId);
const room = client.createRoom(token);

// Create and configure cipher
const cipher = new OdinCipher();
cipher.setPassword(new TextEncoder().encode("shared-secret-password"));

// Apply cipher to room (must be done before joining)
room.setCipher(cipher);

room.join("https://gateway.odin.4players.io");
warning

All participants in a room must use the same cipher password to communicate. Ensure you distribute the password securely.

Verifying Peer Encryption Status

You can verify that a peer is using the same encryption as you:

room.onPeerJoined((event) => {
const status = cipher.getPeerStatus(event.peerId);
console.log(`Peer ${event.peerId} encryption: ${status}`);
// Possible values: "encrypted", "mismatch", "unencrypted", "unknown"
});

Proximity Chat (3D Audio)

Enable distance-based audio for spatial applications like games or virtual worlds:

room.onJoined(() => {
// Set position scale (1 unit = 1 meter)
room.setPositionScale(1.0);

// Update your position (x, y, z)
room.updatePosition(10.0, 0.0, 5.0);
});

// Update position as needed (e.g., in a game loop)
function onPlayerPositionChanged(x, y, z) {
room.updatePosition(x, y, z);
}
tip

Peers are visible to each other only within a unit circle of radius 1.0. Use setPositionScale() to adapt ODIN to your coordinate system.

Connection Diagnostics

Monitor connection quality and troubleshoot issues:

room.onJoined(() => {
// Get connection identifier
const connectionId = room.getConnectionId();
console.log(`Connection ID: ${connectionId}`);

// Get detailed connection statistics
const stats = room.getConnectionStats();
if (stats) {
console.log(`RTT: ${stats.rtt.toFixed(2)} ms`);
console.log(`TX Loss: ${(stats.udpTxLoss * 100).toFixed(2)}%`);
console.log(`RX Loss: ${(stats.udpRxLoss * 100).toFixed(2)}%`);
console.log(`TX Bytes: ${stats.udpTxBytes}`);
console.log(`RX Bytes: ${stats.udpRxBytes}`);
console.log(`Congestion Events: ${stats.congestionEvents}`);
}
});

// Get jitter statistics for an audio stream
room.onMediaStarted((event) => {
const jitterStats = room.getJitterStats(event.media.id);
if (jitterStats) {
console.log(`Packets Total: ${jitterStats.packetsTotal}`);
console.log(`Packets Lost: ${jitterStats.packetsLost}`);
console.log(`Packets Too Late: ${jitterStats.packetsArrivedTooLate}`);
}
});

Audio Recording Example

Record audio from peers to WAV files:

import odin from '@4players/odin-nodejs';
import wav from 'wav';

const { OdinClient } = odin;

const accessKey = "__YOUR_ACCESS_KEY__";
const roomId = "my-room";
const userId = "RecorderBot";

const recordings = {};

async function main() {
const client = new OdinClient();
const token = client.generateToken(accessKey, roomId, userId);
const room = client.createRoom(token);

room.onAudioDataReceived((data) => {
const { mediaId, peerId, samples16 } = data;

// Create recording file if needed
if (!recordings[mediaId]) {
recordings[mediaId] = new wav.FileWriter(`recording_${peerId}.wav`, {
channels: 2,
sampleRate: 48000,
bitDepth: 16
});
}

// Write audio samples
const buffer = Buffer.from(samples16.buffer, samples16.byteOffset, samples16.byteLength);
recordings[mediaId].write(buffer);
});

room.onMediaStopped((event) => {
if (recordings[event.mediaId]) {
recordings[event.mediaId].end();
delete recordings[event.mediaId];
}
});

room.join("https://gateway.odin.4players.io");
}

main();

API Quick Reference

OdinClient

MethodDescription
generateToken(accessKey, roomId, userId)Generate a room token locally
createRoom(token)Create a room instance (recommended)
createRoomWithToken(token)Alias for createRoom

OdinRoom

MethodDescription
join(gateway, userData?)Connect to the room
close()Disconnect from the room
sendMessage(data, peerIds?)Send a message to peers
updatePosition(x, y, z)Update 3D position
setPositionScale(scale)Set position scale factor
setCipher(cipher)Enable E2EE
createAudioStream(sampleRate, channels)Create audio output stream
getConnectionId()Get connection identifier
getConnectionStats()Get connection quality metrics
getJitterStats(mediaId)Get audio jitter metrics

OdinRoom Properties

PropertyTypeDescription
ownPeerIdnumberYour peer ID
connectedbooleanConnection status
availableMediaIdsnumber[]Available media IDs for audio streams

OdinMedia

MethodDescription
sendMP3(filePath)Stream an MP3 file (high-level)
sendWAV(filePath)Stream a WAV file (high-level)
sendBuffer(audioBuffer)Stream AudioBuffer (high-level)
setMediaId(mediaId)Set server-assigned media ID
close()Stop transmission (sends StopMedia RPC) and release resources
sendAudioData(samples)Send raw audio samples (low-level)

OdinCipher

MethodDescription
setPassword(password)Set encryption password
getPeerStatus(peerId)Get peer's encryption status

Events

EventPayload
ConnectionStateChanged{ state, message }
Joined{ roomId, ownPeerId, room, mediaIds }
Left{ reason }
PeerJoined{ peerId, userId, userData, peer }
PeerLeft{ peerId }
MediaStarted{ peerId, media }
MediaStopped{ peerId, mediaId }
MediaActivity{ peerId, mediaId, state }
MessageReceived{ senderPeerId, message }
AudioDataReceived{ peerId, mediaId, samples16, samples32 }

Examples

We have prepared examples to get you started with the ODIN Node.js SDK:

You can also find additional examples in the tests/ folder of the SDK repository.

Troubleshooting

Build Errors

If you encounter build errors, ensure you have the required tools:

# macOS
xcode-select --install

# Ubuntu/Debian
sudo apt-get install build-essential python3

# Windows
npm install --global windows-build-tools

macOS Security Warnings

If you see "code signature not valid" errors:

cd node_modules/@4players/odin-nodejs/build/Debug
xattr -cr *.dylib
codesign -f -s - *.dylib

Connection Issues

  1. Verify your access key is correct
  2. Check your network allows WebSocket connections
  3. Ensure the token hasn't expired

Lifecycle Best Practices

When building long-running applications (like NestJS servers, recording bots, or AI assistants), follow these patterns:

Singleton OdinClient Pattern

Create one OdinClient instance for the entire process lifetime:

class OdinClientService {
constructor() {
// Create once at service initialization
this.client = new OdinClient();
this.rooms = new Map();
}

async joinRoom(sessionId, token, gateway) {
const room = this.client.createRoom(token);
// Set up event handlers...
room.join(gateway);
this.rooms.set(sessionId, { room });
}

async leaveRoom(sessionId) {
const instance = this.rooms.get(sessionId);
if (instance) {
instance.room.close(); // Clean shutdown
this.rooms.delete(sessionId);
}
}
}

Proper Audio Stream Cleanup

Always close audio streams when done:

// Play announcement
const media = room.createAudioStream(44100, 2);
await media.sendMP3('./announcement.mp3');
media.close(); // ← Important! Releases resources and notifies server

Centralized Cleanup with onLeft

Use onLeft for unified cleanup (handles both normal and error cases):

room.onLeft((event) => {
console.log(`Left room. Reason: ${event.reason}`);
// Clean up your own resources
});

// This will trigger the onLeft handler
room.close();
tip

The onLeft event is emitted for both server-initiated disconnects (e.g., timeout, kick) and client-initiated disconnects (room.close()). Use it for centralized cleanup logic.

Support