利用voip push实现类似微信(QQ)电话连续响铃效果

前言

本文大部分参考 iOS利用voip push实现类似微信(QQ)电话连续响铃效果 有兴趣的可以多参考它下
自己代码:gitHubDemo


最近客户要求需要做出类似微信的 视频拉起的呼叫的功能,开始使用的是mqtt的方式来发起的消息拉起,

结果:发现 app退到后台、或者app杀死了,就不会收到了(也就拉起不了了)

试了:APNs的方式,结果APNs根本实现不了连续通知,而且它也不会实现像本地通知那样会有连续响铃的效果(微信一般大概30s左右)

为了实现类似微信的方式,最终:我们通过 voip的方式来实现app的视频的拉起

  • 说明

    • VoIP 推送基于pushKit框架
    • 好处:
      • 只有当VoIP发生推送时,设备才会唤醒,从而节省能源。
      • VoIP推送被认为是高优先级通知,并且毫无延迟地传送。
      • VoIP推送可以包括比标准推送通知提供的数据更多的数据。
      • 如果收到VoIP推送时,您的应用程序未运行,则会自动重新启动。
      • 即使您的应用在后台运行,您的应用也会在运行时处理推送。
  • voip的集成

    • 在Xcode中开启VoIP推送
    voip_01.jpeg
  • 在Apple Developer创建VoIP证书


    voip_02.jpeg
  • 跟APNs证书不同,VoIP证书不区分开发和生产环境,VoIP证书只有一个,生产和开发都可用同一个证书。另外有自己做过推送的应该都知道服务器一般集成的.pem格式的证书,所以还需将证书转成.pem格式,后面会介绍怎么转换.pem证书。
    导入framework:PushKit.framework

  • Objective-C代码集成,导入头文件.

    #import <PushKit/PushKit.h>
    
  • 设置代理

    PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
    pushRegistry.delegate = self;
    pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
    
  • 代理方法

    //应用启动此代理方法会返回设备Token 、一般在此将token上传服务器
    - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type{
    }
    
    //当VoIP推送过来会调用此方法,一般在这里调起本地通知实现连续响铃、接收视频呼叫请求等操作
    - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type { 
    }
    

下面是我项目里面的代码:(本人亲用有效)

RingCall.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface RingCall : NSObject
+ (instancetype)sharedMCCall;
- (void)regsionPush;
@end
NS_ASSUME_NONNULL_END

RingCall.m
#import "RingCall.h"
#import "TalkVideoManager.h"
#import <UserNotifications/UserNotifications.h>
#import <AudioToolbox/AudioToolbox.h>

@interface RingCall ()<VideoCallbackDelegate>{
    UILocalNotification *callNotification;
    UNNotificationRequest *request;//ios 10
    NSTimer *_vibrationTimer;
}
@end

@implementation RingCall
+ (instancetype)sharedMCCall {
    static  RingCall *callInstane;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (callInstane == nil) {
            callInstane = [[RingCall alloc] init];
            [[TalkVideoManager sharedClient] setDelegate:callInstane];
        }
    });
    return callInstane;
}

- (void)regsionPush {
    //iOS 10
    if(@available(iOS 10.0, *)){
        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);
        }];
    }
}

#pragma mark-VideoCallbackDelegate
- (void)onCallRing:(NSString *)CallerName withInfo:(NSDictionary *)info{
    if (@available(iOS 10.0, *)) {
        UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
        UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
        content.body =[NSString localizedUserNotificationStringForKey:[NSString
                                                                       stringWithFormat:@"%@%@", CallerName,
                                                                       @"邀请你进行通话。。。。"] arguments:nil];
        content.userInfo = info;
        UNNotificationSound *customSound = [UNNotificationSound soundNamed:@"weixin.m4a"];
        content.sound = customSound;
        UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger
                                                      triggerWithTimeInterval:1 repeats:NO];
        request = [UNNotificationRequest requestWithIdentifier:@"Voip_Push"
                                                       content:content trigger:trigger];
        [self playShake];
        [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
            
        }];
    }else {
        callNotification = [[UILocalNotification alloc] init];
        callNotification.userInfo = info;
        callNotification.alertBody = [NSString
                                      stringWithFormat:@"%@%@", CallerName,
                                      @"邀请你进行通话。。。。"];
        callNotification.soundName = @"weixin.m4a";
        [self playShake];
        [[UIApplication sharedApplication] presentLocalNotificationNow:callNotification]; 
    }  
}

