最近申请了支付宝的二维码收钱码,其中支付宝有这么一个功能,就是,别人扫描你的二维码给你转帐以后,收到钱会有一条语音推送,”支付宝到帐 1000万“之类的推送消息,无论你的支付宝app有没有被杀死。html
只要你的远程推送开着,而且支付宝的"二维码收钱到帐语音提醒",都打开着,就能够收到。git
打开方式:支付宝点击右上角设置-通用-新消息通知,打开到帐提醒便可。
github
对支付宝进行相关测试:json
一、iOS 10如下的设备收到钱以后无论App是杀死仍是压入后台状态都会播报”支付宝到帐一笔”一句固定的语音
二、iOS 10如下的设备收到钱以后无论App是杀死仍是压入后台状态,而且设备处于静音状态,只是会有一个推送弹框和手机振动
三、iOS 10以上的设备,收到钱以后,无论APP是杀死仍是压入后台状态,不论是静音仍是非静音状态,在收到转帐的时候,会播报”支付宝到帐 * 元”
四、iOS 10以上的设备,在收到语音播报的时候,按音量键是能够调节音量大小的数组
实现以上功能注意的点:bash
iOS 10以上和iOS10如下设备实现方式不同iOS 10以上须要考虑的因素,设备是否被杀死状态,静音非静音状态,音量是否能够调节,是否能够播报随机对应的钱数
而且别人给你转多少钱就会播报到帐多少钱。网络
iOS 10 以前系统实现方案:session
能保证设备在杀死或者压入后台的情况下收到信息,应该是远程推送的功劳了。
iOS 10 以前系统,能够借助远程推送定制铃声的功能来实现,只要在本地添加一段提早录制好的语音,而且在推送内容的时候将sound字段,修改为语音的名称便可app
下面来看一下iOS10以上系统的实现播报自定义语音的实现方式:函数
实现以上功能需借助iOS 10的 NotificationServiceExtension
首先了解下常规的远程推送逻辑
iOS 10之后添加上NotificationServiceExtension以后的推送流程
能够查看相关文档了解: NotificationServiceExtension
NotificationServiceExtension这个东西有啥做用呢?
主要是可以在展现推送内容以前先获取到相关的推送信息,能够更改或者替换相关的推送内容。
怎么实现呢?
不用本身建立UNNotificationServiceExtension类,使用Xcode提供的模板直接选择就能够。若是你的app收到远程推送的话就会加载扩展而且调用didReceiveNotificationRequest:withContentHandler:,而作这些的前提就是,远程推送的那一套配置<证书之类的>得作好,还有就是推送的字典中包含mutable-content而且它的值是1;
iOS 10系统以后远程推送播报语音的实现思路:
设备在收到远程推送的时候,进入Service Extension,将远程推送的信息拦截下来,将须要播报的文字信息经过相关方式翻译成语音,进行播报,播报完毕以后再展现弹框信息。
UNNotificationServiceExtension的功能就是能够拦截到远程推送的信息,而后当调用self.contentHandler(self.bestAttemptContent); 以后就会进行弹框显示了,若是进行了弹框显示,那么UNNotificationServiceExtension的使命意味着结束了。
当拦截到信息,到弹框最多有30秒的时间进行操做。
Your extension has a limited amount of time (no more than 30 seconds) to modify the content and execute the contentHandler
block. If you do not execute that block in a timely manner, the system calls your extension’s serviceExtensionTimeWillExpire method to give you one last chance to execute the block. If you do not, the system presents the notification’s original content to the user.
大体思路定下以后,下一步操做,怎么把文字翻译成语音进行播报
一、使用科大讯飞以及相似的三方库,将远程推送过来的文字,直接使用三方库播放出来
二、使用 AVSpeechSynthesisVoice 相关类将远程推送过来的文字直接转化成语音,进行播报
三、若是播报的是钱数的话,能够在本地将相关可能播报的语音片断录制好,而后根据推送过来的内容标识,对语音片断进行拼接,而后进行播放
若是对Notification Servivice Extension不是很熟悉的,建议先了解一下
iOS10 推送extension之 Service Extension你玩过了吗?
在介绍相关方式以前,先介绍一个测试工具
SmartPush
使用方式也很简单,运行起来,输入相关的推送数据和token,而且选择对应的推送证书,点击推送便可
正式进入主题
推送的内容是:
{
"aps":{
"alert":{
"title":"iOS 10 title",
"subtitle":"iOS 10 subtitle",世上只有妈妈好,有妈的孩子像块宝。投进妈妈的怀抱,幸福哪里找。没妈的孩子像根草。大河向东流,天上的星星参北斗,嘿呀,咿儿呀,嘿 嘿 咿儿呀" }, "my-attachment":"http://img01.taopic.com/160317/240440-16031FU23937.jpg", "mutable-content":1, "category":"myNotificationCategory1", "badge":3 } }复制代码
一、使用科大讯飞以及相似的三方库,将远程推送过来的文字,直接使用三方库播放出来
这个流量用多了应该收费,具体可查看科大讯飞官网
将推送过来的文字转化成语音播放,而后在播放完毕的回调中执行self.contentHandler(self.bestAttemptContent);
@interface NotificationService ()<IFlySpeechSynthesizerDelegate,AVAudioPlayerDelegate,AVSpeechSynthesizerDelegate>
{
AVSpeechSynthesizer *synthesizer;
}
@property (nonatomic, strong) IFlySpeechSynthesizer *iFlySpeechSynthesizer;
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@property (nonatomic, strong)AVAudioPlayer *myPlayer;
@property (nonatomic, strong) NSString *filePath;
// AVSpeechSynthesisVoice 播放完毕以后的回调block
@property (nonatomic, copy)PlayVoiceBlock finshBlock;
// 科大讯飞播放完毕以后的block回调
@property (nonatomic, copy)PlayVoiceBlock kedaFinshBlock;
// 语音合成完毕以后,使用 AVAudioPlayer 播放
@property (nonatomic, copy)PlayVoiceBlock aVAudioPlayerFinshBlock;
@end
@implementation NotificationService
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// Modify the notification content here...
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
__weak __typeof(self)weakSelf = self;
/**************************************************************************/
// 方式1,直接使用科大讯飞播放,成功,可是刚开始的时候可能须要几秒的准备播放时间
[self playVoiceKeDaXunFeiWithMessage:self.bestAttemptContent.body withBlock:^{
weakSelf.contentHandler(weakSelf.bestAttemptContent);
}];
}复制代码
#pragma mark- 使用科大讯飞播放语音
- (void)playVoiceKeDaXunFeiWithMessage:(NSString *)message withBlock:(PlayVoiceBlock)finshBlock
{
if (finshBlock) {
self.kedaFinshBlock = finshBlock;
}
//建立语音配置,appid必需要传入,仅执行一次则可
NSString *initString = [[NSString alloc] initWithFormat:@"appid=%@",@"59db7ce2"];
//全部服务启动前,须要确保执行createUtility
[IFlySpeechUtility createUtility:initString];
/******************************************************/
//获取语音合成单例
_iFlySpeechSynthesizer = [IFlySpeechSynthesizer sharedInstance];
//设置协议委托对象
_iFlySpeechSynthesizer.delegate = self;
//设置合成参数
//设置在线工做方式
[_iFlySpeechSynthesizer setParameter:[IFlySpeechConstant TYPE_CLOUD]
forKey:[IFlySpeechConstant ENGINE_TYPE]];
//设置音量,取值范围 0~100
[_iFlySpeechSynthesizer setParameter:@"50"
forKey: [IFlySpeechConstant VOLUME]];
//发音人,默认为”xiaoyan”,能够设置的参数列表可参考“合成发音人列表”
[_iFlySpeechSynthesizer setParameter:@" xiaoyan "
forKey: [IFlySpeechConstant VOICE_NAME]];
//保存合成文件名,如再也不须要,设置为nil或者为空表示取消,默认目录位于library/cache下
[_iFlySpeechSynthesizer setParameter:@" tts.pcm"
forKey: [IFlySpeechConstant TTS_AUDIO_PATH]];
//启动合成会话
[_iFlySpeechSynthesizer startSpeaking:message];
}
//IFlySpeechSynthesizerDelegate协议实现
//合成结束
- (void) onCompleted:(IFlySpeechError *) error {
NSLog(@"合成结束 error ===== %@",error);
self.kedaFinshBlock();
}复制代码
结果
知足如下两点要求(1)、iOS 10以上的设备,收到推送以后,无论APP是杀死仍是压入后台状态,不论是静音仍是非静音状态,在收到转帐的时候,会播报”到帐 * 元”
(2)、iOS 10以上的设备,在收到语音播报的时候,按音量键是能够调节音量大小的
坑点
说明:当前实现的是将push内容中的body
播放出来
一、若是你收到推送了可是添加了系统的铃声,也就是你在push的json中添加了"sound":"default"
那么就可能会影响推送声音的播放
二、推送有问题
三、播放的语音时长最好不要超过30秒
四、若是说你的远程推送仍是走的iOS10以前的逻辑,那么请检查一下你的推送的json有没有【"mutable-content":1】
二、使用 AVSpeechSynthesisVoice 相关类将远程推送过来的文字直接转化成语音,进行播报
AVSpeechSynthesisVoice 可查看官方文档
流程:
将推送过来的文字转化成语音播放,而后在播放完毕的回调中执行,和科大讯飞相似,不过是苹果系统相关类
相关代码
@interface NotificationService ()<IFlySpeechSynthesizerDelegate,AVAudioPlayerDelegate,AVSpeechSynthesizerDelegate>
{
AVSpeechSynthesizer *synthesizer;
}
@property (nonatomic, strong) IFlySpeechSynthesizer *iFlySpeechSynthesizer;
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@property (nonatomic, strong)AVAudioPlayer *myPlayer;
@property (nonatomic, strong) NSString *filePath;
// AVSpeechSynthesisVoice 播放完毕以后的回调block
@property (nonatomic, copy)PlayVoiceBlock finshBlock;
// 科大讯飞播放完毕以后的block回调
@property (nonatomic, copy)PlayVoiceBlock kedaFinshBlock;
// 语音合成完毕以后,使用 AVAudioPlayer 播放
@property (nonatomic, copy)PlayVoiceBlock aVAudioPlayerFinshBlock;
@end
@implementation NotificationService
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// Modify the notification content here...
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
__weak __typeof(self)weakSelf = self;
/**************************************************************************/
// 方式4,AVSpeechSynthesisVoice使用系统方法,文字转语音播报,成功
[self playVoiceWithAVSpeechSynthesisVoiceWithContent:self.bestAttemptContent.body fishBlock:^{
weakSelf.contentHandler(weakSelf.bestAttemptContent);
}];
}复制代码
#pragma mark- AVSpeechSynthesisVoice文字转语音进行播放,成功
- (void)playVoiceWithAVSpeechSynthesisVoiceWithContent:(NSString *)content fishBlock:(PlayVoiceBlock)finshBlock
{
if (content.length == 0) {
return;
}
if (finshBlock) {
self.finshBlock = finshBlock;
}
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setActive:YES error:nil];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
// 建立嗓音,指定嗓音不存在则返回nil
AVSpeechSynthesisVoice *voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
// 建立语音合成器
AVSpeechSynthesizer *synthesizer = [[AVSpeechSynthesizer alloc] init];
synthesizer.delegate = self;
// 实例化发声的对象
AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:content];
utterance.voice = voice;
utterance.rate = 0.5; // 语速
// 朗读的内容
[synthesizer speakUtterance:utterance];
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didStartSpeechUtterance:(AVSpeechUtterance *)utterance
{
NSLog(@"开始");
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance
{
self.finshBlock();
NSLog(@"结束");
}复制代码
坑点
说明:当前实现的是将push内容中的body
播放出来
一、若是你收到推送了可是添加了系统的铃声,也就是你在push的json中添加了"sound":"default"
那么就可能会影响推送声音的播放
二、播放的语音时长最好不要超过30秒
三、若是说你的远程推送仍是走的iOS10以前的逻辑,那么请检查一下你的推送的json有没有【"mutable-content":1】
四、若是在手机静音的状态下听不到播报的语音
添加设置
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setActive:YES error:nil];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];复制代码
结果
知足如下两点要求
(1)、iOS 10以上的设备,收到推送以后,无论APP是杀死仍是压入后台状态,不论是静音仍是非静音状态,在收到转帐的时候,会播报”到帐 * 元”
(2)、iOS 10以上的设备,在收到语音播报的时候,按音量键是能够调节音量大小的
三、若是播报的是钱数的话,能够在本地将相关可能播报的语音片断录制好,而后根据推送过来的内容标识,对语音片断进行拼接,而后进行播放
流程:
若是播报的内容是相对固定的片断组合体,这里那支付宝举例。
好比提早先录好 如下可能播报的内容
*到帐、 0、 一、 二、 三、 四、 五、 六、 七、 八、 九、 10、 百、 千、 万、 十万、 百万、 千万、 亿、 元 等等
而后根据推送的内容进行相关语音文件的对应,而后拼接,拼接完毕以后生成一个语音文件,而后进行播放
相关代码:
@interface NotificationService ()<IFlySpeechSynthesizerDelegate,AVAudioPlayerDelegate,AVSpeechSynthesizerDelegate>
{
AVSpeechSynthesizer *synthesizer;
}
@property (nonatomic, strong) IFlySpeechSynthesizer *iFlySpeechSynthesizer;
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@property (nonatomic, strong)AVAudioPlayer *myPlayer;
@property (nonatomic, strong) NSString *filePath;
// AVSpeechSynthesisVoice 播放完毕以后的回调block
@property (nonatomic, copy)PlayVoiceBlock finshBlock;
// 科大讯飞播放完毕以后的block回调
@property (nonatomic, copy)PlayVoiceBlock kedaFinshBlock;
// 语音合成完毕以后,使用 AVAudioPlayer 播放
@property (nonatomic, copy)PlayVoiceBlock aVAudioPlayerFinshBlock;
@end
@implementation NotificationService
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// Modify the notification content here...
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
__weak __typeof(self)weakSelf = self;
/*******************************推荐用法*******************************************/
// 方法3,语音合成,使用AVAudioPlayer播放,成功
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setActive:YES error:nil];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
[self hechengVoiceAVAudioPlayerWithFinshBlock:^{
weakSelf.contentHandler(weakSelf.bestAttemptContent);
}];
}复制代码
#pragma mark- 合成音频使用 AVAudioPlayer 播放
- (void)hechengVoiceAVAudioPlayerWithFinshBlock:(PlayVoiceBlock )block
{
if (block) {
self.aVAudioPlayerFinshBlock = block;
}
/************************合成音频并播放*****************************/
AVMutableComposition *composition = [AVMutableComposition composition];
NSArray *fileNameArray = @[@"daozhang",@"1",@"2",@"3",@"4",@"5",@"6"];
CMTime allTime = kCMTimeZero;
for (NSInteger i = 0; i < fileNameArray.count; i++) {
NSString *auidoPath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@",fileNameArray[i]] ofType:@"m4a"];
AVURLAsset *audioAsset = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:auidoPath]];
// 音频轨道
AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:0];
// 音频素材轨道
AVAssetTrack *audioAssetTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];
// 音频合并 - 插入音轨文件
[audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, audioAsset.duration) ofTrack:audioAssetTrack atTime:allTime error:nil];
// 更新当前的位置
allTime = CMTimeAdd(allTime, audioAsset.duration);
}
// 合并后的文件导出 - `presetName`要和以后的`session.outputFileType`相对应。
AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:composition presetName:AVAssetExportPresetAppleM4A];
NSString *outPutFilePath = [[self.filePath stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"xindong.m4a"];
if ([[NSFileManager defaultManager] fileExistsAtPath:outPutFilePath]) {
[[NSFileManager defaultManager] removeItemAtPath:outPutFilePath error:nil];
}
// 查看当前session支持的fileType类型
NSLog(@"---%@",[session supportedFileTypes]);
session.outputURL = [NSURL fileURLWithPath:outPutFilePath];
session.outputFileType = AVFileTypeAppleM4A; //与上述的`present`相对应
session.shouldOptimizeForNetworkUse = YES; //优化网络
[session exportAsynchronouslyWithCompletionHandler:^{
if (session.status == AVAssetExportSessionStatusCompleted) {
NSLog(@"合并成功----%@", outPutFilePath);
NSURL *url = [NSURL fileURLWithPath:outPutFilePath];
self.myPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
self.myPlayer.delegate = self;
[self.myPlayer play];
} else {
// 其余状况, 具体请看这里`AVAssetExportSessionStatus`.
// 播放失败
self.aVAudioPlayerFinshBlock();
}
}];
/************************合成音频并播放*****************************/
}
#pragma mark- AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
if (self.aVAudioPlayerFinshBlock) {
self.aVAudioPlayerFinshBlock();
}
}复制代码
结果
(1)、iOS 10以上的设备,收到推送以后,无论APP是杀死仍是压入后台状态,不论是静音仍是非静音状态,在收到转帐的时候,会播报”到帐 * 元”
(2)、iOS 10以上的设备,在收到语音播报的时候,按音量键是能够调节音量大小的
坑点
(1)、注意上面的坑点
(2)、播放音频的时候有两种播放形式AudioServicesPlayAlertSoundWithCompletion和AVAudioPlayer。
建议使用AVAudioPlayer,由于AVAudioPlayer能知足不受设备静音不静音的影响,能根据音量调节声音的高低。
AudioServicesPlayAlertSoundWithCompletion局限性比较大,会受静音的影响,只会震动,而且没法调整音量的高低。
合成语音以后若是使用AudioServicesCreateSystemSoundID播放的话,有必定的局限性
http://www.hangge.com/blog/cache/detail_771.html
1,系统声音服务介绍:
系统声音服务提供了一个Api,用于播放不超过30秒的声音。它支持的文件格式有限,具体的说只有CAF、AIF和使用PCM或IMA/ADPCM数据的WAV文件。
但此函数没有提供操做声音和控制音量的功能,所以若是是要为多媒体或游戏建立专门声音,就不要使用系统声音服务。
2,系统声音服务支持以下三种类型:
(1)声音:马上播放一个简单的声音文件。若是手机静音,则用户什么也听不见。
(2)提醒:播放一个声音文件,若是手机设为静音或震动,则经过震动提醒用户。
(3)震动:震动手机,而不考虑其余设置。复制代码
说明:
上面并无实现 数字转对应音频文件名称数组的过程,直接实现的是合成音频的方法。
Extension的运行生命周期:
iOS对于扩展的支持已经由最初的6类到了现在iOS10的19类(相信随着iOS的发展扩展的覆盖面也会愈来愈广),固然不一样类型的扩展其用途和用法均不尽相同,可是其工做原理和开发方式是相似的。下面列出扩展的几个共同点:
扩展依附于应用而不能单独发布和部署;
扩展和包含扩展的应用(containing app)生命周期是独立的,分别运行在两个不一样的进程中;
扩展的运行依赖于宿主应用(或者叫载体应用 host app,而不是containing app)其生命周期由宿主应用肯定;
对开发者而言扩展做为一个单独的target而存在;
扩展一般展示在系统UI或者其余应用中,运行应该尽量的迅速而功能单一;
关于断点调试
若是想经过断点调试来了解或者检查推送的流程怎么搞呢?
方法一:
调试 content Extension
方法二:
若是想同时调试各个target怎么办?
将项目运行起来,而后发送一条推送以后,激活Service Extension,若是有须要能够激活content Extension<下拉一下推送条查看就好,前提你的conten Extension可使用>
而后去选择,这个时候不要stop掉程序,根据下图选择完毕以后,在相关的地方打上断点,再次推送。
建议使用方法二,各个流程顺序更加直观
最后献上相关的Demo地址,若是你有更好的建议欢迎留言,若有不正,欢迎来喷。
能够直接用个人Demo进行调试,调试的时候注意修改下bundleId,而后用本身的开发者帐号配置一下相关的push证书就能够了