# ODIN Node.js SDK

[![npm version](https://img.shields.io/npm/v/@4players/odin-nodejs.svg)](https://www.npmjs.com/package/@4players/odin-nodejs)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Discord](https://img.shields.io/discord/XXXXXX?label=Discord&logo=discord)](https://4np.de/discord)

Native Node.js bindings for the [ODIN Voice SDK](https://github.com/4Players/odin-sdk). Build powerful voice chat applications, recording bots, AI integrations, and real-time audio processing tools.

📖 **[Full Documentation](https://docs.4players.io/voice/nodejs/)** | 💬 **[Discord Community](https://4np.de/discord)** | 🎮 **[4Players ODIN](https://www.4players.io/odin)**

---

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

---

## Installation

```bash
npm install @4players/odin-nodejs
```

### Prerequisites

This SDK includes prebuilt binaries for:
- **macOS** (x86_64 and arm64)
- **Windows** (x86_64)
- **Linux** (x86_64)

For other platforms, you'll need a C++ compiler. See [node-gyp requirements](https://github.com/nodejs/node-gyp#installation).

---

## Quick Start

### 1. Get Your Access Key

Sign up at [4Players ODIN](https://www.4players.io/odin) to get your free access key.

### 2. Basic Connection Example

```javascript
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();
```

---

## Docker Usage

The repository includes a ready-to-use `Dockerfile` that shows how to run the SDK inside a Linux/amd64 container. It installs `@4players/odin-nodejs`, is configured for the official ODIN gateway, and runs a lightweight connection test. No manual library-path tweaks are required—the Linux prebuild now embeds `$ORIGIN` so the bundled `libodin*.so` files load automatically.

Build the example image:

```bash
docker build --platform=linux/amd64 -t odin-nodejs-docker-example .
```

Run it by passing your access key (and optionally a room ID, user ID, or custom gateway):

```bash
docker run --rm --platform=linux/amd64 \
  -e ODIN_ACCESS_KEY="ATPClAXgmBgY1ryDk/kTC2Yhitf4fJSx95jpN3F9Xac3" \
  -e ODIN_ROOM_ID="my-room" \
  odin-nodejs-docker-example
```

Environment variables supported by the example:

- `ODIN_ACCESS_KEY` *(required)* – access key used to mint tokens.
- `ODIN_ROOM_ID` *(optional)* – defaults to `odin-sdk-ci-test`.
- `ODIN_USER_ID` *(optional)* – auto-generates a random ID when omitted.
- `ODIN_GATEWAY` *(optional)* – defaults to `https://gateway.odin.4players.io`.
- `RUN_DURATION_MS` *(optional)* – how long to keep the connection open.

You can also use the Dockerfile as a starting point for your own services—replace the provided sample script with your application logic.

---

## Event Handlers

The SDK provides typed event handlers for easy integration:

```javascript
// 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 }
});
```

---

## Audio Recording Example

Record audio from peers to WAV files:

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

const { OdinClient } = odin;

// Configuration
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();
```

---

## Sending Audio

The SDK provides two approaches for sending audio: a **high-level API** for convenience and a **low-level API** for full control.

### High-Level API (Recommended)

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

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

// Configuration
const accessKey = "__YOUR_ACCESS_KEY__";
const roomId = "my-room";
const userId = "AudioBot";

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);

    media.close();
    room.close();
}

main();
```

### Low-Level API

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

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

// Configuration
const accessKey = "__YOUR_ACCESS_KEY__";
const roomId = "my-room";
const userId = "AudioBot";

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
        media.close();
    });

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

main();
```

See [tests/sending-audio/](tests/sending-audio/) for complete examples of both APIs.

---

## End-to-End Encryption (E2EE)

Enable encryption for secure voice communication:

```javascript
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
room.setCipher(cipher);

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

> ⚠️ All participants in a room must use the same cipher password to communicate.

### Verifying Peer Encryption Status

```javascript
// Check if a peer's encryption matches ours
const status = cipher.getPeerStatus(peerId);
console.log(`Peer ${peerId} encryption: ${status}`);
// Possible values: "encrypted", "mismatch", "unencrypted", "unknown"
```

---

## Proximity Chat (3D Audio)

Enable distance-based audio for spatial applications:

```javascript
room.onJoined(() => {
    // Set position scale (1 unit = 1 meter)
    room.setPositionScale(1.0);
    
    // Update your position
    room.updatePosition(10.0, 0.0, 5.0); // x, y, z
});
```

---

## Connection Diagnostics

Monitor connection quality and troubleshoot issues:

```javascript
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
    const jitterStats = room.getJitterStats(mediaId);
    if (jitterStats) {
        console.log(`Packets Total: ${jitterStats.packetsTotal}`);
        console.log(`Packets Lost: ${jitterStats.packetsLost}`);
        console.log(`Packets Too Late: ${jitterStats.packetsArrivedTooLate}`);
    }
});
```

---

## API Reference

### OdinClient

| Method | Description |
|--------|-------------|
| `generateToken(accessKey, roomId, userId)` | Generate a room token locally |
| `createRoom(token)` | Create a room instance (recommended) |
| `createRoomWithToken(token)` | Alias for createRoom |

### OdinRoom

| Method | Description |
|--------|-------------|
| `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

| Property | Type | Description |
|----------|------|-------------|
| `ownPeerId` | `number` | Your peer ID |
| `connected` | `boolean` | Connection status |
| `availableMediaIds` | `number[]` | Available media IDs for audio streams |

### OdinMedia (Audio Stream)

| Method | Description |
|--------|-------------|
| `setMediaId(mediaId)` | Set server-assigned media ID |
| `close()` | Release the stream |
| `sendAudioData(samples)` | Send raw audio samples |
| `sendMP3(filePath)` | Stream an MP3 file (convenience) |
| `sendWAV(filePath)` | Stream a WAV file (convenience) |
| `sendBuffer(audioBuffer)` | Stream AudioBuffer (convenience) |

### OdinCipher (E2EE)

| Method | Description |
|--------|-------------|
| `setPassword(password)` | Set encryption password |
| `getPeerStatus(peerId)` | Get peer's encryption status |

### Events

| Event | Payload |
|-------|---------|
| `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 }` |

---

## Comparison with Web SDK

| Feature | Node.js SDK | Web SDK |
|---------|-------------|---------|
| Platform | Node.js (server) | Browser |
| Performance | Native C++ | WebRTC/JavaScript |
| Raw Audio Access | ✅ Full PCM data | ⚠️ Web Audio API |
| Use Cases | Bots, recording, AI | Client apps |
| E2EE | ✅ OdinCipher | ✅ OdinCipher |

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

---

## Examples

Check the `tests/` folder for complete examples:

- **[connection-test](tests/connection-test/)** - Basic connection, events, and diagnostics
- **[audio-recording](tests/audio-recording/)** - Recording peer audio to WAV files
- **[sending-audio](tests/sending-audio/)** - Sending audio with both high-level and low-level APIs

---

## Troubleshooting

### Build Errors

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

```bash
# 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:

```bash
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

---

## Development

### Building for Other Platforms

This package includes prebuilt binaries for common platforms (macOS x64/arm64, Windows x64, Linux x64). If you need to build for a different platform or architecture, follow these steps:

#### 1. Install Build Requirements

You'll need a C++ compiler toolchain:

```bash
# macOS
xcode-select --install

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

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

#### 2. Download ODIN SDK Libraries

Download the ODIN SDK libraries from the [official releases](https://github.com/4Players/odin-sdk/releases/tag/v1.8.2):

1. Download the appropriate archive for your platform from the release assets
2. Extract the libraries to the correct location:

| Platform | Architecture | Target Directory |
|----------|--------------|------------------|
| Linux | x64 | `libs/bin/linux/x64/` |
| Linux | arm64 | `libs/bin/linux/arm64/` |
| Linux | ia32 | `libs/bin/linux/ia32/` |
| macOS | Universal | `libs/bin/macos/universal/` |
| Windows | x64 | `libs/bin/windows/x64/` |
| Windows | ia32 | `libs/bin/windows/ia32/` |

The SDK archive contains these library files:
- **Linux**: `libodin_static.a`, `libodin.so`, `libodin_crypto_static.a`, `libodin_crypto.so`
- **macOS**: `libodin.dylib`, `libodin_crypto.dylib`, `libodin_static.a`, `libodin_crypto_static.a`
- **Windows**: `odin_static.lib`, `odin.dll`, `odin_crypto_static.lib`, `odin_crypto.dll`

#### 3. Build the Native Module

```bash
# Build in debug mode
npm run build:debug

# Build in release mode
npm run build:release
```

#### 4. Verify the Build

```bash
node -e "const odin = require('./index.cjs'); console.log('ODIN SDK loaded:', !!odin.OdinClient);"
```

### Project Structure

```
├── cppsrc/           # C++ native bindings source code
├── libs/
│   ├── bin/          # ODIN SDK binaries (all platforms)
│   │   ├── linux/    # Linux binaries (x64, arm64, ia32)
│   │   ├── macos/    # macOS binaries (arm64, x64, universal)
│   │   └── windows/  # Windows binaries (x64, ia32)
│   └── include/      # ODIN SDK headers (odin.h, odin_crypto.h)
├── index.cjs         # JavaScript wrapper
├── *.d.ts            # TypeScript type definitions
└── tests/            # Example scripts
```

---

## Contributing

We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Submit a pull request

---

## Support

- 📖 **Documentation**: [docs.4players.io](https://docs.4players.io/voice/nodejs/)
- 💬 **Discord**: [Join our community](https://4np.de/discord)
- 📧 **Email**: support@4players.io
- 🐛 **Issues**: [GitHub Issues](https://github.com/4Players/odin-nodejs/issues)

---

## License

MIT License - see [LICENSE](LICENSE) for details.

---

<p align="center">
  Made with ❤️ by <a href="https://www.4players.io">4Players GmbH</a>
</p>
