All docs / Mobile + native Carrier SDKs

Android WebRTC SDK

One-to-one audio, video and data-channel calls between Carrier friends on Android, with signaling over Carrier. The JitPack build is not 16 KB-aligned; the builds Beagle ships are not published yet.

Version
webrtc-0.0.4 · JitPack
Platform
Android · Java/Kotlin
implementation 'com.github.0xli:Elastos.NET.WebRTC.Android.SDK:webrtc-0.0.4'

Integration verified on 2026-11-06: a minimal Android app depending on the JitPack coordinate com.github.0xli:Elastos.NET.WebRTC.Android.SDK:webrtc-0.0.4 plus org.elastos:carrier:6.0.4 was built with ./gradlew assembleDebug (JDK 17.0.15, Gradle 8.7, Android Gradle Plugin 8.5.1, compileSdk 34, minSdk 21), then run on an Android 14 arm64 emulator: WebrtcClient.createInstance registered with Carrier and returned a client in state init, and Carrier reached onReady. A call between two peers was not exercised.

The Android WebRTC SDK adds one-to-one audio, video and data-channel calls to an app that already runs the Android Carrier SDK (/docs/android-carrier). Carrier carries the signaling: offer, answer, ICE candidates and hang-up travel as Carrier friend-invite messages. Carrier also supplies a TURN server for ICE. Google's WebRTC engine (org.webrtc) handles the media. The public entry point is org.elastos.carrier.webrtc.WebrtcClient.

When to use it

Use it for voice/video calls or a WebRTC data channel between two Carrier friends on Android, with no signaling server of your own. You must add the Carrier SDK yourself, because this AAR compiles against Carrier but does not bundle it. For plain messages, files or raw streams, the Carrier SDK alone is enough.

Limits worth knowing up front:

  • WebrtcClient is a process-wide singleton handling one call at a time with one peer.
  • The callee must be an accepted Carrier friend.
  • The SDK does not request runtime permissions or manage audio focus for you.

Add it to your project

The published coordinate is the git tag webrtc-0.0.4 built by JitPack (build status ok, built 2026-05-18). The documented branch fix/jitpack-agp8-webrtc-0.0.3 is two commits ahead of that tag, and JitPack has no finished build of it. Those commits harden call races (paced invite sends, batched candidates, stale "bye" ignored) and do not change the public API.

What you can resolve today is not what Beagle ships. webrtc-0.0.4 on JitPack is the newest build a third party can resolve. It depends on org.webrtc:google-webrtc:1.0.30039, whose libjingle_peerconnection_so.so (arm64-v8a and x86_64) has 4 KB ELF segment alignment, and on the 2022 Carrier build org.elastos:carrier:6.0.4 (see Android Carrier SDK), which is also not 16 KB-aligned. Google Play may therefore reject an app built with these coordinates, and they lack the fixes Decent Network has made since. The 16 KB-aligned Carrier and WebRTC builds that Beagle Chat for Android ships with are not published yet. Use these coordinates to develop and evaluate; contact developer support before shipping to Play.

settings.gradle needs three repositories besides Google:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()                         // org.elastos:carrier:6.0.4
        maven { url 'https://jitpack.io' }     // com.github.0xli:Elastos.NET.WebRTC.Android.SDK
        // org.webrtc:google-webrtc:1.0.30039 (declared by the WebRTC SDK POM) is not on
        // Maven Central or Google Maven; this third-party GitHub mirror is the only host.
        maven { url 'https://raw.githubusercontent.com/alexgreench/google-webrtc/master' }
    }
}

app/build.gradle:

