iOS VoIP電話:CallKit與PushKit的應用

做者:鎮雷html

蘋果在WWDC2016推出了iOS10系統新功能CallKit framework,代替了原來的CoreTelephony.framework,能夠調起系統的接聽頁進行音視頻通話;iOS8中蘋果新引入了PushKit的框架和一種新的push通知類型:VoIP push,提供區別於普通APNS push的能力,經過這種push方式收到消息時會直接將已經殺掉的APP激活,兩個庫配合使用造成了一套完整的VoIP解決方案。因爲CallKit支持版本較高,並且限定了應用場景,目前集成的APP不是不少,官方文檔和網上博客對相關功能介紹細節都頗有限,這篇文章主要爲了記錄一下項目過程當中遇到的問題。數組

==========bash

效果圖以下,由於CallKit使用的是系統原生的控件, iOS10與iOS11的樣式上有區別:session

屏幕快照 2018-03-30 上午11.42.23.png

==========app

閒魚調用的邏輯圖以下: 框架

屏幕快照 2018-03-29 上午11.04.02.png

==========ide

下面是CallKit和PushKit這兩個庫的簡單介紹:測試

CallKit主要有:CXProvider、CXCallController、CXProviderConfiguration這三個類,使用時須要新建一個CallKit管理類並實現CXProviderDelegate協議。 實現步驟以下:fetch

1,設置CXProviderConfiguration優化

static CXProviderConfiguration* configInternal = nil;
configInternal = [[CXProviderConfiguration alloc] initWithLocalizedName:@"閒魚"];
configInternal.supportsVideo = true;
configInternal.maximumCallsPerCallGroup = 1;
configInternal.maximumCallGroups = 1;
configInternal.supportedHandleTypes = [[NSSet alloc] initWithObjects:[NSNumber numberWithInt:CXHandleTypeGeneric],[NSNumber numberWithInt:CXHandleTypePhoneNumber], nil];
UIImage* iconMaskImage = [UIImage imageNamed:@"IconMask"];
configInternal.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage);

複製代碼

2,初始化CXProvider與CXCallController

self.provider = [[CXProvider alloc] initWithConfiguration: configInternal];
[provider setDelegate:self queue:dispatch_get_main_queue()];
self.callController = [[CXCallController alloc] initWithQueue:dispatch_get_main_queue()];

複製代碼

3,實現通話流程或按鈕的回調方法(每一個回調結束的時候要執行[action fulfill];不然會提示通話失敗)

- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action;
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action;
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action;
- (void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action;
- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action;
- (void)provider:(CXProvider *)provider performSetGroupCallAction:(CXSetGroupCallAction *)action;
- (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action;
……
複製代碼

4,實現呼起電話和結束電話的方法

- (void)reportIncomingCallWithTitle:(NSString *)title Sid:(NSString *)sid{
    CXCallUpdate* update = [[CXCallUpdate alloc] init];
    update.supportsDTMF = false;
    update.supportsHolding = false;
    update.supportsGrouping = false;
    update.supportsUngrouping = false;
    update.hasVideo = false;
    update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:sid];
    update.localizedCallerName = title;
    NSUUID *uuid = [NSUUID UUID];
    //彈出電話頁面
    [self.provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) {
    }];
}

複製代碼
- (void)endCallAction {
    CXEndCallAction* endCallAction = [[CXEndCallAction alloc] initWithCallUUID:self.currentCall];
    CXTransaction* transaction = [[CXTransaction alloc] init];
    [transaction addAction:endCallAction];
    //關閉電話頁面
    [_callController requestTransaction:transaction completion:^(NSError * _Nullable error) {
    }];
}
複製代碼

PushKit主要有3步操做:

1,經過PKPushRegistry註冊VoIP服務(通常在APP啓動代碼裏添加)

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
	PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] 	initWithQueue:dispatch_get_main_queue()];
	pushRegistry.delegate = self;
	pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
	return YES;
}

複製代碼

2,實現PKPushRegistryDelegate獲取token方法

- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type {
    NSString *str = [NSString stringWithFormat:@"%@",credentials.token];
    NSString *tokenStr = [[[str stringByReplacingOccurrencesOfString:@"<" withString:@""]
                           stringByReplacingOccurrencesOfString:@">" withString:@""] stringByReplacingOccurrencesOfString:@" " withString:@""];
    //上傳token處理
}

複製代碼

3,實現PKPushRegistryDelegate接收VoIP消息方法

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type {
    NSDictionary *alert = [payload.dictionaryPayload[@"aps"] objectForKey:@"alert"];
    //調用CallKit處理
}

複製代碼

==========

在作VoIP方案時可能會遇到的問題:

Q:鎖屏時收不到VoIP消息的問題

