Использование AVAudioPlayer в фоновом режиме

В своем приложении я добавил возможности фонового звука и фоновой обработки.

Мой код в настоящее время использует AVAudioPlayer для воспроизведения звука. Хотя воспроизведение хорошее, когда приложение находится на переднем плане с заблокированным экраном, звук имеет некоторую статическую дрожь.

Мое приложение написано с использованием SwiftUI и Combine. Кто-нибудь сталкивался с этой проблемой и что бы вы предложили в качестве обходного пути?

Вот метод play:

    /// Play an `AudioFile`
    /// - Parameters:
    ///   - audioFile: an `AudioFile` struct
    ///   - completion: optional completion, default is `nil`
    func play(_ audioFile: AudioFile,
              completion: (() -> Void)? = nil) {
        if audioFile != currentAudioFile {
            resetPublishedValues()
        }
        currentAudioFile = audioFile
        setupCurrentAudioFilePublisher()
        guard let path = Bundle.main.path(forResource: audioFile.filename, ofType: "mp3") else {
            return
        }
        
        let url = URL(fileURLWithPath: path)
        
        // everybody STFU
        stop()
        
        do {
            // make sure the sound is one
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
            // instantiate instance of AVAudioPlayer
            audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer.prepareToPlay()
            // play the sound
            let queue = DispatchQueue(label: "audioPlayer", qos: .userInitiated, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
            
            queue.async {
                self.audioPlayer.play()
            }
            audioPlayer.delegate = self
        } catch {
            // Not much to go wrong, so leaving alone for now, but need to make `throws` if we handle errors
            print(String(format: "play() error: %@", error.localizedDescription))
        }
    }

Это определение класса:

import AVFoundation
import Combine
import Foundation

/// A `Combine`-friendly wrapper for `AVAudioPlayer` which utilizes `Combine` `Publishers` instead of `AVAudioPlayerDelegate`
class CombineAudioPlayer: NSObject, AVAudioPlayerDelegate, ObservableObject {
    static let sharedInstance = CombineAudioPlayer()
    private var audioPlayer = AVAudioPlayer()
    /*
     FIXME: For now, gonna leave this timer on all the time, but need to refine
     down the road because it's going to generate a fuckload of data on the
     current interval.
     */
    // MARK: - Publishers
    private var timer = Timer.publish(every: 0.1,
                                      on: RunLoop.main,
                                      in: RunLoop.Mode.default).autoconnect()
    @Published public var currentAudioFile: AudioFile?
    public var isPlaying = CurrentValueSubject<Bool, Never>(false)
    public var currentTime = PassthroughSubject<TimeInterval, Never>()
    public var didFinishPlayingCurrentAudioFile = PassthroughSubject<AudioFile, Never>()
    
    private var cancellables: Set<AnyCancellable> = []
    
    // MARK: - Initializer
    private override init() {
        super.init()
        // set it up with a blank audio file
        setupPublishers()
        audioPlayer.setVolume(1.0, fadeDuration: 0)
    }
    
    // MARK: - Publisher Methods
    private func setupPublishers() {
        timer.sink(receiveCompletion: { completion in
            // TODO: figure out if I need anything here
            // Don't think so, as this will always be initialized
        },
        receiveValue: { value in
            self.isPlaying.send(self.audioPlayer.isPlaying)
            self.currentTime.send(self.currentTimeValue)
        })
        .store(in: &cancellables)
        
        didFinishPlayingCurrentAudioFile.sink(receiveCompletion: { _ in
            
        },
        receiveValue: { audioFile in
            self.resetPublishedValues()
        })
        .store(in: &cancellables)
    }
    
    private func setupCurrentAudioFilePublisher() {
        self.isPlaying.send(false)
        self.currentTime.send(0.0)
    }
    
    // MARK: - Playback Methods
    
    /// Play an `AudioFile`
    /// - Parameters:
    ///   - audioFile: an `AudioFile` struct
    ///   - completion: optional completion, default is `nil`
    func play(_ audioFile: AudioFile,
              completion: (() -> Void)? = nil) {
        if audioFile != currentAudioFile {
            resetPublishedValues()
        }
        currentAudioFile = audioFile
        setupCurrentAudioFilePublisher()
        guard let path = Bundle.main.path(forResource: audioFile.filename, ofType: "mp3") else {
            return
        }
        
        let url = URL(fileURLWithPath: path)
        
        // everybody STFU
        stop()
        
        do {
            // make sure the sound is one
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
            // instantiate instance of AVAudioPlayer
            audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer.prepareToPlay()
            // play the sound
            let queue = DispatchQueue(label: "audioPlayer", qos: .userInitiated, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
            
            queue.async {
                self.audioPlayer.play()
            }
            audioPlayer.delegate = self
        } catch {
            // Need to make `throws` if we handle errors
            print(String(format: "play error: %@", error.localizedDescription))
        }
    }
    
    func stop() {
        audioPlayer.stop()
        resetPublishedValues()
    }
    
    private func resetPublishedValues() {
        isPlaying.send(false)
        currentTime.send(0.0)
    }
    
    private var currentTimeValue: TimeInterval {
        audioPlayer.currentTime
    }
    
    /// Use the `Publisher` to determine when a sound is done playing.
    /// - Parameters:
    ///   - player: an `AVAudioPlayer` instance
    ///   - flag: a `Bool` indicating whether the sound was successfully played
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        if let currentAudioFile = currentAudioFile {
            didFinishPlayingCurrentAudioFile.send(currentAudioFile)
        }
        resetPublishedValues()
    }
}
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
39
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Итак, я понял это. У меня было несколько проблем, с которыми пришлось столкнуться. По сути, мне нужно было воспроизводить аудиофайлы в определенное время, когда приложение было в фоновом режиме. Хотя это отлично работает, если звук воспроизводится, когда приложение активно, AVAudioPlayer не позволяет мне запускать что-либо после того, как приложение находится в фоновом режиме, если воспроизведение звука еще не выполняется.

Я не буду вдаваться в подробности, но в итоге я использовал AVQueuePlayer, который инициализировал как часть моего класса CombineAudioPlayer.

  1. Обновите AppDelegate.swift

Я добавил следующие строки в метод AppDelegatedidFinishLaunchingWithOptions.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    do {
        try AVAudioSession.sharedInstance().setCategory(.playback,
                                                        mode: .default)
        try AVAudioSession.sharedInstance().setActive(true)
    } catch {
        print(String(format: "didFinishLaunchingWithOptions error: %@", error.localizedDescription))
    }
    
    return true
}
  1. В моем классе AudioPlayer я объявил AVQueuePlayer. Очень важно, чтобы это было инициализировано классом AudioPlayer, а не внутри метода.

