Skip to end of metadata
Go to start of metadata

Пример iOS приложения для приема входящих звонков с использованием Call Kit

Данный пример может использоваться как основа для написания собственного приложения, использующего Call Kit и пуш-уведомлений для соединения с сервером и приема входящего SIP звонка.

На скриншоте приведены поля для ввода SIP параметров, необходимых для регистрации сессии

При входящем звонке приложение получает пуш-уведомление, даже если оно свернуто или закрыто. Если приложение закрыто, оно подключается к SIP-сессии на сервере с токеном, полученным в уведомлении, и принимает входящий звонок.

Настройки сервера

Для того, чтобы работали пуш-уведомления, на сервере необходимо указать следующие настройки

ПараметрОписание
notification_apns_key_pathРасположение файла ключа Apple Push Notification service
notification_apns_key_idИдентификатор ключа APNs
notification_apns_team_idИдентификатор команды разработки

Например

notification_apns_key_path=/opt/apns_auth_key.p8
notification_apns_team_id=SXZF5547NK
notification_apns_key_id=7NQA96WTFZ

В соответствии с этими настройками, сервер отправляет уведомления в APNs при поступлении входящего звонка

Работа с кодом примера

Для разбора кода возьмем версию примера CallKitDemo Swift, которая доступна для скачивания на GitHub

Классы приложения:

  • класс основного вида приложения CallKitDemoViewController (файл CallKitDemoViewController.swift)
  • класс реализации протокола CXProviderDelegate (файл ProviderDelegate.swift)
  • класс для работы с интерфейсом пользователя и реестром пуш-уведомлений AppDelegate (файл AppDelegate.swift)
  • расширение для создания объекта CXAnswerCallAction по действию пользователя, для ответа на звонок NSUserActivity: StartCallConvertible (файл NSUserActivity+StartCallConvertible.swift)
  • расширение для создания объекта CXAnswerCallAction из URL, для ответа на звонок URL: StartCallConvertible (файл URL+StartCallConvertible.swift)
  • расширение для создания звонка, реализующее протокол INStartCallIntentHandling (файл CallKitIntentExtension/IntentHandler.swift)

1. Импорт API

code

import FPWCSApi2Swift

2. Подключение к серверу и создание SIP-сессии

FPWCSApi2.createSession code

При создании сессии передаются следующие параметры:

  • urlServer - URL  WCS сервера
  • keepAlive - сохранять сессию при отключении клиента
  • sipRegisterRequired - регистрировать сессию на SIP сервере
  • sipLogin - имя пользователя на SIP сервере
  • sipAuthenticationName - имя пользователя для аутентификации на SIP сервере
  • sipPassword - пароль на SIP сервере
  • sipDomain - адрес SIP сервера
  • sipOutboundProxy - адрес SIP сервера
  • sipPort - порт на SIP сервере
  • noticationToken - токен для получения уведомлений о входящих звонках
  • appId - идентификатор приложения
  • appKey - REST hook  приложение на WCS сервере
            let options = FPWCSApi2SessionOptions()
            options.urlServer = wcsUrl.text
            
            options.keepAlive = true
            
            options.sipRegisterRequired = true
            options.sipLogin = sipLogin.text
            options.sipAuthenticationName = sipAuthName.text
            options.sipPassword = sipPassword.text
            options.sipDomain = sipDomain.text
            options.sipOutboundProxy = sipOutboundProxy.text
            options.sipPort = Int(sipPort.text ?? "5060") as NSNumber?
            
            let userDefaults = UserDefaults.standard
            options.noticationToken = userDefaults.string(forKey: "voipToken")
            options.appId = "com.flashphoner.ios.CallKitDemoSwift"

            options.appKey = "defaultApp"

            do {
                let session = try FPWCSApi2.createSession(options)

                processSession(session)

                appDelegate.providerDelegate?.setSession(session)
                session.connect()
            } catch {
                print(error)
            }

3. Получение события об успешном создании сессии

fpwcsSessionStatusEstablished code

При этом сохраняются данные для подключения к сессии при получении уведомления о входящем звонке

        session.on(kFPWCSSessionStatus.fpwcsSessionStatusEstablished, callback: { rSession in
            NSLog("Session established")
            self.saveFields(rSession?.getAuthToken())
            self.toLogoutState()
        })

4. Сохранение текущего объекта WCSSession и настройка обработчиков входящего звонка для сессии

