All docs / Mobile + native Carrier SDKs

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.

Version
No published release
Platform
iOS · Swift
Source
Source repository not public yet

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:

  1. Caller. makeCall(call:) stores the Call on the provider, adds the local tracks, creates an SDP offer and sends it as RtcSignal(type: .offer, …) through the provider. If the provider's send throws, the client calls onOfferSendFailure(callId, error).
  2. Callee. The provider passes the decoded signal to receiveClosure, which setup(provider:) wires to WebRtcClient.receive(message:from:). An offer builds an incoming Call and calls WebRtcDelegate.onInvite(call:completion:). Answer .accept to create and send the SDP answer, .reject to send bye with reason declined, or .busy to send bye with reason busy. Before onInvite, the client asks shouldConfirmRinging(for:) (default false). If that returns true, it sends event: ringing back once per call and channel.
  3. Both sides. ICE candidates are queued and sent as .candidate signals once the remote SDP is in place. State changes come to onWebRtc(_:didChangeState:) as WebRtcCallState: .connecting, .connected, .disconnected, .accept, .error(error:), .hangup(byRemote:), .ended, .busy, .remoteRinging, .remoteOnline, .connectToRoom.
  4. Hang up. endCall(type:) sends bye with a HangupType (.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. videoViewsReadyClosure is called on the main thread. Dispatch UI work to the main queue.
  • WebRtcClient.delegate is weak, so keep your delegate alive. One WebRtcClient handles one call at a time. WebRtcDelegate requires an isBusy property, but this version of the SDK never reads it, so check for calls in progress yourself in onInvite and answer .busy.
  • After endCall (or a remote bye), the client detaches the provider. Call setup(provider:) again before the next call.
  • RtcSignal is Codable. On the wire its fields are type, sdp, candidates, reason, options, action, callId and event, 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.

TypeMembers
WebRtcClientinit(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?
WebRtcDelegatefunc 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)
RTCProvidervar 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
Callinit(remoteAddress:direction:localId:callerName:hasLocalVideo:dataChannel:channel:); remoteAddress, direction (.incoming, .outgoing, .callkit), localId: UUID, hasLocalVideo, callerName, dataChannel, channel, startDate, endDate, state, duration, options
RtcSignalinit(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
MediaOptionsarray literal of MediaOptionItem (.audio, .video, .data); isEnableAudio, isEnableVideo, isEnableDataChannel
EnumsProviderChannel (.socketio, .carrier), ProviderStatus, WebRtcCallState, InviteState (.accept, .reject, .busy), WebRtcError (.dataChannelInitFailed, .dataChannelStateIsNotOpen, .turnServerIsNil)
ViewsLocalVideoView (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.