Мой ViewModel подписывается на уведомление, которое ожидает, что приложение собирается выйти из режима переднего плана, он быстро создает список воспроизведения и запускает его непосредственно перед выходом из приложения.

NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification).sink { _ in
    self.playBackground()
}
.store(in: &cancellables)
private var bgAudioPlayer = AVQueuePlayer()

Затем я создал метод для создания списка воспроизведения для AVQueuePlayer, который выглядит примерно так:

func backgroundPlaylist(from audioFiles: [AudioFile]) -> [AVPlayerItem] {
    guard let firstFile = audioFiles.first else {
        // return empty array, don't wanna unwrap optionals
        return []
    }
    // declare a silence file
    let silence = AudioFile(displayName: "Silence",
                            filename: "1sec-silence")
    // start at zero
    var currentSeconds: TimeInterval = 0
    
    var playlist: [AVPlayerItem] = []
    
    // while currentSeconds is less than firstFile's fire time...
    while currentSeconds < firstFile.secondsInFuture {
        // add 1 second of silence to the playlist
        playlist.append(AVPlayerItem(url: silence.url!))
        // increment currentSeconds and we loop over again, adding more silence
        currentSeconds += 1
    }
    
    // once we're done, add the file we want to play
    playlist.append(AVPlayerItem(url: audioFiles.first!.url!))
                    
    return playlist
}

Наконец, звук воспроизводится следующим образом:

func playInBackground() {
    do {
        // make sure the sound is one
        try AVAudioSession.sharedInstance().setCategory(.playback,
                                                        mode: .default,
                                                        policy: .longFormAudio,
                                                        options: [])
        try AVAudioSession.sharedInstance().setActive(true)
        let playlist = backgroundPlaylist(from: backgroundPlaylist)
        bgAudioPlayer = AVQueuePlayer(items: playlist)
        bgAudioPlayer.play()
    } catch {
        // Not much to mess up, so leaving alone for now, but need to make
        // `throws` if we handle errors
        print(String(format: "playInBackground error: %@",
                        error.localizedDescription))
    }
}

Другие вопросы по теме