Files
MessageKit/Example/Sources/AudioController/BasicAudioController.swift
T
2019-03-01 00:01:43 -08:00

215 lines
8.9 KiB
Swift

/*
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()
}
}