即时通信 IM 接听第一通电话

这篇档将帮助你使用 AtomicXCore SDK 的 DeviceStore、CallStore 以及核心组件 CallCoreView,快速完成接听电话功能。

相关产品
即时通信 IM 产品页,含版本规格、价格与购买入口。

注意:推荐使用更高效的 AI 集成助手

注意:我们为你提供了全新的 AI 集成方式,只需要描述你的需求,即可自动生成集成代码,大幅提升开发效率。

注意:点击这里,立即体验 AI 集成

一、核心功能

AtomicXCore 中用于搭建多人音视频通话场景所需要使用到的核心模块包含以下三个:

模块 功能描述
CallCoreView 通话视图核心组件。自动监听 CallStore 数据并完成画面渲染,同时提供布局切换、头像与图标配置等 UI 定制化能力。
CallStore 通话生命周期管理:拨打电话、接通电话、拒接电话、挂断电话。实时获取参与通话人员音视频状态,通话计时、通话记录等数据。
DeviceStore 音视频设备控制:麦克风(开关 / 音量)、摄像头(开关 / 切换 / 画质)、屏幕共享,设备状态实时监听。

三、步骤1:开通服务

请参见 开通服务,获取体验版或付费版 SDK。

四、步骤2:集成 SDK

1. 添加 Pod 依赖:在你项目的 Podfile 文件中添加 pod 'AtomicXCore' 依赖。

target 'YourProjectTarget' do pod 'AtomicXCore' end

注意:提示:

注意:若项目没有 Podfile 文件:在终端中进入到你的 .xcodeproj 目录下,然后执行 pod init 命令创建 Podfile 文件

2. 安装组件:在终端进入到 Podfile 文件所在的目录,然后执行以下命令安装组件。

pod install –repo-update

注意:提示:

注意:安装完成后,使用 YourProjectName.xcworkspace 文件打开项目

五、步骤3:初始化与登录流程

启动通话服务需依次完成以下三步:

1. 实例化 CallStore:CallStore.shared 初始化通话数据层。

2. 用户登录:调用 LoginStore.shared.login() 完成用户登录。

3. 初始化通话引擎:登录成功后调用 TUICallEngine.createInstance().`init`() 初始化通话信令引擎。

流程图与示例代码如下:

iOS(Swift)import UIKitimport AtomicXCoreimport Combineclass ViewController: UIViewController { var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() // CallStore 初始化 let _ = CallStore.shared // 配置你的信息 let userID = "test_001" // 请替换为你的 UserID let sdkAppID: Int = 1400000001 // 请替换为在控制台得到的 SDKAppID let secretKey = "**************" // 请替换为在控制台得到的 SecretKey // 生成 UserSig(仅用于本地快速测试,正式环境请在服务端生成) let userSig = GenerateTestUserSig.genTestUserSig( userID: userID, sdkAppID: sdkAppID, secretKey: secretKey ) // 登录 LoginStore.shared.login( sdkAppID: Int32(sdkAppID), userID: userID, userSig: userSig ) { result in switch result { case .success: Log.info("login success") // 完成 TUICallEngine 的初始化 TUICallEngine.createInstance().`init`(Int32(sdkAppID), userId: userID, userSig: userSig) { Log.info("TUICallEngine init success") } fail: { code, message in Log.error("TUICallEngine init failed, code: \\(code), message: \\(message ?? "")") } case .failure(let error): Log.error("login failed, code: \\(error.code), error: \\(error.message)") } } }}

参数 类型 说明
userID String 当前用户的唯一 ID,仅包含英文字母、数字、连字符和下划线。为避免多端登录冲突,请勿使用 1、123 等简单 ID。
sdkAppID int 从 控制台 获取,通常是以 140 或 160 开头的 10 位整数。
secretKey String 在 控制台 创建的音视频应用的 SDKSecretKey。
userSig String 用于腾讯云鉴权的票据。请注意:开发环境:你可以采用本地 GenerateTestUserSig.genTestUserSig 函数生成 userSig 或者 通过 UserSig 辅助工具 生成临时的 UserSig。生产环境:为了防止密钥泄露,请务必采用服务端生成 UserSig 的方式。详细信息请参考 服务端生成 UserSig。更多信息请参见 如何计算及使用 UserSig。

六、实现接听通话

接听通话前,请确保已完成登录,这是服务可用的必要前提。接下来,我们将通过 6 个步骤带你实现‘接听一通电话’的功能。

七、步骤1: 创建通话页面

你需要创建一个通话页面,当发起通话时唤起通话页面,实现方式如下:

1. 创建通话页面:你可以新建一个 UIViewController 作为通话宿主页面,用于响应来电时的跳转逻辑。

2. 通话页面绑定 CallCoreView : 通话视图核心组件,自动监听 CallStore 数据并完成画面渲染,同时提供布局切换、头像与图标配置等 UI 定制化能力。

import UIKitimport AtomicXCoreclass CallViewController: UIViewController { private var callCoreView: CallCoreView? override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black let coreView = CallCoreView(frame: view.bounds) coreView.autoresizingMask = [.flexibleWidth, .flexibleHeight] coreView.setLayoutTemplate(.float) view.addSubview(coreView) callCoreView = coreView }}

