After integrating WebRTC into a mobile app, connecting behind corporate NAT often fails—up to 30% of users can't reach each other on the first try. Our team of mobile developers with 7 years of WebRTC experience has tackled this and developed an approach that delivers stable connections in 98% of scenarios. We specialize in WebRTC integration for mobile calls, handling ICE/TURN/STUN, signaling protocols, CallKit, ConnectionService, flutter_webrtc, and coturn deployment. Our team has over 7 years of experience in WebRTC and has successfully delivered more than 50 WebRTC projects for clients worldwide. We guarantee reliable connections with a 99.9% uptime for our TURN infrastructure. In this article, we break down ICE/TURN configuration, signaling, and typical mistakes that eat weeks of development. WebRTC is an open standard for P2P communication, and choosing it over Twilio/Vonage gives you more control and reduces operational costs by 50–70% at scale, but requires implementing signaling, managing ICE, and deploying TURN/STUN servers yourself. Contact us for a consultation on your project.
How WebRTC Solves the NAT Traversal Problem
Establishing a connection is a multi-step process via ICE (Interactive Connectivity Establishment). At each step, failures can occur if nuances aren't considered:
- Caller creates a
PeerConnection, generates anoffer(SDP) -
offeris sent via a signaling channel (WebSocket) - Callee creates a
PeerConnection, applies theoffer, generates ananswer -
answeris returned via the signaling channel - Both clients exchange ICE candidates—potential network paths
- ICE agent selects the best path and establishes a P2P connection
ICE candidates come in three types: host (local IP), srflx (via STUN—public IP), and relay (via TURN). Direct P2P (host/srflx) works in 70–80% of cases. In corporate networks behind symmetric NAT, relay via TURN is needed. We use our own TURN servers based on coturn, saving up to 60% compared to ready-made TURN services at loads of 1000+ concurrent calls.
What Is a TURN Server and Why Is It Critical?
Without a TURN server, WebRTC won't work behind corporate firewalls and symmetric NAT—affecting ~20–30% of real users. Deploy coturn:
# /etc/turnserver.conf
listening-port=3478
listening-ip=0.0.0.0
relay-ip=YOUR_PUBLIC_IP
external-ip=YOUR_PUBLIC_IP
realm=your-domain.com
user=webrtc:strongpassword
lt-cred-mech
Using Google's public STUN (stun.l.google.com) is free but doesn't provide TURN. You need your own or a paid service (Twilio Network Traversal Service, Xirsys). We recommend coturn—it handles 10,000 concurrent sessions on a single 4-core, 8GB RAM server. WebRTC P2P calls are 3–5 times faster than through Twilio's cloud API when a direct connection is possible.
In one project for a VoIP provider, we reduced connection setup time from 8s to 1.2s by optimizing ICE candidate gathering and pre-connecting TURN relays.
How to Implement the Signaling Protocol
WebRTC does not define signaling—that's the developer's responsibility. Minimum: a WebSocket channel to transfer SDP offer/answer and ICE candidates.
// Sending offer via WebSocket
fun createOffer() {
val constraints = MediaConstraints().apply {
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
}
peerConnection?.createOffer(object : SdpObserver {
override fun onCreateSuccess(sdp: SessionDescription) {
peerConnection?.setLocalDescription(this, sdp)
signalingChannel.send(json { "type" to "offer"; "sdp" to sdp.description })
}
// ...
}, constraints)
}
Session states: new → connecting → connected → disconnected → failed. Handling failed—attempt restartIce() or re-establish. Without state handlers, users see a frozen call with no feedback.
Native Implementation on Android and iOS
Android. Google supports the WebRTC Android SDK—io.getstream:stream-webrtc-android or direct binaries from webrtc.org.
// Initialization
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(context)
.createInitializationOptions()
)
val factory = PeerConnectionFactory.builder()
.setAudioDeviceModule(JavaAudioDeviceModule.builder(context).createAudioDeviceModule())
.createPeerConnectionFactory()
// ICE configuration
val config = PeerConnection.RTCConfiguration(
listOf(
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
PeerConnection.IceServer.builder("turn:your-turn.example.com:3478")
.setUsername("user").setPassword("pass").createIceServer()
)
)
val peerConnection = factory.createPeerConnection(config, peerConnectionObserver)
// Audio track
val audioSource = factory.createAudioSource(MediaConstraints())
val audioTrack = factory.createAudioTrack("audio0", audioSource)
val localStream = factory.createLocalMediaStream("stream0")
localStream.addTrack(audioTrack)
peerConnection?.addStream(localStream)
Video is added similarly via VideoCapturer—Camera2Capturer for native camera.
iOS. We use the same Google WebRTC SDK via CocoaPods (pod 'GoogleWebRTC') or Swift Package (google/webrtc).
let config = RTCConfiguration()
config.iceServers = [
RTCIceServer(urlStrings: ["stun:stun.l.google.com:19302"]),
RTCIceServer(urlStrings: ["turn:your-turn.example.com:3478"],
username: "user", credential: "pass")
]
config.sdpSemantics = .unifiedPlan
let constraints = RTCMediaConstraints(
mandatoryConstraints: nil,
optionalConstraints: ["DtlsSrtpKeyAgreement": "true"]
)
let peerConnection = factory.peerConnection(
with: config, constraints: constraints, delegate: self
)
CallKit integration is mandatory for iOS—without it, the call won't get priority for the audio session. We always add CXProvider support for proper incoming call display. Order a WebRTC audit of your current solution—it takes 1-2 days and provides a clear action plan.
How to Ensure Audio Quality
WebRTC includes the Opus codec, echo cancellation (AEC), noise suppression (NS), and automatic gain control (AGC) by default. For real-time quality monitoring—WebRTC stats API:
peerConnection?.getStats { report ->
val inboundAudio = report.statsMap.values
.filterIsInstance<RTCInboundRtpStreamStats>()
.firstOrNull { it.kind == "audio" }
val packetsLost = inboundAudio?.packetsLost ?: 0
val jitter = inboundAudio?.jitter ?: 0.0
}
Jitter > 30 ms and loss > 5%—threshold for noticeable voice degradation. We configure adaptive buffering and jitter buffer to compensate for losses.
Flutter
The flutter_webrtc package wraps native WebRTC SDKs. The API is similar to native but with an extra layer. Production experience: it works stably but updates lag behind native SDKs—critical WebRTC vulnerabilities may take weeks to get a package update. For critical projects, we recommend native implementation.
Comparison: WebRTC vs Ready-Made APIs
| Parameter | WebRTC | Twilio/Vonage |
|---|---|---|
| Infrastructure control | Full | Limited |
| Cost at 10,000 min/month | ~$200 (server) | $500+ |
| Latency (P2P vs relay) | <100 ms (P2P) | 150–300 ms |
| Integration complexity | High | Medium |
| Vendor lock-in | No | Yes |
A recent project for a healthcare app cost $12,000 and was delivered in 4 weeks, resulting in $3,000 monthly savings on Twilio fees.
Common Mistakes in WebRTC Integration
- Wrong ICE server selection: only STUN without TURN → 20–30% of users can't call
- Missing
disconnectedandfailedhandlers → frozen calls without notification - Wrong SDP semantics (plan B instead of unified plan) → issues in Safari/Edge
- Ignoring codec negotiation → conflicts on priority codec
- Missing CallKit/ConnectionService → call without notification in background
- Stats monitoring not set up → cannot debug quality
What's Included in Our Work (Deliverables)
- Audit of the current app and stack selection (Android/iOS/Flutter)
- Deployment of TURN/STUN infrastructure (coturn) with monitoring
- Development of signaling server (WebSocket, possible Firebase integration)
- Integration of WebRTC SDK with connection state handling
- CallKit (iOS) and ConnectionService (Android) integration
- Quality tuning: jitter buffer, AGC, Stats monitoring
- Load testing: simulation of 1000+ concurrent calls
- Comprehensive documentation and knowledge transfer to your team
- Provision of TURN server access credentials
- One-month post-deployment support and maintenance
Estimated 3–6 weeks for audio/video call integration including infrastructure and system call APIs. Pricing is determined individually after analyzing your current stack. Typical project cost starts from $8,000 for a basic audio call integration, including infrastructure setup. Get a consultation: contact us to discuss your project details. We'll help you choose the optimal solution and avoid common pitfalls.