A:開發時遇到一個非鎖屏下能正常收到VoIP push,但鎖屏時常常收不到的問題,經排查,是鎖屏下收到VoIP時APP發生了crash,crash日誌裏顯示的緣由是Termination Reason: Namespace SPRINGBOARD,Code 0x8badf00d,這個錯誤是由於watchdog超時引發,程序啓動時,超過了5-6秒APP會被系統殺掉,而系統在鎖屏的狀態下啓動要比激活狀態慢不少,很容易觸發watchdog的crash。解決的方法就是優化APP啓動時的代碼,把能夠延後的操做盡可能延後執行,同時我對設備的cpu也作的了判斷,armv7的低端設備啓動慢容易超時不使用VoIP,保留APNS發送。

Q:APP啓動時收不到VoIP token問題

A:要接收VoIP token 除了要引入PushKit庫,註冊並實現代理外,還要在工程的Capabilities中打開3個backmode:Background fetch、Remote nofications、Voice over IP,以及Push Notifications(在工程裏打開設置,和手機裏設置的接收通知權限沒有關係,即便用戶將設置裏的APNS關閉也能收到VoIP消息)。

Q:獲取點擊通話記錄事件問題

A:收到的VoIP電話,會出如今系統通話記錄裏,點擊通話記錄,會執行回調

- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * __nullable))restorationHandler 

複製代碼

外部連接喚起都會執行這個方法,須要再根據userActivity.activityType的值(INStartAudioCallIntent或INStartVideoCallIntent,取決於你在喚起CallKit時CXCallUpdate設置的hasVideo值)來判斷是點擊通話記錄行爲。

在通話記錄詳情裏,有我的社交資料,這裏的值是經過CXCallUpdate的remoteHandle帶過去的,這個值通常用一個惟一而又不敏感的值(避免使用電話號碼)用於回撥,咱們使用的是IM會話的sessionId。

IMG_5704.jpeg

IMG_5705.PNG

Q:無聲問題

A:主要是在接通的時候在performAnswerCallAction方法裏將AVAudioSession設置setCategory爲PlayAndRecord。(雙方都須要將AVAudioSession設置爲PlayAndRecord)結束以後關閉音頻,去初始化。

Q:facetime 按鈕隱藏問題

A:由於對方極可能沒有登陸或是安卓手機,facetime大部分狀況下是沒法接通的,但接聽頁中的這個按鈕是沒法隱藏的,不過能夠替換爲本身的視頻按鈕,經過將CXProviderConfiguration的supportsVideo設爲true,facetime按鈕位置就會顯示爲視頻,點擊後跳轉進入APP,並會觸發外部跳轉連接方法

- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * __nullable))restorationHandler 
複製代碼

userActivity.activityType的值是INStartVideoCallIntent(也就是說若是你在CXCallUpdate設置的hasVideo值爲true的時候,將沒法區分這個回調是點擊接聽頁視頻跳轉進來觸發的仍是點擊通話記錄跳轉進來觸發的,因此建議hasVideo設置爲false),咱們再經過這個回調打開閒魚音視頻通話的視頻開關。

Q:埋點問題

A:鎖屏接聽頁上有6個按鈕,分別爲:靜音、撥號鍵盤、免提、添加通話、視頻、閒魚(自定義按鈕,點擊跳轉進入APP),再給各個按鈕設置埋點的時候遇到這個問題:CallKit只提供了靜音和添加通話的回調方法,點擊視頻按鈕能夠在外部跳轉連接方法獲取到,其餘按鈕都沒有相應的回調,免提鍵只能經過監聽AVAudioSessionPortOverride值的變化來獲取,撥號鍵盤和跳轉進入APP的自定義按鈕沒法獲取點擊事件。

Q:兼容老版本問題

A:由於PushKit是從iOS8開始支持,CallKit是從iOS10開始支持,這兩個庫的調用都須要作版本保護,咱們但願的是iOS10之前的版本都保留APNS來通知,iOS8和9的設備即便收到VoIP消息也沒法喚起CallKit功能,因而咱們和消息中心的同窗定的規則是:有要發送push的請求時先查詢到用戶表裏有沒有VoIP token,沒有token時仍然發送APNS消息,客戶端會判斷系統版本,若是是iOS10以前的咱們客戶端就不上傳VoIP token。

Q:VoIP證書問題

A:申請的方法同APNS證書,在蘋果開發中心申請,VoIP證書沒有像APNS證書那樣區分開發證書與發佈證書,兩種場景通用一個證書,生成消息服務端使用的p12證書的流程也和APNS同樣,須要注意的申請VoIP證書的bundleID須要提早配置好APNS證書。

Q:免提鍵閃爍,失效問題