CallCoreView 视图组件功能说明:

功能 说明 参考文档
设置布局模式 支持自由切换布局模式。若未设置,将根据通话人数自动适配布局。 切换布局模式
设置头像 支持通过传入头像资源路径,为特定用户自定义头像。 自定义默认头像
设置音量提示图标 支持根据不同音量等级,配置个性化的音量指示图标。 自定义音量提示的图标
设置网络提示图标 支持根据实时网络质量,配置对应的网络状态提示图标。 自定义网络提示的图标
设置等待接听用户的动画 在多人通话场景下,支持传入 GIF 图像路径,为待接听状态的用户展示动画。 自定义 loading 动画

八、步骤2:添加接听和拒接按钮

你可以参考 DeviceStore 、CallStore 提供的 API ,自定义添加你的按钮。

·DeviceStore 功能说明:麦克风(开关 / 音量)、摄像头(开关 / 切换 / 画质)、屏幕共享,设备状态实时监听。建议将对应方法绑定至按钮点击事件,并通过监听设备状态变更来实时刷新按钮的 UI 状态。

·CallStore 功能说明:接听、挂断、拒接等核心通话控制能力。建议将对应方法绑定至按钮点击事件,并监听通话状态的变化,以确保按钮显示与当前通话阶段保持同步。

·图标资源下载:按钮图标可以直接从 GitHub 下载。这些图标由我们的设计师专为 TUICallKit 打造,无版权风险,可放心使用。

图标:        

·以下是添加"接听"和"拒接"按钮的实现方式:

1.1 添加接听和拒接按钮:添加"接听"与"拒接"按钮,将其点击事件分别绑定至 accept 和 reject 方法。

