Files
MessageKit/Example/Sources/AudioController/BasicAudioController.swift
T
2024-10-30 07:49:27 +01:00

243 lines
8.6 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 AVFoundation
import MessageKit
import UIKit
// MARK: - PlayerState
/// 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
}
// MARK: - BasicAudioController
/// 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.
@MainActor
open class BasicAudioController: NSObject, @preconcurrency AVAudioPlayerDelegate {
// MARK: Lifecycle
// MARK: - Init Methods
public init(messageCollectionView: MessagesCollectionView) {
self.messageCollectionView = messageCollectionView
super.init()
}
// MARK: Open
/// 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
// 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 _: 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: - AVAudioPlayerDelegate
open func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully _: Bool) {
stopAnyOngoingPlaying()
}
open func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error _: Error?) {
stopAnyOngoingPlaying()
}
// MARK: Public
// The `MessagesCollectionView` where the playing cell exist
public weak var messageCollectionView: MessagesCollectionView?
// MARK: Internal
/// The `Timer` that update playing progress
internal var progressTimer: Timer?
// MARK: Private
// MARK: - Fire Methods
@objc
private func didFireProgressTimer(_: 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)
}
}