CallProcessor.swift 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. //
  2. // CallProcessor.swift
  3. // NotificationServiceExtension
  4. //
  5. // Created by huangfeng on 2024/6/6.
  6. // Copyright © 2024 Fin. All rights reserved.
  7. //
  8. import AudioToolbox
  9. import Foundation
  10. class CallProcessor: NotificationContentProcessor {
  11. /// 循环播放的铃声
  12. var soundID: SystemSoundID = 0
  13. /// 播放完毕后,返回的 content
  14. var content: UNMutableNotificationContent? = nil
  15. /// 是否需要停止播放,由主APP发出停止通知赋值
  16. var needsStop = false
  17. func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
  18. guard let call = bestAttemptContent.userInfo["call"] as? String, call == "1" else {
  19. return bestAttemptContent
  20. }
  21. self.content = bestAttemptContent
  22. self.registerObserver()
  23. self.sendLocalNotification(identifier: identifier, content: bestAttemptContent)
  24. self.cancelRemoteNotification(content: bestAttemptContent)
  25. await startAudioWork()
  26. return bestAttemptContent
  27. }
  28. func serviceExtensionTimeWillExpire(contentHandler: (UNNotificationContent) -> Void) {
  29. stopAudioWork()
  30. if let content {
  31. contentHandler(content)
  32. }
  33. }
  34. /// 生成一个本地推送
  35. private func sendLocalNotification(identifier: String, content: UNMutableNotificationContent) {
  36. // 推送id和推送的内容都使用远程APNS的
  37. guard let content = content.mutableCopy() as? UNMutableNotificationContent else {
  38. return
  39. }
  40. content.sound = nil
  41. let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
  42. UNUserNotificationCenter.current().add(request)
  43. }
  44. /// 响铃结束时取消显示远程推送,因为已经用本地推送显示了一遍
  45. private func cancelRemoteNotification(content: UNMutableNotificationContent) {
  46. // 远程推送在响铃结束后静默不显示
  47. // 至于iOS15以下的设备,因不支持这个特性会在响铃结束后再展示一次, 但会取消声音
  48. // 如果设置了 level 参数,就还是以 level 参数为准不做修改
  49. if #available(iOSApplicationExtension 15.0, *), self.content?.userInfo["level"] == nil {
  50. self.content?.interruptionLevel = .passive
  51. } else {
  52. content.sound = nil
  53. }
  54. }
  55. // 开始播放铃声,startAudioWork(completion:) 方法的异步包装
  56. private func startAudioWork() async {
  57. return await withCheckedContinuation { continuation in
  58. self.startAudioWork {
  59. continuation.resume()
  60. }
  61. }
  62. }
  63. /// 铃声播放结束时的回调
  64. var startAudioWorkCompletion: (() -> Void)? = nil
  65. /// 播放铃声
  66. private func startAudioWork(completion: @escaping () -> Void) {
  67. guard let content else {
  68. completion()
  69. return
  70. }
  71. self.startAudioWorkCompletion = completion
  72. let sound = ((content.userInfo["aps"] as? [String: Any])?["sound"] as? String)?.split(separator: ".")
  73. let soundName: String
  74. let soundType: String
  75. if sound?.count == 2, let first = sound?.first, let last = sound?.last {
  76. soundName = String(first)
  77. soundType = String(last)
  78. } else {
  79. soundName = "multiwayinvitation"
  80. soundType = "caf"
  81. }
  82. // 先找自定义上传的铃声,再找内置铃声
  83. guard let audioPath = getSoundInCustomSoundsDirectory(soundName: "\(soundName).\(soundType)") ??
  84. Bundle.main.path(forResource: soundName, ofType: soundType)
  85. else {
  86. completion()
  87. return
  88. }
  89. let fileUrl = URL(string: audioPath)
  90. // 创建响铃任务
  91. AudioServicesCreateSystemSoundID(fileUrl! as CFURL, &soundID)
  92. // 播放震动、响铃
  93. AudioServicesPlayAlertSound(soundID)
  94. // 监听响铃完成状态
  95. let selfPointer = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
  96. AudioServicesAddSystemSoundCompletion(soundID, nil, nil, { sound, clientData in
  97. guard let pointer = clientData else { return }
  98. let processor = unsafeBitCast(pointer, to: CallProcessor.self)
  99. if processor.needsStop {
  100. processor.startAudioWorkCompletion?()
  101. return
  102. }
  103. // 音频文件一次播放完成,再次播放
  104. AudioServicesPlayAlertSound(sound)
  105. }, selfPointer)
  106. }
  107. /// 停止播放
  108. private func stopAudioWork() {
  109. AudioServicesRemoveSystemSoundCompletion(soundID)
  110. AudioServicesDisposeSystemSoundID(soundID)
  111. }
  112. /// 注册停止通知
  113. func registerObserver() {
  114. let notification = CFNotificationCenterGetDarwinNotifyCenter()
  115. let observer = Unmanaged.passUnretained(self).toOpaque()
  116. CFNotificationCenterAddObserver(notification, observer, { _, pointer, _, _, _ in
  117. guard let observer = pointer else { return }
  118. let processor = Unmanaged<CallProcessor>.fromOpaque(observer).takeUnretainedValue()
  119. processor.needsStop = true
  120. }, kStopCallProcessorKey as CFString, nil, .deliverImmediately)
  121. }
  122. func getSoundInCustomSoundsDirectory(soundName: String) -> String? {
  123. // 扩展访问不到主APP中的铃声,需要先共享铃声文件,再实现自定义铃声响铃
  124. guard let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")?.appendingPathComponent("Sounds") else {
  125. return nil
  126. }
  127. let path = soundsDirectoryUrl.appendingPathComponent(soundName).path
  128. if FileManager.default.fileExists(atPath: path) {
  129. return path
  130. }
  131. return nil
  132. }
  133. deinit {
  134. let observer = Unmanaged.passUnretained(self).toOpaque()
  135. let name = CFNotificationName(kStopCallProcessorKey as CFString)
  136. CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, name, nil)
  137. }
  138. }