iOS推送播放语音播报更新

news2024/10/5 16:27:58

88c51c11c938f00d2aaab5e052b7517a.gif

接上篇如何让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.bundlePath
        let fileManager = FileManager.default
        do {
            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.default
    if 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.default
    let 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 in
        var 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.userInfo
       
       do {
           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 in
                   bestAttemptContent.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 = contentHandler
       bestAttemptContent = (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语音播报支持方式总结如下:

1aa3e61e63a40389a1114421a74e7fe8.png

iOS 语音播报

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

a771c3c241a44851ac16ef5a219a19cc.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/570550.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能python:Python写Kafka:介绍及优势

Python写Kafka:介绍及优势 Kafka是目前互联网企业使用最广泛的消息队列系统之一,广泛应用于应用程序之间的异步通信、数据采集、日志收集等领域。Python作为一门通用且易学易用的编程语言,在和Kafka结合时也展现出了其独特的优势。本文将介绍…

我C,最好用的AI工具居然是它!

这几天写了两篇自己的AI实践: 《程序员,如何借力ChatGPT?》; 《普通人,如何借力ChatGPT?》; 很多朋友在后台留言,问我用的是哪一款AI工具。 先说结论。 我最终在ChatGPT,…

超大规模数据库集群保稳系列之二:数据库攻防演练建设实践

总第562篇 2023年 第014篇 本文整理自美团技术沙龙第75期的主题分享《美团数据库攻防演练建设实践》,系超大规模数据库集群保稳系列(内含4个议题的PPT及视频)的第2篇文章。 本文首先介绍了美团当前数据库运维现状、遇到的问题,以及…

Flutter 笔记 | Flutter 可滚动组件

Sliver布局模型 我们介绍过 Flutter 有两种布局模型: 基于 RenderBox 的盒模型布局。基于 Sliver ( RenderSliver ) 按需加载列表布局。 之前我们主要了解了盒模型布局组件,下面学习基于Sliver的布局组件。 通常可滚动组件的子组件可能会非常多、占用…

Protein Cell | 中国农科院基因组所刘永鑫组综述微生物组研究的过去、现在和未来(大众评审截止26号20点)...

微生物组研究展望:过去、现在和未来 Microbiome research outlook: past, present, and future 2023-5-23,Protein & Cell,[IF 15.328] DOI:10.1093/procel/pwad031 原文链接:https://academic.oup.com/proteincel…

adb 命令速查(下)

ADB 关于APP安装、调试和monkey压力测试 作者:炭烤毛蛋 ,查看博主了解更多。 提示:承接上篇《adb 命令速查(中)》,本文将 文章目录 ADB 关于APP安装、调试和monkey压力测试7 adb 关于 apk 的相关操作7.1 安装 apk普通安装带有命…

QQGC?揭秘QQ的AI绘画大模型技术

👉腾小云导读 2022年来,AIGC概念迅速出圈并快速形成产业生态,成为继PGC、UGC之后新的数字内容创作形式。QQ影像中心提出了自研的AI画画技术方案——QQGC,本文将介绍在QQGC基础大模型训练中的实践和探索,接着往下看吧~ …

我用AI帮我唱了首“基尼太美”,颠覆了我的认知!太牛逼了

目录 前言 AI唱"基尼太美"是什么感觉 使用so-vits-svc打造自己专属歌手 1.声音素材整理 2.训练模型 3.让AI唱歌​编辑 AI歌手背后的技术 AI歌手会成为主流吗 写到最后 大家好,我是大侠,AI领域的专业博主 前言 在5月份,孙…

第五篇:强化学习基础之马尔科夫决策过程

你好,我是zhenguo(郭震) 今天总结强化学习第五篇:马尔科夫决策过程 基础 马尔科夫决策过程(MDP)是强化学习的基础之一。下面统一称为:MDP MDP提供了描述序贯决策问题的数学框架。 它将决策问题建模为: 状态…

司空见惯 - 使用dBm表示功率的各种现实情况

前面一篇文章介绍过,使用dBm表示功率时,如何转换为mW。 那现实世界的实际情况中,使用dBm来表示电磁波的能量强度,列表如下: Power level Power Notes 526 dBm 3.61049 W 黑洞碰撞后的引力波辐射的功率&#xff0c…

解决缓存与数据库数据不一致的问题,这篇文章告诉你如何做!

缓存是提高应用程序性能和响应速度的关键组件之一。缓存可以帮助减少数据库查询次数,从而减轻服务器负担并加快页面加载速度。然而,缓存与数据库一致性是分布式系统中常见的问题,因为缓存和数据库之间可能存在数据不一致的情况。为了解决这个…

CyberLink的摄像头应用程序YouCam 10.1版本在win10系统的下载与安装配置教程

目录 前言一、YouCam安装二、使用配置总结 前言 YouCam是由CyberLink公司开发的一款实用的摄像头应用程序,它集成了多种实时视频特效、背景虚化、美颜、屏幕录制等功能。 通过使用该软件内置的相机特效,用户可以将视频聊天或自拍照片变得更加精彩和有趣…

oracle表空间、用户、表的关系和创建

目录 一、表空间 二、用户 (1)Oracle和mysql、sqlserver的区别 (2)创建用户 (3)给用户授权 三、表 (1)创建表 (2)用图像化软件添加表约束 1.主键约束…

TikTok正测试名为“Tako”的AI聊天机器人;武汉大学宣布推出CheeseChat

🚀 近日安徽安庆一起利用AI换脸技术的电信诈骗案件 近日安徽安庆一起利用AI换脸技术的电信诈骗案件,3名涉案人员被抓获并返还被骗款132万元。 此前也有多起利用AI换脸技术进行的电信诈骗案件,甚至还出现在明星直播带货中。 专家提示&#…

ChatGPT无限可能性:自然语言生成的奥秘

💗wei_shuo的个人主页 💫wei_shuo的学习社区 🌐Hello World ! ChatGPT无限可能性:自然语言生成的奥秘 数字化时代:跨越语言和文化障碍 冰岛是北大西洋中部的一个岛国,拥有充满活力的科技产业和…

网络编程初识

如果这篇有没接触过的知识点,请转到网络编程先导知识_小梁今天敲代码了吗的博客-CSDN博客 目录 IPv4和IPv6的概念: 子网掩码 默认网关 ping命令 端口 OSI网络分层模型 TCP/IP四层模型 字节序转换函数 IP地址转换 上一篇介绍了网络编程的先导知…

chatgpt赋能python:Python动态实时轨迹绘图:让数据可视化更生动

Python 动态实时轨迹绘图:让数据可视化更生动 数据可视化是现代数据分析中不可或缺的一部分。在Python语言中,有许多工具和库可以帮助我们将数据转化为可视化的图表。然而,有些情况下,静态图表难以准确有效地展现数据的变化趋势和…

chatgpt赋能python:Python动态Import:优化你的编程体验

Python 动态 Import:优化你的编程体验 在 Python 中, Import 是一个非常常见的操作。它允许你从其他模块中引入需要的函数或者变量,从而避免在不同模块中重复编写代码。在大型项目中, Import 操作可能会变得很混乱,导…

Java内存管理:垃圾回收算法和内存分配的原理和优化

章节一:引言 在当今的软件开发领域,Java是一门广泛应用的编程语言。Java虚拟机(JVM)负责管理Java应用程序的内存,并通过垃圾回收算法和内存分配策略来优化内存使用。本文将详细介绍Java内存管理的原理、垃圾回收算法的…

【熬夜送书 | 第一期】Java生日快乐,不负代码不负君,面向对象面向卿

文章目录 前言一、java是什么?二、好书推荐《Java核心技术》《Java编程思想》Effective Java 中文版(原书第3版)Java语言程序设计基础篇进阶篇(原书第12版)Java并发编程实战软件架构实践(原书第4版&#xf…