iOS AVPlayer 循环播放音频

2,949 阅读1分钟

直接上代码:

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)

来刷新按钮的状态