- (void)onCancelRing {
    //取消通知栏
    if (@available(iOS 10.0, *)) {
        NSMutableArray *arraylist = [[NSMutableArray alloc]init];
        [arraylist addObject:@"Voip_Push"];
        [[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:arraylist];
    }else {
        [[UIApplication sharedApplication] cancelLocalNotification:callNotification];
    }
    [_vibrationTimer invalidate];
}

-(void)playShake{
    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);
}
@end


TalkVideoManager.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@protocol VideoCallbackDelegate <NSObject>
/**
 *  当APP收到呼叫、处于后台时调用、用来处理通知栏类型和铃声。
 *
 *  @param name 呼叫者的名字
 */
- (void)onCallRing:(NSString*)name withInfo:(NSDictionary*)info;
/**
 *  呼叫取消调用、取消通知栏
 */
- (void)onCancelRing;
@end

@interface TalkVideoManager : NSObject
+ (TalkVideoManager *)sharedClient;
- (void)initWithSever;
- (void)setDelegate:(id<VideoCallbackDelegate>)delegate;
//用户挂断/接听  停止震动
-(void)cancleCall;
@end

TalkVideoManager.m
#import "TalkVideoManager.h"
#import <PushKit/PushKit.h>
#import "RingCall.h"
@interface TalkVideoManager ()<PKPushRegistryDelegate>{
    NSString *token;
}

@property (nonatomic,weak)id<VideoCallbackDelegate>mydelegate;
@end

@implementation TalkVideoManager
static TalkVideoManager *instance = nil;
+ (TalkVideoManager *)sharedClient {
    if (instance == nil) {
        instance = [[super allocWithZone:NULL] init];
    }
    return instance;
}

-(void)initWithSever {
    //voip delegate
    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) {
        [[RingCall sharedMCCall] regsionPush];
    }
}

- (void)setDelegate:(id<VideoCallbackDelegate>)delegate {
    self.mydelegate = delegate;
}

#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:@""];
    NSLog(@"token:%@",token);
    //token上传服务器
    [[ACCacheTool shareACCacheTool] setObjectForKey:token key:@"deviceToken"];
}

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type{
    BOOL isCalling = false;
    switch ([UIApplication sharedApplication].applicationState) {
        case UIApplicationStateActive: {
            isCalling = false;
        }
            break;
        case UIApplicationStateInactive: {
            isCalling = false;
        }
            break;
        case UIApplicationStateBackground: {
            isCalling = true;
        }
            break;
        default:
            isCalling = true;
            break;
    }
    NSLog(@"payload==%@",payload.dictionaryPayload);
    if (isCalling){
        //获取推送的内容
        NSString *callerStr = payload.dictionaryPayload[@"aps"][@"alert"];
        //本地通知,实现响铃效果
        [self.mydelegate onCallRing:callerStr withInfo:payload.dictionaryPayload];
        
    }
}

-(void)cancleCall{
    [self.mydelegate onCancelRing];
}
@end

--------------------------------------------------
使用:
在appdelegate.m里面
导入 #import "TalkVideoManager.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
  ......
    //配置voIP
    [[TalkVideoManager sharedClient] initWithSever];
    return YES;
}

- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
   NSLog(@"点击了本地通知进来了---%@",notification.userInfo);
   [[TalkVideoManager sharedClient] cancleCall];
   
   .......//处理数据
}

