iOS10來了,iOS程序員們又有的忙了。
公司產品的核心功能是VoIP語/視頻通話,為了與時俱進,就要適配iOS最新的CallKit。關于CallKit的介紹我就不詳述了,大家可以去看看iOS開發文檔、WWDC或者直接Google。
總的來說,CallKit有三大優勢:
1.提供系統通話界面,這一點在鎖屏時體驗最明顯。
2.VoIP通話權限提升到系統級別,即不是隨便被系統電話打斷,而是可以選擇拒接。
3.支持系統通訊記錄沉淀與喚起。
從這三點“升級”可以看出蘋果是非常看中VoIP的市場,現在我們可以像打系統電話一樣使用VoIP了。
那么,我就開門見山的介紹一些API的使用吧。
CXProvider
The CXProvider class provides a programmatic interface to an object that represents a telephony provider. A CXProvider object is responsible for reporting out-of-band notifications that occur to the system.
我們首先要初始化一個單例的provider。其方法是
- (instancetype)initWithConfiguration:(CXProviderConfiguration *)configuration
這里的CXProviderConfiguration很重要,很多我們顯式看到的信息都是在這里面配置好的。
@interface CXProviderConfiguration : NSObject <NSCopying>
//系統來電頁面顯示的app名稱和系統通訊記錄的信息
@property (nonatomic, readonly, copy) NSString *localizedName;
//來電鈴聲
@property (nonatomic, strong, nullable) NSString *ringtoneSound;
//鎖屏接聽時,系統界面右下角的app圖標,要求40 x 40大小
@property (nonatomic, copy, nullable) NSData *iconTemplateImageData;
//最大通話組
@property (nonatomic) NSUInteger maximumCallGroups; // Default 2
//是否支持視頻
@property (nonatomic) BOOL supportsVideo; // Default NO
//支持的Handle類型
@property (nonatomic, copy) NSSet<NSNumber *> *supportedHandleTypes;
@end
我們初始化provider之后還要設置它代理,以便執行CXProviderDelegate的方法。其方法是:
- (void)setDelegate:(nullable id<CXProviderDelegate>)delegate queue:(nullable dispatch_queue_t)queue;
queue一般直接指定為nil,即在main線程執行callback。
完成初始化之后,provider 就可以為我們服務了,這時候來了一個VoIP電話,那么它應該報告系統,好讓系統按照它的配置彈出一個系統來電界面。其方法是:
- (void)reportNewIncomingCallWithUUID:(NSUUID *)UUID update:(CXCallUpdate *)update completion:(void (^)(NSError *_Nullable error))completion;
其中UUID是每次隨機生成的,標記一次通話;CXCallUpdate有點類似CXConfiguration,也是一些配置信息。
@interface CXCallUpdate : NSObject <NSCopying>
//通話對方的Handle 信息
@property (nonatomic, copy, nullable) CXHandle *remoteHandle;
//對方的名字,可以設置為app注冊的昵稱
@property (nonatomic, copy, nullable) NSString *localizedCallerName;
//通話過程中再來電,是否支持保留并接聽
@property (nonatomic) BOOL supportsHolding;
//是否支持鍵盤撥號
@property (nonatomic) BOOL supportsDTMF;
//本次通話是否有視頻
@property (nonatomic) BOOL hasVideo;
@end
這些配置信息會影響鎖屏時的接聽界面上的按鈕狀態以及多個通話的選擇界面。如果執行成功,completion中的error為nil, 否則,不會彈出系統界面。
由于非本地人為(文章最后解釋)的因素導致的通話結束,需要報告系統通話結束的時間和原因。其方法是:
- (void)reportCallWithUUID:(NSUUID *)UUID endedAtDate:(nullable NSDate *)dateEnded reason:(CXCallEndedReason)endedReason;
如果dateEnded為nil,則認為結束時間是現在。
我們還可以動態更改provider的配置信息CXCallUpdate,比如作為撥打方,開始沒有地方配置通話的界面,就可以在通話開始時更新這些配置信息。 其方法是:
- (void)reportCallWithUUID:(NSUUID *)UUID updated:(CXCallUpdate *)update;
作為撥打方,我們還可以報告通話的狀態,以便讓系統知道我們app的VoIP真正的通話開始時間。
通話連接時:
- (void)reportOutgoingCallWithUUID:(NSUUID *)UUID startedConnectingAtDate:(nullable NSDate *)dateStartedConnecting;
通話連接上:
- (void)reportOutgoingCallWithUUID:(NSUUID *)UUID connectedAtDate:(nullable NSDate *)dateConnected;
CXCallController
The CXCallController class provides the programmatic interface for interacting with and observing calls.
初始化:
- (instancetype)initWithQueue:(dispatch_queue_t)queue
queue也是指定執行callback的線程,默認是main線程。
在開始或結束一次通話時,需要提交action事務請求,這些事務會交給上面的provider執行。
- (void)requestTransaction:(CXTransaction *)transaction completion:(void (^)(NSError *_Nullable error))completion;
Transaction可以通過三種方法添加Action:
- (instancetype)initWithActions:(NSArray<CXAction *> *)actions
- (instancetype)initWithAction:(CXAction *)action;
- (void)addAction:(CXAction *)action;
CXAction是CXCallAction的基類,常見的CXCallAction有:
CXCallAction Subclass | Description |
---|---|
CXAnswerCallAction | Answers an incoming call |
CXStartCallAction | Initiates an outgoing call |
CXEndCallAction | Ends a call |
CXSetHeldCallAction | Places a call on hold or removes a call from hold |
CXSetGroupCallAction | Groups a call with another call or removes a call from a group. |
CXSetMutedCallAction | Mutes or unmutes a call |
CXPlayDTMFCallAction | Plays a DTMF (dual tone multi frequency) tone sequence on a call |
CXProviderDelegate
The CXProviderDelegate protocol defines methods that are called by a CXProvider object when a provider begins or reset, when a transaction is requested, when an action is performed, and when an audio session changes its activation state.
當撥打方成功發起一個通話后,會觸發
- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action;
當接聽方成功接聽一個電話時,會觸發
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action;
當接聽方拒接電話或者雙方結束通話時,會觸發
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action;
當點擊系統通話界面的Mute按鈕時,會觸發
- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action;
流程圖
一個簡單經典的CallKit 通話流程如下圖:
坑
蘋果官方現在還沒有給出Callkit的完整文檔,所以都是自己摸索,難免有很多坑。
-
無聲
剛開始做的時候,會偶然碰到無聲的情況,這個時候發現可以在VoIP通話成功后直接結束系統的通話界面就有聲音了。然后就這么很傻叉地做了,而且發現imo一開始也是這么做的。不過,這樣肯定會帶來問題,最簡單的就是系統通話紀錄的時長顯示不對,因為它是按照callkit上報的開始和結束時間算的,這樣毫無理由地結束當然顯示錯誤。QQ最先寫了一篇文章,講到無聲的處理方法是
在流程開始前setCategory為PlayAndRecord
突然發現自己的代碼里也寫了這句話,由于以前的代碼邏輯就會處理這種音頻問題,所以懷疑是沖突了,反正現在不是很懂,感覺小復雜,去掉就可以了。
-
如何在系統通訊錄中增加選項
既然可以沉淀到系統通話紀錄中,就應該可以在通話紀錄中直接呼出。那么長按系統通訊錄中的“呼叫”如何顯示我們自己的app名稱呢?就像圖中的Whatsup和SpeakerBox一樣。
其實這依賴于CXProviderConfiguration的一個配置項:
configuration.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypePhoneNumber)];
為了支持安裝app就生效,可以在AppDelegate.m的didFinishLaunchingWithOptions方法中去做這個配置。
-
如何從系統通訊中直接呼出
上面解決了選項問題,那么為什么點擊了app的名字沒有任何反應呢?
這需要在AppDelegate.m的continueUserActivity方法中響應。
INInteraction *interaction = userActivity.interaction;
INIntent *intent = interaction.intent;
if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"])
{
INPerson *person = [(INStartAudioCallIntent *)intent contacts][0];
CXHandle *handle = [[CXHandle alloc] initWithType:(CXHandleType)person.personHandle.type value:person.personHandle.value];
[[CallKitManager sharedInstance] startCallAction:handle isVideo:NO];
return YES;
} else if([userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) {
INPerson *person = [(INStartVideoCallIntent *)intent contacts][0];
CXHandle *handle = [[CXHandle alloc] initWithType:(CXHandleType)person.personHandle.type value:person.personHandle.value];
[[CallKitManager sharedInstance] startCallAction:handle isVideo:YES];
return YES;
}
另外,在reportNewIncomingCallWithUUID:update:completion:時要指定remoteHandle為對方的Handle。
-
何種方式結束
上面的介紹,我們知道結束通話可以有兩種方法:
//1
- (void)reportCallWithUUID:(NSUUID *)UUID endedAtDate:(nullable NSDate *)dateEnded reason:(CXCallEndedReason)endedReason;
//2
requestTransaction:CXEndCallAction
那么它們有什么區別,該選擇哪個呢?
這個問題我在stackoverflow上提問了,答案我覺得很清楚,在此感謝這位@user102008解惑!
You do requestTransactionwith a CXEndCallAction when the user actively chooses to end the call from your app's UI. You do
reportCallWithUUID:endedAtDate:reason:
when it ended not due to user action (i.e. not due to
provider:performEndCallAction:). If you take a look at the allowed
CXCallEndedReasons (failed, remote ended, unanswered, answered elsewhere, and declined elsewhere), they are all reasons not due to the user's action.