// // PlayerController.swift // WolneLektury // // Created by Pawel Dabrowski on 21/09/2018. // Copyright © 2018 Fundacja Nowoczesna Polska. All rights reserved. // import UIKit import AVFoundation import MediaPlayer protocol PlayerControllerDelegate: class { func playerControllerDelegateTrackChanged() func playerControllerDelegateUpdatePlayerProgress(timeInterval: TimeInterval) func playerControllerDelegatePlayStateChanged() } class PlayerController: UIResponder, AVAudioPlayerDelegate { static let shared = PlayerController() override init() { super.init() let commandCenter = MPRemoteCommandCenter.shared() commandCenter.playCommand.isEnabled = true commandCenter.pauseCommand.isEnabled = true commandCenter.nextTrackCommand.isEnabled = true commandCenter.previousTrackCommand.isEnabled = true commandCenter.nextTrackCommand.addTarget(self, action: #selector(PlayerController.nextTrackCommand)) commandCenter.previousTrackCommand.addTarget(self, action: #selector(PlayerController.previousTrackCommand)) commandCenter.playCommand.addTarget(self, action: #selector(PlayerController.playCommand)) commandCenter.pauseCommand.addTarget(self, action: #selector(PlayerController.pauseCommand)) if #available(iOS 9.1, *) { commandCenter.changePlaybackPositionCommand.addTarget(self, action: #selector(PlayerController.changePlaybackPositionCommand(event:))) } else { // Fallback on earlier versions } } weak var delegate: PlayerControllerDelegate? private(set) var currentBookDetails: BookDetailsModel? { didSet { if let currentBookDetails = currentBookDetails { audiobookMediaModels = currentBookDetails.getAudiobookMediaModels() if currentBookDetails.currentAudioChapter < audiobookMediaModels!.count { currentAudiobookIndex = currentBookDetails.currentAudioChapter } else { currentAudiobookIndex = 0 } } else{ audiobookMediaModels = nil currentAudiobookIndex = 0 } } } private var audiobookMediaModels: [MediaModel]? private(set) var currentAudiobookIndex: Int = 0 var audioPlayer:AVAudioPlayer? = nil var timer:Timer? var audioLength = 0.0 var currentAudioPath: URL? private(set) var artwork: MPMediaItemArtwork?{ didSet{ updateMediaInfo() } } func setCoverImage(image: UIImage){ artwork = MPMediaItemArtwork(image: image) } func getCurrentTime() -> TimeInterval { guard let audioPlayer = audioPlayer else { return 0 } return audioPlayer.currentTime } func isPlaying() -> Bool { guard let audioPlayer = audioPlayer else { return false } return audioPlayer.isPlaying } func stopAndClear() { stopTimer() audioPlayer?.stop() currentBookDetails = nil } func preparePlayer(bookDetailsModel: BookDetailsModel, delegate: PlayerControllerDelegate, trackIndex: Int?) { stopAndClear() self.delegate = delegate currentBookDetails = bookDetailsModel if let trackIndex = trackIndex { currentAudiobookIndex = trackIndex } else if bookDetailsModel.currentAudioChapter < audiobookMediaModels!.count { currentAudiobookIndex = bookDetailsModel.currentAudioChapter } prepareAudio() delegate.playerControllerDelegateTrackChanged() } func startOrContinuePlaying(bookDetailsModel: BookDetailsModel, delegate: PlayerControllerDelegate) { guard let audioPlayer = audioPlayer, let currentBookDetails = currentBookDetails, currentBookDetails.slug == bookDetailsModel.slug else { startPlaying(bookDetailsModel: bookDetailsModel, delegate: delegate, trackIndex: nil) return } self.delegate = delegate if audioPlayer.isPlaying { delegate.playerControllerDelegateTrackChanged() } else { prepareAudio() playAudio() } } func startPlaying(bookDetailsModel: BookDetailsModel, delegate: PlayerControllerDelegate, trackIndex: Int?) { preparePlayer(bookDetailsModel: bookDetailsModel, delegate: delegate, trackIndex: trackIndex) playAudio() } func getCurentAudiobookMediaModel() -> MediaModel? { guard let audiobookMediaModels = audiobookMediaModels, audiobookMediaModels.count > currentAudiobookIndex else { return nil } return audiobookMediaModels[currentAudiobookIndex] } func updateMediaInfo(){ guard let audioPlayer = audioPlayer, let model = getCurentAudiobookMediaModel() else { return } let artistName = model.artist let songName = model.name if let artwork = artwork { MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyArtist : artistName, MPMediaItemPropertyTitle : songName, MPMediaItemPropertyPlaybackDuration: audioLength, MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime, MPMediaItemPropertyArtwork: artwork] } else { MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyArtist : artistName, MPMediaItemPropertyTitle : songName, MPMediaItemPropertyPlaybackDuration: audioLength, MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime] } } // Prepare audio for playing func prepareAudio(){ guard let currentBook = currentBookDetails, let currentMediaModel = getCurentAudiobookMediaModel() else { return } guard let path = NSObject.audiobookPathIfExists(audioBookUrlString: currentMediaModel.url, bookSlug: currentBook.slug) else { return } currentAudioPath = path audioPlayer = try? AVAudioPlayer(contentsOf: path) guard let audioPlayer = audioPlayer else { return } do { //keep alive audio at background try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback) } catch _ { } do { try AVAudioSession.sharedInstance().setActive(true) } catch _ { } UIApplication.shared.beginReceivingRemoteControlEvents() audioPlayer.delegate = self audioLength = audioPlayer.duration audioPlayer.prepareToPlay() } //MARK:- Player Controls Methods func playAudio(){ audioPlayer?.play() startTimer() saveCurrentTrackNumber() updateMediaInfo() delegate?.playerControllerDelegateTrackChanged() } func playNextAudio(){ guard let audiobookMediaModels = audiobookMediaModels, currentAudiobookIndex < (audiobookMediaModels.count - 1) else { return } currentAudiobookIndex += 1 prepareAudio() playAudio() } func playPreviousAudio(){ guard currentAudiobookIndex > 0 else { return } currentAudiobookIndex -= 1 prepareAudio() playAudio() } func forward() { guard let audioPlayer = audioPlayer, audioPlayer.isPlaying else { return } if audioPlayer.currentTime + 12 < audioPlayer.duration { audioPlayer.currentTime += 10 updateMediaInfo() } else { playNextAudio() } } func rewind() { guard let audioPlayer = audioPlayer, audioPlayer.isPlaying else { return } if audioPlayer.currentTime > 10 { audioPlayer.currentTime -= 10 updateMediaInfo() } else { playPreviousAudio() } } func pauseAudioPlayer(){ audioPlayer?.pause() updateMediaInfo() delegate?.playerControllerDelegatePlayStateChanged() } func playAudioPlayer(){ audioPlayer?.play() updateMediaInfo() delegate?.playerControllerDelegatePlayStateChanged() } func changeTime(timeInterval: TimeInterval) { guard let audioPlayer = audioPlayer else { return } if audioLength > timeInterval { audioPlayer.currentTime = timeInterval } updateMediaInfo() } func saveCurrentTrackNumber(){ guard let bookDetailsModel = currentBookDetails else { return } DatabaseManager.shared.updateCurrentChapters(bookDetailsModel: bookDetailsModel, currentChapter: nil, totalChapters: nil, currentAudioChapter: currentAudiobookIndex, totalAudioChapters: nil) } func startTimer(){ if timer == nil { timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(PlayerController.update(_:)), userInfo: nil,repeats: true) timer?.fire() } else if timer!.isValid == false { timer?.invalidate() timer = nil timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(PlayerController.update(_:)), userInfo: nil,repeats: true) timer?.fire() } } func stopTimer(){ timer?.invalidate() } @objc func update(_ timer: Timer){ guard let audioPlayer = audioPlayer else { return } if !audioPlayer.isPlaying{ return } delegate?.playerControllerDelegateUpdatePlayerProgress(timeInterval: audioPlayer.currentTime) } /* audioPlayerDidFinishPlaying:successfully: is called when a sound has finished playing. This method is NOT called if the player is stopped due to an interruption. */ public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { if flag == true { playNextAudio() } } @objc func changePlaybackPositionCommand(event: MPChangePlaybackPositionCommandEvent) -> MPRemoteCommandHandlerStatus{ guard let audioPlayer = audioPlayer else { return .commandFailed } if audioPlayer.duration - 5 < event.positionTime && audioPlayer.duration > 5 { audioPlayer.currentTime = audioPlayer.duration - 5 } else { audioPlayer.currentTime = event.positionTime } updateMediaInfo() return .success } @objc func nextTrackCommand() -> Void{ forward() } @objc func previousTrackCommand() -> Void{ rewind() } @objc func playCommand() -> Void{ playAudioPlayer() } @objc func pauseCommand() -> Void{ pauseAudioPlayer() } }