AudioCall Cloud
PricingGuides
Log inSign up

SDK Integration Guide

Everything you need to add real-time voice calls to your Android or iOS app. Every code snippet is annotated line-by-line — no prior audio SDK experience required.

On this page
RequirementsInstallationToken serverAndroidiOSAPI referenceCallbacksFAQChangelog
Requirements

Android

Minimum Android version

8.0 — API level 26

Compile SDK

34 or higher

Language

Java 8+ or Kotlin 1.6+

Build system

Gradle 7+

Architectures

arm64-v8a · armeabi-v7a · x86_64

iOS

Minimum iOS version

14.0

Xcode

14 or higher

Language

Objective-C or Swift 5.7+

Dependency manager

CocoaPods 1.11+

Architectures

arm64 (device) · x86_64 (simulator)

Token Server (required — any platform)

Get your App ID and App Secret from the developer dashboard → Apps. Then deploy this endpoint on your backend:

// server.js — Token generation endpoint (Node.js / Express)
// Run this on YOUR backend. Never put the App Secret in your mobile app.

const express = require("express");
const crypto  = require("crypto");
const app     = express();

// Get these from your AudioCall dashboard → Apps → your app
const APP_ID = process.env.AUDIOCALL_APP_ID;     // e.g. "abc123"
const SECRET = process.env.AUDIOCALL_APP_SECRET;  // e.g. "sk_live_..."

/**
 * Token format (base64-encoded):
 *   appId | channel | uid | expireTimestamp | hmac_sha256(secret, payload)
 *
 * The AudioCall signaling server verifies the HMAC and rejects expired tokens.
 */
function generateToken(channel, uid, ttlSeconds = 3600) {
    const expire  = Math.floor(Date.now() / 1000) + ttlSeconds; // Unix timestamp
    const payload = `${APP_ID}|${channel}|${uid}|${expire}`;
    const sig     = crypto.createHmac("sha256", SECRET).update(payload).digest("hex");
    return Buffer.from(`${payload}|${sig}`).toString("base64");
}

// GET /token?channel=room123&uid=user456
app.get("/token", (req, res) => {
    const { channel, uid } = req.query;
    if (!channel || !uid) return res.status(400).json({ error: "channel and uid required" });
    res.json({ token: generateToken(channel, uid), expiresIn: 3600 });
});

app.listen(3000);
Copy
Installation & Integration

Step 1 — Add the dependency

Open your app-level build.gradle, add the dependency, then sync Gradle.

// app/build.gradle
android {
    // Minimum supported Android version is 8.0 (API 26)
    // The SDK uses AudioRecord with VOICE_COMMUNICATION mode which requires API 26+
    minSdk 26

    // compileSdk should be 34 or higher to use the latest audio APIs
    compileSdk 34
}

dependencies {
    // AudioCall SDK — real-time audio engine (WebRTC transport + noise suppression + AEC)
    implementation 'com.audiosdk:audio-call-sdk:1.0.39'
}
Copy
Step 2 — Declare permissions in AndroidManifest.xml
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- RECORD_AUDIO: captures microphone input for the call -->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

    <!-- INTERNET: connects to the AudioCall signaling server and media relay -->
    <uses-permission android:name="android.permission.INTERNET" />

    <!-- MODIFY_AUDIO_SETTINGS: allows switching earpiece ↔ speakerphone at runtime -->
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

    <!-- BLUETOOTH_CONNECT: required for Bluetooth headset routing on Android 12+ -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

</manifest>
Copy
Step 3 — Request microphone permission at runtime

RECORD_AUDIO is classified as a "dangerous" permission. You must ask the user before the first call or joinSession will silently fail.

// RECORD_AUDIO is a "dangerous" permission — you must request it at runtime on Android 6+.
// The SDK will NOT request permissions on your behalf; call this before joinSession().

private static final int MIC_REQUEST_CODE = 101;

private void requestMicPermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
            != PackageManager.PERMISSION_GRANTED) {

        // Show the system permission dialog
        ActivityCompat.requestPermissions(this,
            new String[]{ Manifest.permission.RECORD_AUDIO },
            MIC_REQUEST_CODE);
    } else {
        startCall(); // already granted
    }
}