fpwcsCallStatusFinish, fpwcsCallStatusEstablished code

    func setSession(_ session: FPWCSApi2Session) {
        self.session = session;
        
        
        session.onIncomingCallCallback({ rCall in
            
            guard let call = rCall else {
                return
            }
                        
            call.on(kFPWCSCallStatus.fpwcsCallStatusFinish, callback: {rCall in
                self.viewController.toNoCallState()

                guard let uuid = rCall?.getUuid() else {
                    return
                }
                self.provider.reportCall(with: uuid, endedAt: Date(), reason: .remoteEnded)
            })
            let id = call.getId()
            
            NSLog("CKD - session.onIncomingCallCallback. wcsCallId: " + (id ?? ""))

            
            call.on(kFPWCSCallStatus.fpwcsCallStatusEstablished, callback: {rCall in
                self.viewController.toHangupState(call.getId())
            })
            
            self.viewController.toAnswerState(call.getId())
            self.currentCall = call
            self.actionCall?.fulfill()
        })
    }

5. Запрос действия для ответа на звонок

code

    func answer(_ callId: String) {
        guard let call = self.session?.getCall(callId) else {
            return
        }
        let callController = CXCallController()
        let answerCallAction = CXAnswerCallAction(call: call.getUuid())
        callController.request(CXTransaction(action: answerCallAction),
                                             completion: { error in
                                                 if let error = error {
                                                     print("Error: \(error)")
                                                 } else {
                                                     print("Success")
                                                 }
                                             })
    }

6. Выполнение действия для ответа на звонок

code

    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        NSLog("CKD - CXAnswerCallAction: " + action.callUUID.uuidString)

        guard let call = self.session?.getCallBy(action.callUUID) else {
            if (self.session?.getStatus() == kFPWCSSessionStatus.fpwcsSessionStatusDisconnected || self.session?.getStatus() == kFPWCSSessionStatus.fpwcsSessionStatusFailed) {
                self.session?.connect()
            }
            self.actionCall = action
            return
        }
        self.currentCall = call
        action.fulfill(withDateConnected: NSDate.now)
    }

7. Ответ на звонок

call.answer code

    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
        NSLog("CKD - didActivate \(#function)")
        currentCall?.answer()
    }

8. Выполнение действия для завершения звонка

code

    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        NSLog("CKD - CXEndCallAction: " + action.callUUID.uuidString)
        guard let call = session?.getCallBy(action.callUUID) else {
            action.fulfill()
            return
        }
        self.hangup(call.getId())
        action.fulfill()
            
    }

9. Завершение звонка

call.hangup code

    func hangup(_ callId: String) {
        guard let call = self.session?.getCall(callId) else {
            return
        }
        call.hangup()
        self.provider.reportCall(with: call.getUuid(), endedAt: Date(), reason: .remoteEnded)
    }

10. Настройка данных для приема пуш-уведомлений

code

    func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
        if (type == .voIP) {
            let token = credentials.token.map { String(format: "%02.2hhx", $0) }.joined()
            NSLog("CKD - Voip token: " + token)
            UserDefaults.standard.set(token, forKey: "voipToken")
        }
    }

11. Получение пуш-уведомлений

code

    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
        guard type == .voIP else { return }
        if let id = payload.dictionaryPayload["id"] as? String,
           let uuidString = payload.dictionaryPayload["uuid"] as? String,
           let uuid = UUID(uuidString: uuidString),
           let handle = payload.dictionaryPayload["handle"] as? String
        {
            NSLog("CKD - pushRegistry uuidString: " + uuidString + "; id: " + id + "; handle: " + handle)
            providerDelegate?.reportIncomingCall(uuid: uuid, handle: handle, completion: nil)
        }
    }

12. Обработчик расширения для приема звонков

code

    func handle(intent: INStartAudioCallIntent, completion: @escaping (INStartAudioCallIntentResponse) -> Void) {
        let response: INStartAudioCallIntentResponse
        defer {
            completion(response)
        }

        // Ensure there is a person handle
        guard intent.contacts?.first?.personHandle != nil else {
            response = INStartAudioCallIntentResponse(code: .failure, userActivity: nil)
            return
        }

        let userActivity = NSUserActivity(activityType: String(describing: INStartAudioCallIntent.self))

        response = INStartAudioCallIntentResponse(code: .continueInApp, userActivity: userActivity)
    }



  • No labels