接上篇如何让iOS推送播放语音,之前的结论是iOS如果需要送审商店只能播放本地的mp3文件,这里更新一下:

更新

语音的播放,最终调用的方法是UNNotificationSound(named: xxx),而这个方法官方文档注释如下:

// The sound file to be played for the notification. The sound must be in the Library/Sounds folder of the app's data container or the Library/Sounds folder of an app group data container. If the file is not found in a container, the system will look in the app's bundle.public convenience init(named name: UNNotificationSoundName)

注释里说,语音文件会从这三个地方查找:

  1. APP 的Library/Sounds文件夹

  2. APP和 Extension共享Group的Library/Sounds文件夹

  3. App bundle

而之前文章里介绍的,就是属于第三种情况,直接放在App bundle中的情况。这种情况的局限性在于,每次有新增或者变更,都需要变更同步到项目,然后APP发版用户更新后才能生效。

这种太麻烦了,有没有可能,不用更新版本,并且能直接增加新的语音种类,本篇介绍的就是这种。

实现

不更新版本,增加新的语音种类,就需要考虑,是否能在线下载?看上面的播放方法语音文件的查找目录,考虑是否可以通过在线下载语音文件到 APP 的Library/Sounds文件夹 或者 APP和 Extension共享Group的Library/Sounds文件夹下。

首先考虑第一种情况,如果想要下载到APP的Library/Sounds文件夹下,要怎么做呢?直接在推送时配置下载链接是否可行?

笔者尝试的是,在Notification Service Extension的target中,获取到配置的语音文件链接,然后下载,存储到Library/Sounds文件夹下,下载成功后,再去播放。

验证后发现不可行,因为此时的目录不是APP的Library/Sounds目录,而是推送Target的appex的Library/Sounds目录,而这个目录不在语音文件的查找范围内,所以这种不可行?那如何下载到APP 的Library/Sounds目录下呢?

下载到APP的Library/Sounds

笔者想到有两种可能方案:

  1. 推送时配置下载链接,在APP处理推送方法的地方,进行下载

  2. 单独接口配置下载链接,APP打开时调用,提前下载

首先方案一,APP 处理推送方法是在Notification Service ExtensioncontentHandler之后,而语音播报是在contentHandler时,即,下载在播报之后,这种情况下,第一次的语音是播报不出来的;而且 APP 不打开的情况下,是否允许下载,是否能下载成功都未知,所以不可取。

再来看方案二,方案二的实现是一定没有问题的,通过单独的配置接口,下发语音下载链接,下载到 APP 的Library/Sounds文件夹下,然后推送时,只需要保证播放的名字和文件夹下名字一致即可。只不过,这个方案需要新增一个配置接口,而且需要提前下发配置链接,以保证用户提前下载成功,才能在真正推送时播放对应的文件。

Ps: 如果采用方案二,是可以连语音下载链接都可以省掉,只需要告诉 APP 要播放的内容就可以,APP 内部把要播放的内容转为通过TTS语音库转为语音文件,并存储到Library/Sounds下即可。

APP和Extension共享Group的Library/Sounds文件夹

如果不想新增配置接口,能不能直接在推送时下载呢?

答案是可以的,通过下载到APP和 Extension共享Group的Library/Sounds文件夹这种方案,可以实现推送时下载并播放。具体步骤如下:

  1. 创建 APP 和 Notification Service Extension共享的 Group

  2. didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void)方法中,获取下载链接,并下载

  3. 下载成功后,存储到Group的Library/Sounds文件夹下

  4. 存储成功后,播放

通过这种方案,就可以实现在推送时,配置语音文件链接,从而下载并播放。这种方案需要考虑要下载文件的时间和大小,因为超过一定时间后,Notification Service Extension就会自动回调了;而且文件如果太大,即使下载成功也有可能播放失败。

Ps:这种方案也可以考虑直接推送要播放的内容,然后通过离线语音库合成音频文件,存储到Group的Library/Sounds下,然后播放,这里不做详细介绍,感兴趣可以自己实验。

再来考虑一个问题,假如项目里已经有了某些音频文件,要推送消息时,是否会根据项目中有没有决定加不加语音文件链接?当然不是,产品或者运营推送时是不会判断的,他们一定是无脑加;所以,项目中需要判断,判断按步骤判断项目Bundle 中有没有,再判断APP 的Library/Sounds下有没有,再判断共享Group的Library/Sounds中有没有已下载过,最后才是去下载。

