Discord DAVE support for Node.js and Bun voice libraries.
dave.ts handles DAVE messages and protects Opus audio. It also includes
helpers for Voice Gateway payloads, UDP discovery, RTP packets, and transport
encryption. Your voice library keeps control of sockets, timing, audio codecs,
and SSRC mapping.
The library is experimental. It still needs live Discord testing and an independent security review.
npm install dave.tspnpm add dave.tsbun add dave.tsNode.js 20.19 or newer, or Bun 1.3.14 or newer, is required. The package is ESM-only.
Create one DAVE connection for each Voice Gateway connection.
import { createDaveVoiceConnection } from "dave.ts";
const dave = await createDaveVoiceConnection({
channelId,
selfUserId,
sendGatewayMessage(message) {
voiceSocket.send(message);
},
});
voiceSocket.binaryType = "arraybuffer";
const { dispatch, notices } = await dave.handleGatewayMessage(message);
handleVoiceDispatch(dispatch);
handleDaveNotices(notices);Pass every Voice Gateway text or binary message to
handleGatewayMessage(). The method sends required DAVE replies before it
resolves.
lastSequence holds the latest Voice Gateway v8 sequence. Use it in Heartbeat
and Resume payloads.
protocolVersion is undefined during negotiation. It becomes 0 for
passthrough audio or 1 for protected audio after the transition completes.
Call await dave.close() when the Voice Gateway connection closes.
Call transformOpus() after Opus encoding and before RTP packet creation.
const result = dave.transformOpus({
direction: "send",
frame: encodedOpusPacket,
});
if (result.ok) {
sendProtectedOpus(result.frame);
}Run received audio through DAVE before Opus decoding.
const result = dave.transformOpus({
direction: "receive",
frame: protectedOpusPacket,
senderId: userIdForSsrc(ssrc),
});
if (result.ok) {
opusDecoder.decode(result.frame);
}Failed media returns a typed drop result.
Create one transport cryptor from the Session Description mode and secret key. Keep it for the life of that key.
import { createVoiceRtpCryptor } from "dave.ts";
const transport = createVoiceRtpCryptor({
mode,
secretKey,
});
const packet = transport.encryptRtpPacket({
payload: protectedOpusPacket,
sequence,
ssrc,
timestamp,
});
udpSocket.send(packet);
const received = transport.decryptRtpPacket(inboundPacket);
if (received.ok) {
receiveProtectedOpus(received.payload, received.ssrc);
}Both current Discord modes are supported.
aead_aes256_gcm_rtpsizeaead_xchacha20_poly1305_rtpsize
Call transport.close() when the key expires.
encodeOpusRtpPacket() creates a basic RTP packet with payload type 0x78.
decodeRtpPacket() splits an RTP packet at the unencrypted rtpsize prefix.
After decrypting the body yourself, use extractDecryptedRtpPayload() to
remove RTP extension data and padding.
import {
DAVE_MAX_PROTOCOL_VERSION,
encodeVoiceGatewayClientMessage,
} from "dave.ts";
voiceSocket.send(
encodeVoiceGatewayClientMessage({
kind: "identify",
maxDaveProtocolVersion: DAVE_MAX_PROTOCOL_VERSION,
selfUserId,
serverId,
sessionId,
token,
}),
);
voiceSocket.send(
encodeVoiceGatewayClientMessage({
kind: "heartbeat",
nonce: Date.now(),
sequenceAck: dave.lastSequence,
}),
);Identify, Select Protocol, Resume, Heartbeat, and Speaking are supported.
import {
decodeIpDiscoveryResponse,
encodeIpDiscoveryRequest,
} from "dave.ts";
udpSocket.send(encodeIpDiscoveryRequest(ssrc));
const { address, port } = decodeIpDiscoveryResponse(responsePacket);Use createDaveSession() if your library already handles Voice Gateway
envelopes and ordered writes. Most integrations should use
createDaveVoiceConnection().
Setup and lifecycle failures use DaveError with a stable code. Packet and
payload validation failures throw RangeError. Expected media and transport
decryption failures return typed drop results.
bun install
bun run check
bun run buildMIT