Mac·iOS开发Swift开发

iOS VoIP应用程序(这一篇就够了)

2021-11-22  本文已影响0人  XXViper

一、涉及

1、CallKit

1.1、概述 (对于CallKit我们并不陌生)

1.2、功能场景

CallKit.jpeg

1.3、支持

2、PushKit

2.1、概述

2.2、功能场景

2.3、支持

3、Voice Over IP (VoIP)

3.1、概述

3.2、功能场景

3.3、支持

二、使用

1、唤醒应用,PushKit的配置步骤

1.1、注册VoIP Push Notification通知证书

配置证书.jpeg

ps:

\color{red} {需要注意}:普通的推送分开发环境和生产环境,VoIP证书不进行区分,生产环境和开发环境是通用的。之后选择一个AppID并且上传前面生成的cerSigningRequest文件来完成VoIP证书的创建。

创建完成后,在证书列表可以看到多了个 VoIP服务证书,可以加载此证书进行VoIP推送。

info.plist

1.2、工程配置

a、配置push

配置push.png

b、配置后台获取模块、打开VoIP

配置后台获取.png
打开VoIP.png

c、配置依赖系统库

配置依赖系统库.png

1.3、代码配置

a、导入需要的头文件

#import <PushKit/PushKit.h>
#import <UserNotifications/UserNotifications.h>
#import <AudioToolbox/AudioToolbox.h></pre>

b、注册本地通知设置代理

 PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
    pushRegistry.delegate = self;
    pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
    //ios10注册本地通知
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
        //iOS 10
    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
    [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) {
        if (!error) {
            NSLog(@"request authorization succeeded!");
        }
    }];
    [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
        NSLog(@"%@",settings);
    }];
    }

c、代理实现

如果您的应用程序在\color{red} {收到推送时}(pushRegistry:didReceiveIncomingPushWithPayload:forType:)\color{red} {未运行,您的应用程序将自动启动},您有\color{red} {30s的时间}处理这个事件

#pragma mark -pushkitDelegate

- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type{
    if([credentials.token length] == 0) {
        NSLog(@"voip token NULL");
        return;
    }
    //应用启动获取token,并上传服务器
    token = [[[[credentials.token description] stringByReplacingOccurrencesOfString:@"<"withString:@""]
              stringByReplacingOccurrencesOfString:@">" withString:@""]
             stringByReplacingOccurrencesOfString:@" " withString:@""];
    //token上传服务器
    //[self uploadToken];
}

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type{
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
        UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
        UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
        content.body =[NSString localizedUserNotificationStringForKey:[NSString
                                                                       stringWithFormat:@"%@%@", CallerName,
                                                                       @"邀请你进行通话。。。。"] arguments:nil];;
        UNNotificationSound *customSound = [UNNotificationSound soundNamed:@"voip_call.caf"];
        content.sound = customSound;
        UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger
                                                      triggerWithTimeInterval:1 repeats:NO];
        request = [UNNotificationRequest requestWithIdentifier:@"Voip_Push"
                                                       content:content trigger:trigger];
        [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
            NSLog(@"结束");
        }];
    }else {

        callNotification = [[UILocalNotification alloc] init];
        callNotification.alertBody = [NSString
                                      stringWithFormat:@"%@%@", CallerName,
                                      @"邀请你进行通话。。。。"];

        callNotification.soundName = @"voip_call.caf";
        [[UIApplication sharedApplication]
         presentLocalNotificationNow:callNotification];

    }
    /**
     初始化计时器  每一秒振动一次
     @param playkSystemSound 振动方法
     @return
     */
    if(_vibrationTimer){
        [_vibrationTimer invalidate];
        _vibrationTimer = nil;
    }else{
        _vibrationTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(playkSystemSound) userInfo:nil repeats:YES];
    }
    }

}
//振动
- (void)playkSystemSound{
        AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
}

2、CallKit如何控制

2.1、关于CallKit

下图比较形象的表达了应用程序与CallKit以及系统的关系:

CallKit

CXProvider

CXCallController

