前言
很长一段时间以来,都没有进行文章更新了,最近闲暇时间较多,静下心来打算将最近项目中遇到的问题总结一下。今天主要说说语音播报这点事吧!亲身体会~走过路过不要错过。。。。我也是刚从坑里爬出来的。文章最后我会附上我的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
出现如下图页面后选择 Notification Service Extension
创建成功后,你会发现一个很神奇的事情
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",
}
}
在我的Demo中同时使用了百度TTS语音播报和AVSpeechSynthesis进行语音播报的实现
1)AVSpeechSynthesis
关于AVSpeechSynthesis的介绍和使用大家可以查看我的这篇文章进行了解学习。
a.扩展配置
在我们的推送扩展类中需要进行这样的设置,毕竟iOS10以后才出现了Notification ServiceExtension
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常用正则表达式