iOS13推送语音播报

目前市面上很多支付APP都需要在收款成功后,进行语音提示,例如收钱吧,微信,支付宝等!公司App现在也需要加入这个功能,这里记录下踩过的坑

该功能需要用到 苹果的 Notification Service Extension 这个是iOS10.0推出的。https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension

实现该功能

一,添加 Notification Service Extension

target1.png
target2.png
target3.png

创建之后程序内会出现 NotificationService.h ,NotificationService.m 文件


target4.png

二,然后就是发送推送消息 ,以极光推送为例

(iOS 10 新增的 Notification Service Extension 功能,用 mutable-content 字段来控制。 若使用极光的 Web 控制台,需勾选 “可选设置”中 mutable-content 选项;若使用 RESTFul API 需设置 mutable-content 字段为 true。)

三,拦截推送信息,播放语音

5.png

设置好后我们每次发送推送,都会走到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!


7.png

比如极光推送的控制台就是这个字段

但是这就限制了,必须在打包之前就把语音放进工程目录!只能用固定的语音了!
那么最笨的方案就是内置一万多条语音,然后推送的时候直接让后端用sound来指定播放的语音,但是在包的大小……

网上翻阅很久,后来发现,sound除了播放工程主目录和Library/Sounds,还可以播放AppGroup中Library/Sounds的音频 那这就好办了,我们可以在后台合成,然后下载到AppGroup后修改sound字段进行播放(前端合成到处到指定文件夹应该也可以)

首先打开我们项目的AppGroup


image.png
image.png

打开后记得☑️,然后再打开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);后,主动去阻塞线程一定的时长(音频时长),播放完成后记得删除掉!

你可能感兴趣的:(iOS13推送语音播报)