BackgroundTaskAudio.swift 4.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. // LoopFollow
  2. // BackgroundTaskAudio.swift
  3. import AVFoundation
  4. class BackgroundTask {
  5. // MARK: - Vars
  6. var player = AVAudioPlayer()
  7. private var retryCount = 0
  8. private let maxRetries = 3
  9. // MARK: - Methods
  10. func startBackgroundTask() {
  11. NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
  12. NotificationCenter.default.addObserver(self, selector: #selector(interruptedAudio), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
  13. retryCount = 0
  14. playAudio()
  15. }
  16. func stopBackgroundTask() {
  17. NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
  18. player.stop()
  19. LogManager.shared.log(category: .general, message: "Silent audio stopped", isDebug: true)
  20. }
  21. @objc private func interruptedAudio(_ notification: Notification) {
  22. guard notification.name == AVAudioSession.interruptionNotification,
  23. let userInfo = notification.userInfo,
  24. let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
  25. let type = AVAudioSession.InterruptionType(rawValue: typeValue)
  26. else { return }
  27. switch type {
  28. case .began:
  29. LogManager.shared.log(category: .general, message: "[LA] Silent audio session interrupted (began)")
  30. case .ended:
  31. if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
  32. let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
  33. if !options.contains(.shouldResume) {
  34. LogManager.shared.log(category: .general, message: "[LA] Silent audio interruption ended — shouldResume not set, attempting restart anyway")
  35. }
  36. }
  37. LogManager.shared.log(category: .general, message: "[LA] Silent audio interruption ended — scheduling restart in 0.5s")
  38. retryCount = 0
  39. // Brief delay to let the interrupting app (e.g. Clock alarm) fully release the audio
  40. // session before we attempt to reactivate. Without this, setActive(true) races with
  41. // the alarm and fails with AVAudioSession.ErrorCode.cannotInterruptOthers (560557684).
  42. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
  43. self?.playAudio()
  44. }
  45. @unknown default:
  46. break
  47. }
  48. }
  49. private func playAudio() {
  50. let attemptDesc = retryCount == 0 ? "initial attempt" : "retry \(retryCount)/\(maxRetries)"
  51. do {
  52. let bundle = Bundle.main.path(forResource: "blank", ofType: "wav")
  53. let alertSound = URL(fileURLWithPath: bundle!)
  54. try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: .mixWithOthers)
  55. try AVAudioSession.sharedInstance().setActive(true)
  56. try player = AVAudioPlayer(contentsOf: alertSound)
  57. // Play audio forever by setting num of loops to -1
  58. player.numberOfLoops = -1
  59. player.volume = 0.01
  60. player.prepareToPlay()
  61. player.play()
  62. retryCount = 0
  63. LogManager.shared.log(category: .general, message: "Silent audio playing (\(attemptDesc))", isDebug: true)
  64. } catch {
  65. LogManager.shared.log(category: .general, message: "playAudio failed (\(attemptDesc)), error: \(error)")
  66. if retryCount < maxRetries {
  67. retryCount += 1
  68. LogManager.shared.log(category: .general, message: "playAudio scheduling retry \(retryCount)/\(maxRetries) in 2s")
  69. DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
  70. self?.playAudio()
  71. }
  72. } else {
  73. LogManager.shared.log(category: .general, message: "playAudio failed after \(maxRetries) retries — posting BackgroundAudioFailed")
  74. NotificationCenter.default.post(name: .backgroundAudioFailed, object: nil)
  75. }
  76. }
  77. }
  78. }
  79. extension Notification.Name {
  80. static let backgroundAudioFailed = Notification.Name("BackgroundAudioFailed")
  81. }