iOS极光推送+语音播报(支付宝收款播报)

前言

很长一段时间以来,都没有进行文章更新了,最近闲暇时间较多,静下心来打算将最近项目中遇到的问题总结一下。今天主要说说语音播报这点事吧!亲身体会~走过路过不要错过。。。。我也是刚从坑里爬出来的。文章最后我会附上我的Demo地址

APP需求

PM要求:收款成功后,通过极光推送相关信息,并使用百度TTS进行语音播报(XXX收款到账1.0元),要求APP前台、后台、杀死进程时都可以进行语音播报;

大家看到这里是不是觉得我在吹牛啊。。。程序杀死了还想语音播报,这个怎么可能的?别灰心,请继续往下面看

其实我们没必要让程序一直保持在运行当中,我们可以给他一个推送,让程序唤醒,执行里面的推送扩展方法就可以。有人说支付宝可以播报,哪怕程序被杀死以后也可以,老实说,你可以把支付宝的推送给关闭了,你再试试收付款,你看看能不能播报了? 测试完你很快就能得知支付宝利用的也是推送扩展。 推送扩展哪方面的呢?

新技能

一、首先我们了解一个新的扩展类

Notification Service Extension 通知服务扩展

iOS 10以后推出的新功能(苹果给出的官方解释)
https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension
按照我的理解总结就是:该扩展类能够让你的APP在收到远程推送信息时对推送内容首先进行预处理,处理完成后,在让你的APP进行处理。而且这部分代码和你的APP本身是分离的,这样就给我们创造的一个单独处理推送的地方。废话补多少,看看如何实现。

二、实现步骤

1.为我们的工程添加Notification Service Extension扩展类

File ---> New ---> target

image.png

出现如下图页面后选择 Notification Service Extension


image.png

创建成功后,你会发现一个很神奇的事情


image.png

Finish之后,你就可以在你的工程里看到你app的Notification Service Extension了。需要注意的是,因为是两个完全独立的target所以,你原有项目里的自己写的类,或原有项目里的资源文件,在Notification Service Extension里是完全访问不到的(打包之后也是两个完全独立的bundle)。所以如果你想要使用项目里的资源或者文件,你需要拖到Notification Service Extension目录里面,才可以使用。
其实从这里你已经看出来了,新创建的Target并不属于你的APP中的一部分,而是一个新的工程,但它和你的APP是绑定的,这样,当我们接收到推送的通知信息时,iPhone就知道到底是谁推送过来的,需不需要进行额外处理

2.集成推送

本文中使用到的是极光推送,这个在这里就不做介绍了,可以再极光官网进行集成

3.对推送内容进行预处理

接下来就是业务代码了,在生成的NotificationService.m文件中对推送内容进行处理。需要注意的一件就是,并非所有的推送都会走这个额外的方法,必须是会弹出alert,并且在你的playload中设置了"mutable-content = 1"这个字段,才会进入这个方法;

{
  "aps": {
  "alert": "This is some fancy message.",
  "badge": 1,
  "sound": "default",
  "mutable-content": "1",
  }
}
image.png

在我的Demo中同时使用了百度TTS语音播报和AVSpeechSynthesis进行语音播报的实现

1)AVSpeechSynthesis

关于AVSpeechSynthesis的介绍和使用大家可以查看我的这篇文章进行了解学习。

a.扩展配置
image.png

在我们的推送扩展类中需要进行这样的设置,毕竟iOS10以后才出现了Notification ServiceExtension


image.png
b.当APP接收到很多推送信息时,上一条未报完已经被下一条信息阻断;

我们可以通过创建队列将每一条播报语音放入队列中进行管理

#pragma mark -队列管理推送通知
- (void)addOperation:(NSDictionary *)userInfo {
    NSString *title = userInfo[@"aps"][@"alert"][@"title"];
    NSString *subTitle = userInfo[@"aps"][@"alert"][@"subtitle"];
    NSString *subMessage = userInfo[@"aps"][@"alert"][@"body"];
    NSString *message = [NSString stringWithFormat:@"%@%@%@",title,subTitle,subMessage];
    if ([userInfo[@"isLogin"] isEqualToString:@"Y"] && [userInfo[@"isRead"] isEqualToString:@"Y"]) {
        [[self mainQueue] addOperation:[self customOperation:message]];
    }
}

- (NSOperationQueue *)mainQueue {
    return [NSOperationQueue mainQueue];
}

- (NSOperation *)customOperation:(NSString *)content {
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        AVSpeechUtterance *utterance = nil;
        @autoreleasepool {
            utterance = [AVSpeechUtterance speechUtteranceWithString:content];
            utterance.rate = 0.5;
        }
        utterance.voice = self.synthesisVoice;
        [self.synthesizer speakUtterance:utterance];
    }];
    return operation;
}

到这里呢我们的功能已经完成了一大半,下面我们说说如何使用

- (void)addOperation:(NSDictionary *)userInfo;

在我的Demo中我创建了一个名为AppDelegate+JPushCategory这么一个分类,我把极光推送的集成、注册、包括代理方法写在了这个类里面。

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    
    // Required, iOS 7 Support
    [JPUSHService handleRemoteNotification:userInfo];
    
    [[NSNotificationCenter defaultCenter] postNotificationName:PushNotificationReport object:userInfo];
    
    NSLog(@"iOS7及以上系统,收到通知:%@", userInfo);
    [self addOperation:userInfo];
//    [self ttsReadPushNotification:userInfo];
    completionHandler(UIBackgroundFetchResultNewData);
}

当我收到极光推送的通知信息时我会将每一个推送都添加在我已经创建好的队列中进行播报;

好了...到这里呢我们的功能已经完成了80%,运行一下试试~是不是感觉很神奇呢?细心的朋友会发现当我们的APP进入后台是好像并没有进行语音播报,加上这段代码试试吧!!!
我的Demo写在

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions ;

附上主要代码:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [self JPushapplication:application didFinishLaunchingWithOptions:launchOptions];
    NSError *error = NULL;
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategoryPlayback error:&error];
    if(error) {
        
    }
    
    [session setActive:YES error:&error];
    
    if (error) {
        // Do some error handling
    }
    
    // 让app支持接受远程控制事件
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    [self configureTTSSDK];
    return YES;
}


- (void)applicationWillResignActive:(UIApplication *)application {
    // 开启后台处理多媒体事件
    
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    
    AVAudioSession *session=[AVAudioSession sharedInstance];
    
    [session setActive:YES error:nil];
    
    // 后台播放
    
    [session setCategory:AVAudioSessionCategoryPlayback error:nil];
    
    // 这样做,可以在按home键进入后台后 ,播放一段时间,几分钟吧。但是不能持续播放网络歌曲,若需要持续播放网络歌曲,还需要申请后台任务id,具体做法是:
    
    _backgroundTaskIdentifier=[AppDelegate backgroundPlayerID:_backgroundTaskIdentifier];
    
    // 其中的_bgTaskId是后台任务UIBackgroundTaskIdentifier _bgTaskId;
}
//实现一下backgroundPlayerID:这个方法:

+(UIBackgroundTaskIdentifier)backgroundPlayerID:(UIBackgroundTaskIdentifier)backTaskId{
    
    //设置并激活音频会话类别
    
    AVAudioSession *session=[AVAudioSession sharedInstance];
    
    [session setCategory:AVAudioSessionCategoryPlayback error:nil];
    
    [session setActive:YES error:nil];
    
    //允许应用程序接收远程控制
    
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    
    //设置后台任务ID
    
    UIBackgroundTaskIdentifier newTaskId=UIBackgroundTaskInvalid;
    
    newTaskId=[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
    
    if(newTaskId!=UIBackgroundTaskInvalid&&backTaskId!=UIBackgroundTaskInvalid){
        
        [[UIApplication sharedApplication] endBackgroundTask:backTaskId];
        
    }
    
    return newTaskId;
    
}

大功告成,重新运行发现一切完美,同时也实现了我们开始提到的需求;

2)百度TTS

集成的话我就不多啰嗦了,大家可以查看百度TTS语音播报或者官网查看;
直接上代码:

#pragma mark ------ TTS

-(void)configureTTSSDK{
    NSLog(@"TTS version info: %@", [BDSSpeechSynthesizer version]);
    [BDSSpeechSynthesizer setLogLevel:BDS_PUBLIC_LOG_VERBOSE];
    [[BDSSpeechSynthesizer sharedInstance] setSynthesizerDelegate:self];
//    [self configureOnlineTTS];
    [self configureOfflineTTS];
    
}


-(void)configureOnlineTTS{
    
    [[BDSSpeechSynthesizer sharedInstance] setApiKey:BaiDu_API_Key withSecretKey:BaiDu_Secret_Key];
    
    [[AVAudioSession sharedInstance]setCategory:AVAudioSessionCategoryPlayback error:nil];
    
}

-(void)configureOfflineTTS{
    
    NSError *err = nil;
    // 在这里选择不同的离线音库(请在XCode中Add相应的资源文件),同一时间只能load一个离线音库。根据网络状况和配置,SDK可能会自动切换到离线合成。
    
    NSString* offlineEngineSpeechData = [[NSBundle mainBundle] pathForResource:@"Chinese_And_English_Speech_Female" ofType:@"dat"];
    
    NSString* offlineChineseAndEnglishTextData = [[NSBundle mainBundle] pathForResource:@"Chinese_And_English_Text" ofType:@"dat"];
    
    err = [[BDSSpeechSynthesizer sharedInstance] loadOfflineEngine:offlineChineseAndEnglishTextData speechDataPath:offlineEngineSpeechData licenseFilePath:nil withAppCode:BaiDu_APP_ID];
    if(err){
        NSLog(@"offLineTTS configure error : %@",err.localizedDescription);
    }else{
        NSLog(@"offLineTTS success");
    }
}

- (void)ttsReadPushNotification:(NSDictionary *)userInfo{
    [[BDSSpeechSynthesizer sharedInstance] setPlayerVolume:5];
    [[BDSSpeechSynthesizer sharedInstance] setSynthParam:[NSNumber numberWithInteger:7] forKey:BDS_SYNTHESIZER_PARAM_SPEED];
    NSString *title = userInfo[@"aps"][@"alert"][@"title"];
    NSString *subTitle = userInfo[@"aps"][@"alert"][@"subtitle"];
    NSString *subMessage = userInfo[@"aps"][@"alert"][@"body"];
    NSString *message = [NSString stringWithFormat:@"%@%@%@",title,subTitle,subMessage];
    if ([userInfo[@"isLogin"] isEqualToString:@"Y"] && [userInfo[@"isRead"] isEqualToString:@"Y"]) {
        NSInteger flag = [[BDSSpeechSynthesizer sharedInstance] speakSentence:message withError:nil];
        NSLog(@"TTSFlage -------%ld",flag);
        
    }
}

4.我们创建的NotificationServiceExtension中大致和上面介绍的差不多;你也可以将之前的代码复制粘贴也可以看我这里

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    [self configureTTSSDK];
    // Modify the notification content here...
    self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
//    [self playVoiceWithContent:self.bestAttemptContent.userInfo];
    [self ttsReadPushNotification:self.bestAttemptContent.userInfo];
    self.contentHandler(self.bestAttemptContent);
}

- (void)serviceExtensionTimeWillExpire {
    [[BDSSpeechSynthesizer sharedInstance] cancel];
    self.contentHandler(self.bestAttemptContent);
}
// 新增语音播放代理函数,在语音播报完成的代理函数中,我们添加下面的一行代码
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance {
    //    [self playVoice:@"调用了播放完成函数"];
    
    // 每一条语音播放完成后,我们调用此代码,用来呼出通知栏
    self.contentHandler(self.bestAttemptContent);
}
- (void)playVoiceWithContent:(NSDictionary *)userInfo {
    NSLog(@"NotificationExtension content : %@",userInfo);
    NSString *title = userInfo[@"aps"][@"alert"][@"title"];
    NSString *subTitle = userInfo[@"aps"][@"alert"][@"subtitle"];
    NSString *subMessage = userInfo[@"aps"][@"alert"][@"body"];
    NSString *message = [NSString stringWithFormat:@"%@%@%@",title,subTitle,subMessage];
    if ([userInfo[@"isLogin"] isEqualToString:@"Y"] && [userInfo[@"isRead"] isEqualToString:@"Y"]) {
        AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:message];
        [self.synthesizer stopSpeakingAtBoundary:(AVSpeechBoundaryImmediate)];
        utterance.rate = 0.5;
        utterance.voice = self.synthesisVoice;
        [self.synthesizer speakUtterance:utterance];
    }
}

