mirror of
https://github.com/Finb/Bark.git
synced 2025-12-08 21:36:01 +00:00
parent
9ca2577ad8
commit
f55a776874
@ -100,9 +100,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
}
|
}
|
||||||
|
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
|
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
|
||||||
if UIApplication.shared.applicationState == .active {
|
|
||||||
stopCallNotificationProcessor()
|
|
||||||
}
|
|
||||||
return .alert
|
return .alert
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,11 +196,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
UIApplication.shared.applicationIconBadgeNumber = -1
|
UIApplication.shared.applicationIconBadgeNumber = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
|
||||||
// 如果有响铃通知,则关闭响铃
|
|
||||||
stopCallNotificationProcessor()
|
|
||||||
}
|
|
||||||
|
|
||||||
func applicationWillTerminate(_ application: UIApplication) {
|
func applicationWillTerminate(_ application: UIApplication) {
|
||||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||||
}
|
}
|
||||||
@ -226,9 +218,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 停止响铃
|
|
||||||
func stopCallNotificationProcessor() {
|
|
||||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), CFNotificationName(kStopCallProcessorKey as CFString), nil, nil, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,3 +10,4 @@ import Foundation
|
|||||||
|
|
||||||
|
|
||||||
let kStopCallProcessorKey = "stopCallProcessorNotification"
|
let kStopCallProcessorKey = "stopCallProcessorNotification"
|
||||||
|
let kBarkSoundPrefix = "bark.sounds.30s"
|
||||||
|
|||||||
@ -7,82 +7,31 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import AudioToolbox
|
import AudioToolbox
|
||||||
|
import AVFAudio
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class CallProcessor: NotificationContentProcessor {
|
class CallProcessor: NotificationContentProcessor {
|
||||||
/// 循环播放的铃声
|
/// 铃声文件夹,扩展访问不到主APP中的铃声,需要先共享铃声文件
|
||||||
var soundID: SystemSoundID = 0
|
let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")?.appendingPathComponent("Library/Sounds")
|
||||||
/// 播放完毕后,返回的 content
|
|
||||||
var content: UNMutableNotificationContent? = nil
|
|
||||||
/// 是否需要停止播放,由主APP发出停止通知赋值
|
|
||||||
var needsStop = false
|
|
||||||
|
|
||||||
func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
|
func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
|
||||||
guard let call = bestAttemptContent.userInfo["call"] as? String, call == "1" else {
|
guard let call = bestAttemptContent.userInfo["call"] as? String, call == "1" else {
|
||||||
return bestAttemptContent
|
return bestAttemptContent
|
||||||
}
|
}
|
||||||
self.content = bestAttemptContent
|
// 延长铃声到30s
|
||||||
|
return self.processNotificationSound(content: bestAttemptContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.registerObserver()
|
// MARK: - 铃声
|
||||||
self.sendLocalNotification(identifier: identifier, content: bestAttemptContent)
|
|
||||||
self.cancelRemoteNotification(content: bestAttemptContent)
|
|
||||||
await startAudioWork()
|
|
||||||
return bestAttemptContent
|
|
||||||
}
|
|
||||||
|
|
||||||
func serviceExtensionTimeWillExpire(contentHandler: (UNNotificationContent) -> Void) {
|
|
||||||
stopAudioWork()
|
|
||||||
if let content {
|
|
||||||
contentHandler(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成一个本地推送
|
|
||||||
private func sendLocalNotification(identifier: String, content: UNMutableNotificationContent) {
|
|
||||||
// 推送id和推送的内容都使用远程APNS的
|
|
||||||
guard let content = content.mutableCopy() as? UNMutableNotificationContent else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !content.isCritical { // 重要警告的声音可以无视静音模式,所以别把这特性给弄没了
|
|
||||||
content.sound = nil
|
|
||||||
}
|
|
||||||
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
|
|
||||||
UNUserNotificationCenter.current().add(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 响铃结束时取消显示远程推送,因为已经用本地推送显示了一遍
|
|
||||||
private func cancelRemoteNotification(content: UNMutableNotificationContent) {
|
|
||||||
// 远程推送在响铃结束后静默不显示
|
|
||||||
// 至于iOS15以下的设备,因不支持这个特性会在响铃结束后再展示一次
|
|
||||||
// 如果设置了 level 参数,就还是以 level 参数为准不做修改
|
|
||||||
if #available(iOSApplicationExtension 15.0, *), self.content?.userInfo["level"] == nil {
|
|
||||||
self.content?.interruptionLevel = .passive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始播放铃声,startAudioWork(completion:) 方法的异步包装
|
|
||||||
private func startAudioWork() async {
|
|
||||||
return await withCheckedContinuation { continuation in
|
|
||||||
self.startAudioWork {
|
|
||||||
continuation.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 铃声播放结束时的回调
|
|
||||||
var startAudioWorkCompletion: (() -> Void)? = nil
|
|
||||||
/// 播放铃声
|
|
||||||
private func startAudioWork(completion: @escaping () -> Void) {
|
|
||||||
guard let content else {
|
|
||||||
completion()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.startAudioWorkCompletion = completion
|
|
||||||
|
|
||||||
|
extension CallProcessor {
|
||||||
|
// 将通知铃声延长到30s,并用30s的长铃声替换掉原铃声
|
||||||
|
func processNotificationSound(content: UNMutableNotificationContent) -> UNMutableNotificationContent {
|
||||||
let sound = ((content.userInfo["aps"] as? [String: Any])?["sound"] as? String)?.split(separator: ".")
|
let sound = ((content.userInfo["aps"] as? [String: Any])?["sound"] as? String)?.split(separator: ".")
|
||||||
let soundName: String
|
let soundName: String
|
||||||
let soundType: String
|
let soundType: String
|
||||||
if sound?.count == 2, let first = sound?.first, let last = sound?.last {
|
if sound?.count == 2, let first = sound?.first, let last = sound?.last, last == "caf" {
|
||||||
soundName = String(first)
|
soundName = String(first)
|
||||||
soundType = String(last)
|
soundType = String(last)
|
||||||
} else {
|
} else {
|
||||||
@ -90,65 +39,108 @@ class CallProcessor: NotificationContentProcessor {
|
|||||||
soundType = "caf"
|
soundType = "caf"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先找自定义上传的铃声,再找内置铃声
|
if let longSoundUrl = getLongSound(soundName: soundName, soundType: soundType) {
|
||||||
guard let audioPath = getSoundInCustomSoundsDirectory(soundName: "\(soundName).\(soundType)") ??
|
if let level = content.userInfo["level"] as? String, level == "critical" {
|
||||||
Bundle.main.path(forResource: soundName, ofType: soundType)
|
LevelProcessor.setCriticalSound(content: content, soundName: longSoundUrl.lastPathComponent)
|
||||||
else {
|
} else {
|
||||||
completion()
|
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: longSoundUrl.lastPathComponent))
|
||||||
return
|
}
|
||||||
|
}
|
||||||
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileUrl = URL(string: audioPath)
|
func getLongSound(soundName: String, soundType: String) -> URL? {
|
||||||
// 创建响铃任务
|
guard let soundsDirectoryUrl else {
|
||||||
AudioServicesCreateSystemSoundID(fileUrl! as CFURL, &soundID)
|
|
||||||
// 播放震动、响铃
|
|
||||||
AudioServicesPlayAlertSound(soundID)
|
|
||||||
// 监听响铃完成状态
|
|
||||||
let selfPointer = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
|
|
||||||
AudioServicesAddSystemSoundCompletion(soundID, nil, nil, { sound, clientData in
|
|
||||||
guard let pointer = clientData else { return }
|
|
||||||
let processor = unsafeBitCast(pointer, to: CallProcessor.self)
|
|
||||||
if processor.needsStop {
|
|
||||||
processor.startAudioWorkCompletion?()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 音频文件一次播放完成,再次播放
|
|
||||||
AudioServicesPlayAlertSound(sound)
|
|
||||||
}, selfPointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 停止播放
|
|
||||||
private func stopAudioWork() {
|
|
||||||
AudioServicesRemoveSystemSoundCompletion(soundID)
|
|
||||||
AudioServicesDisposeSystemSoundID(soundID)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 注册停止通知
|
|
||||||
func registerObserver() {
|
|
||||||
let notification = CFNotificationCenterGetDarwinNotifyCenter()
|
|
||||||
let observer = Unmanaged.passUnretained(self).toOpaque()
|
|
||||||
CFNotificationCenterAddObserver(notification, observer, { _, pointer, _, _, _ in
|
|
||||||
guard let observer = pointer else { return }
|
|
||||||
let processor = Unmanaged<CallProcessor>.fromOpaque(observer).takeUnretainedValue()
|
|
||||||
processor.needsStop = true
|
|
||||||
}, kStopCallProcessorKey as CFString, nil, .deliverImmediately)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSoundInCustomSoundsDirectory(soundName: String) -> String? {
|
|
||||||
// 扩展访问不到主APP中的铃声,需要先共享铃声文件,再实现自定义铃声响铃
|
|
||||||
guard let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")?.appendingPathComponent("Sounds") else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let path = soundsDirectoryUrl.appendingPathComponent(soundName).path
|
// 创建铃声文件夹
|
||||||
if FileManager.default.fileExists(atPath: path) {
|
if !FileManager.default.fileExists(atPath: soundsDirectoryUrl.path) {
|
||||||
return path
|
try? FileManager.default.createDirectory(atPath: soundsDirectoryUrl.path, withIntermediateDirectories: true, attributes: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 已经存在处理过的长铃声,则直接返回
|
||||||
|
let longSoundName = "\(kBarkSoundPrefix).\(soundName).\(soundType)"
|
||||||
|
let longSoundPath = soundsDirectoryUrl.appendingPathComponent(longSoundName)
|
||||||
|
if FileManager.default.fileExists(atPath: longSoundPath.path) {
|
||||||
|
return longSoundPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原始铃声路径
|
||||||
|
var path: String = soundsDirectoryUrl.appendingPathComponent("\(soundName).\(soundType)").path
|
||||||
|
if !FileManager.default.fileExists(atPath: path) {
|
||||||
|
// 不存在自定义的铃声,就用内置的铃声
|
||||||
|
path = Bundle.main.path(forResource: soundName, ofType: soundType) ?? ""
|
||||||
|
}
|
||||||
|
guard !path.isEmpty else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
// 将原始铃声处理成30s的长铃声,并缓存起来
|
||||||
let observer = Unmanaged.passUnretained(self).toOpaque()
|
return mergeCAFFilesToDuration(inputFile: URL(fileURLWithPath: path))
|
||||||
let name = CFNotificationName(kStopCallProcessorKey as CFString)
|
}
|
||||||
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, name, nil)
|
|
||||||
|
/// - Author: @uuneo
|
||||||
|
/// - Description:将输入的音频文件重复为指定时长的音频文件
|
||||||
|
/// - Parameters:
|
||||||
|
/// - inputFile: 原始铃声文件路径
|
||||||
|
/// - targetDuration: 重复的时长
|
||||||
|
/// - Returns: 长铃声文件路径
|
||||||
|
func mergeCAFFilesToDuration(inputFile: URL, targetDuration: TimeInterval = 30) -> URL? {
|
||||||
|
guard let soundsDirectoryUrl else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let longSoundName = "\(kBarkSoundPrefix).\(inputFile.lastPathComponent)"
|
||||||
|
let longSoundPath = soundsDirectoryUrl.appendingPathComponent(longSoundName)
|
||||||
|
|
||||||
|
do {
|
||||||
|
// 打开输入文件并获取音频格式
|
||||||
|
let audioFile = try AVAudioFile(forReading: inputFile)
|
||||||
|
let audioFormat = audioFile.processingFormat
|
||||||
|
let sampleRate = audioFormat.sampleRate
|
||||||
|
|
||||||
|
// 计算目标帧数
|
||||||
|
let targetFrames = AVAudioFramePosition(targetDuration * sampleRate)
|
||||||
|
var currentFrames: AVAudioFramePosition = 0
|
||||||
|
// 创建输出音频文件
|
||||||
|
let outputAudioFile = try AVAudioFile(forWriting: longSoundPath, settings: audioFormat.settings)
|
||||||
|
|
||||||
|
// 循环读取文件数据,拼接到目标时长
|
||||||
|
while currentFrames < targetFrames {
|
||||||
|
// 每次读取整个文件的音频数据
|
||||||
|
guard let buffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: AVAudioFrameCount(audioFile.length)) else {
|
||||||
|
// 出错了就终止,避免死循环
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
try audioFile.read(into: buffer)
|
||||||
|
|
||||||
|
// 计算剩余所需帧数
|
||||||
|
let remainingFrames = targetFrames - currentFrames
|
||||||
|
if AVAudioFramePosition(buffer.frameLength) > remainingFrames {
|
||||||
|
// 如果当前缓冲区帧数超出所需,截取剩余部分
|
||||||
|
let truncatedBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, frameCapacity: AVAudioFrameCount(remainingFrames))!
|
||||||
|
let channelCount = Int(buffer.format.channelCount)
|
||||||
|
for channel in 0..<channelCount {
|
||||||
|
let sourcePointer = buffer.floatChannelData![channel]
|
||||||
|
let destinationPointer = truncatedBuffer.floatChannelData![channel]
|
||||||
|
memcpy(destinationPointer, sourcePointer, Int(remainingFrames) * MemoryLayout<Float>.size)
|
||||||
|
}
|
||||||
|
truncatedBuffer.frameLength = AVAudioFrameCount(remainingFrames)
|
||||||
|
try outputAudioFile.write(from: truncatedBuffer)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// 否则写入整个缓冲区
|
||||||
|
try outputAudioFile.write(from: buffer)
|
||||||
|
currentFrames += AVAudioFramePosition(buffer.frameLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置输入文件读取位置
|
||||||
|
audioFile.framePosition = 0
|
||||||
|
}
|
||||||
|
return longSoundPath
|
||||||
|
} catch {
|
||||||
|
print("Error processing CAF file: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,20 +15,9 @@ class LevelProcessor: NotificationContentProcessor {
|
|||||||
return bestAttemptContent
|
return bestAttemptContent
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重要警告
|
if let level = bestAttemptContent.userInfo["level"] as? String, level == "critical" {
|
||||||
if level == "critical" {
|
// 设置重要警告音效
|
||||||
// 默认音量
|
LevelProcessor.setCriticalSound(content: bestAttemptContent)
|
||||||
var audioVolume: Float = 0.5
|
|
||||||
// 指定音量,取值范围是 0 - 10 , 会转换成 0.0 - 1.0
|
|
||||||
if let volume = bestAttemptContent.userInfo["volume"] as? String, let volume = Float(volume) {
|
|
||||||
audioVolume = max(0.0, min(1, volume / 10.0))
|
|
||||||
}
|
|
||||||
// 设置重要警告 sound
|
|
||||||
if let sound = bestAttemptContent.soundName {
|
|
||||||
bestAttemptContent.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(rawValue: sound), withAudioVolume: audioVolume)
|
|
||||||
} else {
|
|
||||||
bestAttemptContent.sound = UNNotificationSound.defaultCriticalSound(withAudioVolume: audioVolume)
|
|
||||||
}
|
|
||||||
return bestAttemptContent
|
return bestAttemptContent
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +37,27 @@ class LevelProcessor: NotificationContentProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LevelProcessor {
|
||||||
|
class func setCriticalSound(content bestAttemptContent: UNMutableNotificationContent, soundName: String? = nil) {
|
||||||
|
guard let level = bestAttemptContent.userInfo["level"] as? String, level == "critical" else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 默认音量
|
||||||
|
var audioVolume: Float = 0.5
|
||||||
|
// 指定音量,取值范围是 0 - 10 , 会转换成 0.0 - 1.0
|
||||||
|
if let volume = bestAttemptContent.userInfo["volume"] as? String, let volume = Float(volume) {
|
||||||
|
audioVolume = max(0.0, min(1, volume / 10.0))
|
||||||
|
}
|
||||||
|
// 设置重要警告 sound
|
||||||
|
let sound = soundName ?? bestAttemptContent.soundName
|
||||||
|
if let sound {
|
||||||
|
bestAttemptContent.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(rawValue: sound), withAudioVolume: audioVolume)
|
||||||
|
} else {
|
||||||
|
bestAttemptContent.sound = UNNotificationSound.defaultCriticalSound(withAudioVolume: audioVolume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension UNMutableNotificationContent {
|
extension UNMutableNotificationContent {
|
||||||
/// 是否是重要警告
|
/// 是否是重要警告
|
||||||
var isCritical: Bool {
|
var isCritical: Bool {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user