跳到主要内容

实现音视频通话

CallKit 是在 CallLib 基础上,增加了一套默认呼叫界面的音视频呼叫功能 SDK,包含了单人、多人音视频呼叫的各种场景和功能,您可以快速的集成它来实现音视频呼叫场景。我们还提供了这个模块的开源代码GitHub · Gitee),您可以根据业务需要进行界面修改。

注意

房间人数上限

  1. 考虑移动设备的带宽(主要是在多路视频情况下)和 UI 交互效果,建议单次通话或房间内,视频不超过 16 人,纯音频不超过 32 人。超过此上限可能影响通话效果。
  2. CallKit 代码中已设置人数上限。默认发起视频呼叫时,最多可选 7 人。发起音频呼叫时,最多可选 20 人。如需调整,建议勿超过建议上限。
  3. CallKit 为开源 SDK,可在源码中修改上限(修改位置:CallSelectMemberActivity.java 中的 NORMAL_VIDEO_NUMBERNORMAL_AUDIO_NUMBER)。

步骤 1:服务开通

您在融云创建的应用默认不会启用音视频服务。在使用融云提供的任何音视频服务前,您需要前往控制台,为应用开通音视频服务。

具体步骤请参阅 开通音视频服务

注意

服务开通、关闭等设置完成后 30 分钟后生效。如需通过 SDK 判断您的 App 是否已成功开通服务,可使用 CallLib 的 isVoIPEnabled 方法。

步骤 2:SDK 导入

您需要导入融云音视频通话能力库 CallLib,和 RTC 业务所依赖的即时通讯能力库 IMLib。根据您的业务需求,可选择导入美颜扩展库。

具体步骤请参阅 导入 CallLib SDK

步骤 3:代码混淆

若开发者发布的 App 启用代码混淆,请务必在 app/proguard-rules.pro 文件添加如下配置:

-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

步骤 4:权限配置

  1. AndroidManifest.xml 中声明 SDK 需要的所有权限。

    <!-- 音视频需要网络权限 和 监听网络状态权限 -->
    <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" />
    <!-- 摄像头采集需要 -->
    <uses-permission android:name="android.permission.CAMERA" />
    <!-- 音频采集需要 -->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
  2. 如果您的应用需要支持 Android 6.0(API 级别 23)或更高版本的设备,您还需要在 App 用户使用对应功能时(例如发起呼叫、接听)请求摄像头(CAMERA)、麦克风(RECORD_AUDIO)权限。详见 Android 开发者官方文档运行时权限请求权限的工作流

步骤 5:初始化

音视频 SDK 是基于即时通信 SDK 作为信令通道的,所以要先初始化 IM SDK。如果不换 AppKey,在整个应用生命周期中,初始化一次即可。建议调用位置放在 Application 的 onCreate() 方法内,或在音视频功能模块的加载位置处。

  • 示例代码:

    public class App extends Application {
    @Override
    public void onCreate() {
    super.onCreate();
    RongIM.init(this, "从控制台申请的 AppKey");
    }
    }

融云即时通信 SDK 采用了后台进程方式来确保稳定性。运行后会发现以下三个进程:

  1. App 进程,进程名为 App 的包名。
  2. 融云 IM 进程,进程名为 ipc。
  3. 融云推送进程,如集成了厂商推送,则不会存在此进程,进程名为 io.rong.push。

步骤 6:连接 IM 服务

音视频用户之间的信令传输依赖于融云的即时通信(IM)服务,因此需要先调用 connect() 与 IM 服务建立好 TCP 长连接。建议在功能模块的加载位置处调用,之后再进行音视频呼叫业务。当模块退出后调用 disconnect()logout() 断开该连接。

RongIM.connect("用户 Token", new RongIMClient.ConnectCallback() {
@Override
public void onSuccess(String userId) {
// 连接成功
}

@Override
public void onError(RongIMClient.ConnectionErrorCode code) {
// 连接失败
}

@Override
public void onDatabaseOpened(RongIMClient.DatabaseOpenStatus databaseOpenStatus) {
// 数据库打开失败
}
});

