直接上代码:
import Foundation
import RxSwift
import RxCocoa
class LoopPlayer: NSObject {
enum LoopAudioState {
case none
case playing
case pause
}
static let shared = LoopPlayer()
private let playerQueue = [AVPlayer(), AVPlayer()]
private var timeObserverToken: Any?
private var crossFadeDuration: Double = 5.0
private var currentPlayer: AVPlayer {
return playingCopy ? playerQueue.last! : playerQueue.first!
}
private var playingCopy: Bool = false
private var inForeground: Bool = true
let playingRelay = BehaviorRelay<LoopAudioState>(value: .none)
var previousState: LoopAudioState = .none
private override init() {
super.init()
let session = AVAudioSession.sharedInstance()
try? session.setActive(false)
config()
bindModel()
}
private func bindModel() {
NotificationCenter.default.rx
.notification(UIApplication.willResignActiveNotification, object: nil)
.bind { [weak self] _ in
self?.inForeground = false
guard let self = self else { return }
self.handleOtherInterruption()
}.disposed(by: rx.disposeBag)
NotificationCenter.default.rx
.notification(UIApplication.didBecomeActiveNotification, object: nil)
.bind { [weak self] _ in
self?.inForeground = true
guard let self = self else { return }
self.handleOtherRestore()
}.disposed(by: rx.disposeBag)
NotificationCenter.default.rx
.notification(AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
.bind { [weak self] notification in
guard let self = self,
let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
switch type {
case .began:
self.handleOtherInterruption()
case .ended:
self.handleOtherRestore()
default: break
}
}.disposed(by: rx.disposeBag)
NotificationCenter.default.rx
.notification(AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance())
.bind { [weak self] notification in
guard let self = self,
let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue),
reason == .oldDeviceUnavailable else { return }
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription,
let previousOutput: AVAudioSessionPortDescription = previousRoute.outputs.first {
let portType: AVAudioSession.Port = previousOutput.portType
if portType == .headphones {
self.pause()
}
}
}.disposed(by: rx.disposeBag)
}
func handleOtherInterruption() {
if playingRelay.value == .playing {
previousState = .playing
pause()
}
}
func handleOtherRestore() {
if previousState == .playing && !BibleAudioPlayer.shared.isPlaying && inForeground {
previousState = .none
play()
}
}
private func config() {
removePeriodicTimeObserver(for: currentPlayer)
var resourceUrl: URL = Bundle.main.url(forResource: "Loop_Music", withExtension: "mp3")
guard let url = resourceUrl else { return }
currentPlayer.replaceCurrentItem(with: AVPlayerItem(url: url))
if let currentItem = currentPlayer.currentItem {
let copy = AVPlayerItem(asset: currentItem.asset)
playerQueue.last?.replaceCurrentItem(with: copy)
}
addPeriodicTimeObserver(for: currentPlayer)
}
func play() {
currentPlayer.play()
playingRelay.accept(.playing)
}
func pause() {
currentPlayer.pause()
playingRelay.accept(.pause)
}
private func addPeriodicTimeObserver(for player: AVPlayer) {
timeObserverToken = currentPlayer.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: .main) { [weak self] (currentTime) in
if let currentItem = self?.currentPlayer.currentItem, currentItem.status == .readyToPlay, let crossFadeDuration = self?.crossFadeDuration {
let totalDuration = currentItem.asset.duration
if CMTimeCompare(currentTime, totalDuration - CMTimeMakeWithSeconds(crossFadeDuration, preferredTimescale: CMTimeScale(NSEC_PER_SEC))) > 0 {
self?.handleCrossFade()
}
}
}
}
private func handleCrossFade() {
removePeriodicTimeObserver(for: currentPlayer)
playingCopy = !playingCopy
addPeriodicTimeObserver(for: currentPlayer)
currentPlayer.seek(to: .zero)
currentPlayer.play()
}
private func removePeriodicTimeObserver(for player: AVPlayer) {
if let timeObserver = timeObserverToken {
player.removeTimeObserver(timeObserver)
timeObserverToken = nil
}
}
}
播放或暂停,可以通过 play() 或 pause() 方法。
同时外部可以通过:
LoopPlayer.shared
.playingRelay
.asObservable()
.bind { state in
// refresh button state
}.disposed(by: rx.disposeBag)
来刷新按钮的状态