Implementing RTC Calls
CallKit is an RTC call functionality SDK built upon CallLib, featuring a default call interface for one-to-one and multi-party RTC call scenarios. You can quickly integrate it to implement RTC call features. We also provide open-source code for this module (GitHub · Gitee), allowing you to customize the UI based on your business needs.
Note
Room Participant Limit
- Considering mobile device bandwidth (especially in multi-party video scenarios) and UI interaction effects, we recommend limiting video calls to 16 participants and audio-only calls to 32 participants. Exceeding these limits may impact call quality.
- CallKit enforces participant limits in its code. By default, video calls support up to 7 participants, while audio calls support up to 20. Adjustments should not exceed the recommended limits.
- As an open-source SDK, you can modify these limits in the source code (see
CallSelectMemberActivity.javaforNORMAL_VIDEO_NUMBERandNORMAL_AUDIO_NUMBER).
Step 1: Service Activation
RTC services are not enabled by default for apps created on RC. Before using any RTC services, you need to activate them in the Console.
Note
Service activation/deactivation takes effect 15 minutes after configuration. To check if your app has successfully activated the service via SDK, use CallLib's isVoIPEnabled method.
Step 2: SDK Integration
You need to integrate the RTC call capability library CallLib and the IM capability library IMLib, which RTC services depend on. Optionally, you can integrate the beautification extension library based on your requirements.
For detailed steps, refer to Integrating CallLib SDK.
Step 3: Code Obfuscation
If the developer's app enables code obfuscation, make sure to add the following configurations in the app/proguard-rules.pro file:
-keepattributes Exceptions,InnerClasses
-keepattributes Signature
#RongRTCLib
-keep public class cn.rongcloud.** {*;}
#RongIMLib
-keep class io.rong.** {*;}
-keep class cn.rongcloud.** {*;}
-keep class * implements io.rong.imlib.model.MessageContent {*;}
-dontwarn io.rong.push.**
-dontnote com.xiaomi.**
-dontnote com.google.android.gms.gcm.**
-dontnote io.rong.**
-ignorewarnings
Step 4: Permission Configuration
-
Declare all required permissions in
AndroidManifest.xml.<!-- Network permissions for RTC -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Camera permission -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Microphone permission -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> -
For apps targeting Android 6.0 (API level 23) or higher, request camera (
CAMERA) and microphone (RECORD_AUDIO) permissions when users initiate or answer calls. Refer to Android's official documentation on runtime permissions and requesting permissions.
Step 5: Initialization
The RTC SDK relies on the IM SDK as its signaling channel, so IM SDK must be initialized first. Initialization is required only once per app lifecycle unless the AppKey changes. We recommend placing this call in the onCreate() method of your Application class or at the entry point of your RTC module.
Sample Code:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
String appKey = "YourAppKey"; // AppKey from the Console, e.g., bos9p5rlcm2ba
InitOption initOption = new InitOption.Builder().build();
IMCenter.init(this, appKey, initOption);
}
}
Multi-Process Considerations
The RC SDK uses a multi-process mechanism by default (to modify this, refer to [Process Documentation]). After initialization, the following processes are launched:
- The app's main process
<app-package-name>:ipc. This is the core IM communication process, isolated from the main process.io.rong.push: The default push process. Its activation depends on push channel policies. For details, see [Enabling Push].
Step 6: Connecting to IM Services
RTC signaling between users relies on RC's IM services, so a TCP long connection must be established via connect(). We recommend calling this at the entry point of your RTC module. Disconnect using disconnect() or logout() when the module exits.
Sample Code
RongIM.connect("User Token", new RongIMClient.ConnectCallback() {
@Override
public void onSuccess(String userId) {
// Connection successful
}
@Override
public void onError(RongIMClient.ConnectionErrorCode code) {
// Connection failed
}
@Override
public void onDatabaseOpened(RongIMClient.DatabaseOpenStatus databaseOpenStatus) {
// Database open failed
}
});
Note
- If network issues cause connection failure, the SDK automatically retries up to 10 times with intervals of 0.05, 0.25, 0.5, 1, 2, 4, 8, 16, 32, and 64 seconds. If still unsuccessful, it retries when network status changes (e.g., reconnection or network switch).
- If the app is killed and relaunched via a push notification, call
connectagain to re-establish the connection.
Step 7: Caller Implementation
Typically, both caller and callee logic coexist in an app, so both need integration.
Initiating a One-to-One Call
Interface
RongCallKit.startSingleCall(context, targetId, mediaType);
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| context | Context | Yes | Context |
| targetId | String | Yes | Target UserId |
| mediaType | CallMediaType | Yes | Call type: audio or video. |
Sample Code
RongCallKit.startSingleCall(MainActivity.this, "Target UserId", RongCallKit.CallMediaType.CALL_MEDIA_TYPE_VIDEO);
Initiating a Multi-Party Call
Interface
RongCallKit.startMultiCall(context, conversationType, targetId, mediaType, userIds);
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| context | Context | Yes | Context |
| conversationType | Conversation.ConversationType | Yes | Conversation type |
| targetId | String | Yes | GroupId where callees are located |
| mediaType | CallMediaType | Yes | Call type: audio or video. |
| userIds | ArrayList<String> | Yes | List of callee UserIds |
Sample Code
String targetId = "GroupId1";
RongCallKit.CallMediaType mediaType = RongCallKit.CallMediaType.CALL_MEDIA_TYPE_VIDEO;
ArrayList<String> userIds = new ArrayList<>();
userIds.add("UserId1");
userIds.add("UserId2");
RongCallKit.startMultiCall(MainActivity.this, Conversation.ConversationType.GROUP, targetId, mediaType, userIds);
Step 8: Callee Implementation
Answering Calls
- When the app is in the foreground, the call interface automatically pops up upon receiving an invitation.
- For Android ≤ 10 devices, the call interface also pops up when the app is in the background.
- For Android > 10 devices, due to system restrictions, only a banner notification is shown, prompting the user to answer or decline.
Call-Related Callbacks
IRongCallListener monitors call states. CallKit's RongCallProxy.java implements this listener and forwards callbacks to methods in BaseCallActivity. You can extend BaseCallActivity and override relevant methods to handle callbacks.
public class MyCallActivity extends BaseCallActivity {
/**
* Call dialed.
* Notifies caller of call details after dialing.
*
* @param callSession Call session.
* @param localVideo Local camera feed.
*/
@Override
public void onCallOutgoing(RongCallSession callSession, SurfaceView localVideo) {
super.onCallOutgoing(callSession, localVideo);
}
/**
* Call connected.
* Notifies when the call is answered.
*
* @param callSession Call session.
* @param localVideo Local camera feed.
*/
@Override
public void onCallConnected(RongCallSession callSession, SurfaceView localVideo) {
super.onCallConnected(callSession, localVideo);
}
/**
* Call disconnected.
* Triggered when the call ends (remote hangup, local hangup, or network issues).
*
* @param callSession Call session.
* @param reason Disconnection reason.
*/
@Override
public void onCallDisconnected(RongCallSession callSession, RongCallCommon.CallDisconnectedReason reason) {
super.onCallDisconnected(callSession, reason);
}
/**
* Remote user ringing.
* Notifies caller when the callee starts ringing.
*
* @param userId Ringing user ID.
*/
@Override
public void onRemoteUserRinging(String userId) {
super.onRemoteUserRinging(userId);
}
/**
* Remote user joined.
* Notifies when a callee joins the call.
*
* @param userId Joining user ID.<br />
* @param mediaType Media type: audio or video.<br />
* @param userType User type: 1 (normal user), 2 (observer).<br />
* @param remoteVideo Remote camera feed. Null if userType is 2.<br />
* For video calls initiated via `startCall()` or `acceptCall()`, mirroring can be adjusted:<br />
* <pre class="prettyprint">
* public void onRemoteUserJoined(String userId, RongCallCommon.CallMediaType mediaType, int userType, SurfaceView remoteVideo) {
* if (null != remoteVideo) {
* ((RongRTCVideoView) remoteVideo).setMirror(boolean); // Enable/disable mirroring
* }
* }
* </pre>
*/
@Override
public void onRemoteUserJoined(String userId, RongCallCommon.CallMediaType mediaType, int userType, SurfaceView remoteVideo) {
super.onRemoteUserJoined(userId, mediaType, userType, remoteVideo);
}
/**
* Remote user invited.
* Notifies when a participant invites another user.
* @param userId Invited user ID. Check observer list via `RongCallClient.getInstance().getCallSession().getObserverUserList().contains(userId)`.
* @param mediaType
*/
@Override
public void onRemoteUserInvited(String userId, RongCallCommon.CallMediaType mediaType) {
super.onRemoteUserInvited(userId, mediaType);
}
/**
* Remote user left.
* Notifies when a participant leaves.
*
* @param userId Leaving user ID.
* @param reason Leave reason.
*/
@Override
public void onRemoteUserLeft(String userId, RongCallCommon.CallDisconnectedReason reason) {
super.onRemoteUserLeft(userId, reason);
}
/**
* Media type changed.
* Notifies when a participant switches between audio and video.
*
* @param userId User ID.
* @param mediaType New media type.
* @param video Camera feed (null if switching to audio).
*/
@Override
public void onMediaTypeChanged(String userId, RongCallCommon.CallMediaType mediaType, SurfaceView video) {
super.onMediaTypeChanged(userId, mediaType, video);
}
/**
* Call error.
*
* @param errorCode Error reason.
*/
@Override
public void onError(RongCallCommon.CallErrorCode errorCode) {
super.onError(errorCode);
}
/**
* Remote camera state changed.
*
* @param userId Remote user ID.
* @param disabled Whether the camera is disabled.
*/
@Override
public void onRemoteCameraDisabled(String userId, boolean disabled) {
super.onRemoteCameraDisabled(userId, disabled);
}
/**
* Remote microphone state changed.
*
* @param userId Remote user ID.
* @param disabled Whether the microphone is disabled.
*/
@Override
public void onRemoteMicrophoneDisabled(String userId, boolean disabled) {
super.onRemoteMicrophoneDisabled(userId, disabled);
}
/**
* Packet loss rate notification.
*
* @param userId Remote user ID.
* @param lossRate Packet loss rate (0-100).
*/
@Override
public void onNetworkReceiveLost(String userId, int lossRate) {
super.onNetworkReceiveLost(userId, lossRate);
}
/**
* Outbound packet loss rate notification.
*
* @param lossRate Packet loss rate (0-100).
* @param delay Network latency (ms).
*/
@Override
public void onNetworkSendLost(int lossRate, int delay) {
super.onNetworkSendLost(lossRate, delay);
}
/**
* First remote video frame received.
*
* @param userId
* @param height
* @param width
*/
@Override
public void onFirstRemoteVideoFrame(String userId, int height, int width) {
super.onFirstRemoteVideoFrame(userId, height, width);
}
/**
* Local audio level.
*
* @param audioLevel
*/
@Override
public void onAudioLevelSend(String audioLevel) {
super.onAudioLevelSend(audioLevel);
}
/**
* Remote audio level.
*
* @param audioLevel
*/
@Override
public void onAudioLevelReceive(HashMap<String, String> audioLevel) {
super.onAudioLevelReceive(audioLevel);
}
/**
* Remote user published custom video stream.
*
* @param userId User ID.
* @param streamId Stream ID.
* @param tag Stream tag.
* @param surfaceView
*/
@Override
public void onRemoteUserPublishVideoStream(String userId, String streamId, String tag, SurfaceView surfaceView) {
super.onRemoteUserPublishVideoStream(userId, streamId, tag, surfaceView);
}
/**
* Remote user unpublished custom video stream.
*
* @param userId User ID.
* @param streamId Stream ID.
* @param tag Stream tag.
*/
@Override
public void onRemoteUserUnpublishVideoStream(String userId, String streamId, String tag) {
super.onRemoteUserUnpublishVideoStream(userId, streamId, tag);
}
}
If the above methods are insufficient, you can modify RongCallProxy.java to implement custom listeners. Example:
public class RongCallProxy implements IRongCallListener {
private IRongCallListener mCallListener; // Add a custom listener.
/*Set your app's listener*/
public void setAppCallListener(IRongCallListener listener) {
this.mAppCallListener = listener;
}
/*Modify callback methods to forward to your custom listener*/
@Override
public void onCallOutgoing(RongCallSession callSession, SurfaceView localVideo) {
if (mCallListener != null) {
mCallListener.onCallOutgoing(callSession, localVideo);
}
/*Forward to custom listener*/
if(mAppCallListener != null) {
mAppCallListener.onCallOutgoing(callSession, localVideo);
}
}
... // Modify other callback methods similarly.
}
After modification, call setAppCallListener() in your app to set up the custom listener.
Step 9: Handling Incoming Calls
Due to Android's fragmentation across versions and OEMs, CallKit handles incoming call notifications differently based on Android version, CallKit version, and app state (foreground/background).
When the app is in the foreground, CallKit displays the incoming call interface.
When the app is in the background:
- For Android < 10 devices, CallKit can display the call interface. If the interface fails to appear, check if the app has "Display over other apps" permission enabled (some devices require manual permission granting).
- For Android ≥ 10 devices, note that background activity launches are restricted (see [Android Official Docs]). If using CallKit ≥ 5.1.9, the SDK shows a prompt with a ringtone. For CallKit < 5.1.9, a notification is shown (with a notification sound). Tapping the notification opens the call interface.
If the app is killed or the user logs out, remote push notifications are required to receive call alerts. For details, refer to Android Push. With push integrated, users can