Use AVAudioPlayer in OperationQueue

業務需求要提供一些ringtone供用戶選擇而且設置爲來電鈴聲.這樣就會涉及到預覽ringtone.這邊預覽ringtone選擇用AVAudioPlayer去播放.選擇AVAudioPlayer的緣由是AVAudioPlayer可控性比較大,能夠播放,暫停,恢復播放等.使用AudioToolbox提供的api也能播放ringtone當是不能知足操做需求,暫停,恢復,故排除AudioToolbox.

要使用AVAudioPlayer播放ringtone必然會涉及到AVAudioSession.AVAudioSession簡而言之: 音頻會話,主要用來管理音頻設置與硬件交互.關於AVAudioSession`詳細的信息讀者可自行去查.播放一個ringtone的簡化流程代碼大概以下:
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord)

try? AVAudioSession.sharedInstance().setActive(true, options: [])

let audioPlayer = try? AVAudioPlayer(contentsOf: ringtoneUrl)

audioPlayer?.play()
複製代碼
爲了避免卡UI通常會放到子線程中去播放:
DispatchQueue.global().async {
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord)
            
try? AVAudioSession.sharedInstance().setActive(true, options: [])
            
let audioPlayer = try? AVAudioPlayer(contentsOf: ringtoneUrl)
            
audioPlayer?.play()
}

複製代碼
看似一切正常,可是當你頻繁的點擊切換ringtone,變成開始,結束,暫停。這樣會致使播放失敗又有多線程競爭音頻資源的問題,而且在iOS10.x上會crash,由於音頻資源不可能切換太快.而且快速切換也會開啓多個子線程,這是不必的.通常來講一個設計合理的app同一時刻只會有一個音頻在播放.因此咱們只須要一個線程專門控制音頻播放便可,這裏推薦Queue來控制,由於Queue api比較友好,而且比較可控.
//做爲一個對象的變量比較好,由於會屢次點擊播放,這樣用同一個queue管理.
lazy var ringToneQueue: OperationQueue = {
    let ringToneQueue = OperationQueue()
    ringToneQueue.name = "rc.ringtone.queue"
    ringToneQueue.maxConcurrentOperationCount = 1
    return ringToneQueue
}()

複製代碼
//回收資源
if self.ringToneQueue.operationCount > 0 {
   self.ringToneQueue.cancelAllOperations()
}

self.ringToneQueue.addOperation({
    //播放ringtone
})


複製代碼
根據業務需求我這邊是把包裝AVAudioPlayer成一個單例,不會屢次建立AVAudioPlayer。
internal class AudioPlayerManager : NSObject {

    internal static let shared: AudioPlayerManager

    internal var currentTime: TimeInterval? { get set }

    internal var audioFilePath: String? { get }

    internal var isPlaying: Bool { get }

    internal var delegates: <<error type>>

    override internal init()

    internal func setCategory(category: AVAudioSession.Category, options: AVAudioSession.CategoryOptions, shouldSetPlayBack: Bool = true) throws

    /** * Prepare some configuration for AudioPlayer. * Call this method to configurate AudioPlayer and than call `play(atTime time: TimeInterval? = nil)` to play audio. * */
    internal func configAudioPlayer(with audioFilePath: String, configAudioPlayerClosure: ((Bool, Error?) -> Void)?)

    /** * * Call this method after call `configAudioPlayer(with audioFilePath: String, configAudioPlayerClosure: ((AVAudioPlayer?) -> Void)?)`. * */
    internal func play(category: AVAudioSession.Category = .playAndRecord, options: AVAudioSession.CategoryOptions = [.defaultToSpeaker], portOverride: AVAudioSession.PortOverride = .none) throws

    internal func play(audioFilePath: String, category: AVAudioSession.Category = .playAndRecord, options: AVAudioSession.CategoryOptions = [.defaultToSpeaker], atTime time: TimeInterval? = nil, portOverride: AVAudioSession.PortOverride = .none) throws

    internal func stop(options: AVAudioSession.SetActiveOptions = [.notifyOthersOnDeactivation])

    internal func pause(options: AVAudioSession.SetActiveOptions = [.notifyOthersOnDeactivation])

    internal func resume()

    internal func setAudioSessionActive(isActive: Bool, options: AVAudioSession.SetActiveOptions = [])
}

複製代碼
這樣播放的代碼大概是這樣:
//回收資源
if self.ringToneQueue.operationCount > 0 {
   self.ringToneQueue.cancelAllOperations()
}

self.ringToneQueue.addOperation({
    //播放ringtone
    if !AudioPlayerManager.shared.isPlaying {
       AudioPlayerManager.play(audioFilePath: selectedTonePath)
    } else {
       if let path = AudioPlayerManager.audioFilePath, path == selectedTonePath {
          AudioPlayerManager.stop()
        } else {
          AudioPlayerManager.play(audioFilePath: selectedTonePath)
        }
    }
})

複製代碼
相關文章
相關標籤/搜索