最近申請了支付寶的二維碼收錢碼,其中支付寶有這麼一個功能,就是,別人掃描你的二維碼給你轉帳以後,收到錢會有一條語音推送,」支付寶到帳 1000萬「之類的推送消息,無論你的支付寶app有沒有被殺死。html
只要你的遠程推送開着,而且支付寶的"二維碼收錢到帳語音提醒",都打開着,就能夠收到。git
打開方式:支付寶點擊右上角設置-通用-新消息通知,打開到帳提醒便可。
github
對支付寶進行相關測試:json
一、iOS 10如下的設備收到錢以後無論App是殺死仍是壓入後臺狀態都會播報」支付寶到帳一筆」一句固定的語音
二、iOS 10如下的設備收到錢以後無論App是殺死仍是壓入後臺狀態,而且設備處於靜音狀態,只是會有一個推送彈框和手機振動
三、iOS 10以上的設備,收到錢以後,無論APP是殺死仍是壓入後臺狀態,不論是靜音仍是非靜音狀態,在收到轉帳的時候,會播報」支付寶到帳 * 元」
四、iOS 10以上的設備,在收到語音播報的時候,按音量鍵是能夠調節音量大小的數組
實現以上功能注意的點:bash
iOS 10以上和iOS10如下設備實現方式不同iOS 10以上須要考慮的因素,設備是否被殺死狀態,靜音非靜音狀態,音量是否能夠調節,是否能夠播報隨機對應的錢數
而且別人給你轉多少錢就會播報到帳多少錢。網絡
iOS 10 以前系統實現方案:session
能保證設備在殺死或者壓入後臺的情況下收到信息,應該是遠程推送的功勞了。
iOS 10 以前系統,能夠藉助遠程推送定製鈴聲的功能來實現,只要在本地添加一段提早錄製好的語音,而且在推送內容的時候將sound字段,修改爲語音的名稱便可app
下面來看一下iOS10以上系統的實現播報自定義語音的實現方式:函數
實現以上功能需藉助iOS 10的 NotificationServiceExtension
首先了解下常規的遠程推送邏輯
iOS 10之後添加上NotificationServiceExtension以後的推送流程
能夠查看相關文檔瞭解: NotificationServiceExtension
NotificationServiceExtension這個東西有啥做用呢?
主要是可以在展現推送內容以前先獲取到相關的推送信息,能夠更改或者替換相關的推送內容。
怎麼實現呢?
不用本身建立UNNotificationServiceExtension類,使用Xcode提供的模板直接選擇就能夠。若是你的app收到遠程推送的話就會加載擴展而且調用didReceiveNotificationRequest:withContentHandler:,而作這些的前提就是,遠程推送的那一套配置<證書之類的>得作好,還有就是推送的字典中包含mutable-content而且它的值是1;
iOS 10系統以後遠程推送播報語音的實現思路:
設備在收到遠程推送的時候,進入Service Extension,將遠程推送的信息攔截下來,將須要播報的文字信息經過相關方式翻譯成語音,進行播報,播報完畢以後再展現彈框信息。
UNNotificationServiceExtension的功能就是能夠攔截到遠程推送的信息,而後當調用self.contentHandler(self.bestAttemptContent); 以後就會進行彈框顯示了,若是進行了彈框顯示,那麼UNNotificationServiceExtension的使命意味着結束了。
當攔截到信息,到彈框最多有30秒的時間進行操做。
Your extension has a limited amount of time (no more than 30 seconds) to modify the content and execute the contentHandler
block. If you do not execute that block in a timely manner, the system calls your extension’s serviceExtensionTimeWillExpire method to give you one last chance to execute the block. If you do not, the system presents the notification’s original content to the user.
大體思路定下以後,下一步操做,怎麼把文字翻譯成語音進行播報
一、使用科大訊飛以及相似的三方庫,將遠程推送過來的文字,直接使用三方庫播放出來
二、使用 AVSpeechSynthesisVoice 相關類將遠程推送過來的文字直接轉化成語音,進行播報
三、若是播報的是錢數的話,能夠在本地將相關可能播報的語音片斷錄製好,而後根據推送過來的內容標識,對語音片斷進行拼接,而後進行播放
若是對Notification Servivice Extension不是很熟悉的,建議先了解一下
iOS10 推送extension之 Service Extension你玩過了嗎?
在介紹相關方式以前,先介紹一個測試工具
SmartPush
使用方式也很簡單,運行起來,輸入相關的推送數據和token,而且選擇對應的推送證書,點擊推送便可
正式進入主題
推送的內容是:
{
"aps":{
"alert":{
"title":"iOS 10 title",
"subtitle":"iOS 10 subtitle",世上只有媽媽好,有媽的孩子像塊寶。投進媽媽的懷抱,幸福哪裏找。沒媽的孩子像根草。大河向東流,天上的星星參北斗,嘿呀,咿兒呀,嘿 嘿 咿兒呀" }, "my-attachment":"http://img01.taopic.com/160317/240440-16031FU23937.jpg", "mutable-content":1, "category":"myNotificationCategory1", "badge":3 } }複製代碼
一、使用科大訊飛以及相似的三方庫,將遠程推送過來的文字,直接使用三方庫播放出來
這個流量用多了應該收費,具體可查看科大訊飛官網
將推送過來的文字轉化成語音播放,而後在播放完畢的回調中執行self.contentHandler(self.bestAttemptContent);
@interface NotificationService ()<IFlySpeechSynthesizerDelegate,AVAudioPlayerDelegate,AVSpeechSynthesizerDelegate>
{
AVSpeechSynthesizer *synthesizer;
}
@property (nonatomic, strong) IFlySpeechSynthesizer *iFlySpeechSynthesizer;
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@property (nonatomic, strong)AVAudioPlayer *myPlayer;
@property (nonatomic, strong) NSString *filePath;
// AVSpeechSynthesisVoice 播放完畢以後的回調block
@property (nonatomic, copy)PlayVoiceBlock finshBlock;
// 科大訊飛播放完畢以後的block回調
@property (nonatomic, copy)PlayVoiceBlock kedaFinshBlock;
// 語音合成完畢以後,使用 AVAudioPlayer 播放
@property (nonatomic, copy)PlayVoiceBlock aVAudioPlayerFinshBlock;
@end
@implementation NotificationService
- (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];
__weak __typeof(self)weakSelf = self;
/**************************************************************************/
// 方式1,直接使用科大訊飛播放,成功,可是剛開始的時候可能須要幾秒的準備播放時間
[self playVoiceKeDaXunFeiWithMessage:self.bestAttemptContent.body withBlock:^{
weakSelf.contentHandler(weakSelf.bestAttemptContent);
}];
}複製代碼
#pragma mark- 使用科大訊飛播放語音
- (void)playVoiceKeDaXunFeiWithMessage:(NSString *)message withBlock:(PlayVoiceBlock)finshBlock
{
if (finshBlock) {
self.kedaFinshBlock = finshBlock;
}
//建立語音配置,appid必需要傳入,僅執行一次則可
NSString *initString = [[NSString alloc] initWithFormat:@"appid=%@",@"59db7ce2"];
//全部服務啓動前,須要確保執行createUtility
[IFlySpeechUtility createUtility:initString];
/******************************************************/
//獲取語音合成單例
_iFlySpeechSynthesizer = [IFlySpeechSynthesizer sharedInstance];
//設置協議委託對象
_iFlySpeechSynthesizer.delegate = self;
//設置合成參數
//設置在線工做方式
[_iFlySpeechSynthesizer setParameter:[IFlySpeechConstant TYPE_CLOUD]
forKey:[IFlySpeechConstant ENGINE_TYPE]];
//設置音量,取值範圍 0~100
[_iFlySpeechSynthesizer setParameter:@"50"
forKey: [IFlySpeechConstant VOLUME]];
//發音人,默認爲」xiaoyan」,能夠設置的參數列表可參考「合成發音人列表」
[_iFlySpeechSynthesizer setParameter:@" xiaoyan "
forKey: [IFlySpeechConstant VOICE_NAME]];
//保存合成文件名,如再也不須要,設置爲nil或者爲空表示取消,默認目錄位於library/cache下
[_iFlySpeechSynthesizer setParameter:@" tts.pcm"
forKey: [IFlySpeechConstant TTS_AUDIO_PATH]];
//啓動合成會話
[_iFlySpeechSynthesizer startSpeaking:message];
}
//IFlySpeechSynthesizerDelegate協議實現
//合成結束
- (void) onCompleted:(IFlySpeechError *) error {
NSLog(@"合成結束 error ===== %@",error);
self.kedaFinshBlock();
}複製代碼
結果
知足如下兩點要求(1)、iOS 10以上的設備,收到推送以後,無論APP是殺死仍是壓入後臺狀態,不論是靜音仍是非靜音狀態,在收到轉帳的時候,會播報」到帳 * 元」
(2)、iOS 10以上的設備,在收到語音播報的時候,按音量鍵是能夠調節音量大小的
坑點
說明:當前實現的是將push內容中的body
播放出來
一、若是你收到推送了可是添加了系統的鈴聲,也就是你在push的json中添加了"sound":"default"
那麼就可能會影響推送聲音的播放
二、推送有問題
三、播放的語音時長最好不要超過30秒
四、若是說你的遠程推送仍是走的iOS10以前的邏輯,那麼請檢查一下你的推送的json有沒有【"mutable-content":1】
二、使用 AVSpeechSynthesisVoice 相關類將遠程推送過來的文字直接轉化成語音,進行播報
AVSpeechSynthesisVoice 可查看官方文檔
流程:
將推送過來的文字轉化成語音播放,而後在播放完畢的回調中執行,和科大訊飛相似,不過是蘋果系統相關類
相關代碼
@interface NotificationService ()<IFlySpeechSynthesizerDelegate,AVAudioPlayerDelegate,AVSpeechSynthesizerDelegate>
{
AVSpeechSynthesizer *synthesizer;
}
@property (nonatomic, strong) IFlySpeechSynthesizer *iFlySpeechSynthesizer;
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@property (nonatomic, strong)AVAudioPlayer *myPlayer;
@property (nonatomic, strong) NSString *filePath;
// AVSpeechSynthesisVoice 播放完畢以後的回調block
@property (nonatomic, copy)PlayVoiceBlock finshBlock;
// 科大訊飛播放完畢以後的block回調
@property (nonatomic, copy)PlayVoiceBlock kedaFinshBlock;
// 語音合成完畢以後,使用 AVAudioPlayer 播放
@property (nonatomic, copy)PlayVoiceBlock aVAudioPlayerFinshBlock;
@end
@implementation NotificationService
- (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];
__weak __typeof(self)weakSelf = self;
/**************************************************************************/
// 方式4,AVSpeechSynthesisVoice使用系統方法,文字轉語音播報,成功
[self playVoiceWithAVSpeechSynthesisVoiceWithContent:self.bestAttemptContent.body fishBlock:^{
weakSelf.contentHandler(weakSelf.bestAttemptContent);
}];
}複製代碼
#pragma mark- AVSpeechSynthesisVoice文字轉語音進行播放,成功
- (void)playVoiceWithAVSpeechSynthesisVoiceWithContent:(NSString *)content fishBlock:(PlayVoiceBlock)finshBlock
{
if (content.length == 0) {
return;
}
if (finshBlock) {
self.finshBlock = finshBlock;
}
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setActive:YES error:nil];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
// 建立嗓音,指定嗓音不存在則返回nil
AVSpeechSynthesisVoice *voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
// 建立語音合成器
AVSpeechSynthesizer *synthesizer = [[AVSpeechSynthesizer alloc] init];
synthesizer.delegate = self;
// 實例化發聲的對象
AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:content];
utterance.voice = voice;
utterance.rate = 0.5; // 語速
// 朗讀的內容
[synthesizer speakUtterance:utterance];
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didStartSpeechUtterance:(AVSpeechUtterance *)utterance
{
NSLog(@"開始");
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance
{
self.finshBlock();
NSLog(@"結束");
}複製代碼
坑點
說明:當前實現的是將push內容中的body
播放出來
一、若是你收到推送了可是添加了系統的鈴聲,也就是你在push的json中添加了"sound":"default"
那麼就可能會影響推送聲音的播放
二、播放的語音時長最好不要超過30秒
三、若是說你的遠程推送仍是走的iOS10以前的邏輯,那麼請檢查一下你的推送的json有沒有【"mutable-content":1】
四、若是在手機靜音的狀態下聽不到播報的語音
添加設置
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setActive:YES error:nil];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];複製代碼
結果
知足如下兩點要求
(1)、iOS 10以上的設備,收到推送以後,無論APP是殺死仍是壓入後臺狀態,不論是靜音仍是非靜音狀態,在收到轉帳的時候,會播報」到帳 * 元」
(2)、iOS 10以上的設備,在收到語音播報的時候,按音量鍵是能夠調節音量大小的
三、若是播報的是錢數的話,能夠在本地將相關可能播報的語音片斷錄製好,而後根據推送過來的內容標識,對語音片斷進行拼接,而後進行播放
流程:
若是播報的內容是相對固定的片斷組合體,這裏那支付寶舉例。
好比提早先錄好 如下可能播報的內容
*到帳、 0、 一、 二、 三、 四、 五、 六、 七、 八、 九、 10、 百、 千、 萬、 十萬、 百萬、 千萬、 億、 元 等等
而後根據推送的內容進行相關語音文件的對應,而後拼接,拼接完畢以後生成一個語音文件,而後進行播放
相關代碼:
@interface NotificationService ()<IFlySpeechSynthesizerDelegate,AVAudioPlayerDelegate,AVSpeechSynthesizerDelegate>
{
AVSpeechSynthesizer *synthesizer;
}
@property (nonatomic, strong) IFlySpeechSynthesizer *iFlySpeechSynthesizer;
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@property (nonatomic, strong)AVAudioPlayer *myPlayer;
@property (nonatomic, strong) NSString *filePath;
// AVSpeechSynthesisVoice 播放完畢以後的回調block
@property (nonatomic, copy)PlayVoiceBlock finshBlock;
// 科大訊飛播放完畢以後的block回調
@property (nonatomic, copy)PlayVoiceBlock kedaFinshBlock;
// 語音合成完畢以後,使用 AVAudioPlayer 播放
@property (nonatomic, copy)PlayVoiceBlock aVAudioPlayerFinshBlock;
@end
@implementation NotificationService
- (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];
__weak __typeof(self)weakSelf = self;
/*******************************推薦用法*******************************************/
// 方法3,語音合成,使用AVAudioPlayer播放,成功
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setActive:YES error:nil];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
[self hechengVoiceAVAudioPlayerWithFinshBlock:^{
weakSelf.contentHandler(weakSelf.bestAttemptContent);
}];
}複製代碼
#pragma mark- 合成音頻使用 AVAudioPlayer 播放
- (void)hechengVoiceAVAudioPlayerWithFinshBlock:(PlayVoiceBlock )block
{
if (block) {
self.aVAudioPlayerFinshBlock = block;
}
/************************合成音頻並播放*****************************/
AVMutableComposition *composition = [AVMutableComposition composition];
NSArray *fileNameArray = @[@"daozhang",@"1",@"2",@"3",@"4",@"5",@"6"];
CMTime allTime = kCMTimeZero;
for (NSInteger i = 0; i < fileNameArray.count; i++) {
NSString *auidoPath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@",fileNameArray[i]] ofType:@"m4a"];
AVURLAsset *audioAsset = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:auidoPath]];
// 音頻軌道
AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:0];
// 音頻素材軌道
AVAssetTrack *audioAssetTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];
// 音頻合併 - 插入音軌文件
[audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, audioAsset.duration) ofTrack:audioAssetTrack atTime:allTime error:nil];
// 更新當前的位置
allTime = CMTimeAdd(allTime, audioAsset.duration);
}
// 合併後的文件導出 - `presetName`要和以後的`session.outputFileType`相對應。
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:outPutFilePath];
session.outputFileType = AVFileTypeAppleM4A; //與上述的`present`相對應
session.shouldOptimizeForNetworkUse = YES; //優化網絡
[session exportAsynchronouslyWithCompletionHandler:^{
if (session.status == AVAssetExportSessionStatusCompleted) {
NSLog(@"合併成功----%@", outPutFilePath);
NSURL *url = [NSURL fileURLWithPath:outPutFilePath];
self.myPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
self.myPlayer.delegate = self;
[self.myPlayer play];
} else {
// 其餘狀況, 具體請看這裏`AVAssetExportSessionStatus`.
// 播放失敗
self.aVAudioPlayerFinshBlock();
}
}];
/************************合成音頻並播放*****************************/
}
#pragma mark- AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
if (self.aVAudioPlayerFinshBlock) {
self.aVAudioPlayerFinshBlock();
}
}複製代碼
結果
(1)、iOS 10以上的設備,收到推送以後,無論APP是殺死仍是壓入後臺狀態,不論是靜音仍是非靜音狀態,在收到轉帳的時候,會播報」到帳 * 元」
(2)、iOS 10以上的設備,在收到語音播報的時候,按音量鍵是能夠調節音量大小的
坑點
(1)、注意上面的坑點
(2)、播放音頻的時候有兩種播放形式AudioServicesPlayAlertSoundWithCompletion和AVAudioPlayer。
建議使用AVAudioPlayer,由於AVAudioPlayer能知足不受設備靜音不靜音的影響,能根據音量調節聲音的高低。
AudioServicesPlayAlertSoundWithCompletion侷限性比較大,會受靜音的影響,只會震動,而且沒法調整音量的高低。
合成語音以後若是使用AudioServicesCreateSystemSoundID播放的話,有必定的侷限性
http://www.hangge.com/blog/cache/detail_771.html
1,系統聲音服務介紹:
系統聲音服務提供了一個Api,用於播放不超過30秒的聲音。它支持的文件格式有限,具體的說只有CAF、AIF和使用PCM或IMA/ADPCM數據的WAV文件。
但此函數沒有提供操做聲音和控制音量的功能,所以若是是要爲多媒體或遊戲建立專門聲音,就不要使用系統聲音服務。
2,系統聲音服務支持以下三種類型:
(1)聲音:馬上播放一個簡單的聲音文件。若是手機靜音,則用戶什麼也聽不見。
(2)提醒:播放一個聲音文件,若是手機設爲靜音或震動,則經過震動提醒用戶。
(3)震動:震動手機,而不考慮其餘設置。複製代碼
說明:
上面並無實現 數字轉對應音頻文件名稱數組的過程,直接實現的是合成音頻的方法。
Extension的運行生命週期:
iOS對於擴展的支持已經由最初的6類到了現在iOS10的19類(相信隨着iOS的發展擴展的覆蓋面也會愈來愈廣),固然不一樣類型的擴展其用途和用法均不盡相同,可是其工做原理和開發方式是相似的。下面列出擴展的幾個共同點:
擴展依附於應用而不能單獨發佈和部署;
擴展和包含擴展的應用(containing app)生命週期是獨立的,分別運行在兩個不一樣的進程中;
擴展的運行依賴於宿主應用(或者叫載體應用 host app,而不是containing app)其生命週期由宿主應用肯定;
對開發者而言擴展做爲一個單獨的target而存在;
擴展一般展示在系統UI或者其餘應用中,運行應該儘量的迅速而功能單一;
關於斷點調試
若是想經過斷點調試來了解或者檢查推送的流程怎麼搞呢?
方法一:
調試 content Extension
方法二:
若是想同時調試各個target怎麼辦?
將項目運行起來,而後發送一條推送以後,激活Service Extension,若是有須要能夠激活content Extension<下拉一下推送條查看就好,前提你的conten Extension可使用>
而後去選擇,這個時候不要stop掉程序,根據下圖選擇完畢以後,在相關的地方打上斷點,再次推送。
建議使用方法二,各個流程順序更加直觀
最後獻上相關的Demo地址,若是你有更好的建議歡迎留言,若有不正,歡迎來噴。
能夠直接用個人Demo進行調試,調試的時候注意修改下bundleId,而後用本身的開發者帳號配置一下相關的push證書就能夠了