通话流程
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 |