Skip to content
intelligentwolfPublic

About

Spatial WebRTC voice backend for OpenSimulator — a per-listener spatial mix that Firestorm and browser viewers both reach. Plugs into the os-webrtc-janus addon; no Janus gateway needed.

Topics

Resources

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Latest commit

 

History

15 Commits

Folders and files

Repository files navigation

wolfvoice

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)

What you need

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.js is a working reference implementation, extracted from WolfStorm.

Why a per-listener mix

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.

Quick start

1. Install the voice server

curl -fsSLO https://raw.githubusercontent.com/intelligentwolf/wolfvoice/main/setup.sh
less setup.sh          # it runs as root; read it first
sudo bash setup.sh

The 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.

2. Build the region module into OpenSim

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.

  1. 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
  2. Generate the solution and build. The 0.9.3.0 source archive ships runprebuild.sh with Windows line endings and without the execute bit, so ./runprebuild.sh fails 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.sh does the same.) The module's DLLs land in bin/: WebRtcVoice.dll, WebRtcVoiceServiceModule.dll, WebRtcVoiceRegionModule.dll (and WebRtcJanusService.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.sh

If 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.

3. Create the keys

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 wolfvoice

Copy 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_key

Regions 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.

4. Configure each region

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 = false

RpcKeyFile 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.

5. Allow voice on the estate and the parcel

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

6. Check 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).

If it does not work

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.

Documentation

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

Capacity

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.

Building from source

Needs a Rust toolchain and cmake (the opus crate compiles libopus from source).

cargo build --release
cargo test              # 75 tests, no network required

Release 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.

Testing it without a viewer

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

Run 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.

Known limitations

  • 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=always covers 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.

Credits

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.

Licence

Apache License 2.0 — see LICENSE.

About

Spatial WebRTC voice backend for OpenSimulator — a per-listener spatial mix that Firestorm and browser viewers both reach. Plugs into the os-webrtc-janus addon; no Janus gateway needed.

Topics

Resources

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages