近期项目中有个需求就是要实现类似微信或者支付宝的收款时的语音播报功能,于是笔者就开始了漫长的踩坑之路。
一、最初的预想方案
刚开始讨论实现方案时,安卓的小伙伴说可以使用WebSocket + 讯飞语音在线合成实现。于是最初的几天笔者自己也一直在这条路上走了很久,基本功能都已经实现了,项目在前台的时候,基本没问题。但是项目一进入后台大概半分钟的时间,就无法播报了。原因是iOS项目如果不做任何处理的话,在进入后台大概30s之后,程序就会进入类似休眠的状态,然后就不会再进行任何操作了
二、开始想办法让程序在后台运行
跟安卓的同事讨论之后,发现安卓有方法可以让程序一直在后台处于活跃状态,于是笔者也开始找寻保持项目后台运行的方法,大概有两种
通过UIBackgroundTaskIdentifier不断向程序索要处理时间(这种方案不知道以前是否可行,现在好像是最多只能保持3分钟的时间)
通过后台音乐或者后台定位的方式保持程序后台长时间运行
这种方案确实是可以在后台长时间运行的。
定位的话,会在状态栏一直有一个蓝色的指示后台定位功能的横条,这个首先被pass
然后是后台播放无声音乐的形式,此种方案理论上可行,也有小伙伴在企业包里面这样做,但是有的小伙伴反应这样在审核的时候可能会悲剧,所以也pass掉
三、寻求新的方案
- WebSocket方案行不通,笔者开始考虑采取 推送 + 讯飞语音在线合成的方案
我们知道APNS收到推送之后在Appdelegate中有两个回调方法
/// 程序在后台时点击推送弹窗的回调方法
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
}
// 程序在前台时收到推送时的回调方法
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
}
在这里我们并没有发现,程序在后台收到推送时,作相应处理的方法,哪到底能不能收到推送后就进行处理呢?
答案是可以
iOS 10 之后 iOS推出了Notification Service Extension,我们可以在收到推送之后,通过这个Extension 我们可以有三十秒的时间来对这个推送进行处理
完成之后长这样
然后我们配置一下NotificationService
然后我们看下NotificationService.swift文件
import UserNotifications
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
// Modify the notification content here...
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
// 在这里我们有大概30秒的时候对此条通知进行处理,如修改通知的title,body等等,contentHandler方法调用之后,我们修改过后的通知将会被发出
contentHandler(bestAttemptContent)
}
}
// 30s的处理时间即将结束时,该方法会被调用,最后一次提醒用户去做处理
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
四、NotificationService 调试
- 运行你的主程序
- 运行NotificationService extension 选择你的主程序
- Debug -> Attach To Process By PID or Name... -> 输入你的主程序项目名称 点击Attach
在完成上述操作之后,再次收到推送的话,就会走NotificationService的逻辑了,可以打断点或者Log测试一下
需要注意的是 在推送的内容中 必须配置mutable-content字段,结构大致如下
aps = {
alert = {
subtitle = "\U5fae\U4fe1\U5230\U8d260.01\U5143";
title = "\U4eac\U8d1d\U5c14\U4e91\U5e97";
};
"mutable-content" = 1;
sound = default;
};
五、语音应该怎么播报?
做完上边的操作之后,我们可以知道什么时候去播报语音了,但是语音又要怎么去播报呢?
笔者这边也是试过几个方案,下边一一说来
- 使用讯飞语音在线合成直接播放 或者 使用苹果系统自带的AVSpeech
笔者刚开始使用讯飞发现不行,然后又测试了系统自带的AVSpeech,发现也不好用,查资料才知道,苹果在近期的版本中,停用的在NotificationService中播放语音的功能,之前的某个版本应该可以这么操作。好吧,此方案Pass
- 使用讯飞语音合成之后存到本地, 然后将语音文件设置成推送的sound
既然不让我播,那我存起来总可以了吧,测试发现讯飞在线生成是可以的,也可以存到本地,但。。。是,UNMutableNotificationContent的sound好像只支持提前添加到项目中的文件,并不支持立即生成之后存到本地,然后再设置的功能。。。
- 在请教了朋友之后,找到了一个笨方法,就是提前将 0-9,个十百千万这种可能会遇到的事先生成语音包,添加到NotificationService中,然后再收到的金额转成文字,分割之后通过对比取对应的语音包,随后发送多个本地通知(不设置title,subTitle等,只设置sound)这样就可以在收到推送之后,通过随后自己处理的多个推送连贯起来就可以实现我们所需要的语音播报了
笔者在项目中预先生成的文件如下(语音包通过百度语音开放平台在线生成百度语音在下生成(拉到中间就有了))
比如说我要播放“支付宝到账100元”,我就会发放多个通知,依次播放wx-pre,1,bai,yuan这几个语音,连贯起来就能达到要求
笔者能力有限,暂时想到的方法就是这个,有好的方法可以多多分享,沟通