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.
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
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)
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);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'
}<!-- 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>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();
}
}
}// 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());
}
});
}// 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();
}
}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 { *; }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.
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.
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.
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.
All releases, changelog entries, and platform compatibility. Always use the latest stable version.
Create a free account to get your App ID and App Secret. Free plan includes unlimited usage.