A:免提鍵默認關閉,會監聽APP裏AVSession的AVAudioSessionPortOverride值,咱們原來有一個邏輯是鏈接中是揚聲器模式,鏈接成功後切換爲聽筒模式,會致使用戶在接聽過程當中接聽頁上的按鈕閃爍,用戶在鏈接中作的免提操做失效問題,因此要保持整個通話流程裏APP裏不要改變揚聲器的設置。

Q:自定義按鈕上的icon設置問題

A:自定義按鈕用的iconMask是圖片的剪影,原有的icon圖片放上去顯示是一個白色的方塊,須要把圖片背景摳除,保存爲有alpha通道的png圖片

Q:審覈問題

A:最近App Store審覈變的更加嚴格,提交審覈時除了提供兩個能夠正常通話的測試帳號外最好再提供一個相關功能的演示視頻,而且演示視頻裏要有APP被殺掉,而後再收到VoIP通知打開的操做。

========= 擴展

蘋果在推出CallKit的時候就將這兩個庫綁定介紹,其實是兩個能夠獨立調用的庫,除了基本的視頻通話功能,CallKit和PushKit分別有其餘的擴展應用:

CallKit能夠用做通信錄擴展功能,用來屏蔽騷擾電話,好比在IM里拉黑了某個用戶,能夠同時將他的手機號碼屏蔽,實現方法以下:

1,建立一個target,選擇Call Directory Extension

2,主程序中獲取受權狀態和保存須要攔截的號碼

CXCallDirectoryManager *manager = [CXCallDirectoryManager sharedInstance];
// 獲取權限狀態
[manager getEnabledStatusForExtensionWithIdentifier:@"XXX" completionHandler:^(CXCallDirectoryEnabledStatus enabledStatus, NSError * _Nullable error) {
   if (!error) {
     if (enabledStatus == CXCallDirectoryEnabledStatusDisabled ) {
       }
   }
}];
複製代碼
NSUserDefaults * userDefaults = [[NSUserDefaults alloc]initWithSuiteName:@「XXX"]; // 黑名單號碼要升序排列 NSArray *sortedArray = [phoneNumberList sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { return [obj1 compare:obj2]; }]; [userDefaults setObject:sortedArray forKey:@"blackPhoneNum"]; [userDefaults synchronize]; CXCallDirectoryManager *manager = [CXCallDirectoryManager sharedInstance]; [manager reloadExtensionWithIdentifier:@「XXX" completionHandler:^(NSError * _Nullable error) {

複製代碼

3,Extension的代碼CallDirectoryHandler.m的方法實現

- (BOOL)addBlockingPhoneNumbersToContext:(CXCallDirectoryExtensionContext *)context {
    NSUserDefaults * userDefaults = [[NSUserDefaults alloc]initWithSuiteName:@「XXX"]; NSArray * array = [userDefaults objectForKey:@"blackPhoneNum"]; [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSString * phoneStr = obj; int64_t phoneInt = [phoneStr integerValue]; CXCallDirectoryPhoneNumber number = phoneInt ; [context addBlockingEntryWithNextSequentialPhoneNumber:number]; }]; return YES; } - (BOOL)addIdentificationPhoneNumbersToContext:(CXCallDirectoryExtensionContext *)context { NSUserDefaults * userDefaults = [[NSUserDefaults alloc]initWithSuiteName:@「XXX"];
    NSArray * array =  [userDefaults objectForKey:@"blackPhoneNum"];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSString * phoneStr  = obj;
        int64_t phoneInt = [phoneStr integerValue];
        CXCallDirectoryPhoneNumber  number = phoneInt ;
        NSString *label = @"黑名單";
        [context addIdentificationEntryWithNextSequentialPhoneNumber:number label:label];
    }];
    return YES;
}
複製代碼

須要注意兩點:

  • 設置的攔截號碼數組中必須爲升序排列;
  • 攔截的國內手機號碼前必須加上86;

不知足的話,在設置中開啓 ‘來電阻止與身份識別’的時候會報應用程序擴展時出現錯誤。

而PushKit的由於權限很大,能夠經過PushKit在後臺打開應用作不少事,並且系統也沒有給用戶提供任何開關來關閉它(因此蘋果對PushKit的審覈是比較嚴格的,須要謹慎使用,保護用戶數據),經過後臺打開APP,能夠實現後臺提早加載某些比較大的資源或crash以後再後臺將數據重置等功能,具體作法歡迎共同探討。

=========

參考:

https://developer.apple.com/reference/callkit

https://developer.apple.com/documentation/pushkit?language=objc

https://developer.apple.com/library/prerelease/content/samplecode/Speakerbox/Introduction/Intro.html

相關文章
相關標籤/搜索