By Shadow Wolf and Lone Wolf, Wolf Territories Grid and IntelligentWolf Ltd.
Spatial WebRTC voice for OpenSimulator — one service that Firestorm and browser-based viewers both reach through the region's own capabilities, so users of different viewers hear each other, positionally.
wolfvoice is a voice service backend for os-webrtc-janus. That addon does the OpenSimulator half — capabilities, provider advertisement, session bookkeeping — and wolfvoice is an alternative to point it at when you want the mixing done differently.
Firestorm 7.1.10+ / browser viewer
│ ProvisionVoiceAccountRequest + VoiceSignalingRequest (LLSD caps)
▼
OpenSim region + os-webrtc-janus addon ← REQUIRED
│ forwards the caps onward as JSON-RPC
▼
wolfvoice ── answers the SDP, terminates DTLS/SRTP/SCTP,
│ mixes a SEPARATE stream per listener
▼
viewers (UDP media)
os-webrtc-janus is a hard requirement. It supplies WebRtcVoice.dll and the
region-side capability handlers that everything here depends on; without it there is
nothing for a viewer to talk to. See step 2 for building it.
New in 0.2.0 (see CHANGELOG.md): a relay key and a separate credential
key, private calls and group voice with signed tickets, positions held to where the
region says the avatar is, room caps, an ICE candidate filter, and a health endpoint that
says only ok without the key. Use it with the hardened region module from step 2.
What you can skip is running a Janus gateway server alongside it. The addon ships
two backends: WebRtcJanusService (which drives Janus) and
WebRtcVoiceServiceConnector (a JSON-RPC client that will post to any URL). wolfvoice
plugs into the second, so you point the addon at wolfvoice instead of at Janus.
For the voice server
- A Linux host with a public IP address and a DNS name pointing at it. TLS is not optional: the SDP carries the server's DTLS fingerprint, so anyone able to rewrite plaintext signalling in flight could become the media endpoint.
- UDP 40000–40999 open to the world. This range is also the ceiling on concurrent sessions — one socket per peer connection.
- TCP 9443 reachable from your region hosts only. From 0.2.0 every region request must also carry the shared relay key, and room tickets are signed with a second key - see docs/SECURITY.md.
- Modest CPU: ~17 ms per listener per second, spread across all cores. See Capacity.
For viewers
- Firestorm 7.1.10 or newer works with no configuration at all — the region tells it which voice system to use. Nothing to install, nothing to tick.
- Browser viewers need a client speaking the same contract.
client/voice_llwebrtc.jsis a working reference implementation, extracted from WolfStorm.
Linden Lab's WebRTC voice puts spatialisation, per-user gain and mute on the server: the viewer sends its position, gain and mute requests up a data channel and receives one already-mixed audio stream plus a participant roster down it.
wolfvoice therefore computes a distinct mix for every listener. If A is east of B, A hears B on their right while B simultaneously hears A on their left — which one shared room mix cannot express, however it is panned.
Janus's AudioBridge plugin is a conference mixer: it produces a single mix for a room,
and it does not carry WebRTC data channels (no incoming_data callback, and Janus's
SDP helper declines an m=application line no plugin has claimed). Those are perfectly
reasonable choices for what AudioBridge is for, but they mean the parts of LL's
protocol that ride the data channel — position, per-user gain, mute, the speaker
roster — have nowhere to travel, and the viewer will not consider voice ready until
that channel opens. wolfvoice accepts the data channel and does the mixing itself,
which is the whole reason it exists.
If you want a straightforward conference-style voice service, os-webrtc-janus with Janus is a simpler deployment and the sensible default. Choose wolfvoice when you want positional audio.
curl -fsSLO https://raw.githubusercontent.com/intelligentwolf/wolfvoice/main/setup.sh
less setup.sh # it runs as root; read it first
sudo bash setup.shThe installer detects your platform, downloads and checksums the matching release
binary, creates an unprivileged service user, obtains a TLS certificate with certbot
(including renewal hooks), creates the relay and credential keys in /etc/wolfvoice/
on a fresh install, and installs a hardened systemd unit.
wolfvoice needs the region half of WebRTC voice: the os-webrtc-janus region module.
Use the hardened version on branch chatsession-p2p-session-id-and-fast-fail of
intelligentwolf/os-webrtc-janus -
it is the one wolfvoice 0.2.0 is tested against, and the only one that sends the relay
key, signs call tickets and feeds avatar positions. It needs a .NET 8 OpenSimulator
(0.9.3.0 or newer); it does not build on 0.9.2.x.
First find out which tree you have, from the OpenSim root:
ls OpenSim/Addons/os-webrtc-janus 2>/dev/null && echo "B: upstream master, module built in"
ls Addons/os-webrtc-janus 2>/dev/null && echo "C: OpenSim-NGC"
ls prebuild.xml 2>/dev/null && echo "prebuild tree (A or B)"A. OpenSimulator 0.9.3.x release, or a git tree from before 26 Feb 2026
(no OpenSim/Addons/os-webrtc-janus). Tested with the opensim-0.9.3.0-source.tar.gz
release.
-
Add the module as an addon-module:
cd opensim-0.9.3.0-source # your OpenSim root git clone -b chatsession-p2p-session-id-and-fast-fail \ https://github.com/intelligentwolf/os-webrtc-janus.git addon-modules/os-webrtc-janus
-
Generate the solution and build. The 0.9.3.0 source archive ships
runprebuild.shwith Windows line endings and without the execute bit, so./runprebuild.shfails there; these are the commands it runs:cp bin/System.Drawing.Common.dll.linux bin/System.Drawing.Common.dll dotnet bin/prebuild.dll /target vs2022 /targetframework net8_0 /excludedir = "obj | bin" /file prebuild.xml dotnet build -c Release OpenSim.sln(In a git checkout
./runprebuild.sh && ./compile.shdoes the same.) The module's DLLs land inbin/:WebRtcVoice.dll,WebRtcVoiceServiceModule.dll,WebRtcVoiceRegionModule.dll(andWebRtcJanusService.dll, unused with wolfvoice).
B. Upstream OpenSimulator master since 26 Feb 2026 (OpenSim/Addons/os-webrtc-janus
exists). The module is built in. Do not clone a second copy into addon-modules/ -
two projects with the same names break the build. Replace the built-in sources with the
hardened ones instead, then build as usual:
git clone -b chatsession-p2p-session-id-and-fast-fail \
https://github.com/intelligentwolf/os-webrtc-janus.git ../os-webrtc-janus-wolf
for d in Janus WebRtcVoice WebRtcVoiceRegionModule WebRtcVoiceServiceModule; do
rm -f OpenSim/Addons/os-webrtc-janus/$d/*.cs
cp ../os-webrtc-janus-wolf/$d/*.cs OpenSim/Addons/os-webrtc-janus/$d/
done
./runprebuild.sh && ./compile.shIf you keep the built-in module unchanged instead, wolfvoice still gives spatial voice, but only while wolfvoice has no relay key (the built-in module sends none, so a keyed wolfvoice answers every request "not authorised"), there are no private calls or group voice, and TCP 9443 is protected by your firewall alone.
C. OpenSim-NGC (Addons/os-webrtc-janus). NGC carries its own, much-extended
version of the module. Do not copy this one into it; we have not tested NGC's module
with wolfvoice.
D. A binary-only distribution (just bin/, no sources): build in a source tree of
the exact same OpenSim version with A or B, then copy the three WebRtc*.dll files
listed above into the distribution's bin/, replacing any that are there.
The modules are loaded when OpenSim starts, so restart the region after installing them. Nothing is needed on the Robust (grid) server: regions talk to wolfvoice directly.
On the voice server (setup.sh does this on a fresh install; when it upgrades an
existing one it leaves this to you, so regions without keys keep working until you are ready):
sudo sh -c 'umask 027; openssl rand -hex 32 > /etc/wolfvoice/rpc_key; openssl rand -hex 32 > /etc/wolfvoice/cred_key'
sudo chgrp wolfvoice /etc/wolfvoice/rpc_key /etc/wolfvoice/cred_key
sudo systemctl restart wolfvoiceCopy both files to every region host, readable only by the user OpenSim runs as:
sudo install -d -m 0750 -o opensim -g opensim /etc/opensim # your OpenSim user
sudo install -m 0400 -o opensim -g opensim rpc_key /etc/opensim/voice_rpc_key
sudo install -m 0400 -o opensim -g opensim cred_key /etc/opensim/voice_cred_keyRegions re-read these files when they change (within a minute); wolfvoice reads its keys when it starts. What each key does is in docs/SECURITY.md.
Create bin/config/wolfvoice.ini in each region's OpenSim directory (a copy of
contrib/wolfvoice.ini). OpenSim reads every .ini in
bin/config/ after OpenSim.ini, as overrides, so a shared OpenSim.ini stays
untouched.
[WebRtcVoice]
Enabled = true
SpatialVoiceService = WebRtcVoice.dll:WebRtcVoiceServiceConnector
NonSpatialVoiceService = WebRtcVoice.dll:WebRtcVoiceServiceConnector
; The host name on wolfvoice's certificate - not an IP address. The certificate is
; always verified (chain and name), whatever NoVerifyCertChain says.
WebRtcVoiceServerURI = https://voice.example.org:9443
RpcKeyFile = /etc/opensim/voice_rpc_key
CredKeyFile = /etc/opensim/voice_cred_key
MessageDetails = false
[VivoxVoice]
enabled = falseRpcKeyFile and CredKeyFile above are the defaults, so they can be left out if you
use those paths. Other settings, all optional: PositionFeed (default true),
AcceptLegacyCallCredentials (default true, see docs/SECURITY.md),
StunServers (STUN servers given to the viewer; default Google's public ones).
In a simulator that runs several regions, [WebRtcVoice] applies to all of them.
Restart the region. When it works, the region log shows:
[WEBRTC VOICE SERVICE CONNECTOR]: WebRtcVoiceServiceConnector enabled
[WEBRTC VOICE SERVICE MODULE]: WebRtcVoiceService enabled
[REGION WEBRTC VOICE]: avatar positions go to the voice server once a second while anyone here is in voice
[REGION WEBRTC VOICE]: no ConferenceApiUrl - conference chats and Wolf Chat voice rooms are off (group voice is not affected)
[REGION WEBRTC VOICE]: enabled
[WEBRTC VOICE SERVICE MODULE]: Adding WebRtcVoiceService to region <your region>
The ConferenceApiUrl line is normal: ad-hoc conferences and Wolf Chat rooms are
optional Wolf Territories features that need a service of their own. If you also see
no voice relay key - private voice calls are off, the region cannot read
RpcKeyFile.
Both are required, and this is the most common reason for "nothing happens":
- Region/Estate → Estate → Allow Voice Chat
- About Land → Sound → Allow Voice Chat
With either unset the viewer decides locally that voice is unavailable and never asks the region for anything, so nothing appears in any log. The region also refuses voice itself on such a parcel (and to avatars banned from it).
# from a region host
curl https://voice.example.org:9443/
# ok
curl -H "X-Wolfvoice-Key: $(sudo cat /etc/opensim/voice_rpc_key)" https://voice.example.org:9443/
# {"bridges":0,"npcs":0,"pending":0,"rooms":0,"service":"wolfvoice","sessions":0}Then talk in-world: wolfvoice logs each session it opens (journalctl -u wolfvoice -f).
| You see | Cause and fix |
|---|---|
| Nothing in either log when someone opens voice | Voice is off on the estate or parcel (step 5), or the viewer is older than Firestorm 7.1.10. |
Region log: [WOLFVOICE HTTP] refused the voice server's TLS certificate |
WebRtcVoiceServerURI uses an IP or a name not on the certificate, or the certificate is self-signed or expired. Use the certificate's host name. |
wolfvoice log: refused: wrong or missing relay key |
The region's RpcKeyFile differs from wolfvoice's rpc_key, is not readable by OpenSim, or the region runs a module without keys (built-in master module, older addon). |
Region log: no voice relay key - private voice calls are off |
The region cannot read RpcKeyFile. Spatial voice works only if wolfvoice has no key either. |
| Text IMs time out ("session initialization is timed out") | The region runs an addon without the ChatSessionRequest fix. Install the branch above (the fix is also in upstream OpenSimulator as opensim/opensim#69). |
| Build: duplicate project names | Two copies of the module: one in OpenSim/Addons/ and one in addon-modules/. Use route B. |
./runprebuild.sh: Permission denied or $'\r': command not found |
The 0.9.3.0 source archive; run the prebuild command directly (route A, step 2). |
| "voice room is full" / channel full | A private call holds 2, a conference 25, a group or Wolf Chat room 64. |
More in docs/TROUBLESHOOTING.md.
| docs/SERVER.md | Installing, running and operating the service; TLS; firewall; capacity |
| docs/OPENSIM.md | The addon, region configuration, estate/parcel flags, rolling out to many regions |
| docs/SECURITY.md | The relay and credential keys, changing over to two keys, and what the region and wolfvoice enforce |
| docs/CLIENT.md | Viewer support, and writing your own client |
| docs/PROTOCOL.md | The complete wire contract, with citations into the Firestorm and OpenSim sources |
| docs/TROUBLESHOOTING.md | Every failure we hit, and how to tell them apart |
Measured with cargo run --release --example load_test on a 12-core VM:
| Clients | Speaking | Service CPU | Per listener | Frames delivered |
|---|---|---|---|---|
| 10 | 5 | 12 % of one core | 12.3 ms/s | all |
| 40 | 20 | 57 % of one core | 14.2 ms/s | all |
| 120 | 60 | 203 % of one core | 16.9 ms/s | all, no ticks dropped |
Cost is roughly linear in listeners — the per-listener Opus encode dominates and the O(N²) frame summing is comparatively free.
The mixer parallelises on both available axes: each room's tick is its own task, and each listener within a room is its own task too. That second axis is the one that matters, because the interesting case is a single busy region. The 120-listener run above is the proof: 203 % of one core means the work genuinely spans cores, and every frame still arrived.
So the first limit you will now meet is the media port range — 1000 concurrent
sessions, not CPU (12 cores at ~17 ms per listener is on the order of 700
listeners before saturation). Widen MEDIA_PORT_LO/HI and your firewall together
if you need more.
If the host ever cannot keep up, a whole 20 ms tick is skipped rather than queued —
audible, but bounded — and the service logs mixer overloaded with a count.
Needs a Rust toolchain and cmake (the opus crate compiles libopus from source).
cargo build --release
cargo test # 75 tests, no network requiredRelease binaries for Linux (x86_64 and aarch64, static musl and glibc) are built by GitHub Actions - see .github/workflows/release.yml, which also explains why macOS and Windows are build-from-source only.
# The harnesses present the relay key and sign tickets the way the region does.
export WOLFVOICE_RPC_KEY="$(sudo cat /etc/wolfvoice/rpc_key)"
export WOLFVOICE_CRED_KEY="$(sudo cat /etc/wolfvoice/cred_key)"
# Correctness: two synthetic viewers, real SDP, real Opus, real mixing.
cargo run --release --example two_clients -- https://127.0.0.1:9443
# -> AUDIO_MIXED=true ROSTER_DELIVERED=true LEAVE_NOTIFIED=true / PASS
# Firestorm's exact behaviour (spatial, calls, group rooms, hardening): 34 checks.
cargo run --release --example fs_compat -- https://127.0.0.1:9443
# Multi-party rooms: join order, rejoin, late joiner: 14 checks.
cargo run --release --example room_e2e -- https://127.0.0.1:9443
# Capacity: N clients, M speaking, reports service CPU.
cargo run --release --example load_test -- https://127.0.0.1:9443 40 20Run these on the voice host (the JSON-RPC port is firewalled to region hosts, but loopback is permitted). Both harnesses share the server's own WebRTC crate, so they cannot catch a shared misreading of the wire format — a real viewer remains the final test.
- No TURN, and no way to add one. Firestorm hardcodes its STUN servers to
stun:stunN.<grid>.secondlife.io, which do not resolve outside Second Life. Media still connects, because the server advertises a routable host candidate and learns the viewer's address from the connectivity check — but a user whose network blocks outbound UDP cannot use voice at all. The client is told, rather than failing silently. - Single instance. If wolfvoice is down, voice is down everywhere it is
configured.
Restart=alwayscovers a crash, not a host failure. - Private calls and group voice (
channel_type: multiagent) need the hardened region module, and have had far less exercise than spatial voice.
By Shadow Wolf and Lone Wolf, Wolf Territories Grid and IntelligentWolf Ltd (formerly Wolf Software Systems Ltd), for the Wolf Territories Grid, and released for the wider OpenSimulator community.
Optional Wolf Territories extras are in the code but off unless you configure them,
and need no Wolf service to stay off: story/NPC voices (WOLFVOICE_NPC_HOSTS), a
Discord voice bridge (a bridge key; needs a bridge client that is not part of this
repository) and, in the region module, ad-hoc conferences and Wolf Chat voice rooms
(ConferenceApiUrl).
wolfvoice stands on os-webrtc-janus by Robert Adams, which brought WebRTC voice to OpenSimulator and supplies the region-side capability handlers, provider advertisement and session plumbing this service depends on. wolfvoice is only a backend for it; the hard integration work with OpenSimulator is his.
The wire protocol is Linden Lab's, as implemented in Firestorm.
Apache License 2.0 — see LICENSE.