跳到主要内容

实现音视频通话

CallKit 基于 CallLib SDK 基础增加了一套默认呼叫界面,包含了单人、多人音视频呼叫的各种场景和功能。

提示

房间人数上限

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

环境要求

适用于 HarmonyOS 的 CallKit SDK 的最低要求是:

  • DevEco Studio NEXT Release(5.0.3.900) 及以上。
  • HarmonyOS SDK API 12 及以上。
  • 手机(真机)系统版本号:NEXT.0.0.31

前置条件

  • 创建融云开发者账号,获取 App Key。注册成功后,融云控制台会默认自动创建您的首个应用,默认生成开发环境下的 App Key,使用国内数据中心。注意:同一个应用的开发环境与生产环境提供不同的 App Key,两个环境之间数据隔离。
  • 完成开通音视频服务。您需要开通音视频通话服务。

快速上手

步骤 1 导入 SDK

导入融云音视频通话能力 UI 库 CallKit,具体步骤请参阅 导入 CallKit SDK

步骤 2 初始化

RTC 音视频能力是基于 IM 作为信令通道的,CallKit 又依赖于 IMKit & CallLib,所以要先初始化 IM 。如果不换 AppKey,在整个应用生命周期中,初始化一次即可。建议调用位置放在应用启动位置处,或在音视频功能模块的加载位置处。 在 UIAbility 的 onCreate() 方法中,调用初始化方法,传入生产或开发环境的 App Key。

TypeScript
// 在 UIAbility 中获取 context
let context = this.context

let initOption = new InitOption();
let appKey = "从融云后台获取的 appKey";
IMEngine.getInstance().init(context, appKey, initOption);

步骤 3 连接 IM 服务

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

TypeScript
// 连接 IM
IMEngine.getInstance().connect(token, 20).then(result => {
if (EngineError.Success === result.code) {
// 连接成功
let userId = result.userId;
return
}
if (EngineError.ConnectTokenExpired === result.code) {
// Token 过期,从 APP 服务请求新 token,获取到新 token 后重新 connect()
} else if (EngineError.ConnectionTimeout === result.code) {
// 连接超时,弹出提示,可以引导用户等待网络正常的时候再次点击进行连接
} else {
//其它业务错误码,请根据相应的错误码作出对应处理。
}
});

步骤 4 实现监听

在通话前需要设置 RCCallRCCallListener 监听,在 CallKit 页面跳转时提供必要的 UI 组件。

TypeScript
RCCall.getInstance().callListener = {
/**
* 获取当前 Ability 页面对应的 WindowStage 实例。
*
* 说明:
* - 在 Ability 的生命周期方法 `onWindowStageCreate` 中,首次创建并缓存 `windowStage`。
* - 通过 `didWindowStage` 方法获取缓存的 `windowStage`,方便在其他逻辑中调用。
*
* 示例:
* ```ts
* onWindowStageCreate(windowStage: window.WindowStage): void {
* // 主窗口创建完成,缓存 windowStage 以便后续使用
* AppStorage.setOrCreate('windowStage', windowStage);
* }
* ```
*
* @returns 当前 Ability 缓存的 WindowStage 实例
*/
didWindowStage: () => {
let windowStage = AppStorage.get('windowStage') as window.WindowStage
return windowStage
},
/**
* 多人通话选人页面导航栈, 用于选人页面导航, 必须设置, 否则无法导航到选人页面
* ⚠️ 注意:
* 该方法必须返回当前页面中实际使用的 Navigation 容器绑定的 NavPathStack 对象。
*
* * 正确示例(页面中定义并绑定了 navStack):
*
* ```ts
* private navStack: NavPathStack = new NavPathStack();
*
* Navigation({ stack: this.navStack }) {
* // 页面内容
* }
*/
didMultiCallNavPathStack: () => {
return this.navStack; // ✅ 返回实际绑定在 UI 上的导航栈
},
/**
* 点击 CallKit 横幅或左上角胶囊后跳转回到的 Ability 页面名
* ⚠️ 注意:
* 此处的页面名须使用 module.json5 中 module -> abilities -> name 定义字义的字符串
*/
didCallKitNavAbilityName: () => {
/// 该方法返回,所集成项目的 UIAbility
return 'CallKitEntryAbility'
},
/**
* 是否允许选择某个用户
* 非必传,默认允许选择
* @param userId 用户 userId
*/
didUserSelectable: async (userId: string): Promise<boolean> => {
/**
* 根据条件判断是否允许选择该用户
* 不允许时返回 false
*/
return false;
},
/**
* 按钮插件过滤
* 非必传,插件默认显示
* @param id 会话对象
* @param mediaType 媒体类型,音频或视频
* @returns boolean
*/
didPluginFilter: (id: ConversationIdentifier, mediaType: RCCallMediaType): boolean => {
// 如需过滤掉视频插件
if (mediaType === RCCallMediaType.VIDEO) {
return false;
};
}
}

步骤 5 呼叫方

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

发起单人呼叫

TypeScript
/**
* 发起单聊呼叫
* @param userId 被叫端Id
* @param mediaType 呼叫媒体类型
*/
public startSingleCall(targetId: string, mediaType: RCCallMediaType)
参数类型必填说明
targetIdstring对方的用户ID
mediaTypeRCCallMediaType媒体类型
  • 示例代码:
