美文网首页
iOS13推送语音播报

iOS13推送语音播报

作者: 年少也曾轻狂 | 来源:发表于2020-09-15 11:13 被阅读0次

    目前市面上很多支付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来指定播放的语音,但是在包的大小……
    网传支付宝曾经有一个版本就是这么干的…… (只是网传,不负责真实性啊t0t)

    网上翻阅很久,后来发现,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推送语音播报

          本文链接:https://www.haomeiwen.com/subject/lwmxyktx.html