A native iOS calling app powered by Bandwidth's WebRTC (BRTC) SDK, demonstrating outbound PSTN dialing and live call quality monitoring.
Note: This project uses the Bandwidth BRTC SDK, which is currently in beta.
- Outbound PSTN Calling — Dial any phone number from the app. Audio flows over WebRTC and bridges to the PSTN via Bandwidth's Voice API.
- Live Call Stats — Real-time quality overlay showing jitter, packet loss, round-trip time, bitrate, codec, and audio level — pulled directly from WebRTC's
getStats()API. - Call History — Track recent calls with direction, duration, and timestamps.
- Full PSTN ↔ WebRTC Bridge — Server-side call orchestration using Bandwidth BXML.
┌──────────────┐ WebRTC ┌──────────────┐ Voice API ┌──────────┐
│ iOS App │ ◄──────────────► │ Node Server │ ◄────────────► │ PSTN │
│ (BRTC SDK) │ │ (Express) │ BXML │ Network │
└──────────────┘ └──────────────┘ └──────────┘
│ │
│ CallKit │ Bandwidth
│ (native call UI) │ OAuth + Endpoints API
▼ ▼
iOS System Bandwidth Platform
Outbound call flow:
- User dials a number in the app
- App sends an outbound connection request via the BRTC SDK
- Server receives the request, creates a Voice API call to the PSTN number
- PSTN party answers → server returns
<Connect><Endpoint>BXML to bridge audio - Audio flows: iOS App ↔ WebRTC ↔ Bandwidth ↔ PSTN
- Xcode 15+ with iOS 17.0 SDK
- iPhone running iOS 17+ (CallKit requires a real device; the simulator uses a fallback UI)
- Node.js 18+
- ngrok (or similar tunneling tool) for exposing the local server to Bandwidth's webhooks
- Bandwidth account with:
- Account ID, OAuth Client ID, and Client Secret
- A Voice API Application configured with callback URLs
- A phone number associated with the application
Clone the repository using git clone and change your directory to the cloned repo.
The server needs a public URL for Bandwidth to send webhooks. Start ngrok pointing at port 3000:
ngrok http 3000Copy the https:// forwarding URL (e.g., https://a1b2c3d4.ngrok-free.app).
cd server
cp .env.example .envEdit .env with your Bandwidth credentials:
# Bandwidth OAuth API Credentials
ACCOUNT_ID=your_account_id
BW_ID_CLIENT_ID=your_client_id
BW_ID_CLIENT_SECRET=your_client_secret
# BRTC Endpoint configuration
CALLBACK_BASE_URL=https://your-ngrok-url.ngrok-free.app
APPLICATION_ID=your_application_id
FROM_NUMBER=+15551234567
# Optional
# BW_ID_HOSTNAME=https://api.bandwidth.com
# VOICE_URL=https://voice.bandwidth.com/api/v2
# HTTP_BASE_URL=https://api.bandwidth.com/v2
# Server port (optional, defaults to 3000)
PORT=3000| Variable | Description |
|---|---|
ACCOUNT_ID |
Your Bandwidth account ID |
BW_ID_CLIENT_ID |
OAuth client ID from the Bandwidth Dashboard |
BW_ID_CLIENT_SECRET |
OAuth client secret |
CALLBACK_BASE_URL |
Your public server base URL (for example, your ngrok URL) |
APPLICATION_ID |
Voice API Application ID (must have callback URLs configured) |
FROM_NUMBER |
Bandwidth phone number in E.164 format (e.g., +15551234567) |
BW_ID_HOSTNAME |
Optional OAuth host override (defaults to https://api.bandwidth.com) |
VOICE_URL |
Optional Voice API base URL override |
HTTP_BASE_URL |
Optional BRTC API base URL override |
In the Bandwidth Dashboard, set the following callback URLs on your Voice API Application to point to your ngrok URL:
| Callback | URL |
|---|---|
| Call Initiated Callback URL | https://your-ngrok-url.ngrok-free.app/callbacks/bandwidth |
| Call Status Callback URL | https://your-ngrok-url.ngrok-free.app/calls/status |
cd server
npm install
npm startYou should see:
BRTC token server running on http://localhost:3000
Account: 12345
App ID: cd13951d-...
From: +15551234567
Callback: https://your-ngrok-url.ngrok-free.app/callbacks/bandwidth
- Open
BRTCSampleApp/BRTCSampleApp.xcodeprojin Xcode - Wait for Swift Package Manager to resolve the
BandwidthRTCandWebRTCdependencies - Select your connected iPhone as the build target
- Build and run (⌘R)
- On the Connect screen, enter your server URL (e.g.,
http://192.168.1.100:3000or your ngrok URL) - Tap Connect
Outbound: Switch to the Keypad tab, dial a number, tap the green call button.
Inbound PSTN: Call your FROM_NUMBER from any phone. The call will ring through to the app.
├── Sources/BandwidthRTC/ # BRTC Swift SDK (Swift Package)
│ ├── BandwidthRTC.swift # Main public API
│ ├── Signaling/ # WebSocket signaling & JSON-RPC
│ ├── Types/ # Public types (RtcStream, CallStatsSnapshot, etc.)
│ ├── Utilities/ # Logging
│ └── WebRTC/ # Peer connection management
│ └── PeerConnectionManager.swift
│
├── BRTCSampleApp/ # iOS Sample App (Xcode project)
│ └── BRTCSampleApp/
│ ├── App/ # App entry point & root ContentView
│ ├── Models/ # CallRecord data model
│ ├── ViewModels/ # CallViewModel (call state machine)
│ ├── Views/ # SwiftUI views
│ │ ├── ConnectView.swift # Server connection screen
│ │ ├── CallView.swift # Dialing, ringing, and in-call screens
│ │ ├── DialpadView.swift # 12-key dialpad component
│ │ ├── RecentsView.swift # Call history list
│ │ └── StatsOverlayView.swift # Live call quality overlay
│ └── Services/ # Token fetching, CallKit, call history
│
├── server/ # Node.js server
│ ├── server.js # Express server — OAuth, endpoints, BXML, call bridging
│ ├── package.json
│ └── .env.example # Environment variable template
│
└── Package.swift # Swift Package manifest
| Layer | Technology |
|---|---|
| iOS App | SwiftUI, CallKit, AVFoundation |
| SDK | BandwidthRTC Swift Package (WebRTC M114) |
| Server | Node.js, Express |
| Voice | Bandwidth Voice API, BXML |
| Tunneling | ngrok (development) |
The Node.js server exposes the following:
| Method | Path | Description |
|---|---|---|
GET |
/token |
Creates a WebRTC endpoint and returns a JWT for the BRTC SDK |
POST |
/callbacks/bandwidth |
Handles BRTC events and incoming PSTN call routing |
POST |
/callbacks/bandwidth/status |
Voice API disconnect events |
POST |
/calls/answer |
BXML callback for outbound calls — bridges to WebRTC endpoint |
POST |
/calls/status |
Call status updates |
POST |
/calls/simulate-incoming-answer |
BXML callback for simulated incoming calls |
GET |
/health |
Health check |
GET |
/debug/endpoints |
Inspect the in-memory endpoint map |
When running the app on a real iPhone, it needs to reach the Node.js server over the network. There are two approaches:
If your iPhone and Mac are on a network that blocks device-to-device traffic (common on corporate WiFi), you can share your Mac's network directly to the iPhone over USB:
- Connect your iPhone to your Mac via USB cable
- On your Mac, go to System Settings > General > Sharing > Internet Sharing
- Set "Share your connection from" to Wi-Fi
- Check iPhone USB in the "To computers using" list
- Enable Internet Sharing
- Find the bridge IP address:
This typically returns something like
ipconfig getifaddr bridge100
192.168.3.1. - In the app's Connect screen, enter
http://<bridge-ip>:3000(e.g.,http://192.168.3.1:3000)
If your Mac and iPhone are on the same Wi-Fi network and the network allows device-to-device traffic:
- Find your Mac's local IP:
ipconfig getifaddr en0
- In the app's Connect screen, enter
http://<mac-ip>:3000(e.g.,http://192.168.1.100:3000)
Note: Many corporate and guest Wi-Fi networks use client isolation, which blocks connections between devices. If the connection times out, use Option A or ngrok instead.
If neither local option works, use ngrok to create a public tunnel:
ngrok http 3000Enter the https:// forwarding URL in the app. This also works for testing from any network.
iPhone can't connect to the server
- If using a local IP and the connection times out, your network likely blocks device-to-device traffic. Try Internet Sharing over USB (see above) or ngrok.
- Verify the server is running and reachable from your Mac:
curl http://localhost:3000/health - Make sure you include
http://in the server URL
"No WebRTC endpoint available" when calling the Bandwidth number
- Make sure the iOS app is connected (green "Connected" state) before calling
- Check that the server logs show the endpoint was created (
GET /tokensucceeded)
Calls disconnect immediately with "rejected"
- Verify your ngrok tunnel is running and the URL in
.envmatches - Check that the Bandwidth Application's callback URLs point to your ngrok URL
- Ensure
APPLICATION_IDmatches the application associated withFROM_NUMBER
CallKit doesn't show the incoming call screen
- CallKit only works on a real device, not the iOS Simulator
- The app includes a fallback ringing UI for simulator testing
Audio issues (one-way or no audio)
- Ensure the iOS device has granted microphone permission
- Check that the audio session is configured correctly (the SDK handles this automatically)
- Verify both WebRTC peer connections are established in the server logs
Stats overlay not appearing
- Stats only appear during an active call (after the remote party answers)
- If the overlay doesn't show for outbound calls, ensure the server returned
accepted: true
Copyright (c) Bandwidth Inc. All rights reserved.