android {
    compileSdk 34
    defaultConfig {
        minSdk 21
        multiDexEnabled true
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'com.github.0xli:Elastos.NET.WebRTC.Android.SDK:webrtc-0.0.4'
    // The WebRTC AAR compiles against Carrier but does not bundle it: add it yourself.
    implementation 'org.elastos:carrier:6.0.4'
}

AndroidManifest.xml: the SDK's own manifest declares only INTERNET. Declare what a call needs, and keep the tools:replace that the Carrier AAR requires:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <application
        android:label="Call sample"
        tools:replace="android:label">
        ...
    </application>
</manifest>

Request RECORD_AUDIO (and CAMERA for video) at runtime before makeCall / answerCall. The repository's demo app also declares BLUETOOTH and CHANGE_NETWORK_STATE, which you need for Bluetooth headset routing through WebRTCAudioManager.

Resolved runtime dependencies (from :app:dependencies):

+--- com.github.0xli:Elastos.NET.WebRTC.Android.SDK:webrtc-0.0.4
|    +--- org.webrtc:google-webrtc:1.0.30039
\--- org.elastos:carrier:6.0.4

The POM also pulls guava 28.1-android, gson, commons-lang3, commons-codec, slf4j-api, logback-android, awaitility, org.osgi.framework and androidx.annotation. The verified debug APK came to about 23 MB.

16 KB pages: libjingle_peerconnection_so.so from google-webrtc:1.0.30039 and libcarrierjni.so from Carrier 6.0.4 use 4 KB ELF alignment. Neither meets Google Play's 16 KB page-size requirement for apps targeting Android 15 and later. The branch's own notes record the plan to move to a 16 KB-aligned WebRTC build, but no published coordinate contains that change yet.

Initialize

Start Carrier first (see /docs/android-carrier), then create the client once:

import org.elastos.carrier.webrtc.PeerConnectionParametersBuilder;
import org.elastos.carrier.webrtc.WebrtcClient;
import org.elastos.carrier.webrtc.call.CallHandler;
import org.elastos.carrier.webrtc.call.CallReason;

rtc = WebrtcClient.createInstance(this, carrier, new CallHandler() {
    @Override public void onInvite(String friendId, boolean audio, boolean video, boolean data) {
        // Arrives on the Carrier thread: post to the main thread for UI.
        main.post(() -> rtc.answerCall());
    }
    @Override public void onAnswer() { Log.i(TAG, "answered"); }
    @Override public void onActive() { Log.i(TAG, "call active"); }
    @Override public void onEndCall(CallReason reason) { Log.i(TAG, "ended: " + reason.getName()); }
    @Override public void onIceConnected() { }
    @Override public void onIceDisConnected() { }
    @Override public void onConnectionError(String description) { Log.e(TAG, description); }
    @Override public void onConnectionClosed() { }
    @Override public void onMessage(ByteBuffer buffer, boolean binary) { }
}, PeerConnectionParametersBuilder.builder().enableVideo(false).build());
// createInstance returns null if registering the Carrier extension failed.
Log.i(TAG, "WebrtcClient: " + (rtc != null ? "created" : "null") + ", state " +
        (rtc != null ? rtc.getCallState().getName() : "-"));

A real app shows an incoming-call UI in onInvite instead of answering immediately.

Verified output (webrtc-0.0.4 + carrier 6.0.4, Android 14 arm64 emulator, 2026-11-06):

I CarrierExtension: CarrierExtension instance created
D WebrtcClient: initialize: success
I CallSample: WebrtcClient: created, state init
I CallSample: carrier ready

createInstance(Context, Carrier, CallHandler, PeerConnectionParameters) returns the existing instance if one exists, ignoring the new arguments. WebrtcClient.getInstance() returns it later. Passing null parameters uses the builder defaults: video on at 1280x720 and 30 fps, vp8, opus, data channel off.

Identity

The WebRTC SDK has no identity of its own. Peers are addressed by their Carrier userId (see /docs/android-carrier). getPeerAddress() returns the current remote userId, and isInitiator() tells whether this side placed the call.

Friends

The WebRTC SDK doesn't manage friends. You can only call an accepted Carrier friend, because signaling is sent with the Carrier extension's friend invite. Make friends with Carrier.addFriend / acceptFriend first.

Messages

During a call you can open a WebRTC data channel. Pass dataEnabled = true to makeCall, or set enableData(true) on the builder:

rtc.sendMessage(ByteBuffer.wrap("hi".getBytes(StandardCharsets.UTF_8)), false);

The peer receives it in CallHandler.onMessage(ByteBuffer buffer, boolean binary). For messages outside a call, use Carrier.sendFriendMessage.

Files

The WebRTC SDK has no file API. Use Carrier file transfer (/docs/android-carrier) or your own framing over the data channel.

Sessions and calls

Place a call (audio-only here):

rtc.makeCall(friendUserId, /* audio */ true, /* video */ false, /* data */ false);

Video, camera and mute controls:

rtc.makeCall(friendUserId, true, true, false);   // audio + video, no data channel
rtc.renderVideo(local, remote);                  // main thread
rtc.switchCamera();
rtc.setResolution(640, 480, 30);
rtc.setAudioEnable(false);                       // mute mic
rtc.setVideoEnable(true);

local and remote are org.webrtc.SurfaceViewRenderer views from your layout. The SDK initialises them with its own EglBase and releases them when the call ends. swapVideoRenderer(boolean) swaps the two views.

Ending a call:

if (incomingNotAnswered) {
    rtc.rejectCall();              // peer gets onEndCall(CallReason.REJECT)
} else if (rtc.getCallState() != CallState.INIT) {
    rtc.hangupCall();              // peer gets onEndCall(CallReason.NORMAL_HANGUP)
}

Call states (CallState): INIT, INVITING, CONNECTING, RINGING (an offer has arrived and onInvite has fired), ACTIVE (media connected and onActive has fired).

Signaling over Carrier. Each signaling message is a JSON string sent with the Carrier extension's friend invite. Its type field is one of:

  • offer: carries sdp and an options array of "audio", "video", "data".
  • answer: carries sdp.
  • candidate: carries a candidates array. webrtc-0.0.4 sends one candidate per message; the branch head batches them and paces the sends.
  • removal-candidates
  • bye: carries a reason, which is CallReason -1 (reject), 0 (hang-up) or 1 (error).

Every message also carries a callId (an upper-case UUID chosen by the caller), and the callee echoes it. These invites use a separate extension channel, so they don't show up in your CarrierHandler.onFriendInviteRequest.

ICE servers. For each call the SDK asks Carrier for the network's TURN server (CarrierExtension.getTurnServerInfo()) and uses it as both stun: and turn: with the returned credentials. You can add your own servers before calling:

// Call before makeCall()/answerCall(); merged after the Carrier-provided TURN server.
WebrtcClient.setSupplementalIceServers(Collections.singletonList(
        PeerConnection.IceServer.builder("turn:turn.example.com:3478")
                .setUsername("user").setPassword("pass").createIceServer()));

Audio routing (optional). org.elastos.carrier.webrtc.util.WebRTCAudioManager takes care of speaker, earpiece, wired headset and Bluetooth routing. The SDK does not start it for you.

WebRTCAudioManager am = WebRTCAudioManager.create(context);
am.start((selected, available) -> Log.i("Audio", selected + " of " + available));
am.selectAudioDevice(WebRTCAudioManager.AudioDevice.SPEAKER_PHONE);
// call am.stop() when the call ends

Offline messages (Express)

Not applicable. Calls need both peers online, and the SDK does not use Express.

Threading and lifecycle

  • onInvite, onAnswer, and onEndCall caused by a remote bye all run on the Carrier thread: the SDK handles Carrier extension invites inline.
  • onActive, onIceConnected, onIceDisConnected, onConnectionError, onConnectionClosed and onMessage arrive on WebRTC / SDK worker threads.
  • None of these callbacks run on the main thread. Post to the main thread before you touch views, and call renderVideo from the main thread.
  • On the branch head (not yet in webrtc-0.0.4), answerCall() throws IllegalStateException if there is no pending remote offer, for example after the caller hung up, and a late bye from an earlier call (different callId) is ignored.
  • hangupCall() sends bye and releases the peer connection, the renderers and EglBase. The singleton lives for the whole process and has no public destroy method. Hang up before you kill() the Carrier instance.
  • makeCall throws IllegalArgumentException for a null peer or for your own userId, and WebrtcException on SDK errors.

API reference

WebrtcClient (extends org.elastos.carrier.CarrierExtension)

MemberSignature
createstatic WebrtcClient createInstance(Context context, Carrier carrier, CallHandler callHandler, CarrierPeerConnectionClient.PeerConnectionParameters peerConnectionParameters)
getstatic WebrtcClient getInstance()
ICEstatic void setSupplementalIceServers(List<PeerConnection.IceServer> servers)
callvoid makeCall(String peerAddress), void makeCall(String peerAddress, boolean videoEnabled), void makeCall(String peerAddress, boolean audioEnabled, boolean videoEnabled, boolean dataEnabled) (all throws WebrtcException)
answer / endvoid answerCall(), void rejectCall() throws WebrtcException, void hangupCall()
stateCallState getCallState(), String getPeerAddress(), boolean isInitiator()
videovoid renderVideo(SurfaceViewRenderer localRenderer, SurfaceViewRenderer remoteRenderer), void swapVideoRenderer(boolean isSwap), void switchCamera(), void setResolution(int width, int height, int fps)
media togglesvoid setAudioEnable(boolean), void setVideoEnable(boolean), void setDataEnable(boolean), void setMediaEnable(boolean video, boolean audio, boolean data)
datavoid sendMessage(ByteBuffer byteBuffer, boolean binary) throws WebrtcException

CallHandler (org.elastos.carrier.webrtc.call)

onInvite(String friendId, boolean audio, boolean video, boolean data), onAnswer(), onActive(), onEndCall(CallReason reason), onIceConnected(), onIceDisConnected(), onConnectionError(String description), onConnectionClosed(), onMessage(ByteBuffer buffer, boolean binary).

Enums

TypeValues
CallStateINIT(0), INVITING(1), CONNECTING(2), RINGING(3), ACTIVE(4); getValue(), getName()
CallReasonREJECT(-1), NORMAL_HANGUP(0), ERROR(1); getValue(), getName()

PeerConnectionParametersBuilder

static builder(), build() returning CarrierPeerConnectionClient.PeerConnectionParameters. Setters: enableVideo, enableData, videoWidth, videoHeight, videoFps, videoMaxBitrate, VideoCodec (sic, capital V), videoCodecHwAcceleration, videoFlexfecEnabled, audioStartBitrate, audioCodec, noAudioProcessing, aecDump, saveInputAudioToFile, useOpenSLES, disableBuiltInAEC, disableBuiltInAGC, disableBuiltInNS, disableWebRtcAGCAndHPF, tracing, enableRtcEventLog.

WebRTCAudioManager (org.elastos.carrier.webrtc.util)

static create(Context), start(AudioManagerEvents), stop(), setDefaultAudioDevice(AudioDevice), selectAudioDevice(AudioDevice), getAudioDevices(), getSelectedAudioDevice(), updateAudioDeviceState(). AudioDevice is one of SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE. AudioManagerEvents.onAudioDeviceChanged(AudioDevice selected, Set<AudioDevice> available).

Source