注意

  • 如调用此接口时,遇到网络不好导致连接失败,SDK 会自动启动重连机制进行最多 10 次重连,重连时间间隔分别为 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 秒。在这之后如果仍没有连接成功,还会在检测到设备网络状态变化,比如网络恢复或切换网络时再次尝试重连。
  • 如 App 在被杀死后,接收到了推送通知,点击通知拉起应用时,需要再次调用 connect 方法进行连接。

步骤 7:呼叫方

通常 App 内呼叫和被叫方逻辑会同时存在,所以需要分别集成。

发起单人呼叫

RongCallKit.startSingleCall(MainActivity.this, "对方 UserId", RongCallKit.CallMediaType.CALL_MEDIA_TYPE_VIDEO);
参数类型必填说明
contextContext上下文
targetIdString对方的 UserId
mediaTypeCallMediaType会话媒体类型,包括纯音频呼叫和音视频呼叫。

发起多人呼叫

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);
参数类型必填说明
contextContext上下文
conversationTypeConversation.ConversationType会话类型
targetIdString被叫用户所在共同群组的 GroupId
mediaTypeCallMediaType会话媒体类型,包括纯音频呼叫和音视频呼叫。
userIdsArrayList<String>被叫用户 UserId 列表

步骤 8:接听方

接听呼叫

  • App 在前台时,当收到邀请时会自动弹出呼叫界面。
  • App 在后台,且 Android 系统 ≤ 10 的手机,仍然会自动弹出呼叫界面。
  • App 在后台,且 Android 系统 > 10 的手机,因受到新规则的限制,只能在手机上方弹出横幅通知栏,提示用户接听或挂断。

呼叫相关回调

IRongCallListener 是呼叫状态的监听类,CallKit 的 RongCallProxy.java 已经实现了该监听,并且会回调到 BaseCallActivity 中的各个方法,您可以继承 BaseCallActivity,根据需要复写其中的对应方法,即可获取对应的呼叫回调。

