業(yè)務(wù)需求要提供一些ringtone供用戶選擇并且設(shè)置為來電鈴聲.這樣就會涉及到預(yù)覽ringtone.這邊預(yù)覽ringtone選擇用AVAudioPlayer去播放.選擇AVAudioPlayer的原因是AVAudioPlayer可控性比較大,可以播放,暫停,恢復(fù)播放等.使用AudioToolbox提供的api也能播放ringtone當(dāng)是不能滿足操作需求,暫停,恢復(fù),故排除AudioToolbox.
要使用AVAudioPlayer播放ringtone必然會涉及到AVAudioSession.AVAudioSession簡而言之: 音頻會話,主要用來管理音頻設(shè)置與硬件交互.關(guān)于AVAudioSession`詳細(xì)的信息讀者可自行去查.播放一個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()
}
看似一切正常,但是當(dāng)你頻繁的點擊切換ringtone,變成開始,結(jié)束,暫停。這樣會導(dǎo)致播放失敗又有多線程競爭音頻資源的問題,并且在iOS10.x上會crash,因為音頻資源不可能切換太快.并且快速切換也會開啟多個子線程,這是沒必要的.一般來說一個設(shè)計合理的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
})
根據(jù)業(yè)務(wù)需求我這邊是把包裝AVAudioPlayer成一個單例,不會多次創(chuàng)建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)
}
}
})
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。