AVPlayer真好玩

陳小嬰
12 min readJan 23, 2021

--

沒什麼,就是最近摸AVFoundation的紀錄

寫了那麼久的APP最近才有機會接觸影音播放,但又是一陣喜怒哀樂,所以寫成經驗談跟大家分享。

AVPlayer

play、pause、rate 這幾個屬性方法相對好懂,就不介紹了,底部有很多大大的教學文件參考連結,多閱讀有益身心健康。

a.跳轉播放進度

avPlayer.seek(to: time) { (_) in}

b.監聽當前播放進度:

playTimeObserver = avPlayer.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 2), queue: timeQueue, using: { (_) in    //avPlayer.currentTime().seconds})

監聽完要記得釋放掉。

deinit {    avPlayer.removeTimeObserver(playTimeObserver as Any)}

c.播放狀態

TimeControlStatus屬性僅適用iOS 10以上且沒有停止的狀態,我為了向下相容所以就自訂播放狀態。

enum PlayStatus {   case Default   case Playing   case Pause   case Stop}

d.開啟背景播放權限

let audioSession = AVAudioSession.sharedInstance()do {try audioSession.setActive(true)   try audioSession.setCategory(AVAudioSession.Category.playback)} catch {   NSLog("Failed to set audio session category.")}

RemoteCommand

遠程控制指的是螢幕鎖屏時顯示的音樂播放器畫面,因為也有很多邏輯所以我就另外封包。

// 顯示音樂詳細資料
private
let infoCenter = MPNowPlayingInfoCenter.default()
// 播放事件遠程控制
private
let commandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared()

a.鎖屏顯示當前播放訊息

var nowPlayingInfo = [String: Any]()nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = albumNamenowPlayingInfo[MPMediaItemPropertyTitle] = mediaNamenowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.audio.rawValuenowPlayingInfo[MPNowPlayingInfoCollectionIdentifier] = mediaIdnowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = mediaDurationvar albumImage = UIImage(named: "none") ?? UIImage()// 顯示網路圖片
if
let url = URL(string: albumImagePath), let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
albumImage = image}nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: albumImage.size) { _ in return albumImage }MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo

b.開啟遠程控制事件

// 開啟遠程控制事件
UIApplication.shared.beginReceivingRemoteControlEvents()

c.更新狀態

// 更新鎖屏播放進度
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time
// 更新播放速度
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = rate

d.鎖屏遠程控制事件

為了不要所有監聽和事件都混在一起,所以我定義了一個Rx的監聽物件:commandSubject來處理每個遠程控制指令,然後由監聽的對象執行播放。

commandCenter.playCommand.isEnabled = truecommandCenter.playCommand.addTarget { [weak self] _ in    self?.commandSubject.onNext(.Play)    return .success}

還有其他指令:

commandCenter.pauseCommand.isEnabled = truecommandCenter.nextTrackCommand.isEnabled = truecommandCenter.previousTrackCommand.isEnabled = true

iOS 9.1 以上才有時間選擇滑動條。

commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in    let seconds = (event as?      MPChangePlaybackPositionCommandEvent)?.positionTime ?? 0    let time = CMTime(seconds: seconds, preferredTimescale: 1)    self?.commandSubject.onNext(.Seek(time))return .success}

AVPlayerObserver

實在太多事件要監聽了,我也單獨封包成專門監聽的物件,一樣全部都要在不監聽的時候要釋放掉。

a.監聽播放完成

finishNotification = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil, queue: observerQueue) { [weak self] (_) in}

b. 監聽播放失敗:NSNotification.Name.AVPlayerItemFailedToPlayToEndTime

c.監聽異常中斷:

NSNotification.Name.AVPlayerItemPlaybackStalled

d.監聽其他APP播放音樂造成中斷

AVAudioSession.interruptionNotification

e.監聽電話造成的中斷和中斷原因結束

NotificationCenter.default.addObserver(self, selector: #selector(handleCaptureSessionInterrupted), name: NSNotification.Name.AVCaptureSessionWasInterrupted, object: nil)NotificationCenter.default.addObserver(self, selector: #selector(handleCaptureSessionInterrupted), name: NSNotification.Name.AVCaptureSessionInterruptionEnded, object: nil)

handleCaptureSessionInterrupted 針對不同事件做不同處理。

guard let userInfo = notification.userInfo else { return }guard let interruptionType = userInfo[AVAudioSessionInterruptionTypeKey] as? AVAudioSession.InterruptionType else { return }switch interruptionType {case .began:  NSLog("音頻被中斷")case .ended:  NSLog("中斷原因結束")  guard let options = userInfo[AVAudioSessionInterruptionOptionKey] as? AVAudioSession.InterruptionOptions else { return }  guard options == .shouldResume else { return }  // continue playdefault:   NSLog("interrupted \(interruptionType)")}

f.設備連線、斷線通知

NotificationCenter.default.addObserver(self, selector: #selector(handleRouteChange), name: AVAudioSession.routeChangeNotification, object: nil)

handleRouteChange判斷連線/斷線。

guard let userInfo = notification.userInfo else { return }guard let reason = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt else { return }switch AVAudioSession.RouteChangeReason(rawValue: reason) {case .newDeviceAvailable:  NSLog("設備連線")case .oldDeviceUnavailable:  NSLog("設備斷線")case .categoryChange:  NSLog("categoryChange")default:  NSLog("routeChang \(reason)")}

以下是踩坑紀錄,坑坑洞洞請酌量閱讀。

第一個坑:模擬器無法顯示鎖屏控制畫面

沒錯,為了這件事花了一整天,我真蠢。果然模擬器真的只是模擬……

第二個坑:使用Play開始播放會變回速度1.0

How to change the play speed rate on AVPlayer (swift)

由於還得摸IAP和Apple sign,所以我就土法煉鋼在每次播放都重新設定速度,目前還沒想到更好的方式。

--

--

陳小嬰

喜愛動物又注重環保的iOS工程師就是我。Write the code change the world.