2.2、呼入、来电处理(CXProvider

a、通过CXProviderConfiguration初始化一个全局的CXProvider实例,并指定代理&可选队列

b、接收到VOIP通知后弹出通话界面,需要使用CXProvider传递CXCallUpdate给系统来进行控制。如下图:

CXProvider

c、使用全局的CXProvider实例传递CXCallUpdate调起通话界面的简单的代码:

CXCallUpdate * callUpdate = [[CXCallUpdate alloc]init];
callUpdate.supportsGrouping = YES;
callUpdate.supportsDTMF = YES;
callUpdate.hasVideo = YES;
callUpdate.supportsHolding = YES;
[callUpdate setLocalizedCallerName:nickName];
CXHandle * handle = [[CXHandle alloc]initWithType:CXHandleTypePhoneNumber value:from];
callUpdate.remoteHandle = handle;
 [[self shareInstance].callProvider reportNewIncomingCallWithUUID:[self shareInstance].uuid update:callUpdate completion:^(NSError * _Nullable error) {
     LOG(@"吊起界面");
}];

d、之后系统会将一些用户操作通过CXAction传递会APP,如下:

CXAction

e、 在CXProviderDelegate中处理这些系统回调事件

//当接收到呼叫重置时 调用的函数,这个函数必须被实现,其不需做任何逻辑,只用来重置状态
- (void)providerDidReset:(CXProvider *)provider;
//呼叫开始时回调 
- (void)providerDidBegin:(CXProvider *)provider;
//音频会话激活状态的回调
- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession;
//音频会话停用的回调
- (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession;
//行为超时的回调 
- (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action;
//有事务被提交时调用 
//如果返回YES 则表示事务被捕获处理 后面的回调都不会调用 如果返回NO 则表示事务不被捕获,会回调后面的函数
- (BOOL)provider:(CXProvider *)provider executeTransaction:(CXTransaction *)transaction;
//点击开始按钮的回调
- (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;
//DTMF功能回调
- (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action;

f、锁屏和应用程序在后台的效果分别如下所示:

image image image

通过CXProviderDelegate相关函数来处理系统通话界面的某些操作回调给应用程序。

2.3、呼出,拨号处理(CXCallController

a、APP中进行的操作如果需要通知系统,需要使用CXCallController通过CXTransaction传递。

CXCallController

b、创建一个新的呼叫控制器CXCallController,可用指定队列创建。获取活动调用观察者CXCallObserver指定观察者的代理CXCallObserverDelegate

c、呼出

let uuid = UUID()
let handle = CXHandle(type: .emailAddress, value: "jappleseed@apple.com") 
let startCallAction = CXStartCallAction(call: uuid)
startCallAction.destination = handle 
let transaction = CXTransaction(action: startCallAction)
callController.request(transaction) { error in
    if let error = error {
        print("Error requesting transaction: \(error)")
    } else {
        print("Requested transaction successfully")
    }
}

d、接收者接听电话后,系统调用CXProvider的代理方法:
接听者在这个方法中\color{red} {配置音视频会话(第3部分有举例)}

//点击开始按钮的回调
- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action;

e、APP中的其他操作也可以举一反三:例如App内的通讯需要添加到系统的历史通话列表:

f、通过CXCallObserverDelegate回调来处理系统事件

extension ViewController: CXCallObserverDelegate {
    func callObserver(_ callObserver: CXCallObserver,callChanged call: CXCall) {

        if call.isOutgoing {

            print("电话播出")

            if call.hasConnected {
                print("电话接通")
                operation(state: CurrentState.HasConnected)
            }
            if call.hasEnded {
                print("电话挂断")
                operation(state: CurrentState.HasEnded)
            }
            if call.isOnHold {
                print("无人接听挂断")
                operation(state: CurrentState.IsOnHold)
            }
        } else {
            print("other error")
        }
    }
}

2.4、来电拦截与号码识别

上面介绍了CallKit的结合PushKit&VoIP可实现的通讯功能,有通讯功能就需要进行联系人识别与黑名单。

社交网络应用程序有一个Call Directory应用程序扩展程序,可以下载并添加用户所有朋友的电话号码。

因此,当用户接到Jane的来电时,系统会显示类似“(应用程序名称)来电显示:Jane Appleseed”而不是“未知来电者”。

CallKit框架中还有一部分内容可以结合\color{red} {Call Directory Extension)}来实现好吗拦截与识别。

Call Directory Extension:

a、创建应用扩展target

选择Call Directory Extension:

Extension

主程序会自动生成Call Directory应用程序扩展的主要对象(继承自CXCallDirectoryProvider的Handler)。

b、应用程序通知扩展程序更新号码库(切换app账号等场景)

CXCallDirectoryManager的api就比较简单了,凭场景组合使用:

CXCallDirectoryManager
/// 单例
@property (readonly, class) CXCallDirectoryManager *sharedInstance;
/// 利用扩展程序bundle id(在extension的info.plist中)来查询是否开启来电识别 
func getEnabledStatusForExtension(withIdentifier: String, completionHandler: (CXCallDirectoryManager.EnabledStatus, Error?) -> Void)
/// 通知系统更新(主app与extension的数据通信、参考group共享数据的编程方式来实现)
func reloadExtension(withIdentifier: String, completionHandler: ((Error?) -> Void)?)
/// 打开设置页面
func openSettings(completionHandler: ((Error?) -> Void)?)
settings

c、扩展程序中的增删电话操作

系统默认生成的应用程序扩展的主要对象中已经替我们优雅的生成了必要的代码,我们仅需取group(主程序给扩展程序使用的数据)中取出来设置就可以

override func beginRequest(with context: CXCallDirectoryExtensionContext) {
        context.delegate = self
    // Check whether this is an "incremental" data request. If so, only provide the set of phone number blocking
    // and identification entries which have been added or removed since the last time this extension's data was loaded.
    // But the extension must still be prepared to provide the full set of data at any time, so add all blocking
    // and identification phone numbers if the request is not incremental.
    if context.isIncremental {
        addOrRemoveIncrementalBlockingPhoneNumbers(to: context)
        addOrRemoveIncrementalIdentificationPhoneNumbers(to: context)
    } else {
        addAllBlockingPhoneNumbers(to: context)
        addAllIdentificationPhoneNumbers(to: context)
    }
    context.completeRequest()
}

d、CXCallDirectoryExtensionContext是操作上下文,不需要初始化,会自系统的方法传递出来

//是否支持增量更新
@property (nonatomic, readonly, getter=isIncremental) BOOL incremental API_AVAILABLE(ios(11.0));
//添加一个黑名单号码
- (void)addBlockingEntryWithNextSequentialPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber;
//移除一个黑名单号码
- (void)removeBlockingEntryWithPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber API_AVAILABLE(ios(11.0));
//移除所有的黑名单号码
- (void)removeAllBlockingEntries API_AVAILABLE(ios(11.0));
//添加一个身份识别
- (void)addIdentificationEntryWithNextSequentialPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber label:(NSString *)label;
//移除一个身份识别
- (void)removeIdentificationEntryWithPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber API_AVAILABLE(ios(11.0));
//移除所有身份识别
- (void)removeAllIdentificationEntries API_AVAILABLE(ios(11.0));
//完成操作后 需要手动调用此函数
- (void)completeRequestWithCompletionHandler:(nullable void (^)(BOOL expired))completion; 

3、结合音视频对话SDK建立通话(以声网 Agora SDK使用举例)

3.1、基本流程

image

3.2、代码使用

a、使用 App ID创建AgoraRtcEngineKit

private lazy var rtcEngine: AgoraRtcEngineKit = AgoraRtcEngineKit.sharedEngine(withAppId: <#Your AppId#>, delegate: self)

b、设置好ChannelProfile和本地预览视图

override func viewDidLoad() {
    super.viewDidLoad()
    rtcEngine.setChannelProfile(.communication)
    let canvas = AgoraRtcVideoCanvas()
    canvas.uid = 0
    canvas.view = localVideoView
    canvas.renderMode = .hidden
    rtcEngine.setupLocalVideo(canvas)
}

c、在AgoraRtcEngineDelegate的远端用户加入频道事件中设置远端视图:

extension ViewController: AgoraRtcEngineDelegate {
    func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
        let canvas = AgoraRtcVideoCanvas()
        canvas.uid = uid
        canvas.view = remoteVideoView
        canvas.renderMode = .hidden
        engine.setupRemoteVideo(canvas)
        remoteUid = uid
        remoteVideoView.isHidden = false
   }
}

d、实现通话开始、静音、结束的方法:

extension ViewController {
    func startSession(_ session: String) {
        rtcEngine.startPreview()
        rtcEngine.joinChannel(byToken: nil, channelId: session, info: nil, uid: 0, joinSuccess: nil)
    }
    func muteAudio(_ mute: Bool) {
        rtcEngine.muteLocalAudioStream(mute)
    }
    func stopSession() {
        remoteVideoView.isHidden = true
        rtcEngine.leaveChannel(nil)
        rtcEngine.stopPreview()
    }
}

一个简单的视频通话应用搭建就完成了。双方只要调用startSession(_:)方法加入同一个频道,就可以进行视频通话。

三、参考

吃水不忘挖井人 :

CallKit

PushKit

Voice Over IP (VoIP) Best Practices

iOS VoIP实践

iOS使用VOIP与CallKit实现体验优质的网络通讯功能

Swift3.0拨打电话,获取通话状态(接通,挂断...)

iOS CallKit框架 详解

应用扩展编程指南

如何结合CallKit和AgoraSDK实现视频VoIP通话应用

上一篇 下一篇

猜你喜欢

热点阅读