iOS13推送語音播報(bào)

目前市面上很多支付APP都需要在收款成功后,進(jìn)行語音提示,例如收錢吧,微信,支付寶等!公司App現(xiàn)在也需要加入這個(gè)功能,這里記錄下踩過的坑

該功能需要用到 蘋果的 Notification Service Extension 這個(gè)是iOS10.0推出的。https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension

實(shí)現(xiàn)該功能

一,添加 Notification Service Extension

target1.png
target2.png
target3.png

創(chuàng)建之后程序內(nèi)會(huì)出現(xiàn) NotificationService.h ,NotificationService.m 文件


target4.png

二,然后就是發(fā)送推送消息 ,以極光推送為例

(iOS 10 新增的 Notification Service Extension 功能,用 mutable-content 字段來控制。 若使用極光的 Web 控制臺(tái),需勾選 “可選設(shè)置”中 mutable-content 選項(xiàng);若使用 RESTFul API 需設(shè)置 mutable-content 字段為 true。)

三,攔截推送信息,播放語音

5.png

設(shè)置好后我們每次發(fā)送推送,都會(huì)走到NotificationService中的這個(gè)回調(diào),獲取到推送中附帶的信息(ps:如果發(fā)現(xiàn)沒走回調(diào),請對照上一步,查看 極光控制臺(tái)mutable-content 是否勾選,后臺(tái)或其他方式推送要此字段設(shè)置為1);

(1)ios12以前

ios12以前,這個(gè)功能還是比較好做的,收到推送后,調(diào)用語音庫AVSpeechSynthesisVoice讀出來就可以,

av= [[AVSpeechSynthesizer alloc]init];
av.delegate=self;//掛上代理
AVSpeechSynthesisVoice*voice = [AVSpeechSynthesisVoicevoiceWithLanguage:@"zh-CN"];//設(shè)置發(fā)音,這是中文普通話
AVSpeechUtterance*utterance = [[AVSpeechUtterance   alloc]initWithString:@"需要播報(bào)的文字"];//需要轉(zhuǎn)換的文字
utterance.rate=0.6;// 設(shè)置語速,范圍0-1,注意0最慢,1最快;
utterance.voice= voice;
[avspeakUtterance:utterance];//開始

或者內(nèi)置幾段語音進(jìn)行合成后再進(jìn)行播放

//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.創(chuàng)建兩個(gè)音頻軌道,并獲取兩個(gè)音頻素材的軌道
    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.將兩段音頻插入音軌文件,進(jìn)行合并
    //音頻合并- 插入音軌文件
    // `startTime`參數(shù)要設(shè)置為第一段音頻的時(shí)長,即`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. 導(dǎo)出合并后的音頻文件
    //`presetName`要和之后的`session.outputFileType`相對應(yīng)
    //音頻文件目前只找到支持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];
    }
    // 查看當(dāng)前session支持的fileType類型
    NSLog(@"---%@",[session supportedFileTypes]);
    session.outputURL = [NSURL fileURLWithPath:self.filePath];
    session.outputFileType = AVFileTypeAppleM4A; //與上述的`present`相對應(yīng)
    session.shouldOptimizeForNetworkUse = YES;   //優(yōu)化網(wǎng)絡(luò)
    [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;
}

該方法可以內(nèi)置1-10,點(diǎn)、元等單音頻后拼接成需要的語音,然后利用
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:outPutFilePath] error:nil];
[_audioPlayer play];
播放出來
具體合成方法參考
http://www.itdecent.cn/p/a739c200b3c8
http://www.itdecent.cn/p/3e357e3129b8
或者最簡單的方案,集成訊飛,百度等三方合成語音

(2)iOS13播報(bào)

在iOS12.1發(fā)布后,上述方案已經(jīng)不行了,

據(jù)說蘋果給出的解釋是 Notification Service Extension是為了豐富推送體驗(yàn),主要是為了富文本推送圖片的處理,所以在Notification Service Extension中禁用了play播放器相關(guān)!有需要的可以使用官方的sound字段播放自定義的語音

關(guān)于sound字段

sound字段是官方推送的一個(gè)默認(rèn)字段,蘋果官方文檔說明可以將音頻放到工程主目錄,或者Libray/Sounds,在推送到達(dá)時(shí),系統(tǒng)將根據(jù)sound字段在目錄中找到對應(yīng)音頻播放,支持的格式aiff,caf,wav!


7.png

比如極光推送的控制臺(tái)就是這個(gè)字段

但是這就限制了,必須在打包之前就把語音放進(jìn)工程目錄!只能用固定的語音了!
那么最笨的方案就是內(nèi)置一萬多條語音,然后推送的時(shí)候直接讓后端用sound來指定播放的語音,但是在包的大小……

網(wǎng)上翻閱很久,后來發(fā)現(xiàn),sound除了播放工程主目錄和Library/Sounds,還可以播放AppGroup中Library/Sounds的音頻 那這就好辦了,我們可以在后臺(tái)合成,然后下載到AppGroup后修改sound字段進(jìn)行播放(前端合成到處到指定文件夾應(yīng)該也可以)

首先打開我們項(xiàng)目的AppGroup


image.png
image.png

打開后記得??,然后再打開Notification Service Extension 的AppGroup 也就是圖中名為PushDemo的的targets,也要同樣操作一遍


之后接到通知,解析出下載鏈接,下載完在本地修改sound字段,交由系統(tǒng)播報(bào)(應(yīng)該也可以本地拼接后合成到處到對應(yīng)文件夾,筆者當(dāng)時(shí)沒有嘗試,各位可以自己嘗試)

- (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];
   
    // 這個(gè)info 內(nèi)容就是通知信息攜帶的數(shù)據(jù),后面我們?nèi)≌Z音播報(bào)的文案,通知欄的title,以及通知內(nèi)容都是從這個(gè)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];
       //默認(rèn)的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對應(yīng)的文件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;
             // 文件移動(dòng)到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(@"失敗");
        }
         
    }];
    
    //啟動(dòng)下載任務(wù)
    [_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;
}

當(dāng)音頻下載處理完成后記得調(diào)用self.contentHandler(self.bestAttemptContent);
只有當(dāng)調(diào)用self.contentHandler(self.bestAttemptContent);之后,才會(huì)彈出頂部橫幅,并開始播報(bào),橫幅消失時(shí)音頻會(huì)停止,實(shí)測橫幅時(shí)長大概6s!所以音頻需要處理控制在6s之內(nèi)!

測試這種方案ios13播放沒用問題,ios12上沒有正確播放,如果有好的修改方案,歡迎私信

需要注意的問題

1.網(wǎng)上大都說支持三種格式 aiff、caf以及wav,但實(shí)測也支持MP3格式
2.處理完成后一定要記得調(diào)用 self.contentHandler(self.bestAttemptContent);,否則不會(huì)出現(xiàn)通知橫幅
3.下載失敗最好準(zhǔn)備一段默認(rèn)語音播報(bào)
4.多條推送同時(shí)到達(dá)問題,可以寫個(gè)隊(duì)列,調(diào)用self.contentHandler(self.bestAttemptContent);后,主動(dòng)去阻塞線程一定的時(shí)長(音頻時(shí)長),播放完成后記得刪除掉!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容