|
@@ -0,0 +1,213 @@
|
|
|
+/*
|
|
|
+ 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
|
|
|
+
|
|
|
+/// 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()
|
|
|
+ }
|
|
|
+
|
|
|
+}
|