TypeScript
// 使用时填写真实 id
let targetId = '被叫端UserId'
// 媒体类型 RCCallMediaType
let mediaType = RCCallMediaType.VIDEO
RCCall.getInstance().startSingleCall(targetId, mediaType)

发起多人呼叫

  1. 调用 startMultiCall 会先弹出群组选择成员界面,所以发起多人呼叫前,需要优先实现 IMKit 的 UserDataProvider 接口,来提供群组信息,群成员信息等数据来源。
TypeScript
RongIM.getInstance().userDataService().setUserDataProvider({        
/**
* 获取群组信息数据,当需要展示群组头像、名称时,如果 SDK 没有对应的信息时触发该方法
*```
* 获取到群组信息后,SDK 会缓存该数据,并触发监听
* SDK 缓存之后就会一直使用,不会自动更新。如果群组信息发生变化,需要调用 UserDataService.updateUserInfo 主动刷新 SDK 信息
*```
* @param groupId 群 id
* @returns 群组信息。注意:如果返回 undefined ,SDK则不会更新缓存。
*/
fetchUserInfo: (userId: string) => {
return new Promise<UserInfoModel | undefined>((resolve, reject) => {
// 调试代码
if (userId === 'test1') {
let userInfo = new UserInfoModel(userId, 'test1_name', 'https://xxx.png')
resolve(userInfo)
}
})
},
/**
* 获取群组成员信息数据,当需要展示群组成员头像、名称时,如果 SDK 没有对应的信息时触发该方法
*```
* 获取到群组成员信息后,SDK 会缓存该数据,并触发监听
* SDK 缓存之后就会一直使用,不会自动更新。如果群组成员信息发生变化,需要调用 UserDataService.updateGroupMemberInfo 主动刷新 SDK 信息
*```
* @param groupId 群 id
* @param userId 用户 Id
* @returns 群组成员信息。注意:如果返回 undefined ,SDK则不会更新缓存。
*/
fetchGroupMemberInfo: (groupId: string, userId: string) => {
return new Promise<GroupMemberInfoModel | undefined>((resolve, reject) => {
// 调试代码
let member: GroupMemberInfoModel = new GroupMemberInfoModel(groupId, userId, '', '')
if (userId === 'test1') {
member = new GroupMemberInfoModel(groupId, userId, 'test1_name',
'https://xxx')
}
resolve(member)
})
},
/**
* 获取群成员信息数据列表,当需要展示群成员头像、名称时,如果 SDK 没有对应的信息时触发该方法
*```
* 获取到群组成员信息后,SDK 会缓存该数据,并触发监听
* SDK 缓存之后就会一直使用,不会自动更新。如果群组成员信息发生变化,需要调用 UserDataService.updateGroupMemberInfo 主动刷新 SDK 信息
*```
* @param groupId 群 id
* @returns 群成员信息列表
*/
fetchGroupMemberInfos: (groupId: string) => {
return new Promise<Array<GroupMemberInfoModel> | undefined>((resolve, reject) => {
/// 使用 app server 提供的群成员列表信息,以下是示例测试数据
let members: Array<GroupMemberInfoModel> = new Array<GroupMemberInfoModel>()
for (let i = 0; i < 5; i++) {
let member: GroupMemberInfoModel = new GroupMemberInfoModel('', '', '', '')
member.userId = `test${i + 1}`
members.push(member)
}
resolve(members)
});
},
/**
* 获取群组信息数据,当需要展示群组头像、名称时,如果 SDK 没有对应的信息时触发该方法
*```
* 获取到群组信息后,SDK 会缓存该数据,并触发监听
* SDK 缓存之后就会一直使用,不会自动更新。如果群组信息发生变化,需要调用 UserDataService.updateGroupInfo 主动刷新 SDK 信息
*```
* @param groupId 群 id
* @returns 群组信息。注意:如果返回 undefined ,SDK则不会更新缓存。
*/
fetchGroupInfo: (groupId: string) => {
return new Promise<GroupInfoModel | undefined>((resolve, reject) => {
if (groupId === 'test_group') {
let model = new GroupInfoModel('test_group', '测试群', '', 3)
resolve(model)
}
else {
resolve(undefined)
}
})
}
})
  1. 实现了步骤1 中的相关协议方法提供了群信息数据,调用 startMultiCall 会先弹出选择成员界面。如下图所示,选择联系人点击 确认 后 CallKit 发起多人呼叫。
Example banner
TypeScript
 /**
* 发起群聊呼叫
* @param targetId 群组Id
* @param mediaType 呼叫媒体类型
*/
public startMultiCall(targetId: string, mediaType: RCCallMediaType)
  • 参数说明:

    参数类型必填说明
    targetIdstring群组ID
    mediaTypeRCCallMediaType媒体类型
  • 示例代码:

TypeScript
// 多人呼叫时,需要填写 IM 群组id
let targetId = 'groupId'
// 媒体类型 RCCallMediaType
let mediaType = RCCallMediaType.VIDEO
RCCall.getInstance().startMultiCall(targetId, mediaType)

步骤 6 接听方

鸿蒙 CallKit 中已经默认实现了 CallLib 库提供 RCCallClientListener 监听回调。被叫端收到呼叫,会自动弹出通话界面,点击来电页面的接听按钮即可接听通话。

App 在后台或者未启动时会收到来电通知,点击来电通知条 App 打开后会显示来电页面。