sendbird-uikit-ios/Sources/Manager/SBUVoiceRecorder.swift

316 lines
11 KiB
Swift

//
// SBUVoiceRecorder.swift
// SendbirdUIKit
//
// Created by Tez Park on 2022/12/22.
// Copyright © 2022 Sendbird, Inc. All rights reserved.
//
import UIKit
import AVFoundation
// TODO: Voice -> SBU prefix
/// This is an enumeration for voice recorder status.
public enum VoiceRecorderStatus: Int {
case none
case prepared
case recording
case completed
}
/// This is an enumeration for voice recorder error status.
public enum VoiceRecorderErrorStatus: Int {
case none
case fileCreation
case audioSessionSetting
case recorderPreparation
case record
case finishRecording
case recorderEncodeError
}
public protocol SBUVoiceRecorderDelegate: AnyObject {
/// Called when the error was received.
/// - Parameters:
/// - recorder: `SBUVoiceRecorder` object.
/// - errorStatus: `VoiceRecorderErrorStatus`.
func voiceRecorderDidReceiveError(_ recorder: SBUVoiceRecorder, errorStatus: VoiceRecorderErrorStatus)
/// Called when the recorder was prepared
/// - Parameters:
/// - recorder: `SBUVoiceRecorder` object.
/// - voiceFileInfo: Prepared voice file info object.
func voiceRecorderDidPrepare(_ recorder: SBUVoiceRecorder, voiceFileInfo: SBUVoiceFileInfo)
/// Called when the permission was updated.
/// - Parameters:
/// - recorder: `SBUVoiceRecorder` object.
/// - granted: `true` when permission was granted.
func voiceRecorderDidUpdateRecordPermission(_ recorder: SBUVoiceRecorder, granted: Bool)
/// Called when the record time was updated.
/// - Parameters:
/// - recorder: `SBUVoiceRecorder` object.
/// - time: current record time.
func voiceRecorderDidUpdateRecordTime(_ recorder: SBUVoiceRecorder, time: TimeInterval)
/// Called when the recorder was finished.
/// - Parameters:
/// - recorder: `SBUVoiceRecorder` object.
/// - voiceFileInfo: Recorded voice file info object.
func voiceRecorderDidFinishRecord(_ recorder: SBUVoiceRecorder, voiceFileInfo: SBUVoiceFileInfo)
}
public class SBUVoiceRecorder: NSObject, AVAudioRecorderDelegate {
// MARK: Properties
public private(set) var status: VoiceRecorderStatus = .none
public private(set) var progressTimer: Timer?
lazy var audioSession: AVAudioSession = AVAudioSession.sharedInstance()
var audioRecorder: AVAudioRecorder?
var voiceFileInfo = SBUVoiceFileInfo()
weak var delegate: SBUVoiceRecorderDelegate?
var currentRecordingTime: Double = 0 // ms
let minRecordingTime: Double = SBUGlobals.voiceMessageConfig.recorder.minRecordingTime // 1000 ms
let maxRecordingTime: Double = SBUGlobals.voiceMessageConfig.recorder.maxRecordingTime // 60000 ms
// MARK: - Initializer
/// Initializes `SBUVoiceRecorder` class with delegate and checkPermission.
/// - Parameters:
/// - delegate: `SBUVoiceRecorderDelegate` instance.
public init(delegate: SBUVoiceRecorderDelegate?) {
super.init()
self.delegate = delegate
SBUPermissionManager.shared.requestRecordAcess(onDenied: { [weak self] in
guard let self = self else { return }
SBULog.error("[Failed] Request record permission")
self.showPermissionAlert()
})
}
deinit {
self.audioRecorder?.stop()
self.restoreCategory()
self.stopProgressTimer()
self.delegate = nil
}
func showPermissionAlert() {
let settingButton = SBUAlertButtonItem(title: SBUStringSet.Settings) { _ in
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil)
}
}
let cancelButton = SBUAlertButtonItem(
title: SBUStringSet.Cancel,
completionHandler: { [weak self] _ in
guard let self = self else { return }
self.resetRecorder()
self.delegate?.voiceRecorderDidUpdateRecordPermission(self, granted: false)
}
)
SBUAlertView.show(
title: SBUStringSet.Alert_Allow_Microphone_Access,
oneTimetheme: SBUTheme.componentTheme,
confirmButtonItem: settingButton,
cancelButtonItem: cancelButton) {
self.resetRecorder()
self.delegate?.voiceRecorderDidUpdateRecordPermission(self, granted: false)
}
}
// MARK: - Recorder controls
/// Starts audio recording.
@discardableResult
public func record() -> Bool {
switch SBUPermissionManager.shared.currentRecordAccessStatus {
case .granted:
if self.status != .prepared {
if self.prepareToRecord() == false {
SBULog.error("[Failed] Recorder preparation")
self.status = .none
}
}
self.status = .recording
if self.audioRecorder?.record() == true {
self.startProgressTimer()
return true
} else {
self.status = .none
SBULog.error("[Failed] Record")
self.delegate?.voiceRecorderDidReceiveError(self, errorStatus: .record)
return false
}
default:
SBUPermissionManager.shared.requestRecordAcess(onDenied: { [weak self] in
guard let self = self else { return }
SBULog.error("[Failed] Request record permission")
self.showPermissionAlert()
})
self.status = .none
self.delegate?.voiceRecorderDidReceiveError(self, errorStatus: .recorderPreparation)
return false
}
}
// Stops audio recording.
public func stop() {
self.currentRecordingTime = (self.audioRecorder?.currentTime ?? 0.0) * 1000
self.audioRecorder?.stop()
self.restoreCategory()
}
func restoreCategory() {
if let storedConfig = SBUGlobals.voiceMessageConfig.storedAudioSessionConfig {
SBUGlobals.voiceMessageConfig.storedAudioSessionConfig = nil
try? self.audioSession.setCategory(
storedConfig.category,
mode: storedConfig.mode,
options: storedConfig.categoryOptions
)
try? self.audioSession.setActive(true)
} else {
try? self.audioSession.setActive(false)
}
}
/// Resets audio recorder
public func resetRecorder() {
self.stop()
self.stopProgressTimer()
self.voiceFileInfo = SBUVoiceFileInfo()
self.status = .none
self.currentRecordingTime = 0
self.audioRecorder?.deleteRecording()
self.audioRecorder = nil
}
// MARK: - Preparations
/// Settings category and active option of `AVAudioSession` and prepares `AVAudioRecorder` settings.
/// - Returns: `true` when preparation is successful.
func prepareToRecord() -> Bool {
// AVAudioSession
do {
if SBUGlobals.voiceMessageConfig.storedAudioSessionConfig == nil {
SBUGlobals.voiceMessageConfig.storedAudioSessionConfig = .init(
category: audioSession.category,
categoryOptions: audioSession.categoryOptions,
mode: audioSession.mode
)
}
try self.audioSession.setCategory(.playAndRecord)
try self.audioSession.overrideOutputAudioPort(.speaker)
try self.audioSession.setActive(true)
} catch {
SBULog.error("[Failed] Audio session preparation error: \(error.localizedDescription)")
self.delegate?.voiceRecorderDidReceiveError(self, errorStatus: .audioSessionSetting)
return false
}
// AVAudioPlayer
let fileName = Date().sbu_toString(
dateFormat: SBUDateFormatSet.VoiceMessage.fileNameFormat,
localizedFormat: false
)
let fileExtension = "m4a"
let fullFileName = "\(fileName).\(fileExtension)"
guard let voiceFilePath = SBUCacheManager.File.diskCache.voiceTempPath(fileName: fullFileName) else {
SBULog.error("[Failed] Create voice file")
self.delegate?.voiceRecorderDidReceiveError(self, errorStatus: .recorderPreparation)
return false
}
let settings = SBUGlobals.voiceMessageConfig.recorder.settings
do {
self.audioRecorder = try AVAudioRecorder(url: voiceFilePath, settings: settings)
self.audioRecorder?.delegate = self
self.voiceFileInfo.fileName = fullFileName
self.voiceFileInfo.filePath = voiceFilePath
} catch {
SBULog.error("[Failed] Audio recorder preparation error: \(error.localizedDescription)")
self.delegate?.voiceRecorderDidReceiveError(self, errorStatus: .recorderPreparation)
return false
}
if self.audioRecorder?.prepareToRecord() != true {
return false
}
SBULog.info("[Succeeded] Audio recorder preparation")
self.status = .prepared
self.delegate?.voiceRecorderDidPrepare(self, voiceFileInfo: self.voiceFileInfo)
return true
}
// MARK: - Timer
func startProgressTimer() {
self.stopProgressTimer()
self.progressTimer = Timer.scheduledTimer(
timeInterval: 0.1,
target: self,
selector: #selector(updateRecordTime),
userInfo: nil,
repeats: true
)
}
func stopProgressTimer() {
self.progressTimer?.invalidate()
self.progressTimer = nil
}
@objc func updateRecordTime() {
guard let audioRecorder = self.audioRecorder else { return }
if audioRecorder.isRecording {
let currentTime = audioRecorder.currentTime * 1000
self.delegate?.voiceRecorderDidUpdateRecordTime(self, time: currentTime)
if currentTime >= self.maxRecordingTime {
self.stop()
}
}
}
// MARK: - AVAudioRecorderDelegate
public func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully success: Bool) {
self.stopProgressTimer()
if self.currentRecordingTime < self.minRecordingTime {
SBULog.info("Recorder will be canceled because it is less than the minimum recording time.")
self.resetRecorder()
return
}
if success {
self.voiceFileInfo.playtime = self.currentRecordingTime
self.status = .completed
self.delegate?.voiceRecorderDidFinishRecord(self, voiceFileInfo: self.voiceFileInfo)
} else {
SBULog.error("[Failed] Finish recording")
self.status = .none
self.delegate?.voiceRecorderDidReceiveError(self, errorStatus: .finishRecording)
self.resetRecorder()
}
}
public func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
SBULog.error("[Failed] Recorder encode error: \(error.debugDescription)")
self.resetRecorder()
self.delegate?.voiceRecorderDidReceiveError(self, errorStatus: .recorderEncodeError)
}
}