iOS推送实现语音播报实践

一、实现思路

1、应用活跃时,合成语音,播放语音

2、应用被杀死,唤醒应用,合成语音,播放语音

二、唤醒应用

1、voip push service (iOS8以上版本)

短暂唤醒应用,处理轻量级业务。业务处理完成后,应用休眠。

PushKit是苹果在iOS8之后推出的新框架,iOS10之后,苹果更是禁止VOIP应用在后台使用socket长链接,PushKit可以说是为了VOIP而生,满足实时性的同时,还能达到省电的效果,搭配苹果自己的CallKit(大陆已被禁止),可以呈现出类似原生电话通话的效果。

PushKit区别与普通APNs的地方是,它不会弹出通知,而是直接唤醒你的APP,进入回调,也就是说,可以在没点击APP启动的情况下,就运行我们自己写的代码,当然,推送证书和注册、回调的方法也和APNs不同,代码注册流程如下:

#pragma mark 注册pushkit 和 代理方法
- (void)registPushKit{
    //注册voip service 服务
    float version = [UIDevice currentDevice].systemVersion.floatValue;
       if (version >= 8.0) {
        PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:nil];
        pushRegistry.delegate = self;
        pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
    }
}


- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type{
    //服务注册成功,获取token
    NSString *str = [NSString stringWithFormat:@"%@",credentials.token];
    NSString * _tokenStr = [[[str stringByReplacingOccurrencesOfString:@"<" withString:@""]

                             stringByReplacingOccurrencesOfString:@">" withString:@""] stringByReplacingOccurrencesOfString:@" " withString:@""];
    [[NSString stringWithFormat:@"pushkit_didUpdatePushCredentials: %@", _tokenStr] saveTolog];
    NSLog(@"pushkit token %@", _tokenStr);
    //上报token
    ......
}
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type {
    //收到voip推送,应用唤醒,
    //合成语音,播放语音
    ....
}

证书:voip service 需要在apple开发账号中 注册对应的 voip service证书。跟APNs证书不同,VoIP证书不区分开发和生产环境,VoIP证书只有一个,生产和开发都可用同一个证书。其他步骤和注册apns类似。此处省略相关流程。

注意点:voip service 专为 音视频通话应用服务,如果应用无相关功能,上架有大概率被拒。

2、serivce Extension (iOS10以上版本)

iOS10添加了很多Extension,与通知相关的extension为Notification Service Extension。

我们先来了解一下Service Extension,这个东西主要是干啥的呢?
主要是,让我们在收到远程推送的时候<必须是远程推送>,展示之前对通知进行修改,因为我们收到远程推送之前会先去执行Service Extension中的代码。这样就可以在收到远程推送展示之前为所欲为了。

极光的JPushExtension基于extension来统计推送的到达率。

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    
    //解析通知信息,合成语音,播放语音
    ....
    self.contentHandler(self.bestAttemptContent);
}

注意点:extention唤起应用的方式,不受官方审核限制。

播放时长受限,大概5秒

iOS12以上无法播放语音。

三、语音生成/合成

当收到语音信息后,如推送附带的语音信息,需要将语音信息转成可播放的语音,大致有以下三种方式

1、使用AVSpeechSynthesis框架,直接将文字转换成语音播报


 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
 [[AVAudioSession sharedInstance] setActive:YES error:nil];
 //创建语音合成器
 AVSpeechSynthesizer *avSpeech = [[AVSpeechSynthesizer alloc] init];
 //实例化发声对象
 AVSpeechUtterance *avSpeechterance = [AVSpeechUtterance speechUtteranceWithString:@"收款10元"];
 //中文发音
 AVSpeechSynthesisVoice *voiceType = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
 avSpeechterance.voice = voiceType;
 avSpeechterance.pitchMultiplier = 0.1;//声调
 avSpeechterance.volume = 1;//音量
 avSpeechterance.rate = 0.5;//语速
 avSpeechterance.pitchMultiplier = 1.1;
 //朗读
 [avSpeech speakUtterance:avSpeechterance];

声音僵硬,不好听

如果对合成音效果不满意,可以导入第三方语音库进行处理。

2、内置语音片段,AVComposition相关类实现离线合成,播放语音

提前录制可能要播报的内容:

支付宝到账、 0、 1、 2、 3、 4、 5、 6、 7、 8、 9、 十、 百、 千、 万、 十万、 百万、 千万、 亿、 元 等等

这样的几种录音,然后用相关的名字命名好<相关的规则自己命名就好>。比如push过来的是内容是 10010,那么转化成的录音文件名称的数组就是
@[@"支付宝到账",@"1",@"万",@"0",@"1",@"十",@"元"]。然后找到这几个文件,然后按照顺序拼接成一个语音文件进行播放

- (void)syntheticSpeech
{
    /************************合成音频并播放*****************************/
    NSMutableArray *audioAssetArray = [[NSMutableArray alloc] init];
    NSMutableArray *durationArray = [[NSMutableArray alloc] init];
    [durationArray addObject:@(0)];


    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]];
        [audioAssetArray addObject:audioAsset];

        // 音频轨道
        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];
            static SystemSoundID soundID = 0;
            AudioServicesCreateSystemSoundID((__bridge CFURLRef _Nonnull)(url), &soundID);
            AudioServicesPlayAlertSoundWithCompletion(soundID, ^{
                NSLog(@"播放完成");
            });
        } else {
            // 其他情况, 具体请看这里`AVAssetExportSessionStatus`.
        }
    }];
    /************************合成音频并播放*****************************/
}

播放语音内容相对固定,录音片段需提前导入

3、在线合成

当收到推送内容后,在线请求语音数据,进行播放。在线合成方案的效果则相对更像人声,富有感情。

请求耗时,可能出现唤醒期间无法完成的情况。

四、语音播放

1、notification service Extension

苹果在iOS12.1版本以上,在extension中使用 AVFoundation框架播放音频无效。Notification Service Extension errors in iOS 12.1 with AVFoundation

大概的意思是大部分的扩展应用extensions不能使用播放音频,所以苹果做了限制。苹果推崇的做法是使用弹框的方式播放音频,而且扩展中使用background mode 模式下的play aduio,上架也会被拒掉

当前补救思路:把远程通知在扩展里拆分成多个本地通知,每个本地通知声音是单个的音频,顺序发出。app内预先存入大量的语音片段,扩展中依次发送本地通知。系统解析本地通知,从app内获取自定义声音,组合成语音播放。

比如:“支付宝收款10元”。扩展依次发送本地通知 :通知一(声音“支付宝”) + 通知二(声音”10“) + 通知三 (声音“元”)。app依次弹出3个通知声音,组成一句播报。

app内大量的语音片段会导致包体过大。发送多个本地通知会导致手机震动多次,且播报声音僵硬,不自然。

2、当主应用处于后台时,AVSpeechSynthesis框架无法播放。可使用AVAudioSession进行后台播放。这种情况下,可先合成语音,转成apple支持的格式,存入沙盒。类似于讯飞、百度语音sdk都支持。


//合成语音,保存在沙盒中。取路径,进行播放
    NSString *string = [[NSBundle mainBundle] pathForResource:@"incomingCall" ofType:@"mp3"];
    NSURL *url = [NSURL fileURLWithPath:string];
    NSData *data = [NSData dataWithContentsOfFile:string];
    NSError *error = nil;
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategoryPlayback error:&error];

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

推荐阅读更多精彩内容