From f55a776874d4d6c68c1fa7305620c95ba1e61ed2 Mon Sep 17 00:00:00 2001 From: Fin Date: Fri, 17 Jan 2025 11:13:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=BC=E5=AE=B9=20call=3D1=20=E5=92=8C=20lev?= =?UTF-8?q?el=3Dcritical=20close:=20#256?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Bark/AppDelegate.swift | 13 - Common/SharedDefines.swift | 1 + .../Processor/CallProcessor.swift | 222 +++++++++--------- .../Processor/LevelProcessor.swift | 38 +-- 4 files changed, 132 insertions(+), 142 deletions(-) diff --git a/Bark/AppDelegate.swift b/Bark/AppDelegate.swift index 71323fb..e62a4e2 100644 --- a/Bark/AppDelegate.swift +++ b/Bark/AppDelegate.swift @@ -100,9 +100,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { - if UIApplication.shared.applicationState == .active { - stopCallNotificationProcessor() - } return .alert } @@ -199,11 +196,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD UIApplication.shared.applicationIconBadgeNumber = -1 } - func applicationDidBecomeActive(_ application: UIApplication) { - // 如果有响铃通知,则关闭响铃 - stopCallNotificationProcessor() - } - func applicationWillTerminate(_ application: UIApplication) { // 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 } - - /// 停止响铃 - func stopCallNotificationProcessor() { - CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), CFNotificationName(kStopCallProcessorKey as CFString), nil, nil, true) - } } diff --git a/Common/SharedDefines.swift b/Common/SharedDefines.swift index 92dc2b5..e1ef61f 100644 --- a/Common/SharedDefines.swift +++ b/Common/SharedDefines.swift @@ -10,3 +10,4 @@ import Foundation let kStopCallProcessorKey = "stopCallProcessorNotification" +let kBarkSoundPrefix = "bark.sounds.30s" diff --git a/NotificationServiceExtension/Processor/CallProcessor.swift b/NotificationServiceExtension/Processor/CallProcessor.swift index 8fd5466..a92e85c 100644 --- a/NotificationServiceExtension/Processor/CallProcessor.swift +++ b/NotificationServiceExtension/Processor/CallProcessor.swift @@ -7,82 +7,31 @@ // import AudioToolbox +import AVFAudio import Foundation class CallProcessor: NotificationContentProcessor { - /// 循环播放的铃声 - var soundID: SystemSoundID = 0 - /// 播放完毕后,返回的 content - var content: UNMutableNotificationContent? = nil - /// 是否需要停止播放,由主APP发出停止通知赋值 - var needsStop = false + /// 铃声文件夹,扩展访问不到主APP中的铃声,需要先共享铃声文件 + let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")?.appendingPathComponent("Library/Sounds") func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent { guard let call = bestAttemptContent.userInfo["call"] as? String, call == "1" else { return bestAttemptContent } - self.content = bestAttemptContent - - self.registerObserver() - 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() - } - } + // 延长铃声到30s + return self.processNotificationSound(content: bestAttemptContent) } +} - /// 铃声播放结束时的回调 - var startAudioWorkCompletion: (() -> Void)? = nil - /// 播放铃声 - private func startAudioWork(completion: @escaping () -> Void) { - guard let content else { - completion() - return - } - self.startAudioWorkCompletion = completion - +// MARK: - 铃声 + +extension CallProcessor { + // 将通知铃声延长到30s,并用30s的长铃声替换掉原铃声 + func processNotificationSound(content: UNMutableNotificationContent) -> UNMutableNotificationContent { let sound = ((content.userInfo["aps"] as? [String: Any])?["sound"] as? String)?.split(separator: ".") let soundName: 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) soundType = String(last) } else { @@ -90,65 +39,108 @@ class CallProcessor: NotificationContentProcessor { soundType = "caf" } - // 先找自定义上传的铃声,再找内置铃声 - guard let audioPath = getSoundInCustomSoundsDirectory(soundName: "\(soundName).\(soundType)") ?? - Bundle.main.path(forResource: soundName, ofType: soundType) - else { - completion() - return - } - - let fileUrl = URL(string: audioPath) - // 创建响铃任务 - 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 + if let longSoundUrl = getLongSound(soundName: soundName, soundType: soundType) { + if let level = content.userInfo["level"] as? String, level == "critical" { + LevelProcessor.setCriticalSound(content: content, soundName: longSoundUrl.lastPathComponent) + } else { + content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: longSoundUrl.lastPathComponent)) } - // 音频文件一次播放完成,再次播放 - AudioServicesPlayAlertSound(sound) - }, selfPointer) - } - - /// 停止播放 - private func stopAudioWork() { - AudioServicesRemoveSystemSoundCompletion(soundID) - AudioServicesDisposeSystemSoundID(soundID) + } + return content } - /// 注册停止通知 - 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.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 { + func getLongSound(soundName: String, soundType: String) -> URL? { + guard let soundsDirectoryUrl else { return nil } - let path = soundsDirectoryUrl.appendingPathComponent(soundName).path - if FileManager.default.fileExists(atPath: path) { - return path + // 创建铃声文件夹 + if !FileManager.default.fileExists(atPath: soundsDirectoryUrl.path) { + try? FileManager.default.createDirectory(atPath: soundsDirectoryUrl.path, withIntermediateDirectories: true, attributes: nil) } - return 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 + } + + // 将原始铃声处理成30s的长铃声,并缓存起来 + return mergeCAFFilesToDuration(inputFile: URL(fileURLWithPath: path)) } - deinit { - let observer = Unmanaged.passUnretained(self).toOpaque() - 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...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 + } } } diff --git a/NotificationServiceExtension/Processor/LevelProcessor.swift b/NotificationServiceExtension/Processor/LevelProcessor.swift index 271cee7..63a0b59 100644 --- a/NotificationServiceExtension/Processor/LevelProcessor.swift +++ b/NotificationServiceExtension/Processor/LevelProcessor.swift @@ -15,20 +15,9 @@ class LevelProcessor: NotificationContentProcessor { return bestAttemptContent } - // 重要警告 - if level == "critical" { - // 默认音量 - 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) - } + if let level = bestAttemptContent.userInfo["level"] as? String, level == "critical" { + // 设置重要警告音效 + LevelProcessor.setCriticalSound(content: 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 { /// 是否是重要警告 var isCritical: Bool {