目前市面上很多支付APP都需要在收款成功后,进行语音提示,例如收钱吧,微信,支付宝等!公司App现在也需要加入这个功能,这里记录下踩过的坑
该功能需要用到 苹果的 Notification Service Extension 这个是iOS10.0推出的。https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension
实现该功能
一,添加 Notification Service Extension
创建之后程序内会出现 NotificationService.h ,NotificationService.m 文件
二,然后就是发送推送消息 ,以极光推送为例
(iOS 10 新增的 Notification Service Extension 功能,用 mutable-content 字段来控制。 若使用极光的 Web 控制台,需勾选 “可选设置”中 mutable-content 选项;若使用 RESTFul API 需设置 mutable-content 字段为 true。)
三,拦截推送信息,播放语音
设置好后我们每次发送推送,都会走到NotificationService中的这个回调,获取到推送中附带的信息(ps:如果发现没走回调,请对照上一步,查看 极光控制台mutable-content 是否勾选,后台或其他方式推送要此字段设置为1);
(1)ios12以前
ios12以前,这个功能还是比较好做的,收到推送后,调用语音库AVSpeechSynthesisVoice读出来就可以,
av= [[AVSpeechSynthesizer alloc]init];
av.delegate=self;//挂上代理
AVSpeechSynthesisVoice*voice = [AVSpeechSynthesisVoicevoiceWithLanguage:@"zh-CN"];//设置发音,这是中文普通话
AVSpeechUtterance*utterance = [[AVSpeechUtterance alloc]initWithString:@"需要播报的文字"];//需要转换的文字
utterance.rate=0.6;// 设置语速,范围0-1,注意0最慢,1最快;
utterance.voice= voice;
[avspeakUtterance:utterance];//开始
或者内置几段语音进行合成后再进行播放
//MARK:音频凭借
- (void)audioMergeClick{
//1.获取本地音频素材
NSString *audioPath1 = [[NSBundle mainBundle]pathForResource:@"一" ofType:@"mp3"];
NSString *audioPath2 = [[NSBundle mainBundle]pathForResource:@"元" ofType:@"mp3"];
AVURLAsset *audioAsset1 = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:audioPath1]];
AVURLAsset *audioAsset2 = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:audioPath2]];
//2.创建两个音频轨道,并获取两个音频素材的轨道
AVMutableComposition *composition = [AVMutableComposition composition];
//音频轨道
AVMutableCompositionTrack *audioTrack1 = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:0];
AVMutableCompositionTrack *audioTrack2 = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:0];
//获取音频素材轨道
AVAssetTrack *audioAssetTrack1 = [[audioAsset1 tracksWithMediaType:AVMediaTypeAudio] firstObject];
AVAssetTrack *audioAssetTrack2 = [[audioAsset2 tracksWithMediaType:AVMediaTypeAudio]firstObject];
//3.将两段音频插入音轨文件,进行合并
//音频合并- 插入音轨文件
// `startTime`参数要设置为第一段音频的时长,即`audioAsset1.duration`, 表示将第二段音频插入到第一段音频的尾部。
[audioTrack1 insertTimeRange:CMTimeRangeMake(kCMTimeZero, audioAsset1.duration) ofTrack:audioAssetTrack1 atTime:kCMTimeZero error:nil];
[audioTrack2 insertTimeRange:CMTimeRangeMake(kCMTimeZero, audioAsset2.duration) ofTrack:audioAssetTrack2 atTime:audioAsset1.duration error:nil];
//4. 导出合并后的音频文件
//`presetName`要和之后的`session.outputFileType`相对应
//音频文件目前只找到支持m4a 类型的
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:self.filePath];
session.outputFileType = AVFileTypeAppleM4A; //与上述的`present`相对应
session.shouldOptimizeForNetworkUse = YES; //优化网络
[session exportAsynchronouslyWithCompletionHandler:^{
if (session.status == AVAssetExportSessionStatusCompleted) {
NSLog(@"合并成功----%@", outPutFilePath);
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:outPutFilePath] error:nil];
[_audioPlayer play];
} else {
// 其他情况, 具体请看这里`AVAssetExportSessionStatus`.
}
}];
}
- (NSString *)filePath {
if (!_filePath) {
_filePath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
NSString *folderName = [_filePath stringByAppendingPathComponent:@"MergeAudio"];
BOOL isCreateSuccess = [kFileManager createDirectoryAtPath:folderName withIntermediateDirectories:YES attributes:nil error:nil];
if (isCreateSuccess) _filePath = [folderName stringByAppendingPathComponent:@"xindong.m4a"];
}
return _filePath;
}
该方法可以内置1-10,点、元等单音频后拼接成需要的语音,然后利用
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:outPutFilePath] error:nil];
[_audioPlayer play];
播放出来
具体合成方法参考
https://www.jianshu.com/p/a739c200b3c8
https://www.jianshu.com/p/3e357e3129b8
或者最简单的方案,集成讯飞,百度等三方合成语音
(2)iOS13播报
在iOS12.1发布后,上述方案已经不行了,
据说苹果给出的解释是 Notification Service Extension是为了丰富推送体验,主要是为了富文本推送图片的处理,所以在Notification Service Extension中禁用了play播放器相关!有需要的可以使用官方的sound字段播放自定义的语音
关于sound字段
sound字段是官方推送的一个默认字段,苹果官方文档说明可以将音频放到工程主目录,或者Libray/Sounds,在推送到达时,系统将根据sound字段在目录中找到对应音频播放,支持的格式aiff,caf,wav!
比如极光推送的控制台就是这个字段
但是这就限制了,必须在打包之前就把语音放进工程目录!只能用固定的语音了!
那么最笨的方案就是内置一万多条语音,然后推送的时候直接让后端用sound来指定播放的语音,但是在包的大小……
网上翻阅很久,后来发现,sound除了播放工程主目录和Library/Sounds,还可以播放AppGroup中Library/Sounds的音频 那这就好办了,我们可以在后台合成,然后下载到AppGroup后修改sound字段进行播放(前端合成到处到指定文件夹应该也可以)
首先打开我们项目的AppGroup
打开后记得☑️,然后再打开Notification Service Extension 的AppGroup 也就是图中名为PushDemo的的targets,也要同样操作一遍
之后接到通知,解析出下载链接,下载完在本地修改sound字段,交由系统播报(应该也可以本地拼接后合成到处到对应文件夹,笔者当时没有尝试,各位可以自己尝试)
- (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];
// 这个info 内容就是通知信息携带的数据,后面我们取语音播报的文案,通知栏的title,以及通知内容都是从这个info字段中获取
NSDictionary *info = self.bestAttemptContent.userInfo;
NSString * urlStr = [info objectForKey:@"soundUrl"];
[self loadWavWithUrl:urlStr];
// self.contentHandler(self.bestAttemptContent);
}
-(void)loadWavWithUrl:(NSString *)urlStr{
NSLog(@"开始下载");
NSURL *url = [NSURL URLWithString:urlStr];
//默认的congig
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
//session
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
self.task = [session downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (!error) {
NSLog(@"下载完成");
NSString * name = [NSString stringWithFormat:@"%u.wav",arc4random()%50000 ];
//获取保存文件的路径
NSString *path = self.filePath;
//将url对应的文件copy到指定的路径
NSFileManager *fileManager = [NSFileManager defaultManager];
if(![fileManager fileExistsAtPath:path]){
[fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
}
NSString * soundStr = [NSString stringWithFormat:@"%@",name];
NSString *savePath = [path stringByAppendingPathComponent:[NSString stringWithFormat:@"%@",soundStr]];
if ([fileManager fileExistsAtPath:savePath]) {
[fileManager removeItemAtPath:savePath error:nil];
}
NSURL *saveURL = [NSURL fileURLWithPath:savePath];
NSError * saveError;
// 文件移动到cache路径中
[[NSFileManager defaultManager] moveItemAtURL:location toURL:saveURL error:&saveError];
if (!saveError)
{
AVURLAsset *audioAsset=[AVURLAsset URLAssetWithURL:saveURL options:nil];
self.bestAttemptContent.sound = soundStr;
self.contentHandler(self.bestAttemptContent);
}
}else{
NSLog(@"失败");
}
}];
//启动下载任务
[_task resume];
}
- (NSString *)filePath {
if (_filePath) {
return _filePath;
}
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.jiutianyunzhu.BPMall"];
NSString *groupPath = [groupURL path];
_filePath = [groupPath stringByAppendingPathComponent:@"Library/Sounds"];
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:_filePath]) {
[fileManager createDirectoryAtPath:_filePath withIntermediateDirectories:NO attributes:nil error:nil];
}
return _filePath;
}
当音频下载处理完成后记得调用self.contentHandler(self.bestAttemptContent);
只有当调用self.contentHandler(self.bestAttemptContent);
之后,才会弹出顶部横幅,并开始播报,横幅消失时音频会停止,实测横幅时长大概6s!所以音频需要处理控制在6s之内!
测试这种方案ios13播放没用问题,ios12上没有正确播放,如果有好的修改方案,欢迎私信
需要注意的问题
1.网上大都说支持三种格式 aiff、caf以及wav,但实测也支持MP3格式
2.处理完成后一定要记得调用 self.contentHandler(self.bestAttemptContent);,否则不会出现通知横幅
3.下载失败最好准备一段默认语音播报
4.多条推送同时到达问题,可以写个队列,调用self.contentHandler(self.bestAttemptContent);后,主动去阻塞线程一定的时长(音频时长),播放完成后记得删除掉!