iOS(Swift)import UIKitimport AtomicXCoreclass CallViewController: UIViewController { private var buttonReject: UIButton? private var buttonAccept: UIButton? // 1.创建页面容器 override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black // 其他初始化代码 // 1.添加接听和拒接按钮 addControlButtons() } private func addControlButtons() { let buttonWidth: CGFloat = 80 let buttonHeight: CGFloat = 80 let spacing: CGFloat = 60 let bottomMargin: CGFloat = 100 let totalWidth = buttonWidth * 2 + spacing let startX = (view.bounds.width – totalWidth) / 2 let buttonY = view.bounds.height – bottomMargin – buttonHeight // 创建接听按钮 buttonAccept = createButton( frame: CGRect(x: startX, y: buttonY, width: buttonWidth, height: buttonHeight), title: "接听", backgroundColor: .systemGreen ) buttonAccept?.addTarget(self, action: #selector(touchAcceptButton), for: .touchUpInside) view.addSubview(buttonAccept!) // 创建拒接按钮 buttonReject = createButton( frame: CGRect(x: startX + buttonWidth + spacing, y: buttonY, width: buttonWidth, height: buttonHeight), title: "拒接", backgroundColor: .systemRed ) buttonReject?.addTarget(self, action: #selector(touchRejectButton), for: .touchUpInside) view.addSubview(buttonReject!) } // 2.接听按钮点击事件绑定 accept @objc private func touchAcceptButton() { CallStore.shared.accept(completion: nil) } // 2.拒接按钮点击事件绑定 reject @objc private func touchRejectButton() { CallStore.shared.reject(completion: nil) } // 创建圆形按钮的辅助方法 private func createButton(frame: CGRect, title: String, backgroundColor: UIColor) -> UIButton { let button = UIButton(type: .system) button.frame = frame button.setTitle(title, for: .normal) button.setTitleColor(.white, for: .normal) button.backgroundColor = backgroundColor button.layer.cornerRadius = frame.width / 2 button.titleLabel?.font = UIFont.systemFont(ofSize: 16) return button }}

1.2 拨打方取消通话或你拒接时销毁界面:无论拨打方取消呼叫,还是接收方拒接,均会触发 onCallEnded(通话结束)事件。建议监听此事件,以便在通话终止时及时关闭(销毁)通话界面。

import UIKitimport AtomicXCoreimport Combineclass CallViewController: UIViewController { private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() // … 其他初始化代码 // 1.通话事件监听 addListener() } private func addListener() { CallStore.shared.callEventPublisher .receive(on: DispatchQueue.main) .sink { [weak self] event in if case .onCallEnded = event { // 2.通话结束关闭页面 self?.dismiss(animated: true) } } .store(in: &cancellables) }}

九、步骤3:申请音视频权限

建议在呼叫接通前,先行检测音视频权限。若权限缺失,请引导用户动态申请。具体方法如下:

1. 声明权限:首先,在 Info.plist 文件中添加以下两项,并填写对应的使用说明,这些说明将在系统请求权限时向用户显示。

<key>NSCameraUsageDescription</key><string>需要访问你的摄像头,并可用于视频通话、群组视频通话等功能</string><key>NSMicrophoneUsageDescription</key><string>需要访问你的麦克风,并可用于音频通话、群组音频通话、视频通话、群组视频通话等功能</string>

2. 动态权限申请:建议在收到来电时,或结合具体业务场景(如点击呼叫前),按需发起音视频权限的申请。

import AVFoundationimport UIKitextension UIViewController { // 检查麦克风权限 func checkMicrophonePermission(completion: @escaping (Bool) -> Void) { let status = AVCaptureDevice.authorizationStatus(for: .audio) switch status { case .authorized: completion(true) case .notDetermined: AVCaptureDevice.requestAccess(for: .audio) { granted in DispatchQueue.main.async { completion(granted) } } case .denied, .restricted: completion(false) @unknown default: completion(false) } } // 检查摄像头权限 func checkCameraPermission(completion: @escaping (Bool) -> Void) { let status = AVCaptureDevice.authorizationStatus(for: .video) switch status { case .authorized: completion(true) case .notDetermined: AVCaptureDevice.requestAccess(for: .video) { granted in DispatchQueue.main.async { completion(granted) } } case .denied, .restricted: completion(false) @unknown default: completion(false) } } // 显示权限提示 func showPermissionAlert(message: String) { let alert = UIAlertController( title: "权限请求", message: message, preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) } }) alert.addAction(UIAlertAction(title: "取消", style: .cancel)) present(alert, animated: true) }}

十、步骤4:来电播放提示

你可以监听当前用户的通话状态,在收到来电时播放铃声或振动,接听、挂断后停止播放来电提示,实现方法如下:

1. 数据层订阅:订阅 CallStore.shared.state.value.selfInfo , 建立当前登录用户信息的响应式监听。

2. 播放/停止铃声或振动:若当前用户的通话状态 (selfInfo.status) 为等待接听状态(.waiting)时播放铃声或振动,若当前用户的通话状态 (selfInfo.status) 为已接听状态(.accept)时停止铃声或振动。

import UIKitimport AtomicXCoreimport Combineimport AVFoundationclass MainViewController: UIViewController { private var audioPlayer: AVAudioPlayer? private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() // 1.监听当前用户的通话状态 observeSelfStatus() } // 监听当前用户的通话状态 private func observeSelfStatus() { CallStore.shared.state .subscribe(StatePublisherSelector<CallState, CallParticipantStatus>(keyPath: \\.selfInfo.status)) .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] status in // 2.播放/停止来电提示 self?.handleRingtoneByStatus(status) } .store(in: &cancellables) } // 根据当前用户通话状态播放来电提示 private func handleRingtoneByStatus(_ status: CallParticipantStatus) { switch status { case .waiting: // 等待接听时开始播放铃声 // playRingtone() case .accept: // 接听后停止播放铃声 // stopRingtone() default: // 其他状态停止播放铃声 // stopRingtone() } }}

十一、步骤5:来电打开媒体设备

收到来电时,你可以通过 onCallReceived 事件获取本次通话的媒体类型。为了提供更佳的用户体验,建议在唤起通话界面时,根据通话类型预先开启对应的媒体设备。实现步骤如下:

1. 监听来电事件:订阅 onCallReceived 事件。

2. 根据来电媒体类型打开设备:若为语音通话仅开启麦克风,若为视频通话开启麦克风和摄像头。

import UIKitimport AtomicXCoreimport Combineclass MainViewController: UIViewController { private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() // … 其他初始化代码 // 1.监听来电事件 addListener() } // 监听来电事件 private func addListener() { CallStore.shared.callEventPublisher .receive(on: DispatchQueue.main) .sink { [weak self] event in if case .onCallReceived(let callId, let mediaType, let userData) = event { // 2.根据来电媒体类型打开设备 self?.openDeviceForMediaType(mediaType) } } .store(in: &cancellables) } // 根据通话媒体类型打开设备 private func openDeviceForMediaType(_ mediaType: CallMediaType) { DeviceStore.shared.openLocalMicrophone(completion: nil) if mediaType == .video { let isFront = true DeviceStore.shared.openLocalCamera(isFront: isFront, completion: nil) } }}

openLocalCamera 接口参数详细说明:

参数名 类型 必填 说明
isFront Bool 是 是否开启前置摄像头。true : 开启前置摄像头。false :开启后置摄像头。
completion CompletionHandler 否 操作完成回调,用于返回开启摄像头的结果。若开启失败则会返回错误码和错误信息。

openLocalMicrophone 接口参数详细说明:

参数名 类型 必填 说明
completion CompletionHandler 否 操作完成回调,用于返回开启麦克风的结果。若开启失败则会返回错误码和错误信息。

十二、步骤6:来电唤起通话界面

在你的项目中监听来电事件(onCallReceived),当收到来电时唤起通话页面。实现方式如下:

1. 监听来电事件:订阅 onCallReceived 事件。

2. 唤起通话界面:收到来电时唤起通话页面。

import UIKitimport AtomicXCoreimport Combineclass MainViewController: UIViewController { private var cancellables = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() // … 其他初始化代码 // 1.监听来电事件 addListener() } // 监听来电事件 private func addListener() { CallStore.shared.callEventPublisher .receive(on: DispatchQueue.main) .sink { [weak self] event in if case .onCallReceived(let callId, let mediaType, let userData) = event { // 2.唤起通话页面 let callVC = CallViewController() callVC.modalPresentationStyle = .fullScreen self?.present(callVC, animated: true) } } .store(in: &cancellables) }}

onCallReceived 事件详细说明:

参数 类型 说明
callId String 此次通话的唯一标识。
mediaType CallMediaType 通话媒体类型,用于指定发起音频通话还是视频通话。CallMediaType.video : 视频通话。CallMediaType.audio : 语音通话。

十三、运行效果

当你完成以上 6 步后,"接听一通电话"运行效果如下:

十四、定制页面

CallCoreView 提供了完善的 UI 定制能力,支持头像及音量提示等图标的自由替换。为助力快速集成,你可以直接从 GitHub 下载。无版权风险,可放心使用。

十五、自定义音量提示的图标

你可以调用 CallCoreView 组件的 setVolumeLevelIcons 设置音量大小等级不同的提示图标。

·setVolumeLevelIcons 示例代码:

// 设置音量提示图标let volumeLevelIcons: [VolumeLevel: String] = [ .mute: "对应图标资源的路径"]callCoreView.setVolumeLevelIcons(icons: volumeLevelIcons)

·setVolumeLevelIcons 接口参数详细说明:

参数 类型 是否必填 说明
icons [VolumeLevel: String] 是 【字段含义】音量等级与图标资源的映射表。字典结构说明如下:key ( VolumeLevel ) 表示音量等级:VolumeLevel.mute :表示麦克风关闭,静音状态。VolumeLevel.low :表示音量范围 (0 ~ 25]。VolumeLevel.medium : 表示音量范围 (25 ~ 50]。VolumeLevel.high : 表示音量范围在 (50 ~ 75]VolumeLevel.peak : 表示音量范围在 (75 ~ 100]。Value ( String ) 表示对应音量等级的图标资源路径。

·音量提示图标:

图标 说明
 【图标含义】音量提示图标。【推荐用法】你可以将该图标等级设置为 VolumeLevel.low 或 VolumeLevel.medium ,当用户音量大于对应等级时显示。
 【图标含义】静音图标。【推荐用法】你可以将该图标等级设置为 VolumeLevel.mute ,当该用户静音时显示。

十六、自定义网络提示的图标

你可以调用 CallCoreView 组件的 setNetworkQualityIcons 设置不同网络状态的提示图标。

·setNetworkQualityIcons 示例代码:

// 设置网络质量图标let networkQualityIcons: [NetworkQuality: String] = [ .bad: "对应图标的路径"]callCoreView.setNetworkQualityIcons(icons: networkQualityIcons)

·setNetworkQualityIcons 接口参数详细说明:

参数 类型 是否必填 说明
icons [NetworkQuality: String] 是 网络质量与图标资源的映射表。字典结构说明如下:Key ( NetworkQuality ) : 表示网络质量等级NetworkQuality.unknown :未知网络状态。NetworkQuality.excellent:网络状态极佳。NetworkQuality.good : 网络状态较好。NetworkQuality.poor : 网络状态较差。NetworkQuality.bad : 网络状态差。NetworkQuality.veryBad :网络状态极差。NetworkQuality.down :网络断开。Value ( String ) : 对应网络状态的图标资源路径。

·网络较差的提示图标:

图标 说明
 【图标含义】网络较差的提示图标。【推荐用法】你可以将该图标等级设置为 NetworkQuality.bad、NetworkQuality.veryBad 或 NetworkQuality.down ,当网络较差时显示该图标。

十七、自定义默认头像

你可以调用 CallCoreView 的 setParticipantAvatars 接口设置用户头像。建议你监听响应式数据 allParticipants(所有参与通话的成员):当获取到用户头像时设置并展示;若用户未设置头像或加载失败,则显示默认头像(占位图)。

·setParticipantAvatars 示例代码:

// 设置用户头像var avatars: [String: String] = [:]let userId = "" // 用户 IDlet avatarPath = "" // 用户默认头像资源的路径avatars[userId] = avatarPathcallCoreView.setParticipantAvatars(avatars: avatars)

·setParticipantAvatars 接口参数详细说明:

参数 类型 是否必填 说明
avatars [String: String] 是 用户头像映射表。Key : 用户的 userID 。Value : 该用户的头像资源绝对路径。

·默认头像资源:

图标 说明
 【图标含义】默认头像【推荐用法】当用户头像加载失败或无头像时,你可以给该用户设置此默认头像。

十八、自定义 loading 动画

你可以调用 CallCoreView 的 setWaitingAnimation 接口,为等待中用户设置等待动画获得更好的体验。

·setWaitingAnimation 示例代码:

iOS// 设置等待动画let waitingAnimationPath = "" // 等待动画 GIF 图像资源的路径callCoreView.setWaitingAnimation(path: waitingAnimationPath)

·setWaitingAnimation 接口参数详细说明:

参数 类型 是否必填 说明
path String 是 GIF 格式图像资源的绝对路径。

·等待接听的动画:

图标 说明
 【图标含义】用户等待接听动画。【推荐用法】群组通话时设置的动画。设置后,当用户的状态为等待接听时,显示该动画。

十九、添加通话计时提示

通话计时可通过响应式数据 activeCall 的 duration 字段实时获得,实时显示通话计时的实现方式如下:

1. 数据层订阅:订阅 CallStore.observerState.activeCall , 建立当前活跃通话的响应式监听。

2. 绑定通话计时数据:将 activeCall.duration 字段绑定至 UI 控件。该字段为响应式数据,会自动驱动 UI 实时刷新,无需手动维护定时器。

import UIKitimport AtomicXCoreimport Combineclass TimerView: UILabel { private var cancellables = Set<AnyCancellable>() override init(frame: CGRect) { super.init(frame: frame) setupView() } required init?(coder: NSCoder) { super.init(coder: coder) setupView() } private func setupView() { textColor = .white textAlignment = .center font = .systemFont(ofSize: 16) } override func didMoveToWindow() { super.didMoveToWindow() if window != nil { registerActiveCallObserver() } else { cancellables.removeAll() } } private func registerActiveCallObserver() { CallStore.shared.state.subscribe() .map { $0.activeCall } .removeDuplicates { $0.duration == $1.duration } .receive(on: DispatchQueue.main) .sink { [weak self] activeCall in // 更新通话计时 self?.updateDurationView(activeCall: activeCall) } .store(in: &cancellables) } private func updateDurationView(activeCall: CallInfo) { let currentDuration = Int(activeCall.duration) let minutes = currentDuration / 60 let seconds = currentDuration % 60 text = String(format: "%02d:%02d", minutes, seconds) }}

注意:说明:

注意:若你想了解更多通话状态响应式数据,详细可参考:CallState。

21、自定义用户头像/昵称

通话开始前,你可以通过 setSelfInfo 方法,设置自己的昵称和头像。

·setSelfInfo 示例代码:

import AtomicXCorevar userProfile = UserProfile()userProfile.userID = "" // 你的 userIduserProfile.avatarURL = "" // 头像的 urluserProfile.nickname = "" // 需要设置的昵称LoginStore.shared.setSelfInfo(userProfile: userProfile) { result in switch result { case .success: // 设置成功回调 case .failure(let error): // 设置失败回调 }}

·setSelfInfo 接口参数详细说明:

参数 类型 是否必填 说明
userProfile UserProfile 是 用户信息结构体:userID :用户的 ID。avatarURL : 用户头像的 URL。nickname :用户的昵称。更多字段详情可参考 UserProfile。
completion CompletionHandler 否 【字段含义】操作完成回调,用于返回接通电话的结果。

22、切换布局模式

CallCoreView 内置三种布局模式,你可以调用 setLayoutTemplate 设置布局模式。若未主动配置,CallCoreView 将根据通话人数自动适配:1 V 1 场景下默认采用 Float 模式,多人通话场景下则自动切换为 Grid 模式。不同布局模式的说明如下:

Float 模式 Grid 模式 PIP 模式
  
布局逻辑:呼叫等待时全屏显示己方画面;接通后全屏显示对方画面,己方画面以悬浮小窗展示。交互特性:支持小窗拖拽移动,点击小窗可实现大小画面互换。 布局逻辑:所有成员画面呈网格状平铺排列成宫格模式布局,适用 2 人以上通话,支持点击放大画面功能。交互特性:支持点击特定成员画面放大查看。 布局逻辑:1v1 场景固定显示对方画面,多人场景:采用当前发言者(Active Speaker) 策略,自动识别并全屏展示正在说话的用户。交互特性:等待时显示自己的画面,接通后还会显示通话计时。

·setLayoutTemplate 示例代码:

// 创建 CallCoreViewlet callCoreView = CallCoreView(frame: view.bounds)callCoreView.autoresizingMask = [.flexibleWidth, .flexibleHeight]// 设置布局模式let template = CallLayoutTemplate.gridcallCoreView.setLayoutTemplate(template)

·setLayoutTemplate 接口参数详细说明:

参数 类型 是否必填 说明
template CallLayoutTemplate 是 【字段含义】CallCoreView 的布局模式CallLayoutTemplate.float :布局逻辑:呼叫等待时全屏显示己方画面;接通后全屏显示对方画面,己方画面以悬浮小窗展示。交互特性:支持小窗拖拽移动,点击小窗可实现大小画面互换。CallLayoutTemplate.grid :布局逻辑:所有成员画面呈网格状平铺排列成宫格模式布局,适用 2 人以上通话,支持点击放大画面功能。交互特性:支持点击特定成员画面放大查看。CallLayoutTemplate.pip : 布局逻辑:1v1 场景固定显示对方画面,多人场景:采用当前发言者(Active Speaker) 策略,自动识别并全屏展示正在说话的用户。交互特性:等待时显示自己的画面,接通后还会显示通话计时。

23、实现应用内悬浮窗

AtomicXCore SDK 提供了 CallPipView 组件,可以在应用内实现悬浮窗效果。当你的通话界面被其他页面覆盖时(例如用户点击了返回键但通话未结束),可以显示一个悬浮的小窗口,让用户随时查看通话状态并快速返回通话界面。

第一步:创建悬浮窗控制器。

import UIKitimport AtomicXCoreimport Combine/** * 悬浮窗控制器 * * 用于显示通话悬浮窗,内部包含 CallCoreView */class FloatWindowViewController: UIViewController { var tapGestureAction: (() -> Void)? private var cancellables = Set<AnyCancellable>() private lazy var callCoreView: CallCoreView = { let view = CallCoreView(frame: self.view.bounds) view.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.setLayoutTemplate(.pip) // 设置为 Pip 布局模式 view.isUserInteractionEnabled = false // 禁用交互,让点击事件穿透到父视图 return view }() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor(white: 0.1, alpha: 1.0) view.layer.cornerRadius = 10 view.layer.masksToBounds = true view.addSubview(callCoreView) // 添加点击手势 let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) view.addGestureRecognizer(tapGesture) // 延迟监听状态变化,避免在创建时立即被关闭 DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in self?.observeCallStatus() } } @objc private func handleTap() { tapGestureAction?() } /** * 监听通话状态变化 * 当通话结束时自动关闭悬浮窗 */ private func observeCallStatus() { CallStore.shared.state .subscribe(StatePublisherSelector<CallState, CallParticipantStatus>(keyPath: \\.selfInfo.status)) .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] status in if status == .none { // 通话结束,发送通知隐藏悬浮窗 NotificationCenter.default.post(name: NSNotification.Name("HideFloatingWindow"), object: nil) } } .store(in: &cancellables) } deinit { cancellables.removeAll() }}

第二步:在主界面实现悬浮窗管理逻辑。

import UIKitimport AtomicXCoreclass MainViewController: UIViewController { private var floatWindow: UIWindow? override func viewDidLoad() { super.viewDidLoad() // 监听显示悬浮窗的通知 NotificationCenter.default.addObserver( self, selector: #selector(showFloatingWindow), name: NSNotification.Name("ShowFloatingWindow"), object: nil ) // 监听隐藏悬浮窗的通知 NotificationCenter.default.addObserver( self, selector: #selector(hideFloatingWindow), name: NSNotification.Name("HideFloatingWindow"), object: nil ) } /** * 显示应用内悬浮窗 */ @objc private func showFloatingWindow() { // 检查通话是否进行中 let selfStatus = CallStore.shared.state.value.selfInfo.status guard selfStatus == .accept else { return } // 如果悬浮窗已存在,不重复创建 guard floatWindow == nil else { return } // ⚠️ 关键:必须使用当前的 windowScene 来创建 window guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } // 创建悬浮窗(宽高比 9:16) let pipWidth: CGFloat = 100 let pipHeight: CGFloat = pipWidth * 16 / 9 let pipX = UIScreen.main.bounds.width – pipWidth – 20 let pipY: CGFloat = 100 // 创建悬浮窗 Window(关联到 windowScene) let window = UIWindow(windowScene: windowScene) window.windowLevel = .alert + 1 window.backgroundColor = .clear window.frame = CGRect(x: pipX, y: pipY, width: pipWidth, height: pipHeight) // 创建悬浮窗控制器 let floatVC = FloatWindowViewController() floatVC.tapGestureAction = { [weak self] in self?.openCallViewController() } window.rootViewController = floatVC self.floatWindow = window // 设置可见 window.isHidden = false window.makeKeyAndVisible() // 立即将主窗口恢复为 key window if let mainWindow = windowScene.windows.first(where: { $0 != window }) { mainWindow.makeKey() } } /** * 隐藏应用内悬浮窗 */ @objc private func hideFloatingWindow() { floatWindow?.isHidden = true floatWindow = nil } /** * 打开通话界面(从悬浮窗点击进入) */ private func openCallViewController() { // 先隐藏悬浮窗 hideFloatingWindow() // 获取当前最顶层的 ViewController guard let topVC = getTopViewController() else { return } let callVC = CallViewController() callVC.modalPresentationStyle = .fullScreen topVC.present(callVC, animated: true) } /** * 获取当前最顶层的 ViewController */ private func getTopViewController() -> UIViewController? { guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }), let rootVC = keyWindow.rootViewController else { return nil } var topVC = rootVC while let presentedVC = topVC.presentedViewController { topVC = presentedVC } return topVC } deinit { NotificationCenter.default.removeObserver(self) }}

第三步:在通话界面添加悬浮窗触发逻辑。

import UIKitimport AtomicXCoreclass CallViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 进入通话界面时,发送通知隐藏悬浮窗 NotificationCenter.default.post(name: NSNotification.Name("HideFloatingWindow"), object: nil) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // 离开通话界面时,检查通话是否进行中 let selfStatus = CallStore.shared.state.value.selfInfo.status if selfStatus == .accept { // 如果通话还在进行中,发送通知显示悬浮窗 NotificationCenter.default.post(name: NSNotification.Name("ShowFloatingWindow"), object: nil) } }}

24、实现应用外画中画

AtomicXCore SDK 支持通过底层 TRTC 引擎实现应用外画中画功能。当应用进入后台时,通话画面可以以系统画中画形式悬浮在其他应用之上,用户可以边使用其他应用边进行视频通话。

注意:说明:

注意:1. 需要在 Xcode 的 Signing & Capabilities 中添加 Background Modes 能力,并勾选 Audio, AirPlay, and Picture in Picture。

注意:2. 需要 iOS 15.0 及以上版本支持。

1. 配置画中画参数:你需要设置画中画窗口的填充模式、用户视频区域、画布参数等参数。

import AtomicXCore// 填充模式枚举enum PictureInPictureFillMode: Int, Codable { case fill = 0 // 填充模式 case fit = 1 // 适应模式}// 用户视频区域struct PictureInPictureRegion: Codable { let userId: String // 用户 ID let width: Double // 宽度(0.0-1.0,相对于画布) let height: Double // 高度(0.0-1.0,相对于画布) let x: Double // X 坐标(0.0-1.0,相对于画布左上角) let y: Double // Y 坐标(0.0-1.0,相对于画布左上角) let fillMode: PictureInPictureFillMode // 填充模式 let streamType: String // 流类型("high" 或 "low") let backgroundColor: String // 背景颜色}// 画布配置struct PictureInPictureCanvas: Codable { let width: Int // 画布宽度 let height: Int // 画布高度 let backgroundColor: String // 背景颜色}// 画中画参数struct PictureInPictureParams: Codable { let enable: Bool // 是否启用画中画 let cameraBackgroundCapture: Bool? // 是否在后台采集摄像头 let canvas: PictureInPictureCanvas? // 画布配置(可选) let regions: [PictureInPictureRegion]? // 用户视频区域列表(可选)}// 画中画请求struct PictureInPictureRequest: Codable { let api: String // API 名称 let params: PictureInPictureParams // 参数}

2. 启用画中画:你可以通过 configPictureInPicture 方法启用/关闭画中画功能。

let params = PictureInPictureParams( enable: true, cameraBackgroundCapture: true, canvas: nil, regions: nil)let request = PictureInPictureRequest( api: "configPictureInPicture", params: params)// 编码为 JSON 字符串let encoder = JSONEncoder()if let data = try? encoder.encode(request), let jsonString = String(data: data, encoding: .utf8) { TUICallEngine.createInstance().callExperimentalAPI(jsonObject: jsonString)}

25、设置通话过程中屏幕常亮

通话过程中保持屏幕常亮是通话应用的基本需求。iOS 提供了 UIApplication.shared.isIdleTimerDisabled 属性来实现此功能。

class CallViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // 禁用自动锁屏,保持屏幕常亮 UIApplication.shared.isIdleTimerDisabled = true } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // 恢复自动锁屏 UIApplication.shared.isIdleTimerDisabled = false }}

26、开启后台采集音频/视频常见问题

为了确保应用在进入后台时仍能正常采集音频和视频(例如用户锁屏或切换到其他应用),你需要完成:iOS 后台模式权限配置、设置音频会话,实现方式如下:

1. iOS 后台模式权限配置:在 Xcode 中打开你的项目

·选择项目的 Target → Signing & Capabilities。

·点击 + Capability 。

·搜索并添加 Background Modes。

·勾选以下三个选项:

1.1.1 Audio, AirPlay, and Picture in Picture(保持音频采集和画中画功能)。

1.1.2 Voice over IP(支持 VoIP 通话)。

1.1.3 Remote notifications(可选,用于接收离线推送)。

配置完成后,你的 Info.plist 文件会自动添加以下内容:

<key>UIBackgroundModes</key><array> <string>audio</string> <string>voip</string> <string>remote-notification</string></array>

2. 配置音频会话(AVAudioSession):为了确保通话音频在后台正常工作,你需要在通话开始前配置音频会话。建议在通话界面的 viewDidLoad 或发起通话前设置:

import AVFoundation/** * 设置音频会话,支持后台音频采集 * * 建议在以下场景调用: * 1. 通话界面的 viewDidLoad 中 * 2. 发起通话 (calls) 之前 * 3. 接听通话 (accept) 之前 */private func setupAudioSession() { let audioSession = AVAudioSession.sharedInstance() do { // 设置音频会话类别为播放和录音 // .allowBluetooth: 支持蓝牙耳机 // .allowBluetoothA2DP: 支持高质量蓝牙音频(A2DP 协议) try audioSession.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP]) // 激活音频会话 try audioSession.setActive(true) } catch { // 音频会话配置失败 }}

3. 播放铃声时的特殊处理(可选):如果你需要在等待接听时播放铃声,并且希望铃声通过扬声器播放,可以临时切换音频会话为 .playback 模式:

/** * 播放铃声时切换音频会话 * * 使用场景:当你使用 AVAudioPlayer 播放铃声时 */private func setAudioSessionForRingtone() { let audioSession = AVAudioSession.sharedInstance() do { // 切换为播放模式 try audioSession.setCategory(.playback, options: [.allowBluetooth, .allowBluetoothA2DP]) // 强制使用扬声器播放铃声 try audioSession.overrideOutputAudioPort(.speaker) try audioSession.setActive(true) } catch { // 铃声音频会话配置失败 }}/** * 停止播放铃声后恢复为通话模式 */private func restoreAudioSessionForCall() { let audioSession = AVAudioSession.sharedInstance() do { // 恢复为播放和录音模式 try audioSession.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP]) try audioSession.setActive(true) } catch { // 恢复音频会话失败 }}

27、下一步

恭喜你,已经完成了“接听一通电话”,接下来,你可参考 拨打第一通电话 实现拨打电话功能。

29、在通话邀请超时时间内,被邀请者如果离线再上线,能否收到来电事件?

单人通话时,如果在超时时间内上线,会触发来电邀请;群组通话,如果在超时时间内上线,会拉起未处理的20条群消息,如果存在通话邀请,则触发来电邀请事件。

30、联系我们

如果你在使用过程中,有什么建议或者意见,可以 联系我们,感谢你的反馈。

赞 (0)
上一篇 4天前
下一篇 4天前