public class MyCallActivity extends BaseCallActivity {
/**
* 电话已拨出。
* 主叫端拨出电话后,通过回调 onCallOutgoing 通知当前 call 的详细信息。
*
* @param callSession 通话实体。
* @param localVideo 本地 camera 信息。
*/
@Override
public void onCallOutgoing(RongCallSession callSession, SurfaceView localVideo) {
super.onCallOutgoing(callSession, localVideo);
}

/**
* 已建立通话。
* 通话接通时,通过回调 onCallConnected 通知当前 call 的详细信息。
*
* @param callSession 通话实体。
* @param localVideo 本地 camera 信息。
*/
@Override
public void onCallConnected(RongCallSession callSession, SurfaceView localVideo) {
super.onCallConnected(callSession, localVideo);
}

/**
* 通话结束。
* 通话中,对方挂断,己方挂断,或者通话过程网络异常造成的通话中断,都会回调 onCallDisconnected。
*
* @param callSession 通话实体。
* @param reason 通话中断原因。
*/
@Override
public void onCallDisconnected(RongCallSession callSession, RongCallCommon.CallDisconnectedReason reason) {
super.onCallDisconnected(callSession, reason);
}

/**
* 被叫端正在振铃。
* 主叫端拨出电话,被叫端收到请求,发出振铃响应时,回调 onRemoteUserRinging。
*
* @param userId 振铃端用户 id。
*/
@Override
public void onRemoteUserRinging(String userId) {
super.onRemoteUserRinging(userId);
}

/**
* 被叫端加入通话。
* 主叫端拨出电话,被叫端收到请求后,加入通话,回调 onRemoteUserJoined。
*
* @param userId 加入用户的 id。<br />
* @param mediaType 加入用户的媒体类型,audio or video。<br />
* @param userType 加入用户的类型,1:正常用户,2:观察者。<br />
* @param remoteVideo 加入用户者的 camera 信息。如果 userType为2,remoteVideo对象为空;<br />
* 如果对端调用{@link RongCallClient#startCall(int, boolean, Conversation.ConversationType, String, List, List, RongCallCommon.CallMediaType, String, StartCameraCallback)} 或
* {@link RongCallClient#acceptCall(String, int, boolean, StartCameraCallback)}开始的音视频通话,则可以使用如下设置改变对端视频流的镜像显示:<br />
* <pre class="prettyprint">
* public void onRemoteUserJoined(String userId, RongCallCommon.CallMediaType mediaType, int userType, SurfaceView remoteVideo) {
* if (null != remoteVideo) {
* ((RongRTCVideoView) remoteVideo).setMirror( boolean);//观看对方视频流是否镜像处理
* }
* }
* </pre>
*/
@Override
public void onRemoteUserJoined(String userId, RongCallCommon.CallMediaType mediaType, int userType, SurfaceView remoteVideo) {
super.onRemoteUserJoined(userId, mediaType, userType, remoteVideo);
}

/**
* 通话中的某一个参与者,邀请好友加入通话,发出邀请请求后,回调 onRemoteUserInvited。
* @param userId 被邀请者的ID ,可以通过RongCallClient.getInstance().getCallSession().getObserverUserList().contains(userId) ,查看加入的用户是否在观察者列表中
* @param mediaType
*/
@Override
public void onRemoteUserInvited(String userId, RongCallCommon.CallMediaType mediaType) {
super.onRemoteUserInvited(userId, mediaType);
}

/**
* 通话中的远端参与者离开。
* 回调 onRemoteUserLeft 通知状态更新。
*
* @param userId 远端参与者的 id。
* @param reason 远端参与者离开原因。
*/
@Override
public void onRemoteUserLeft(String userId, RongCallCommon.CallDisconnectedReason reason) {
super.onRemoteUserLeft(userId, reason);
}

/**
* 当通话中的某一个参与者切换通话类型,例如由 audio 切换至 video,回调 onMediaTypeChanged。
*
* @param userId 切换者的 userId。
* @param mediaType 切换者,切换后的媒体类型。
* @param video 切换者,切换后的 camera 信息,如果由 video 切换至 audio,则为 null。
*/
@Override
public void onMediaTypeChanged(String userId, RongCallCommon.CallMediaType mediaType, SurfaceView video) {
super.onMediaTypeChanged(userId, mediaType, video);
}

/**
* 通话过程中,发生异常。
*
* @param errorCode 异常原因。
*/
@Override
public void onError(RongCallCommon.CallErrorCode errorCode) {
super.onError(errorCode);
}

/**
* 远端参与者 camera 状态发生变化时,回调 onRemoteCameraDisabled 通知状态变化。
*
* @param userId 远端参与者 id。
* @param disabled 远端参与者 camera 是否可用。
*/
@Override
public void onRemoteCameraDisabled(String userId, boolean disabled) {
super.onRemoteCameraDisabled(userId, disabled);
}

/**
* 远端参与者 麦克风 状态发生变化时,回调 onRemoteMicrophoneDisabled 通知状态变化。
*
* @param userId 远端参与者 id。
* @param disabled 远端参与者 Microphone 是否可用。
*/
@Override
public void onRemoteMicrophoneDisabled(String userId, boolean disabled) {
super.onRemoteMicrophoneDisabled(userId, disabled);
}


/**
* 接收丢包率信息回调
*
* @param userId 远端用户的ID
* @param lossRate 丟包率:0-100
*/
@Override
public void onNetworkReceiveLost(String userId, int lossRate) {
super.onNetworkReceiveLost(userId, lossRate);
}

/**
* 发送丢包率信息回调
*
* @param lossRate 丢包率,0-100
* @param delay 发送端的网络延迟
*/
@Override
public void onNetworkSendLost(int lossRate, int delay) {
super.onNetworkSendLost(lossRate, delay);
}

/**
* 收到某个用户的第一帧视频数据
*
* @param userId
* @param height
* @param width
*/
@Override
public void onFirstRemoteVideoFrame(String userId, int height, int width) {
super.onFirstRemoteVideoFrame(userId, height, width);
}

/**
* 本端音量大小回调
*
* @param audioLevel
*/
@Override
public void onAudioLevelSend(String audioLevel) {
super.onAudioLevelSend(audioLevel);
}

/**
* 对端音量大小回调
*
* @param audioLevel
*/
@Override
public void onAudioLevelReceive(HashMap<String, String> audioLevel) {
super.onAudioLevelReceive(audioLevel);
}

/**
* 远端用户发布了自定义视频流
*
* @param userId 发布了自定义视频流的用户
* @param streamId 自定义视频流Id
* @param tag 流标签
* @param surfaceView
*/
@Override
public void onRemoteUserPublishVideoStream(String userId, String streamId, String tag, SurfaceView surfaceView) {
super.onRemoteUserPublishVideoStream(userId, streamId, tag, surfaceView);
}

/**
* 远端用户取消发布自定义视频流
*
* @param userId 取消发布自定义视频流的用户
* @param streamId 自定义视频流Id
* @param tag 流标签
*/
@Override
public void onRemoteUserUnpublishVideoStream(String userId, String streamId, String tag) {
super.onRemoteUserUnpublishVideoStream(userId, streamId, tag);
}
}

