Voice chat
CrowdyJS sends proximity voice the same way it sends webcam video:
opaque packets over the GraphQL UDP proxy (or the binary relay), fanned out by
chunk + distance. There is no WebRTC session and the platform never decodes
the audio. Your game captures, encodes, and plays.
Permission
sendAudioPacket needs use_voice_chat on the sender's access tier and
on the grid under the target chunk. A new app's default tier already carries
this key. A send without it is refused with UNAUTHORIZED (error code 7) on
the genericError / errors lane.
Sending: udp.sendAudioPacket
The recommended browser payload (used by The Construct and Blocks with Friends):
- G.711 µ-law
- 8 kHz mono
- ~60 ms frames (480 bytes — well inside the spatial payload budget)
- wrapping
sequenceNumber(uint8) so receivers drop late or duplicate frames
await game.udp.sendAudioPacket({
appId,
chunk: session.self.chunk!,
uuid: session.self.uuid,
audioData: base64MuLawFrame, // 480 bytes µ-law
distance: 1,
sequenceNumber: (seq = (seq + 1) & 0xff),
});
There is no success echo. Prefer the fire-and-forget send; a refusal arrives
as a correlated GenericErrorResponse. Enable realtime.binaryTransport: true
when gameClientBootstrap.binaryRelayEnabled is true so voice (and video)
does not ride one GraphQL mutation per frame.
Receiving: handlers.audio
game.udp.subscribe({
audio: (n) => {
if (n.uuid === session.self.uuid) return;
playMuLaw(n.uuid, decodeBase64(n.audioData), n.sequenceNumber);
},
actorLeft: (n) => forgetSpeaker(n.uuid),
}, appId);
Drop duplicate or stale sequenceNumbers (an 8-bit wrap; a long gap is a new
utterance). Release per-speaker Web Audio nodes on actorLeft.
References
- The Construct starter:
src/platform/media/VoiceService.ts - Blocks with Friends:
src/audio/VoiceChat.ts - Unreal: Voice chat
- Webcam video — the same spatial fan-out, for JPEG/WebP frames