@Override
public void onRequestPermissionsResult(int requestCode,
        String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == MIC_REQUEST_CODE) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            startCall(); // user approved — safe to join
        } else {
            Toast.makeText(this, "Microphone permission required for calls", Toast.LENGTH_LONG).show();
        }
    }
}
Copy
Step 4 — Fetch a token and join a call
// Fetch a token from YOUR server, then join the call.
// Use OkHttp (or Retrofit) to make the HTTP request.

private void fetchTokenAndJoin(String roomId, String userId) {
    String url = "https://your-server.com/token"
               + "?channel=" + roomId
               + "&uid="     + userId;

    new OkHttpClient().newCall(new Request.Builder().url(url).build())
        .enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String token = new JSONObject(response.body().string()).getString("token");
                // Switch to main thread before touching the UI or calling joinSession
                runOnUiThread(() -> engine.joinSession(token, roomId, userId));
            }
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() ->
                    Toast.makeText(CallActivity.this, "Could not get call token", Toast.LENGTH_SHORT).show());
            }
        });
}
Copy
Step 5 — Full Activity integration (every line explained)
// CallActivity.java — complete, production-ready integration

import com.audiosdk.sdk.RtcEngine;
import com.audiosdk.sdk.IRtcEventHandler;

public class CallActivity extends AppCompatActivity implements IRtcEventHandler {

    // Copy APP_ID from your AudioCall dashboard → Apps → your app
    private static final String APP_ID = "YOUR_APP_ID_HERE";

    private RtcEngine engine;
    private boolean   isMuted   = false;
    private boolean   isSpeaker = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_call);

        // Creates the engine. Lightweight — audio capture does NOT start here.
        engine = RtcEngine.create(APP_ID, this);
    }

    // ── Call control ──────────────────────────────────────────────────────────

    // Call AFTER fetching a token from your server (see token section above).
    private void startCall(String token, String roomId, String userId) {
        // Starts audio capture, connects to signaling server, joins the room.
        // All IRtcEventHandler callbacks fire on the main thread.
        engine.joinSession(token, roomId, userId);
    }

    private void endCall() {
        // Stops audio, leaves the room, releases native resources.
        // Always call before destroy().
        engine.leaveSession();
    }

    private void toggleMute() {
        isMuted = !isMuted;
        engine.muteLocalAudioStream(isMuted);  // true = silence mic, false = restore
        muteButton.setText(isMuted ? "Unmute" : "Mute");
    }

    private void toggleSpeaker() {
        isSpeaker = !isSpeaker;
        engine.enableSpeakerphone(isSpeaker);  // true = loudspeaker, false = earpiece
    }

    // ── IRtcEventHandler callbacks ────────────────────────────────────────────

    @Override
    public void onJoinSessionSuccess(String channel, String uid, int elapsed) {
        // elapsed = ms it took to connect. Start your call timer here.
        runOnUiThread(() -> statusText.setText("Connected · " + channel));
    }

    @Override
    public void onLeaveSession(RtcEngine.RtcStats stats) {
        // stats.totalDuration = call seconds, stats.txBytes / rxBytes = data transferred
        runOnUiThread(() -> finish());
    }

    @Override
    public void onConnectionStateChanged(int state, int reason) {
        // 1=Disconnected 2=Connecting 3=Connected 4=Reconnecting 5=Failed
        runOnUiThread(() -> {
            if (state == 4) statusText.setText("Reconnecting…");
            if (state == 5) { endCall(); Toast.makeText(this, "Connection failed", Toast.LENGTH_SHORT).show(); }
        });
    }

    @Override
    public void onUserJoined(String uid, int elapsed) {
        runOnUiThread(() -> addParticipant(uid)); // remote user joined — update UI
    }

    @Override
    public void onUserOffline(String uid, int reason) {
        // reason 0 = left normally, 1 = dropped (network timeout)
        runOnUiThread(() -> removeParticipant(uid));
    }

    @Override
    public void onAudioVolumeIndication(String uid, int volume) {
        // volume 0–255, every ~100ms. uid="" = your own mic. Drive speaking indicators.
        runOnUiThread(() -> updateVolumeBar(uid, volume));
    }

    @Override
    public void onError(int code, String message) {
        // Log in development. Surface critical errors to the user in production.
        Log.e("AudioCall", "Error " + code + ": " + message);
    }

    // ── Cleanup ───────────────────────────────────────────────────────────────

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // Releases the native audio engine, threads, and sockets.
        // Failing to call this will leave audio running in the background.
        if (engine != null) engine.destroy();
    }
}
Copy
Step 6 — ProGuard / R8 rules (release builds only)

