📝 Blog Summary
This guide walks engineering teams through configuring FreeSWITCH as a WebRTC MCU server, covering architecture decisions (MCU vs SFU vs Mesh). Step-by-step setup across vars.xml, internal.xml, and conference.conf.xml, and a troubleshooting matrix for the most common deployment failures. It also includes an honest cost breakdown comparing self-hosting against CPaaS pricing, helping teams decide whether to build in-house or bring in specialists.
Every engineering lead chasing a self-hosted video conferencing stack eventually reaches the same fork in the road. You can pay per-minute to a CPaaS provider forever, or you can spin up your own FreeSWITCH WebRTC MCU and own the media path end-to-end. This is usually the moment teams realize they need experienced WebRTC developers in the room.
This guide is the version of that journey we wish we’d had bookmarked. It covers the architecture decision (MCU vs SFU vs Mesh). The actual internal.xml, conference.conf.xml, and vars.xml snippets you need. A troubleshooting matrix for the inevitable failures, and an honest cost breakdown so you can decide whether to build in-house or hire it out.
What is a WebRTC MCU Server?
A WebRTC MCU (Multipoint Control Unit) is a centralized media server that receives audio and video streams from every participant. It mixes them server-side into a single composite stream and sends it back to each endpoint. It trades server CPU for client bandwidth, which is exactly the trade you want for low-power devices and constrained networks.
To understand where the MCU sits in the larger picture, it helps to look at what it actually does on the wire:
- Decodes every inbound stream from every participant
- Mixes audio into a single track and composites video into a single layout
- Re-encodes the result and sends one outbound stream per participant
- Handles transcoding between codecs (Opus, G.711, VP8, H.264) when endpoints disagree
The result is a session where a participant on a budget Android phone gets the same call quality as one on a 32-core workstation, because both are sending and receiving exactly one stream.
MCU vs SFU vs Mesh: Which Architecture is Right for You?
Choosing between MCU, SFU, and Mesh is the single biggest decision in your real-time video stack. It dictates server cost, client bandwidth, scalability ceiling, and whether features like recording and live streaming are easy or painful to add.
FreeSWITCH can do MCU brilliantly and SFU adequately, so this decision also tells you whether FreeSWITCH is even the right platform.
Here is the practical decision framework, stripped of vendor marketing:
| Factor | Mesh (P2P) | SFU (Selective Forwarding) | MCU (Mixing) |
|---|---|---|---|
| Participant ceiling | 4–6 | 50–500 | 8–25 (per server) |
| Server CPU | None | Low (just routing) | High (decode + encode) |
| Client bandwidth (uplink) | N × stream | 1 × stream | 1 × stream |
| Client bandwidth (downlink) | (N-1) × stream | (N-1) × stream | 1 × stream |
| Recording complexity | Hard | Medium (per stream) | Trivial (one stream) |
| Transcoding support | None | Limited | Native |
| Best for | 1:1 calls, quick demos | Large meetings, webinars | Telehealth, contact centers, PSTN bridges, low-end devices |
| FreeSWITCH fit | Not the right tool | Workable via mod_mediabug | Native, best-in-class |
Pick MCU (and FreeSWITCH) when: You need to bridge PSTN/SIP into a WebRTC conference. Your participants are on weak networks or low-end devices. You need easy recording and live streaming, or you need legally-mandated single-track session recording.
Pick SFU (and look at Janus, Jitsi, or LiveKit) when: You’re running webinars with 50+ viewers. Your participants all have modern browsers and decent bandwidth. Per-participant CPU cost matters more than per-participant bandwidth.
Pick Mesh when: It’s a 1-on-1 product feature,e and you don’t want to run any media servers at all.
TL;DR: if “telephony bridge,” “recording,” or “low-end device” appears anywhere in your requirements, you want an MCU, and FreeSWITCH is the open-source default.
Why FreeSWITCH for WebRTC MCU?
FreeSWITCH is the most battle-tested open-source media server for MCU workloads, and that’s not a small claim. It powers carrier-grade deployments at telcos, contact centers, and telehealth platforms. They need PSTN-to-WebRTC bridging without incurring CPaaS per-minute rates.
Here is what actually matters when you’re evaluating it against alternatives:
- Native conference mixing via mod_conference, with audio mixing and video compositing built in rather than bolted on
- PSTN interop out of the box because SIP is FreeSWITCH’s first language, not an afterthought
- Codec coverage spanning Opus, G.711, G.722, VP8, H.264, and H.265, with transcoding between any pair
- WebSocket Secure (WSS) signaling is built into Sofia-SIP, so browsers connect over the same listener that handles regular SIP.
- License terms (MPL 1.1) that don’t force you to open-source your application code
- Modular architecture, so you load only what you need and skip the rest.
The trade-off you accept: FreeSWITCH expects you to know what you’re doing. The documentation is technical. The defaults are often wrong for WebRTC. Furthermore, the error messages assume you read the source.
What Do You Need Before Configuring FreeSWITCH as a WebRTC MCU?
Before you touch a single XML file, your environment needs to be in the right shape. Skipping any item on this list will cost you hours of debugging later. Most “FreeSWITCH won’t start” forum posts trace back to a missing prerequisite rather than a config bug.
Here is the baseline you need on the server:
- OS: Debian 12 or Ubuntu 22.04 LTS (most stable for FreeSWITCH 1.10.x)
- CPU: 8 vCPU minimum for ~20 mixed HD video participants; scale linearly from there
- RAM: 8 GB minimum, 16 GB recommended
- Network: Public IPv4, ports 5060/5061 (SIP), 7443 (WSS), and 16384–32768 UDP (RTP) open
- FreeSWITCH: Version 1.10.10 or later, built with mod_conference, mod_sofia , mod_verto , mod_av
- TLS certificate: Valid certificate from a public CA (Let’s Encrypt works) for your FQDN; self-signed certs will break browsers
- DNS: A record pointing your FQDN to the server’s public IP, fully propagated
You also need working knowledge of SIP signaling. An understanding of how WebRTC SDP offer/answer flows, and comfort with reading /var/log/freeswitch/freeswitch.log when things break.
How Do You Configure FreeSWITCH as a WebRTC MCU Server Step by Step?
The configuration below is what we run in production. Every snippet is annotated so you know what each line does and why it matters. Because copy-pasting XML without understanding it is how you end up having to debug.
The seven steps that follow take you from a fresh Debian install to a working MCU that a browser can connect to.
Step 1: Install FreeSWITCH
Installation is the easy part. However, using the wrong repo will leave you on an old version that misses critical WebRTC fixes.
Run these commands on a fresh Debian 12 server:
bash # Add SignalWire token (free, required since FreeSWITCH 1.10.7+) # Get yours at https://signalwire.com/freeswitch TOKEN=YOUR_SIGNALWIRE_TOKEN # Add the repo wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf chmod 600 /etc/apt/auth.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ bookworm main" \ > /etc/apt/sources.list.d/freeswitch.list # Install apt-get update apt-get install -y freeswitch-meta-all
After installation completes, verify the version with fs_cli -x "version"; anything below 1.10.10 means you pulled from the wrong repo.
Step 2: Configure vars.xml for WSS and SSL
The vars.xml file defines the global variables that all other config files inherit. This is where you tell FreeSWITCH which IP address to bind to. Where your TLS certs are located, and which WSS port to listen on.
Edit /etc/freeswitch/vars.xml and add or modify these lines:
xml
<!-- ============================================ -->
<!-- WebRTC MCU global vars - vars.xml -->
<!-- ============================================ -->
<!-- Public IP and domain - REPLACE with your values -->
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=YOUR.PUBLIC.IP.HERE"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=YOUR.PUBLIC.IP.HERE"/>
<X-PRE-PROCESS cmd="set" data="domain=conference.yourdomain.com"/>
<X-PRE-PROCESS cmd="set" data="domain_name=$${domain}"/>
<!-- WSS binding - this is what the browser connects to -->
<!-- Format: ip:port - use :: for IPv4+IPv6, or specific IP -->
<X-PRE-PROCESS cmd="set" data="wss_binding=:7443"/>
<!-- TLS for SIP over TLS (SIPS) -->
<X-PRE-PROCESS cmd="set" data="sip_tls_version=tlsv1.2,tlsv1.3"/>
<X-PRE-PROCESS cmd="set" data="sip_tls_ciphers=ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"/>
<!-- TLS certificate paths - chain order matters -->
<!-- wss.pem must be: privkey + fullchain in a single file -->
<X-PRE-PROCESS cmd="set" data="ssl_dir=/etc/freeswitch/tls"/>
<!-- Default conference profile for incoming WebRTC calls -->
<X-PRE-PROCESS cmd="set" data="default_conference_profile=video-mcu"/>
The wss_binding line is the one Google indexes; get the format right (:7443 for all interfaces, or 1.2.3.4:7443 for a specific IP), and the rest follows.
Step 3: Build the TLS Certificate Bundle
FreeSWITCH expects a single PEM file containing your private key plus the full certificate chain in a specific order. Getting the order wrong is the #1 cause of “Browser shows insecure cert” errors.
Run these commands to assemble the bundle correctly:
bash
# Assuming Let's Encrypt certs at /etc/letsencrypt/live/conference.yourdomain.com/
mkdir -p /etc/freeswitch/tls
# Order matters: privkey FIRST, then fullchain (cert + intermediates)
cat /etc/letsencrypt/live/conference.yourdomain.com/privkey.pem \
/etc/letsencrypt/live/conference.yourdomain.com/fullchain.pem \
> /etc/freeswitch/tls/wss.pem
# Same file can serve SIP-TLS
cp /etc/freeswitch/tls/wss.pem /etc/freeswitch/tls/agent.pem
cp /etc/freeswitch/tls/wss.pem /etc/freeswitch/tls/cafile.pem
# Permissions - freeswitch user must read these
chown -R freeswitch:freeswitch /etc/freeswitch/tls
chmod 600 /etc/freeswitch/tls/*.pem
If your browser still complains after a restart, run openssl s_client -connect conference.yourdomain.com:7443 -showcerts and verify that the leaf cert appears before the intermediate cert.
Step 4: Configure internal.xml for WSS Binding
The internal.xml SIP profile is where WSS is actually enabled. This is the file most tutorials get wrong because they forget that wss-binding is a profile-level param, not a global one.
Edit /etc/freeswitch/sip_profiles/internal.xml and ensure these params exist inside<settings>:
xml
<profile name="internal">
<aliases>
<alias name="default"/>
</aliases>
<gateways>
</gateways>
<domains>
<domain name="all" alias="true" parse="false"/>
</domains>
<settings>
<!-- ============================================ -->
<!-- Standard SIP params -->
<!-- ============================================ -->
<param name="user-agent-string" value="FreeSWITCH-MCU"/>
<param name="auth-calls" value="true"/>
<param name="apply-inbound-acl" value="domains"/>
<param name="local-network-acl" value="localnet.auto"/>
<!-- ============================================ -->
<!-- SIP and SIP-TLS bindings -->
<!-- ============================================ -->
<param name="sip-port" value="5060"/>
<param name="tls" value="true"/>
<param name="tls-bind-params" value="transport=tls"/>
<param name="tls-sip-port" value="5061"/>
<param name="tls-cert-dir" value="$${ssl_dir}"/>
<param name="tls-passphrase" value=""/>
<param name="tls-verify-date" value="true"/>
<param name="tls-verify-policy" value="none"/>
<param name="tls-verify-depth" value="2"/>
<param name="tls-version" value="$${sip_tls_version}"/>
<!-- ============================================ -->
<!-- WSS BINDING - this is the WebRTC entry point -->
<!-- ============================================ -->
<!-- ws-binding for plain WebSocket (dev only, NEVER in prod) -->
<!-- <param name="ws-binding" value=":7080"/> -->
<!-- wss-binding for secure WebSocket (required for browsers) -->
<param name="wss-binding" value="$${wss_binding}"/>
<!-- ============================================ -->
<!-- RTP/Media params critical for WebRTC -->
<!-- ============================================ -->
<param name="rtp-ip" value="$${local_ip_v4}"/>
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
<param name="sip-ip" value="$${local_ip_v4}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
<!-- DTLS-SRTP is mandatory for WebRTC -->
<param name="dtls-srtp" value="true"/>
<param name="rtp-secure-media" value="mandatory"/>
<param name="inbound-zrtp-passthru" value="false"/>
<!-- ICE + media handling -->
<param name="apply-candidate-acl" value="localnet.auto"/>
<param name="apply-candidate-acl" value="wan_v4.auto"/>
<param name="liberal-dtmf" value="true"/>
<param name="enable-3pcc" value="true"/>
<!-- Codec preferences - Opus first for WebRTC -->
<param name="inbound-codec-prefs" value="opus,G722,PCMU,PCMA,VP8,H264"/>
<param name="outbound-codec-prefs" value="opus,G722,PCMU,PCMA,VP8,H264"/>
<!-- NDLB flags for browser compatibility -->
<param name="NDLB-broken-auth-hash" value="true"/>
<param name="NDLB-received-in-nat-reg-contact" value="true"/>
</settings>
</profile>
The four parameters that matter most for WebRTC are wss-binding, dtls-srtp, ext-rtp-ip, and apply-candidate-acl. If any of those are wrong, calls will signal fine, but media will die silently.
Step 5: Configure conference.conf.xml for MCU Mixing
This is where the MCU actually happens. The conference.conf.xml file defines profiles that control mixing rate, video layout, recording, and codec selection for the conference bridge.
Edit/etc/freeswitch/autoload_configs/conference.conf.xml and add the video-mcu profile:
xml
<configuration name="conference.conf" description="Audio Conference">
<advertise>
<room name="video-mcu@$${domain}" status="FREE"/>
</advertise>
<profiles>
<!-- ============================================ -->
<!-- video-mcu: 720p mixed video conference -->
<!-- ============================================ -->
<profile name="video-mcu">
<!-- Audio mixing -->
<param name="domain" value="$${domain}"/>
<param name="rate" value="48000"/> <!-- Opus-friendly -->
<param name="interval" value="20"/> <!-- 20ms ptime -->
<param name="energy-level" value="100"/> <!-- VAD threshold -->
<param name="comfort-noise" value="true"/>
<param name="caller-controls" value="default"/>
<!-- Video mixing - this is what makes it an MCU, not SFU -->
<param name="video-mode" value="mux"/> <!-- mux = MCU mixing -->
<param name="video-layout-name" value="group-grid"/>
<param name="video-layout-group" value="grid"/>
<param name="video-canvas-size" value="1280x720"/>
<param name="video-canvas-bgcolor" value="#333333"/>
<param name="video-layout-bgcolor" value="#000000"/>
<param name="video-codec-bandwidth" value="2mb"/>
<param name="video-fps" value="30"/>
<param name="video-codec-config-profile-name" value="conference"/>
<!-- Recording (optional but trivial in MCU mode) -->
<param name="auto-record" value="/var/recordings/${conference_name}-${strftime(%Y%m%d-%H%M%S)}.mp4"/>
<!-- Conference flags -->
<param name="conference-flags" value="livearray-sync,json-events,rfc-4579,video-floor-only,audio-always,minimize-video-encoding"/>
<!-- Member flags -->
<param name="member-flags" value="video-mute-exit-canvas,join-only"/>
<!-- TTS for prompts -->
<param name="tts-engine" value="flite"/>
<param name="tts-voice" value="kal"/>
</profile>
</profiles>
<!-- ============================================ -->
<!-- Video layouts - defines on-screen tile arrangement -->
<!-- ============================================ -->
<layout-settings>
<layouts>
<layout name="2x2">
<image x="0" y="0" scale="180" floor="true"/>
<image x="180" y="0" scale="180"/>
<image x="0" y="180" scale="180"/>
<image x="180" y="180" scale="180"/>
</layout>
</layouts>
<groups>
<group name="grid">
<layout>2x2</layout>
</group>
</groups>
</layout-settings>
</configuration>
The single line that distinguishes this from an SFU configuration is <param name="video-mode" value="mux"/>. This tells FreeSWITCH to mix streams into a single composite rather than route them individually.
Step 6: Set Up the Dialplan to Route WebRTC Calls into the Conference
A dialplan tells FreeSWITCH what to do when a call arrives. Without one, your WebRTC client connects, but the call goes nowhere. The dialplan below routes any call to a conf- prefixed extension into the video-mcu conference profile.
Create /etc/freeswitch/dialplan/default/00_webrtc_mcu.xml:
xml
<include>
<context name="default">
<!-- Match any call to conf-XXXX and drop it into MCU -->
<extension name="webrtc_mcu_conference">
<condition field="destination_number" expression="^conf-(\d+)$">
<action application="answer"/>
<action application="set" data="conference_member_flags=video-mute-exit-canvas"/>
<action application="set" data="rtp_secure_media=mandatory"/>
<action application="conference" data="$1@video-mcu"/>
</condition>
</extension>
</context>
</include>
After saving, run fs_cli -x "reloadxml" and the new dialplan is live without a full restart.
Step 7: Restart, Verify, and Place a Test Call
With all files in place, it’s time to restart FreeSWITCH and confirm that the listener is actually up on port 7443.
Run these commands in order:
bash # Restart cleanly systemctl restart freeswitch # Watch logs for errors tail -f /var/log/freeswitch/freeswitch.log # In another terminal, confirm WSS is bound ss -tlnp | grep 7443 # Should show: LISTEN 0 ... 0.0.0.0:7443 ... freeswitch # Check the SIP profile loaded correctly fs_cli -x "sofia status profile internal." # Look for: WS-BIND-URL and WSS-BIND-URL lines
To place a test call, point a Verto or SIP.js client at wss://conference.yourdomain.com:7443 and dial conf-1000, you should land in an empty conference with audio prompts.
What Are the Most Common FreeSWITCH WebRTC MCU Errors and How Do You Fix Them?
Every FreeSWITCH WebRTC MCU deployment fails in the same handful of ways. The error messages rarely point to the real problem. The matrix below is the cheat sheet we wish existed when we first started shipping these.
Here are the seven failures you will almost certainly run into, with their real causes and fixes:
| Symptom | Most Likely Cause | Fix |
|---|---|---|
| Calls connect, but no audio/video | ext-rtp-ip not set, or SDP advertises private IP |
Set ext-rtp-ip to your public IPv4 in vars.xml ; confirm with fs_cli -x "sofia status " |
The browser shows “insecure cert” or ERR_CERT_AUTHORITY_INVALID |
Cert chain order wrong in wss.pem |
Rebuild bundle: cat privkey.pem fullchain.pem wss.pem, privkey FIRST |
sofia.c:2778 Error Creating SIP UA |
Port 5061 in use, or wss.pem missing/unreadable |
lsof -i :5061 to find conflict; verify /etc/freeswitch/tls/wss.pem exists and is owned by freeswitch |
| ICE failed in the browser console | Server’s public IP not in apply-candidate-acl or firewall blocking UDP 16384–32768 |
Add wan_v4.auto to apply-candidate-acl; open RTP port range in firewall |
| DTLS handshake fails | dtls-srtp not enabled, or cert mismatch |
Set in internal.xml ; restart Sofia profile |
| The video is black or frozen for some participants | Codec mismatch (one client has VP8, another H.264) | Force single codec in inbound-codec-prefs, or rely on MCU transcoding (CPU cost) |
| WSS connection drops after ~30s | Reverse proxy (nginx/HAProxy) timing out idle WebSocket | Set proxy_read_timeout 3600s in nginx; or connect the browser directly to 7443 |
Two diagnostic commands handle 80% of the rest: fs_cli -x "sofia status profile internal reg" for SIP/WSS issues, and fs_cli -x "conference video-mcu list" for conference-state issues.
If you’re specifically stuck on the “calls connect, but no audio” failure mode, our deeper dive into WebRTC One-Way Audio covers the SDP-level fixes used in production.
How Do You Test a FreeSWITCH WebRTC MCU Configuration?
Testing isn’t just “did the call connect?” You need to verify signaling, media, and mixing independently because a failure at any layer can look similar on the client side.
Work through this checklist before declaring your deployment production-ready:
- Signaling: Browser DevTools → Network → WS filter; confirm WSS handshake completes with
101 Switching Protocols - SDP exchange: Check that the SDP answer contains
a=fingerprint:(DTLS) anda=ice-ufrag:lines - Media flow:
fs_cli -x "show channels"should show your call withsecure=srtp-dtls - Mixing: Join with two clients; both should see the composite video, not individual streams
- Audio Quality: Run for 5+ minutes; listen for jitter, clipping, or echo. For deeper diagnostics of RTP, jitter, and packet loss issues that surface under load, the FreeSWITCH-specific tuning guide covers kernel and codec-level fixes.
- Recording: Confirm an MP4 lands in
/var/recordings/after the call ends - Load: Use
sippor a headless Chromium farm to simulate 10, then 20, then 50 concurrent participants, and watch CPU
If any step fails, the troubleshooting matrix above maps directly to which layer broke.
What Does It Really Cost to Run a FreeSWITCH WebRTC MCU Server?
Self-hosting feels free until you price the engineering hours, and that math is what most “FreeSWITCH tutorials” leave out.
Here’s the honest breakdown for a production deployment supporting ~100 concurrent participants across multiple rooms.
The recurring infrastructure cost is the easy part:
- Server: 16 vCPU / 32 GB dedicated bare metal or beefy cloud instance, $200–$600/month
- Bandwidth: ~1 Mbps per mixed HD participant outbound, $50–$200/month at typical egress rates
- TLS certs: $0 (Let’s Encrypt) to $200/year (commercial wildcard)
- Monitoring: Prometheus + Grafana stack — $0 self-hosted, $100+/month managed
The hidden cost is engineering time:
- Initial build: 80–160 hours for a senior VoIP engineer to ship a production-ready deployment
- Ongoing Maintenance: 10–20 hours/month for patching, monitoring, and incident response
- On-call: Someone has to answer the page when WSS dies at 3 AM
At average senior engineering rates, that’s $15,000–$40,000 for the initial build and $2,000–$6,000/month thereafter. Compare that to CPaaS per-minute pricing for your expected usage. The break-even point usually lands somewhere between 50,000 and 200,000 conference minutes per month.
Build in-house if you have FreeSWITCH expertise on staff. You need carrier-grade customization; you’re processing enough volume for CPaaS pricing to dominate your COGS. Otherwise, you have regulatory requirements (HIPAA, data residency) that rule out third-party SaaS. Once you’re past the single-server limits, the playbook to architect WebRTC systems for 10k+ concurrent users covers the clustering and load-balancing patterns that hold up at scale.
Outsource to a FreeSWITCH Development Team if: You need it shipped in weeks, not quarters. Your team’s strength is application code, not VoIP infrastructure, or you want a predictable hand-off where someone else owns the on-call rotation.
Wrapping Up
A working FreeSWITCH WebRTC MCU isn’t magic; it’s vars.xml, internal.xml , and conference.conf.xml configured correctly. A clean TLS bundle and the discipline to test every layer before going live. Get those right, and you have a media stack that handles mixed conferences, PSTN bridging, and recording at a fraction of the cost of CPaaS.
The harder question is whether building it is the best use of your team’s time in the next quarter. If FreeSWITCH isn’t core to what you ship, trial and error gets expensive fast. Teams that want a production-tuned deployment without the eight-week learning curve usually bring in Hire VoIP Developer. That’s the shortcut from “it almost works” to “it ships.”