具体代码大致如下:

  1. 首先判断项目Bundle 中有没有音频文件,由于在Extension中获取不到主项目的bundle,所以需要在打开 APP 时,存储已存在的音频文件名字到共享Group,然后在Extension中通过 Group 获取音频文件名字判断:

    // 存储已存在的音频文件名字到共享 Group, 在 App 打开时调用
    static func updateAppMainBundleMP3FileResources() {let path = Bundle.main.bundlePathlet fileManager = FileManager.defaultdo {let allContentList = try fileManager.contentsOfDirectory(atPath: path)let validContentList = allContentList.filter { $0.hasSuffix(".mp3") }print(validContentList)let shareDefaults = UserDefaults(suiteName: shareDefaultsSuiteName())shareDefaults?.setValue(validContentList, forKey: kMainAppMp3FileKey)shareDefaults?.synchronize()} catch {print(error)}
    }
    // 共享的Group
    static func shareDefaultsSuiteName() -> String {let name = "group.com.xxx.pushGroup"return name
    }
  2. 判断APP 的Library/Sounds下有没有对应音频文件,这一步需要考虑,是否采用了 APP提供单独接口下发语音文件链接,如果没有,则不需要考虑;笔者这里没有,所以不做详细演示。其逻辑大致如下:

  • 获取项目Library/Sounds文件夹

  • 获取文件夹下所有音频文件

  • 合并存储音频文件名字到共享 Group 中

判断共享Group的Library/Sounds中有没有已下载过:

fileprivate static let kMainAppMp3FileKey = "kMainAppMp3FileKey"
static func isVoiceInfoExist(voiceName: String) -> Bool {/**The /Library/Sounds directory of the app’s container directory.The /Library/Sounds directory of one of the app’s shared group container directories.The main bundle of the current executable.*/// 判断bundle中有没有let soundStr = voiceName + ".mp3"let shareDefaults = UserDefaults(suiteName: shareDefaultsSuiteName())if let validContentList = shareDefaults?.value(forKey: kMainAppMp3FileKey) as? [String],validContentList.contains(soundStr) {// 文件存在return true}// 判断 /Library/Sounds 文件夹下有没有let fileManager = FileManager.defaultif let soundsDirectoryURL = getLibrarySoundsDir() {let filePath = (soundsDirectoryURL as NSString).appendingPathComponent(voiceName + ".mp3")print("------", filePath)if fileManager.fileExists(atPath: filePath) {return true}}// 文件不存在存在return false
}// 获取共享 Group 的`Library/Sounds`文件夹
static func getLibrarySoundsDir() -> String? {let fileManager = FileManager.defaultlet groupIdentifer = shareDefaultsSuiteName()let sharedContainerURL: URL? = fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifer)if let soundsDirectoryURL = sharedContainerURL?.appendingPathComponent("Library/Sounds") {let fileExist = fileManager.fileExists(atPath: soundsDirectoryURL.path)if !fileExist {do {try fileManager.createDirectory(atPath: soundsDirectoryURL.path,withIntermediateDirectories: true)} catch {print(error)}}return soundsDirectoryURL.path}return nil
}

下载音频文件:

// 下载音频文件
static func downloadAndSave(url: URL, voiceName: String, handler: @escaping (_ localURL: URL?) -> Void) {let task = URLSession.shared.dataTask(with: url) { data, res, error invar localURL: URL?if let data = data {let librarySoundDir = getLibrarySoundsDir()let filePath = (librarySoundDir as? NSString)?.appendingPathComponent(voiceName + ".mp3")if let urlStr = filePath {let targetUrl = URL(fileURLWithPath: urlStr)do {_ = try data.write(to: targetUrl)} catch {print(error)}print("url------", targetUrl)localURL = targetUrl}}handler(localURL)}task.resume()
}

最后统一调用:

import UserNotifications
import AVFoundation
class NotificationServiceUtil {func playVoice(with bestAttemptContent: UNMutableNotificationContent, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {let userInfo = bestAttemptContent.userInfodo {try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)try AVAudioSession.sharedInstance().setActive(true)} catch {print(error)}// 要播放的语音文件名字guard let voiceName = userInfo["voiceName"] as? String else {contentHandler(bestAttemptContent)return}// 判断本地是否有语音文件, 有则播放; 没有则下载或者尝试系统语音播放let isVoiceExists = NotificationServiceUtil.isVoiceInfoExist(voiceName: voiceName)let soundStr = voiceName + ".mp3"let soundName = UNNotificationSoundName(soundStr)if isVoiceExists {bestAttemptContent.sound = UNNotificationSound(named: soundName)contentHandler(bestAttemptContent)} else {// 下载链接if let voiceUrlStr = userInfo["voiceUrl"] as? String,let voiceUrlUrl = URL(string: voiceUrlStr) {// 下载NotificationServiceUtil.downloadAndSave(url: voiceUrlUrl, voiceName: voiceName) { localURL inbestAttemptContent.sound = UNNotificationSound(named: soundName)contentHandler(bestAttemptContent)}} else {contentHandler(bestAttemptContent)}}}
}

NotificationService中调用:

import UserNotifications
class NotificationService: UNNotificationServiceExtension {var contentHandler: ((UNNotificationContent) -> Void)?var bestAttemptContent: UNMutableNotificationContent?fileprivate lazy var util = NotificationServiceUtil()override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {self.contentHandler = contentHandlerbestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)if let bestAttemptContent = bestAttemptContent {// 播放处理util.playVoice(with: bestAttemptContent, withContentHandler: contentHandler)}}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)}}
}

总结

iOS语音播报支持方式总结如下:

iOS 语音播报

iOS 语音播报实现流程如下:

iOS推送播放语音播报更新相关推荐

