Voice Receive (Relay)
Receive real-time audio from Discord voice channels via NodeLink.
Voice Receive (Relay)
NodeLink supports an experimental feature that allows you to receive and relay audio frames from Discord voice channels to your application via a binary WebSocket.
Experimental Feature
This feature is currently experimental and may undergo breaking changes in the protocol. Use with caution in production.
Configuration
To enable voice receive, you must configure it in your config.js or via environment variables.
// config.js
voiceReceive: {
enabled: true,
format: "opus" // "opus" or "pcm_s16le" (48kHz, Stereo)
}- format:
opussends raw opus frames (low bandwidth).pcm_s16ledecodes audio on the server and sends 16-bit Little Endian PCM (high bandwidth).
Connecting
Voice receive uses a separate WebSocket endpoint:
WS /v4/websocket/voice/{guildId}
You must provide the same Authorization and User-Id headers as the main WebSocket.
Binary Protocol
The Voice Relay uses a custom binary framing to minimize overhead. Unlike the main WebSocket, frames are sent as raw binary data, not JSON.
Frame Structure
Each packet consists of a header followed by the audio payload.
| Offset | Length | Type | Description |
|---|---|---|---|
| 0 | 1 | UInt8 | Opcode: 1 (Start), 2 (Stop), 3 (Data) |
| 1 | 1 | UInt8 | Format: 0 (Opus), 2 (PCM) |
| 2 | 1 | UInt8 | Guild ID Length (L1) |
| 3 | L1 | String | Guild ID |
| 3+L1 | 1 | UInt8 | User ID Length (L2) |
| 4+L1 | L2 | String | User ID |
| 4+L1+L2 | 4 | UInt32BE | SSRC (Discord Sync Source) |
| 8+L1+L2 | 4 | UInt32BE | Timestamp |
| 12+L1+L2 | Var | Binary | Payload (Audio Data) |
Opcodes
- 1 (Start): Sent when a user starts speaking. Payload is empty.
- 2 (Stop): Sent when a user stops speaking. Payload is empty.
- 3 (Data): Sent for every audio frame received.
Implementation Example (Node.js)
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:3000/v4/websocket/voice/123456789', {
headers: {
'Authorization': 'youshallnotpass',
'User-Id': '987654321',
'Client-Name': 'MyBot/1.0.0'
}
});
ws.on('message', (data) => {
if (!(data instanceof Buffer)) return;
const op = data.readUInt8(0);
const format = data.readUInt8(1);
let offset = 2;
const guildLen = data.readUInt8(offset++);
const guildId = data.toString('utf8', offset, offset + guildLen);
offset += guildLen;
const userLen = data.readUInt8(offset++);
const userId = data.toString('utf8', offset, offset + userLen);
offset += userLen;
const ssrc = data.readUInt32BE(offset);
offset += 4;
const timestamp = data.readUInt32BE(offset);
offset += 4;
const payload = data.slice(offset);
if (op === 3) {
// Process audio payload
console.log(`Received ${payload.length} bytes from ${userId}`);
}
});