目前市面上很多支付APP都需要在收款成功后,進(jìn)行語音提示,例如收錢吧,微信队萤,支付寶等!公司App現(xiàn)在也需要加入這個功能,這里記錄下踩過的坑
該功能需要用到 蘋果的 Notification Service Extension 這個是iOS10.0推出的冯吓。https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension
實(shí)現(xiàn)該功能
一,添加 Notification Service Extension
創(chuàng)建之后程序內(nèi)會出現(xiàn) NotificationService.h ,NotificationService.m 文件
二拴签,然后就是發(fā)送推送消息 纹份,以極光推送為例
(iOS 10 新增的 Notification Service Extension 功能苟跪,用 mutable-content 字段來控制。 若使用極光的 Web 控制臺蔓涧,需勾選 “可選設(shè)置”中 mutable-content 選項件已;若使用 RESTFul API 需設(shè)置 mutable-content 字段為 true。)
三元暴,攔截推送信息篷扩,播放語音
設(shè)置好后我們每次發(fā)送推送,都會走到NotificationService中的這個回調(diào)茉盏,獲取到推送中附帶的信息(ps:如果發(fā)現(xiàn)沒走回調(diào)鉴未,請對照上一步,查看 極光控制臺mutable-content 是否勾選鸠姨,后臺或其他方式推送要此字段設(shè)置為1)铜秆;
(1)ios12以前
ios12以前,這個功能還是比較好做的讶迁,收到推送后连茧,調(diào)用語音庫AVSpeechSynthesisVoice讀出來就可以,
av= [[AVSpeechSynthesizer alloc]init];
av.delegate=self;//掛上代理
AVSpeechSynthesisVoice*voice = [AVSpeechSynthesisVoicevoiceWithLanguage:@"zh-CN"];//設(shè)置發(fā)音,這是中文普通話
AVSpeechUtterance*utterance = [[AVSpeechUtterance alloc]initWithString:@"需要播報的文字"];//需要轉(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)建兩個音頻軌道,并獲取兩個音頻素材的軌道
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è)置為第一段音頻的時長虐唠,即`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.reibang.com/p/a739c200b3c8
http://www.reibang.com/p/3e357e3129b8
或者最簡單的方案搓幌,集成訊飛杆故,百度等三方合成語音
(2)iOS13播報
在iOS12.1發(fā)布后,上述方案已經(jīng)不行了溉愁,
據(jù)說蘋果給出的解釋是 Notification Service Extension是為了豐富推送體驗处铛,主要是為了富文本推送圖片的處理,所以在Notification Service Extension中禁用了play播放器相關(guān)拐揭!有需要的可以使用官方的sound字段播放自定義的語音
關(guān)于sound字段
sound字段是官方推送的一個默認(rèn)字段撤蟆,蘋果官方文檔說明可以將音頻放到工程主目錄,或者Libray/Sounds堂污,在推送到達(dá)時家肯,系統(tǒng)將根據(jù)sound字段在目錄中找到對應(yīng)音頻播放,支持的格式aiff,caf,wav盟猖!
比如極光推送的控制臺就是這個字段
但是這就限制了讨衣,必須在打包之前就把語音放進(jìn)工程目錄!只能用固定的語音了式镐!
那么最笨的方案就是內(nèi)置一萬多條語音反镇,然后推送的時候直接讓后端用sound來指定播放的語音,但是在包的大小……
網(wǎng)上翻閱很久娘汞,后來發(fā)現(xiàn)愿险,sound除了播放工程主目錄和Library/Sounds,還可以播放AppGroup中Library/Sounds的音頻 那這就好辦了,我們可以在后臺合成价说,然后下載到AppGroup后修改sound字段進(jìn)行播放(前端合成到處到指定文件夾應(yīng)該也可以)
首先打開我們項目的AppGroup
打開后記得??辆亏,然后再打開Notification Service Extension 的AppGroup 也就是圖中名為PushDemo的的targets,也要同樣操作一遍
之后接到通知鳖目,解析出下載鏈接扮叨,下載完在本地修改sound字段,交由系統(tǒng)播報(應(yīng)該也可以本地拼接后合成到處到對應(yīng)文件夾领迈,筆者當(dāng)時沒有嘗試彻磁,各位可以自己嘗試)
- (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 內(nèi)容就是通知信息攜帶的數(shù)據(jù)碍沐,后面我們?nèi)≌Z音播報的文案,通知欄的title衷蜓,以及通知內(nèi)容都是從這個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;
// 文件移動到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(@"失敗");
}
}];
//啟動下載任務(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);
之后累提,才會彈出頂部橫幅,并開始播報磁浇,橫幅消失時音頻會停止斋陪,實(shí)測橫幅時長大概6s!所以音頻需要處理控制在6s之內(nèi)置吓!
測試這種方案ios13播放沒用問題无虚,ios12上沒有正確播放,如果有好的修改方案衍锚,歡迎私信
需要注意的問題
1.網(wǎng)上大都說支持三種格式 aiff友题、caf以及wav,但實(shí)測也支持MP3格式
2.處理完成后一定要記得調(diào)用 self.contentHandler(self.bestAttemptContent);戴质,否則不會出現(xiàn)通知橫幅
3.下載失敗最好準(zhǔn)備一段默認(rèn)語音播報
4.多條推送同時到達(dá)問題度宦,可以寫個隊列,調(diào)用self.contentHandler(self.bestAttemptContent);后告匠,主動去阻塞線程一定的時長(音頻時長)戈抄,播放完成后記得刪除掉!