If you enable minifyEnabled true in your release build, add these rules to prevent R8 from stripping SDK classes.

# proguard-rules.pro
# The SDK uses JNI — prevent R8 from stripping or renaming SDK classes.

-keep class com.audiosdk.** { *; }
-dontwarn com.audiosdk.**
-keep interface com.audiosdk.sdk.IRtcEventHandler { *; }
-keep class com.audiosdk.sdk.RtcEngine { *; }
-keep class com.audiosdk.sdk.RtcEngine$RtcStats { *; }
Copy
API Reference — RtcEngine

RtcEngine.create(appId, delegate)

Create the engine. Lightweight — audio does not start yet. Call once at startup.

joinSession(token, roomId, userId)

Start audio capture and join the room. Token must be fresh (< 1 hour old).

leaveSession()

Stop audio, leave the room, release media resources. Always call before destroy().

muteLocalAudioStream(muted)

true = silence your mic (others still audible). false = restore. Safe to call mid-call.

enableSpeakerphone(enabled)

true = loudspeaker. false = earpiece. iOS auto-routes to Bluetooth if connected.

destroy()

Release all native resources: AudioUnit/AudioRecord, threads, sockets. Call in onDestroy/dealloc.

Callbacks — IRtcEventHandler

onJoinSessionSuccess(channel, uid, elapsed)

Room joined, audio flowing. elapsed = ms to connect. Start your call timer here.

onLeaveSession(stats)

Call ended or you left. stats.totalDuration / txBytes / rxBytes. Dismiss call UI here.

onConnectionStateChanged(state, reason)

1=Disconnected 2=Connecting 3=Connected 4=Reconnecting 5=Failed. Show status UI.

onUserJoined(uid, elapsed)

Remote participant entered the room. Add to your participant list.

onUserOffline(uid, reason)

Remote participant left (0) or dropped (1). Remove from participant list.

onAudioVolumeIndication(uid, volume)

Volume 0–255 every ~100ms. uid='' is your own mic. Drive speaking indicators.

onError(code, message)

Non-fatal warning or recoverable error. Log it; surface critical ones to the user.

Connection State Reference

1 — DISCONNECTED

Initial state. Not connected.

2 — CONNECTING

Handshake in progress. Do not call joinSession again.

3 — CONNECTED

Fully connected. Audio flowing. Start your timer.

4 — RECONNECTING

Lost connection, retrying automatically. Show a spinner.

5 — FAILED

Could not reconnect. Call leaveSession() and show an error.

Common Questions

Yes. Call leaveSession() to end the current call, then joinSession() again with a fresh token. Only call destroy() when the Activity/ViewController is being destroyed.

The call continues — tokens are only verified during joinSession(). If you need to re-join after expiry, fetch a new token and call joinSession() again.

Yes. All participants who joinSession() with the same roomId are connected in a multi-party audio conference. There is no participant limit in the SDK itself.

Yes, but use a foreground Service with a visible notification. Bind the RtcEngine lifecycle to the Service and declare FOREGROUND_SERVICE permission.

Yes. Enable 'Audio, AirPlay, and Picture in Picture' under Xcode → Target → Signing & Capabilities → Background Modes.

Android emulator supports mic input. iOS Simulator does not forward microphone — use a real iPhone. For CI, BrowserStack or a Mac-in-cloud service works.

Most likely RECORD_AUDIO permission was not granted before calling joinSession(). Check that onRequestPermissionsResult received PERMISSION_GRANTED before joining.

SDK Version History

All releases, changelog entries, and platform compatibility. Always use the latest stable version.

Ready to integrate?

Create a free account to get your App ID and App Secret. Free plan includes unlimited usage.

Create free accountGo to dashboard