说白了:就是通过voip发的消息回调,来进行发送本地通知,然后点击本地通知,再做相应的数据逻辑处理。

  • 建立本地测试环境(自己搭建一个简单的测试环境先测试通,然后再与服务器对接)

    • 制作.pem格式证书

      1、将之前生成的voip.cer SSL证书双击导入钥匙串
      2、打开钥匙串访问,在证书中找到对应voip.cer生成的证书,右键导出并选择.p12格式,这里我们命名为voippush.p12,这里导出需要输入密码(随意输入,别忘记了)。
      3、目前我们有两个文件,voip.cer SSL证书和voippush.p12私钥,新建文件夹命名为VoIP、并保存两个文件到VoIP文件夹。
      4、把.cer的SSL证书转换为.pem文件,打开终端命令行cd到VoIP文件夹、执行以下命令
      openssl x509 -in voip.cer  -inform der -out VoiPCert.pem
      5、把.p12私钥转换成.pem文件,执行以下命令(这里需要输入之前导出设置的密码)
      openssl pkcs12 -nocerts -out VoIPKey.pem -in voippush.p12
      6、再把生成的两个.pem整合到一个.pem文件中
      cat VoiPCert.pem VoIPKey.pem > ck.pem
      最终生成的ck.pem文件一般就是服务器用来推送的。
      
    • 新建php文件(保存名字为push.php)

      <?php
      
      // Put your device token here (without spaces):
      $deviceToken = '这里填写手机注册的devideToken';
          
      $passphrase = '这里填写导出p12文件的密码';
      
      // Put your alert message here:
      $message = '要推送的内容';
      $info =  array("id"=>"1","address"=>"IANA"); //自己写的一个info字典(自己后台可以自定义)
      
      ////////////////////////////////////////////////////////////////////////////////
      
      $ctx = stream_context_create();
      stream_context_set_option($ctx, 'ssl', 'local_cert', 'ck.pem'); //这里的ck.pem需要和导出的证书名字要一致
      stream_context_set_option($ctx, 'ssl', 'passphrase', $passphrase);
      stream_context_set_option($ctx, 'ssl', 'verify_peer', false);
      
      // Open a connection to the APNS server
      //ssl://gateway.sandbox.push.apple.com:2195(测试环境)
      //ssl://gateway.push.apple.com:2195(生产环境)
      $fp = stream_socket_client('ssl://gateway.sandbox.push.apple.com:2195', $err,$errstr, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx);
      
      if (!$fp)
          exit("Failed to connect: $err $errstr" . PHP_EOL);
      
      echo 'Connected to APNS' . PHP_EOL;
      
      // Create the payload body
      $body['aps'] = array(
          'content-available' => '1',
          'alert' => $message,
          'sound' => 'weixin.m4a',
          'badge' => 0,
          'info'=> $info,
          );
      
      // Encode the payload as JSON
      $payload = json_encode($body);
      
      // Build the binary notification
      $msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken) . pack('n', strlen($payload)) . $payload;
      
      // Send it to the server
      $result = fwrite($fp, $msg, strlen($msg));
      
      if (!$result)
          echo 'Message not delivered' . PHP_EOL;
      else
          echo 'Message successfully delivered' . PHP_EOL;
      
      // Close the connection to the server
      fclose($fp);   
      ?>
      
      
    • 推送测试

      • ck.pem 文件和push.php 放在同一个文件夹下(必须)

      • 一般测试VoIP推送的稳定性最好是通过Hoc证书打包在生产环境中测试

      • 打开terminal ,cd到 push.php 文件目录下

      • 输入:php push.php (不出意外就可以收到推送啦)

        voip_03.png
  • 补充点(摘录 iOS利用voip push实现类似微信(QQ)电话连续响铃效果

    • 当app要上传App Store时,请在iTunes connect上传页面右下角备注中填写你用到VoIP推送的原因,附加上音视频呼叫用到VoIP推送功能的demo演示链接,演示demo必须提供呼出和呼入功能,demo我一般上传到优酷。
    • 经过大量测试,VoIP当应用被杀死(双击划掉)并且黑屏大部分情况都能收到推送,很小的情况会收不到推送消息,经测试可能跟手机电量消耗还有信号强弱有关。 再强调一遍,测试稳定性请在生产环境测试。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343