mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
215 lines
8.9 KiB
Swift
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. progressView 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 because 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()
|
|
}
|
|
|
|
}
|