A Swift iOS SDK (BandwidthRTC) that wraps WebRTC to provide real-time audio calling via the Bandwidth BRTC gateway. Distributed as a signed XCFramework. iOS 15+, Swift 5.9+, SPM-only.
Sources/BandwidthRTC/
├── BandwidthRTC.swift # Main public client class
├── Exports.swift # Re-exports WebRTC types
├── Types/ # All public data types and enums
├── Signaling/ # WebSocket actor + JSON-RPC 2.0 message types
│ └── RPC/
├── WebRTC/ # Dual peer connection management
├── Media/ # MixingAudioDevice (custom RTCAudioDevice)
└── Utilities/ # Logger
Plugins/GenerateSDKVersion/ # Build plugin — reads VERSION file, generates SDKVersion+Generated.swift
Tests/BandwidthRTCTests/
├── *Tests.swift # Unit tests
└── Mocks/ # Protocol-backed mocks for all major dependencies
.github/
├── workflows/
│ ├── build.yml # Runs on every push; enforces version bump on PRs
│ ├── draft_release.yml # Runs on push to main; creates draft GitHub release
│ └── release_publish.yml # Runs when release is published; builds + attaches XCFramework
└── actions/build-xcframework/ # Composite action: Xcode setup → archive → zip
VERSION # Single source of truth for the SDK version (e.g. 1.0.1)
public final class BandwidthRTCClientis the only public entry point- Dependencies (
SignalingClient,PeerConnectionManager,MixingAudioDevice) are injected via constructor — use mocks in tests - Event callbacks are simple optional closures set as properties:
onStreamAvailable,onReady,onRemoteDisconnected, etc.
- An
actor— all signaling state is protected by actor isolation - Sends JSON-RPC 2.0 requests over WebSocket; correlates responses by auto-incremented ID via
CheckedContinuation - Server notifications (e.g.
sdpOffer,ready) are dispatched to registered handlers viaonEvent(method:handler:) - Default gateway:
wss://gateway.pv.prod.global.aws.bandwidth.com/prod/gateway-service/api/v1/endpoints
- Maintains two
RTCPeerConnections: one publish (send-only) and one subscribe (receive-only) - Both connect on
connect()with an empty initial SDP handshake; tracks are added only onpublish() - Protocol-backed (
PeerConnectionManagerProtocol) for test injection
- Implements
RTCAudioDevice— ownsAVAudioSessionconfiguration - Recording: taps
AVAudioEngineinput node → delivers PCM to WebRTC + firesonLocalAudioLevelcallback - Playout:
AVAudioSourceNodepulls Int16 PCM from WebRTC → Float32 → firesonRemoteAudioLevelcallback - Fixed 48kHz / mono / 10ms (480 samples) — pre-allocated buffers, no heap alloc on audio thread
All major subsystems are abstracted behind protocols:
SignalingClientProtocol→MockSignalingClientPeerConnectionManagerProtocol→MockPeerConnectionManagerWebSocketProtocol→MockWebSocket
Tests live in Tests/BandwidthRTCTests/. Pass mocks into BandwidthRTCClient init; never use real network or WebRTC in unit tests.
The VERSION file is the single source of truth. No hardcoded version strings anywhere.
⚠️ Every PR must bumpVERSION. CI (build.yml) checks that the version on the branch is strictly greater thanorigin/mainand will fail if it is not. Increment patch for bug fixes, minor for new features, major for breaking changes.
VERSIONcontains a plain semver string (e.g.1.0.1)- The
GenerateSDKVersionbuild plugin readsVERSIONat build time and generatesSDKVersion+Generated.swift— this is howSDKVersion.currentgets its value for both local and CI builds - Do not add a committed
SDKVersion.swiftto Sources — the plugin generates that symbol into its work directory
Bump VERSION in PR → CI enforces it's higher than main → merge → draft release created automatically → publish release → XCFramework built and attached
- On non-main branches: checks
VERSION>origin/main:VERSION(fails with hint if not) - Reads
VERSION→ passes asmarketing_versionto composite action - Builds XCFramework (runs unit tests too)
- Uploads artifact
BandwidthRTC-{VERSION}(3-day retention)
- Reads
VERSION - If a draft release tagged
v{VERSION}already exists: updates it with auto-generated notes - If a published release tagged
v{VERSION}already exists: fails with an error (bumpVERSION) - Otherwise: creates a new draft release tagged
v{VERSION}with auto-generated notes
- Reads
VERSION - Builds XCFramework (no tests)
- Uploads
BandwidthRTC.xcframework.zipto the release assets - Creates annotated major (
v{MAJOR}) and minor (v{MAJOR}.{MINOR}) version tags if they don't already exist
Inputs: marketing_version, run_tests (default false)
Output: zip_path
Steps: select Xcode → cache simulator runtime → optionally run tests → archive iOS + iOS Simulator → create XCFramework → zip
Key xcodebuild flags: BUILD_LIBRARY_FOR_DISTRIBUTION=YES, CODE_SIGN_IDENTITY="", OTHER_SWIFT_FLAGS="-Xfrontend -no-verify-emitted-module-interface"
- No storyboards, no UIKit — SDK only, no app target
- Swift concurrency throughout — use
async/await; callbacks are@Sendable @unchecked Sendableon public types that wrap WebRTC objects (WebRTC itself is not Sendable)- No force unwraps in production code
- Logging via
Logger.shared— levels:.off .error .warn .info .debug .trace; default is.warn - JSON-RPC types are in
Signaling/RPC/— each method gets its own file withParamsandResultstructs BandwidthRTCErroris the only error type surfaced to callers — map internal errors before throwing
| Package | Version | Purpose |
|---|---|---|
stasel/WebRTC |
114.0.0 (exact) |
Core WebRTC engine |
No other external dependencies. Do not add dependencies without a strong reason.
- Do not add a committed
SDKVersion.swiftto Sources —SDKVersion.currentis generated by the build plugin fromVERSION - Do not add git-tag-based versioning logic —
VERSIONfile is intentional - Do not add a CI step that overwrites
SDKVersion.swift— the plugin handles it at build time - Do not modify
AVAudioSessioncategory/mode outside ofMixingAudioDevice— it owns the audio session - Do not use
release-drafteror similar —draft_release.ymlhandles releases directly