通话流程
Background Mode
Target > Signing & Capabilities > Background Modes
PushKit
推送权限
请求到推送权限之后,需要调用 register
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {[weak self] granted, error in
if granted {
self?.updateNotificationToken()
}
UIApplication.shared.registerForRemoteNotifications()
}
停止推送
如果不需要继续接收推送,需要调用unregister
UIApplication.shared.unregisterForRemoteNotifications()
设置 PKPushRegistry
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
设置 PKPushRegistry
var voipRegistry = PKPushRegistry(queue: nil)
func setupPush() {
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [PKPushType.voIP]
UIApplication.shared.registerForRemoteNotifications()
}
PKPushRegistryDelegate
推送的token会发生变化(过期、更新),在下面的方法可以监听并做相应的处理
// token更新的时候调用 在这里提交/缓存新的token
public func pushRegistry(_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType)
// token失效的时候调用 在这里清空缓存的token
public func pushRegistry(_ registry: PKPushRegistry,
didInvalidatePushTokenFor
type: PKPushType)
收到通话推送时的核心操作,只要是VOIP推送必须上报,不上报必Crash。
上报详情看 CallKit 的 上报IncomingCall章节
public func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
if type == .voIP {
reportInComingCall()
}
}
CallKit
主要用到的类如下
CXProviderConfiguration配置callkitCXProvider接收 callkit 操作的类CXCallController系统通话页面CXAction通话页面的操作,可修改callkitCXTransaction提交操作的载体
初始化
- 开发者监听用户对
CallKit的操作,需要通过Provider。 CXProviderConfiguration用于初始化Provider,并决定CallKit 开放的功能
创建 Config
func createConfig() -> CXProviderConfiguration {
let providerConfiguration = CXProviderConfiguration(localizedName: "Call")
// true=callKit显示Video按钮;false=callKit显示FaceTime按钮
providerConfiguration.supportsVideo = false
// group 同时设置1 callKit 可以禁用 addCall按钮
providerConfiguration.maximumCallGroups = 1
providerConfiguration.maximumCallsPerCallGroup = 1
// callKit App按钮的图标
providerConfiguration.iconTemplateImageData = data
// 铃声
providerConfiguration.ringtoneSound = sound
// 展示在系统的最近通话
providerConfiguration.includesCallsInRecents = true
// 类型
providerConfiguration.supportedHandleTypes = [.generic]
return providerConfiguration
}
创建 Provider
func setupProvider() {
let config = createConfig()
provider = CXProvider(configuration: config)
provider.setDelegate(self, queue: nil)
}
上报 IncomingCall
只要是 voip推送(IncomingCall) 必须上报,不上报必Crash。
上报之后会拉起 CallKit 的推送UI
func reportInComingCall(callerID: String, completion: (() -> Void)?) {
// 判断一下是否满足自己的通话条件
if isIllegalCall(callerID) {
// 不满足也要上报假通话 不然crash
reportFakeCall(completion: completion)
return
}
// 上报真实的通话
reportCall(callerID: callerID, completion: completion)
}
上报假通话
func reportFakeCall(completion: (() -> Void)?) {
let update = CXCallUpdate()
update.supportsHolding = false
update.supportsGrouping = false
update.supportsUngrouping = false
update.supportsDTMF = false
update.hasVideo = false
// 虚假UUID 32位 有格式要求
let fakeUUID = UUID(uuidString: "EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE")!
provider.reportNewIncomingCall(with: fakeUUID, update: update) { [weak self] _ in
completion?()
// 直接挂断虚假通话
self?.provider.reportCall(with: uuid, endedAt: nil, reason: .unanswered)
}
}
上报真实通话
func reportCall(callerID: String, completion: (() -> Void)?) {
let update = CXCallUpdate()
let callerUUID = UUID()
update.remoteHandle = CXHandle(type: .generic, value: callerID)
update.localizedCallerName = callerID
provider.reportNewIncomingCall(with: callerUUID, update: update) { [weak self] error in
if let error {
print(error)
}
completion?()
}
}
监听用户操作
用户操作CallKit页面上的按钮都会通过`CXProviderDelegate`回调
用户点击了接听
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
action.fulfill()
}
用户点击了挂断
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
action.fulfill()
}
用户点击静音
在这里提交操作和同步自定义页面UI
action.fulfill() 必须告知 callkit 这个操作完成了,不然状态会刷回去
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
print(action.isMuted)
action.fulfill()
}
输出设备状态变化
在这里同步自定义页面的音频设备UI
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {}
提交操作到CallKit
常见的 Action
-
接听:
`CXAnswerCallAction` -
挂断:
`CXEndCallAction` -
静音:
`CXSetMutedCallAction` -
挂起:
`CXSetHeldCallAction` -
群组:
`CXSetGroupCallAction` -
双频多音:
`CXPlayDTMFCallAction`
提交步骤都
- 创建一个
`CXAction` - 根据 aciton 创建
`CXTransaction` `CXCallController`提交 transaction
提交成功之后会触发 `CXProviderDelegate` ,注意处理状态同步的时候不要相互调用
func setupMute(isMute: Bool) {
guard let uuid = activeCall.uuid else {
return
}
let action = CXSetMutedCallAction(call: uuid, muted: isMute)
let transaction = CXTransaction(action: action)
callController.request(transaction) { error in
if let error {
Log.e(error)
return
}
}
}
func hangup(uuid: UUID) {
let action = CXEndCallAction(call: uuid)
let transaction = CXTransaction(action: action)
callController.request(transaction) { error in
if let error {
Log.e(error)
}
}
}
Speaker处理
配置好 AudioSession,CallKit UI会自动更新
func setupAudioSession(speakerOn: Bool) {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playAndRecord,
mode: .voiceChat,
options: speakerOn ? .defaultToSpeaker : .allowBluetooth)
try audioSession.overrideOutputAudioPort(speakerOn ? .speaker : .none)
try audioSession.setActive(true)
} catch let error {
Log.e(error)
}
}
AVAudioSessionCategory
| AVAudioSessionCategory | 播放 | 录音 | 打断其他App | 受静音键影响 |
|---|---|---|---|---|
| Ambient | ✅ | ❌️ | ✅ | ❌️ |
| SoloAmbient | ✅ | ❌️ | ✅ | ✅ |
| Playback | ✅ | ❌️ | ✅ | ❌️ |
| Record | ❌️ | ✅ | ✅ | ❌️ |
| PlayAndRecord | ✅ | ✅ | ✅ | ❌️ |
| AudioProcessing | ❌️ | ❌️ | ✅ | ❌️ |
| MultiRoute | ✅ | ❌️ | ✅ | ❌️ |
AVAudioSessionMode
| AVAudioSessionMode | AVAudioSessionCategory | 默认AVAudioSessionCategoryOption | 场景 |
|---|---|---|---|
| Default | All | - | |
| VoiceChat | PlayAndRecord | AllowBluetooth DefaultToSpeaker | VoIP |
| VideoChat | PlayAndRecord | AllowBluetooth DefaultToSpeaker | 视频聊天 |
| GameChat | PlayAndRecord | AllowBluetooth DefaultToSpeaker | 游戏 |
| VideoRecording | PlayAndRecord Record | AVCaptureSession API | 摄像头采集视频 |
| Measurement | PlayAndRecord Record Playback | - | 最小系统 |
| MoviePlayback | Playback | - | 视频播放 |
AVAudioSessionCategoryOption
| AVAudioSessionCategoryOption | 可用的AVAudioSessionCategory | 作用 |
|---|---|---|
| MixWithOthers | Playback PlayAndRecord MultiRoute | 不会打断其他应用程序的音频播放 |
| DuckOthers | PlayAndRecord Record | 会话时降低其他程序的音频播放声音 |
| AllowBluetooth | All | 允许蓝牙设备 |
| DefaultToSpeaker | All | 默认选择内置扬声器 |
| InterruptSpokenAudioAndMixWithOthers | All | 使用时打断其他应用,结束时自动恢复其他应用的播放 |
| AllowBluetoothA2DP | All | 允许A2DP蓝牙设备 |
| AllowAirPlay | All | 允许AirPlay |