/* MIT License Copyright (c) 2017-2019 MessageKit Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import UIKit import AVFoundation import MessageKit /// The `PlayerState` indicates the current audio controller state public enum PlayerState { /// The audio controller is currently playing a sound case playing /// The audio controller is currently in pause state case pause /// The audio controller is not playing any sound and audioPlayer is nil case stopped } /// The `BasicAudioController` update UI for current audio cell that is playing a sound /// and also creates and manage an `AVAudioPlayer` states, play, pause and stop. open class BasicAudioController: NSObject, AVAudioPlayerDelegate { /// The `AVAudioPlayer` that is playing the sound open var audioPlayer: AVAudioPlayer? /// The `AudioMessageCell` that is currently playing sound open weak var playingCell: AudioMessageCell? /// The `MessageType` that is currently playing sound open var playingMessage: MessageType? /// Specify if current audio controller state: playing, in pause or none open private(set) var state: PlayerState = .stopped // The `MessagesCollectionView` where the playing cell exist public weak var messageCollectionView: MessagesCollectionView? /// The `Timer` that update playing progress internal var progressTimer: Timer? // MARK: - Init Methods public init(messageCollectionView: MessagesCollectionView) { self.messageCollectionView = messageCollectionView super.init() } // MARK: - Methods /// Used to configure the audio cell UI: /// 1. play button selected state; /// 2. progresssView progress; /// 3. durationLabel text; /// /// - Parameters: /// - cell: The `AudioMessageCell` that needs to be configure. /// - message: The `MessageType` that configures the cell. /// /// - Note: /// This protocol method is called by MessageKit every time an audio cell needs to be configure open func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) { if playingMessage?.messageId == message.messageId, let collectionView = messageCollectionView, let player = audioPlayer { playingCell = cell cell.progressView.progress = (player.duration == 0) ? 0 : Float(player.currentTime/player.duration) cell.playButton.isSelected = (player.isPlaying == true) ? true : false guard let displayDelegate = collectionView.messagesDisplayDelegate else { fatalError("MessagesDisplayDelegate has not been set.") } cell.durationLabel.text = displayDelegate.audioProgressTextFormat(Float(player.currentTime), for: cell, in: collectionView) } } /// Used to start play audio sound /// /// - Parameters: /// - message: The `MessageType` that contain the audio item to be played. /// - audioCell: The `AudioMessageCell` that needs to be updated while audio is playing. open func playSound(for message: MessageType, in audioCell: AudioMessageCell) { switch message.kind { case .audio(let item): playingCell = audioCell playingMessage = message guard let player = try? AVAudioPlayer(contentsOf: item.url) else { print("Failed to create audio player for URL: \(item.url)") return } audioPlayer = player audioPlayer?.prepareToPlay() audioPlayer?.delegate = self audioPlayer?.play() state = .playing audioCell.playButton.isSelected = true // show pause button on audio cell startProgressTimer() audioCell.delegate?.didStartAudio(in: audioCell) default: print("BasicAudioPlayer failed play sound becasue given message kind is not Audio") } } /// Used to pause the audio sound /// /// - Parameters: /// - message: The `MessageType` that contain the audio item to be pause. /// - audioCell: The `AudioMessageCell` that needs to be updated by the pause action. open func pauseSound(for message: MessageType, in audioCell: AudioMessageCell) { audioPlayer?.pause() state = .pause audioCell.playButton.isSelected = false // show play button on audio cell progressTimer?.invalidate() if let cell = playingCell { cell.delegate?.didPauseAudio(in: cell) } } /// Stops any ongoing audio playing if exists open func stopAnyOngoingPlaying() { guard let player = audioPlayer, let collectionView = messageCollectionView else { return } // If the audio player is nil then we don't need to go through the stopping logic player.stop() state = .stopped if let cell = playingCell { cell.progressView.progress = 0.0 cell.playButton.isSelected = false guard let displayDelegate = collectionView.messagesDisplayDelegate else { fatalError("MessagesDisplayDelegate has not been set.") } cell.durationLabel.text = displayDelegate.audioProgressTextFormat(Float(player.duration), for: cell, in: collectionView) cell.delegate?.didStopAudio(in: cell) } progressTimer?.invalidate() progressTimer = nil audioPlayer = nil playingMessage = nil playingCell = nil } /// Resume a currently pause audio sound open func resumeSound() { guard let player = audioPlayer, let cell = playingCell else { stopAnyOngoingPlaying() return } player.prepareToPlay() player.play() state = .playing startProgressTimer() cell.playButton.isSelected = true // show pause button on audio cell cell.delegate?.didStartAudio(in: cell) } // MARK: - Fire Methods @objc private func didFireProgressTimer(_ timer: Timer) { guard let player = audioPlayer, let collectionView = messageCollectionView, let cell = playingCell else { return } // check if can update playing cell if let playingCellIndexPath = collectionView.indexPath(for: cell) { // 1. get the current message that decorates the playing cell // 2. check if current message is the same with playing message, if so then update the cell content // Note: Those messages differ in the case of cell reuse let currentMessage = collectionView.messagesDataSource?.messageForItem(at: playingCellIndexPath, in: collectionView) if currentMessage != nil && currentMessage?.messageId == playingMessage?.messageId { // messages are the same update cell content cell.progressView.progress = (player.duration == 0) ? 0 : Float(player.currentTime/player.duration) guard let displayDelegate = collectionView.messagesDisplayDelegate else { fatalError("MessagesDisplayDelegate has not been set.") } cell.durationLabel.text = displayDelegate.audioProgressTextFormat(Float(player.currentTime), for: cell, in: collectionView) } else { // if the current message is not the same with playing message stop playing sound stopAnyOngoingPlaying() } } } // MARK: - Private Methods private func startProgressTimer() { progressTimer?.invalidate() progressTimer = nil progressTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(BasicAudioController.didFireProgressTimer(_:)), userInfo: nil, repeats: true) } // MARK: - AVAudioPlayerDelegate open func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { stopAnyOngoingPlaying() } open func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { stopAnyOngoingPlaying() } }