  1. iOS 推送后台语音播报

    推送通知 注意:这里说的推送通知跟NSNotification有所区别 NSNotification是抽象的,不可见的 推送通知是可见的(能用肉眼看到) iOS中提供了2种推送通知 本地推送通知(Lo ...

  2. iOS 推送语音播报(类似支付宝微信的收款提醒)

    项目需求: 近期项目有个需求,实现类似支付宝微信收款后的语音播报如:支付宝到账xx元.要求是APP在前台运行.锁屏.杀死进程后都会有语音播报. 预想方案: 1.通过UIBackgroundTaskId ...

  3. iOS 推送(苹果原生)

    来自:https://www.jianshu.com/p/3fc46a8764ed 前言 推送对App的重要性不言而喻,是每一个iOS开发者必修的技能.网上的资料对于初学者并不友好(至少对于我来说), ...

  4. iOS 推送通知及推送扩展

    概述 iOS中的通知包括本地推送通知和远程推送通知,两者在iOS系统中都可以通过弹出横幅的形式来提醒用户,点击横幅会打开应用.在iOS 10及之后版本的系统中,还支持通知扩展功能(UNNotifica ...

  5. iOS推送流程(APNS)

    iOS推送流程(APNS) 一.APNS(Apple Push Notification Service) 苹果推送通知服务(APNs)是推送通知的网关,iPhone ipad 对于应用程序在后台运行 ...

  6. iOS 推送手机消息背后的技术

    作者:allenzzhao,腾讯  IEG运营开发工程师 消息推送我们几乎每天都会用到,但你知道iOS中的消息推送是如何实现的吗?本文将从推送权限申请,到本地和远程消息推送,再到App对推送消息的处理 ...

  7. 一步一步教你做ios推送

    最近在研究ios的推送问题,遇到了一些问题,最终整理了一下.放在这里和大家分享 APNS的推送机制 首先我们看一下苹果官方给出的对ios推送机制的解释.如下图 Provider就是我们自己程序的后台服 ...

  8. 一步一步教你做ios推送 pem证书制作 php推送

    一步一步教你做ios推送 分类: ios2013-03-03 21:48 3385人阅读 评论(8) 收藏 举报 ios推送客户端服务器 最近在研究ios的推送问题,遇到了一些问题,最终整理了一下.放 ...

  9. iOS 推送要点整合

    本文旨在对 iOS 推送(以下简称 推送)进行一个完整的剖析,如果你之前对推送一无所知,那么在你认真地阅读了全文后必将变成一个推送老手,你将会对其中的各种细节和原理有充分的理解.以下是 pikacod ...

最新文章

  1. 搜狗分身技术再进化,让AI合成主播“动”起来
  2. 获取项目文件在服务器的真实路径
  3. android低功耗蓝牙连接失败_低功耗蓝牙 AoA定位系统为室内定位和资产跟踪 提供亚米级精度位置服务...
  4. SAP Fiori Elements save按钮的实现细节
  5. IIS负载均衡-Application Request Route详解第四篇:使用ARR实现三层部署架构
  6. 微信支付—微信H5支付「微信内部浏览器」
  7. 765g处理器可以用两年吗?
  8. microsoft visual studio遇到了问题,需要关闭
  9. jQuery(七)、效果和动画
  10. termux安装python2_termux怎么安装python
  11. 3套看漫画学python视频教程
  12. 单片机c语言串口中断函数,12手把手教你学单片机的C语言程序设计_中断服务函数.pdf...
  13. 爬虫访问中,如何解决网站限制IP的问题?
  14. 阿铭Linux_网站维护学习笔记20190227
  15. linux mint mac桌面图标,在Ubuntu、Linux Mint上安装Mac OS X主题
  16. 信息系统项目管理师---第五章 项目范围管理
  17. 人工智能技术知识图谱
  18. 一种以STC89C51为核心控制器的积水清除与利用装置解决方案
  19. (x)html文档的结构,XHTML文档_xhtml文档的基本结构_HTML/XHTML-站长之家
  20. python : 超参数优化工具笔记 Tune with PyTorch Quick Start+基础概念

热门文章

  1. 第十一届蓝桥杯C/C++B组 试题E:玩具蛇(题目+题解)
  2. 计网 | Wireshark抓包和分析腾讯视频点播详细过程
  3. 基于imx6q平台移植usbwifi: rt5370sta
  4. 什么是PV,UV,PR值
  5. php输出计算器,一个简单的在线计算器
  6. php json_encode {}_javascript - PHP json_encode将数字编码为字符串
  7. Cherno C++系列笔记17——P52~P54 处理多返回值、模板、堆和栈内存的比较
  8. java文件头工具类_判断文件类型工具类
  9. boost库在visual studio、DevC++和vscode上的环境配置
  10. 煽情的儿子535=随笔