iOS WebRTC SDK
Preview: audio/video calls for iOS on WebRTC, with signaling over Carrier — the call stack inside Beagle Chat for iOS. Source is not public yet and nothing is published.
Integration not yet verified. The source repository of this SDK is not public yet, and no distribution of it is published, so a third-party app cannot add it today. The API below was read from the SDK source on the branch that ships in Beagle Chat for iOS. The call sample on this page was compiled (Xcode 27.0, iOS Simulator) against a copy of that source and a from-source build of the Swift Carrier SDK, but no call was placed, and you cannot reproduce that build outside the project. Treat this page as a preview of what is coming.
The iOS WebRTC SDK (module ElastosWebRTCSDK) adds one-to-one audio and video calls, plus an optional WebRTC data channel, to an iOS app on the Decent Network. Media runs over Google's WebRTC. Signaling (offer, answer, ICE candidates, hang-up) goes through a provider object that your app supplies. Over Carrier, that provider sends the signals as JSON through the Swift Carrier SDK's CarrierExtension invite channel, and uses the TURN server that Carrier hands out. Beagle Chat for iOS uses this SDK for its calls.
When to use it
Use it when an iOS app needs calls between Carrier friends. Every other part of the network (identity, friends, messages, files, offline delivery) comes from the Swift Carrier SDK, which you need in any case. On Android, use the Android WebRTC SDK. In JavaScript, use @decentnetwork/peer-webrtc.
Add it to your project
You cannot add it yet:
- The source repository is private. No tag, Swift package, CocoaPods pod or binary of this SDK has been published.
- The CocoaPods pod named
ElastosWebRTCSDK(1.0.0 to 1.0.3, 2020 to 2021) is the older upstream Elastos SDK, not this one. Its source repository no longer exists, so it does not install either.
For planning, here is what the SDK is made of. It is a Swift package, ElastosWebRTCSDK, with swift-tools-version 5.9 and platforms iOS 16 / macOS 13. It depends on the public stasel/WebRTC package (147.0.0 and up) and on a small helper package, ACUtils (which pulls in Kingfisher), that ships alongside it. The package does not depend on the Carrier SDK: your app links both and connects them through an RTCProvider (see below).
Initialize
Create a WebRtcClient with a WebRtcDelegate, then attach a signaling provider with setup(provider:). The provider below sends signals over Carrier. It needs a started, ready Carrier (see Initialize on the Carrier page).
import Foundation
import ElastosCarrierSDK
import ElastosWebRTCSDK // re-exports WebRTC, ACFoundation, ACAppKit
/// Carries WebRTC signals (offer/answer/candidates/bye) as JSON over the
/// Carrier extension invite channel, and supplies the Carrier TURN server.
final class CarrierSignalProvider: RTCProvider {
var receiveClosure: ((RtcSignal, String) -> Void)?
var call: Call?
var iceServers: [RTCIceServer] = []
var channel: ProviderChannel = .carrier
var status: ProviderStatus = .connecting
private let ext: CarrierExtension
init(carrier: Carrier) throws {
ext = CarrierExtension(carrier)
try ext.registerExtension { [weak self] _, from, data in
guard let data = data?.data(using: .utf8),
let signal = try? JSONDecoder().decode(RtcSignal.self, from: data) else { return }
self?.receiveClosure?(signal, from)
}
if let turn = try? ext.turnServerInfo() {
iceServers = [RTCIceServer(urlStrings: ["turn:\(turn.server):\(turn.port)"],
username: turn.username, credential: turn.password)]
}
}
func send(_ signal: RtcSignal, friendId: String, closure: @escaping BoolClosure) throws {
let json = String(decoding: try JSONEncoder().encode(signal), as: UTF8.self)
try ext.sendInviteFriendRequest(to: friendId, withData: json) { _, _, status, _, _ in
closure(status == 0)
}
}
func connect() { status = .connected }
func disconnected() { status = .disconnected(retryCount: 0) }
}
final class CallController: WebRtcDelegate {
lazy var client = WebRtcClient(delegate: self)
var isBusy: Bool { false }
func start(carrier: Carrier) throws {
client.setup(provider: try CarrierSignalProvider(carrier: carrier))
}
func call(friendId: String, video: Bool) {
let call = Call(remoteAddress: friendId, direction: .outgoing, localId: UUID(),
hasLocalVideo: video, channel: .carrier)
client.makeCall(call: call)
}
func hangUp() { client.endCall(type: .normal) }
// Incoming offer: accept, reject or report busy.
func onInvite(call: Call, completion: @escaping InviteClosure) {
completion(.accept)
}
func onWebRtc(_ client: WebRtcClient, didChangeState state: WebRtcCallState) {
print("call state: \(state)")
}
func onReceiveMessage(_ data: Data, isBinary: Bool, channelId: Int) {}
func onSendDataFailure(error: Error) {}
}The SDK always adds stun:stun.l.google.com:19302 to the ICE servers, followed by whatever the provider returns in iceServers. Beagle's own provider follows the same pattern: JSON-encoded RtcSignal over CarrierExtension, with TURN from turnServerInfo(). It also routes and repairs signals in ways not shown here.
Identity
The SDK has no identity of its own. A call's peer is a Carrier friend, addressed by userId (Call.remoteAddress). See Identity on the Swift Carrier page.
Friends
Not part of this SDK. Calls go to existing Carrier friends, and the provider can only reach a friend through Carrier.
Messages
During a call, the WebRTC data channel can carry text and files: sendText(_:), sendFile(_:) and sendImage(_:) on WebRtcClient. Received data arrives in WebRtcDelegate.onReceiveMessage(_:isBinary:channelId:). The data channel exists only when the call's options include .data. An incoming call gets .data if the caller offered it. makeCall(call:) uses Call.options, which contain only .audio / .video, so in this version an outgoing call does not open a data channel. For chat messages, use Carrier's sendFriendMessage.
Files
sendFile(_ path: String) and sendImage(_ image: UIImage) split the file into JSON chunks (data in base64, fileId, index, mime, end) over the data channel, subject to the limits in Messages. For files outside a call, use Carrier file transfer.
Sessions and calls
The call flow, from the source:
- Caller.
makeCall(call:)stores theCallon the provider, adds the local tracks, creates an SDP offer and sends it asRtcSignal(type: .offer, …)through the provider. If the provider'ssendthrows, the client callsonOfferSendFailure(callId, error). - Callee. The provider passes the decoded signal to
receiveClosure, whichsetup(provider:)wires toWebRtcClient.receive(message:from:). An offer builds an incomingCalland callsWebRtcDelegate.onInvite(call:completion:). Answer.acceptto create and send the SDP answer,.rejectto sendbyewith reasondeclined, or.busyto sendbyewith reasonbusy. BeforeonInvite, the client asksshouldConfirmRinging(for:)(defaultfalse). If that returnstrue, it sendsevent: ringingback once per call and channel. - Both sides. ICE candidates are queued and sent as
.candidatesignals once the remote SDP is in place. State changes come toonWebRtc(_:didChangeState:)asWebRtcCallState:.connecting,.connected,.disconnected,.accept,.error(error:),.hangup(byRemote:),.ended,.busy,.remoteRinging,.remoteOnline,.connectToRoom. - Hang up.
endCall(type:)sendsbyewith aHangupType(.normal,.declined,.busy,.close) and releases the camera, peer connection and data channel.
Media controls on WebRtcClient: isEnableAudio and isEnableVideo (mute / camera off), setLoudSpeaker(enabled:), switchCamera(position:) and stopCapture(). For video calls, the client creates localVideoView (LocalVideoView, the camera preview) and remoteVideoView (RemoteVideoView, Metal-rendered) on the main thread and then calls videoViewsReadyClosure, where you add them to your view hierarchy. On the simulator the camera is not started.
CallKit. The SDK does not use CallKit. Call.localId is a UUID so that it can serve as a CallKit call id, and Call.Direction has a .callkit case, but the CallKit and PushKit integration lives in the app (in Beagle, its own CallKit and push managers). discardUnansweredCall(callId:) and noteRemoteEnded(callId:) exist to support that app-side hand-off.
Offline messages (Express)
Not part of this SDK. A call needs the friend to be reachable, and signals go only through the provider you attach. ProviderChannel has a .socketio case for a second, app-supplied signaling provider (Beagle uses one for its offline path). The SDK itself ships only the protocol, not a socket.io client.
Threading and lifecycle
- Delegate callbacks come on the thread where the signal or WebRTC event arrived. Over Carrier, that is the Carrier background queue.
videoViewsReadyClosureis called on the main thread. Dispatch UI work to the main queue. WebRtcClient.delegateisweak, so keep your delegate alive. OneWebRtcClienthandles one call at a time.WebRtcDelegaterequires anisBusyproperty, but this version of the SDK never reads it, so check for calls in progress yourself inonInviteand answer.busy.- After
endCall(or a remotebye), the client detaches the provider. Callsetup(provider:)again before the next call. RtcSignalisCodable. On the wire its fields aretype,sdp,candidates,reason,options,action,callIdandevent, so peers on other platforms can share the same JSON.
API reference
Preview only. This is the public API on the unreleased source and may change before a release.
| Type | Members |
|---|---|
WebRtcClient | init(delegate: WebRtcDelegate), func setup(provider: RTCProvider), var provider: RTCProvider?, weak var delegate, var friendId: String?, var callId: String?, var options: MediaOptions (read-only), func makeCall(call: Call), func endCall(type: HangupType), func send(signal: RtcSignal, closure: @escaping BoolClosure) throws, func receive(message: RtcSignal, from: String), var isEnableAudio: Bool, var isEnableVideo: Bool, func setLoudSpeaker(enabled: Bool), func switchCamera(position: AVCaptureDevice.Position), func stopCapture(), func sendText(_:) throws, func sendFile(_:) throws, func sendImage(_:) throws, var localVideoView: LocalVideoView?, var remoteVideoView: RemoteVideoView?, var videoViewsReadyClosure: (() -> Void)?, var latestRemoteVideoTrack: RTCVideoTrack?, var onOfferSendFailure: ((String, Error) -> Void)?, func discardUnansweredCall(callId: UUID) -> Bool, func noteRemoteEnded(callId: UUID), var remoteEndedCallId: UUID? |
WebRtcDelegate | func onInvite(call: Call, completion: @escaping InviteClosure), func onWebRtc(_ client: WebRtcClient, didChangeState state: WebRtcCallState), func onReceiveMessage(_ data: Data, isBinary: Bool, channelId: Int), func onSendDataFailure(error: Error), var isBusy: Bool { get }, func shouldConfirmRinging(for call: Call) -> Bool (default false) |
RTCProvider | var receiveClosure: ((RtcSignal, String) -> Void)?, func send(_ signal: RtcSignal, friendId: String, closure: @escaping BoolClosure) throws, func connect(), func disconnected(), var call: Call?, var iceServers: [RTCIceServer], var channel: ProviderChannel, var status: ProviderStatus |
Call | init(remoteAddress:direction:localId:callerName:hasLocalVideo:dataChannel:channel:); remoteAddress, direction (.incoming, .outgoing, .callkit), localId: UUID, hasLocalVideo, callerName, dataChannel, channel, startDate, endDate, state, duration, options |
RtcSignal | init(type:sdp:candidates:reason:options:action:callId:event:); SdpType (offer, answer, candidate, remove-candidates, prAnswer, bye, action, event), Action (accept, reject), Event (ringing, online, connectToRoom), HangupType |
MediaOptions | array literal of MediaOptionItem (.audio, .video, .data); isEnableAudio, isEnableVideo, isEnableDataChannel |
| Enums | ProviderChannel (.socketio, .carrier), ProviderStatus, WebRtcCallState, InviteState (.accept, .reject, .busy), WebRtcError (.dataChannelInitFailed, .dataChannelStateIsNotOpen, .turnServerIsNil) |
| Views | LocalVideoView (UIView, camera preview), RemoteVideoView (UIView, RTCVideoRenderer, Metal), VideoCaptureController (startCapture(), stopCapture(), switchCamera(isUsingFrontCamera:)) |
Source
Not public yet. The source repository is private, so there is no link to it here. This page documents the package Packages/ElastosWebRTCSDK-iOS on branch feat/agentnet-proto-v2, commit b8830f25cd57ea77466204b45fc7753810d9fffb (2026-11). The signaling side runs on the public Swift Carrier SDK, and the media engine is the public stasel/WebRTC package.