如果上述方法不适合,您还可以通过修改 RongCallProxy.java 的代码,实现自己应用的监听。示例如下:

public class RongCallProxy implements IRongCallListener {

private IRongCallListener mCallListener; // 增加一个监听。

/*设置自己应用的监听*/
public void setAppCallListener(IRongCallListener listener) {
this.mAppCallListener = listener;
}

/*修改对应的通话状态回调的方法,使其回调到您设置的应用自身的监听*/
@Override
public void onCallOutgoing(RongCallSession callSession, SurfaceView localVideo) {
if (mCallListener != null) {
mCallListener.onCallOutgoing(callSession, localVideo);
}
/*增加的代码,回调应用设置的监听*/
if(mAppCallListener != null) {
mAppCallListener.onCallOutgoing(callSession, localVideo);
}
}
... // 根据您的需要,同样的方式修改其它通话状态回调函数。
}

修改完上述方法后,在您的应用里调用 setAppCallListener() 设置您自己的监听。

步骤 9:获取来电通知

因 Android 系统多版本,多硬件厂商,面临碎片化等问题,在处理音视频来电通知时,SDK 根据 Android 版本、CallKit 版本、与 App 处于前台/后台的状态有不同的处理方式。

应用程序在前台时,CallKit 可在远端发起呼叫时启动来电界面。

应用程序处于后台时,CallKit 的处理方式如下:

  • 如果当前设备为 Android 10 之前版本的手机,CallKit 可启动来电界面。如果无法弹出 CallKit 的呼叫界面,请检查您的设备是否允许 App 使用「后台弹出界面」权限(某些 Android 设备上要求用户去系统设置中手动为 App 打开"后台弹出界面"的权限)。
  • 如果当前设备为 Android 10 及之后版本的手机,请注意从 Android 10 开始,系统禁止应用在后台启动 Activity(参见 Android 官方说明)。如果您集成的 CallKit ≧ 5.1.9,SDK 会弹窗提示,并长响铃。如果您集成的 CallKit < 5.1.9,SDK 会弹出通知栏通知用户,提示音是通知音。App 用户点击通知后可打开通话页面。

应用程序长时间在后台可能被系统回收,或者 App 用户下线时,则必须集成远程推送才能收到来电推送通知。详细请参考 Android 推送。集成离线推送后,即使 App 已经被系统回收,也可以收到呼叫的推送通知。

注意

集成 FCM 推送时您可能需要自行实现音视频信令消息(呼叫邀请、挂断等)的通知弹出逻辑。

如遇到关于来电通知的问题,可参考以下知识库:

接入扩展插件

  • CallKit 可以接入官方美颜插件或相芯美颜插件。注意,相芯美颜插件要求 CallKit 版本 ≧ 5.4.0。详见 CallLib 文档美颜插件