iOS10來了绎签,iOS程序員們又有的忙了。
公司產(chǎn)品的核心功能是VoIP語/視頻通話蒲稳,為了與時俱進(jìn)氮趋,就要適配iOS最新的CallKit。關(guān)于CallKit的介紹我就不詳述了江耀,大家可以去看看iOS開發(fā)文檔剩胁、WWDC或者直接Google。
總的來說祥国,CallKit有三大優(yōu)勢:
1.提供系統(tǒng)通話界面,這一點(diǎn)在鎖屏?xí)r體驗(yàn)最明顯昵观。
2.VoIP通話權(quán)限提升到系統(tǒng)級別晾腔,即不是隨便被系統(tǒng)電話打斷,而是可以選擇拒接啊犬。
3.支持系統(tǒng)通訊記錄沉淀與喚起灼擂。
從這三點(diǎn)“升級”可以看出蘋果是非常看中VoIP的市場觉至,現(xiàn)在我們可以像打系統(tǒng)電話一樣使用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>
//系統(tǒng)來電頁面顯示的app名稱和系統(tǒng)通訊記錄的信息
@property (nonatomic, readonly, copy) NSString *localizedName;
//來電鈴聲
@property (nonatomic, strong, nullable) NSString *ringtoneSound;
//鎖屏接聽時纤控,系統(tǒng)界面右下角的app圖標(biāo),要求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之后還要設(shè)置它代理碉纺,以便執(zhí)行CXProviderDelegate的方法船万。其方法是:
- (void)setDelegate:(nullable id<CXProviderDelegate>)delegate queue:(nullable dispatch_queue_t)queue;
queue一般直接指定為nil,即在main線程執(zhí)行callback骨田。
完成初始化之后耿导,provider 就可以為我們服務(wù)了,這時候來了一個VoIP電話态贤,那么它應(yīng)該報告系統(tǒng)碎节,好讓系統(tǒng)按照它的配置彈出一個系統(tǒng)來電界面。其方法是:
- (void)reportNewIncomingCallWithUUID:(NSUUID *)UUID update:(CXCallUpdate *)update completion:(void (^)(NSError *_Nullable error))completion;
其中UUID是每次隨機(jī)生成的抵卫,標(biāo)記一次通話;CXCallUpdate有點(diǎn)類似CXConfiguration,也是一些配置信息胎撇。
@interface CXCallUpdate : NSObject <NSCopying>
//通話對方的Handle 信息
@property (nonatomic, copy, nullable) CXHandle *remoteHandle;
//對方的名字介粘,可以設(shè)置為app注冊的昵稱
@property (nonatomic, copy, nullable) NSString *localizedCallerName;
//通話過程中再來電,是否支持保留并接聽
@property (nonatomic) BOOL supportsHolding;
//是否支持鍵盤撥號
@property (nonatomic) BOOL supportsDTMF;
//本次通話是否有視頻
@property (nonatomic) BOOL hasVideo;
@end
這些配置信息會影響鎖屏?xí)r的接聽界面上的按鈕狀態(tài)以及多個通話的選擇界面晚树。如果執(zhí)行成功姻采,completion中的error為nil, 否則,不會彈出系統(tǒng)界面爵憎。
由于非本地人為(文章最后解釋)的因素導(dǎo)致的通話結(jié)束慨亲,需要報告系統(tǒng)通話結(jié)束的時間和原因。其方法是:
- (void)reportCallWithUUID:(NSUUID *)UUID endedAtDate:(nullable NSDate *)dateEnded reason:(CXCallEndedReason)endedReason;
如果dateEnded為nil,則認(rèn)為結(jié)束時間是現(xiàn)在宝鼓。
我們還可以動態(tài)更改provider的配置信息CXCallUpdate,比如作為撥打方刑棵,開始沒有地方配置通話的界面,就可以在通話開始時更新這些配置信息愚铡。 其方法是:
- (void)reportCallWithUUID:(NSUUID *)UUID updated:(CXCallUpdate *)update;
作為撥打方蛉签,我們還可以報告通話的狀態(tài)胡陪,以便讓系統(tǒng)知道我們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也是指定執(zhí)行callback的線程碍舍,默認(rèn)是main線程柠座。
在開始或結(jié)束一次通話時,需要提交action事務(wù)請求,這些事務(wù)會交給上面的provider執(zhí)行片橡。
- (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.
當(dāng)撥打方成功發(fā)起一個通話后,會觸發(fā)
- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action;
當(dāng)接聽方成功接聽一個電話時捧书,會觸發(fā)
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action;
當(dāng)接聽方拒接電話或者雙方結(jié)束通話時吹泡,會觸發(fā)
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action;
當(dāng)點(diǎn)擊系統(tǒng)通話界面的Mute按鈕時,會觸發(fā)
- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action;
流程圖
一個簡單經(jīng)典的CallKit 通話流程如下圖:
坑
蘋果官方現(xiàn)在還沒有給出Callkit的完整文檔鳄厌,所以都是自己摸索荞胡,難免有很多坑。
-
無聲
剛開始做的時候了嚎,會偶然碰到無聲的情況泪漂,這個時候發(fā)現(xiàn)可以在VoIP通話成功后直接結(jié)束系統(tǒng)的通話界面就有聲音了。然后就這么很傻叉地做了歪泳,而且發(fā)現(xiàn)imo一開始也是這么做的萝勤。不過,這樣肯定會帶來問題呐伞,最簡單的就是系統(tǒng)通話紀(jì)錄的時長顯示不對敌卓,因?yàn)樗前凑誧allkit上報的開始和結(jié)束時間算的,這樣毫無理由地結(jié)束當(dāng)然顯示錯誤伶氢。QQ最先寫了一篇文章趟径,講到無聲的處理方法是
在流程開始前setCategory為PlayAndRecord
突然發(fā)現(xiàn)自己的代碼里也寫了這句話,由于以前的代碼邏輯就會處理這種音頻問題癣防,所以懷疑是沖突了蜗巧,反正現(xiàn)在不是很懂,感覺小復(fù)雜蕾盯,去掉就可以了幕屹。
-
如何在系統(tǒng)通訊錄中增加選項(xiàng)
既然可以沉淀到系統(tǒng)通話紀(jì)錄中,就應(yīng)該可以在通話紀(jì)錄中直接呼出级遭。那么長按系統(tǒng)通訊錄中的“呼叫”如何顯示我們自己的app名稱呢望拖?就像圖中的Whatsup和SpeakerBox一樣。
其實(shí)這依賴于CXProviderConfiguration的一個配置項(xiàng):
configuration.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypePhoneNumber)];
為了支持安裝app就生效挫鸽,可以在AppDelegate.m的didFinishLaunchingWithOptions方法中去做這個配置说敏。
-
如何從系統(tǒng)通訊中直接呼出
上面解決了選項(xiàng)問題,那么為什么點(diǎn)擊了app的名字沒有任何反應(yīng)呢掠兄?
這需要在AppDelegate.m的continueUserActivity方法中響應(yīng)像云。
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。
-
何種方式結(jié)束
上面的介紹迅诬,我們知道結(jié)束通話可以有兩種方法:
//1
- (void)reportCallWithUUID:(NSUUID *)UUID endedAtDate:(nullable NSDate *)dateEnded reason:(CXCallEndedReason)endedReason;
//2
requestTransaction:CXEndCallAction
那么它們有什么區(qū)別腋逆,該選擇哪個呢?
這個問題我在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.