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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345