- (AVSpeechSynthesisVoice *)synthesisVoice {
    if (!_synthesisVoice) {
        _synthesisVoice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
    }
    return _synthesisVoice;
}

- (AVSpeechSynthesizer *)synthesizer {
    if (!_synthesizer) {
        _synthesizer = [[AVSpeechSynthesizer alloc] init];
        _synthesizer.delegate = self;
    }
    return _synthesizer;
}

#pragma mark ------ TTS

-(void)configureTTSSDK{
    NSLog(@"TTS version info: %@", [BDSSpeechSynthesizer version]);
    [BDSSpeechSynthesizer setLogLevel:BDS_PUBLIC_LOG_DEBUG];
    [[BDSSpeechSynthesizer sharedInstance] setSynthesizerDelegate:self];
//    [self configureOnlineTTS];
    [self configureOfflineTTS];
    
}


-(void)configureOnlineTTS{
    
    [[BDSSpeechSynthesizer sharedInstance] setApiKey:BaiDu_API_Key withSecretKey:BaiDu_Secret_Key];
    
    [[AVAudioSession sharedInstance]setCategory:AVAudioSessionCategoryPlayback error:nil];
    //    [[BDSSpeechSynthesizer sharedInstance] setSynthParam:@(BDS_SYNTHESIZER_SPEAKER_DYY) forKey:BDS_SYNTHESIZER_PARAM_SPEAKER];
    //    [[BDSSpeechSynthesizer sharedInstance] setSynthParam:@(10) forKey:BDS_SYNTHESIZER_PARAM_ONLINE_REQUEST_TIMEOUT];
    
}

-(void)configureOfflineTTS{
    
    NSError *err = nil;
    // 在这里选择不同的离线音库(请在XCode中Add相应的资源文件),同一时间只能load一个离线音库。根据网络状况和配置,SDK可能会自动切换到离线合成。
    
    NSString* offlineEngineSpeechData = [[NSBundle mainBundle] pathForResource:@"Chinese_Speech_Female" ofType:@"dat"];
    
    NSString* offlineChineseAndEnglishTextData = [[NSBundle mainBundle] pathForResource:@"Chinese_Text" ofType:@"dat"];
    
    err = [[BDSSpeechSynthesizer sharedInstance] loadOfflineEngine:offlineChineseAndEnglishTextData speechDataPath:offlineEngineSpeechData licenseFilePath:nil withAppCode:BaiDu_APP_ID];
    if(err){
        NSLog(@"offLineTTS configure error : %@",err.localizedDescription);
    }else{
        NSLog(@"offLineTTS success");
    }
}

- (void)ttsReadPushNotification:(NSDictionary *)userInfo{
    [[BDSSpeechSynthesizer sharedInstance] setPlayerVolume:5];
    [[BDSSpeechSynthesizer sharedInstance] setSynthParam:[NSNumber numberWithInteger:7] forKey:BDS_SYNTHESIZER_PARAM_SPEED];
    NSString *title = userInfo[@"aps"][@"alert"][@"title"];
    NSString *subTitle = userInfo[@"aps"][@"alert"][@"subtitle"];
    NSString *subMessage = userInfo[@"aps"][@"alert"][@"body"];
    NSString *message = [NSString stringWithFormat:@"%@%@%@",title,subTitle,subMessage];
    if ([userInfo[@"isLogin"] isEqualToString:@"Y"] && [userInfo[@"isRead"] isEqualToString:@"Y"]) {
        NSInteger flag = [[BDSSpeechSynthesizer sharedInstance] speakSentence:message withError:nil];
        NSLog(@"TTSFlage -------%ld",(long)flag);
        
    }
}

到这里我们的介绍已经全部完成了,希望会给大家以后的开发中提供帮助,喜欢的朋友给个 赞;
附上Demo地址:
极光语音播报

后续会为大家讲解 iOS常用正则表达式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容