Files
featherVoice/TUIKit/TUICallKit/TUICallKit-Swift/Service/TUIAudioMessageRecordService.swift

276 lines
12 KiB
Swift
Raw Normal View History

2025-08-08 10:49:36 +08:00
//
// TUIAudioMessageRecordService.swift
// TUICallKit
//
// Created by vincepzhang on 2023/8/16.
//
import Foundation
import AVFAudio
import TUICore
import TUICallEngine
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
class TUIAudioRecordInfo {
var path: String = ""
var sdkAppId: Int = 0
var signature: String = ""
}
class TUIAudioMessageRecordService: NSObject, TUIServiceProtocol, TUINotificationProtocol, TRTCCloudDelegate, TUICallObserver {
static let instance = TUIAudioMessageRecordService()
var audioRecordInfo: TUIAudioRecordInfo?
var category: AVAudioSession.Category?
var categoryOptions: AVAudioSession.CategoryOptions?
var callback: TUICallServiceResultCallback?
override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(loginSuccessNotification),
name: NSNotification.Name.TUILoginSuccess, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc
func loginSuccessNotification() {
TUICallEngine.createInstance().addObserver(self)
}
}
// MARK: TUIServiceProtocol
extension TUIAudioMessageRecordService {
func onCall(_ method: String, param: [AnyHashable : Any]?) -> Any? {
return nil
}
func onCall(_ method: String, param: [AnyHashable : Any]?, resultCallback: @escaping TUICallServiceResultCallback) -> Any? {
callback = resultCallback
if method == TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod {
guard let param = param else {
notifyAudioMessageRecordEvent(method: TUICore_RecordAudioMessageNotify_StartRecordAudioMessageSubKey,
errorCode: Int(TUICore_RecordAudioMessageNotifyError_InvalidParam), path: "")
return nil
}
if TUICallState.instance.selfUser.value.callStatus.value != TUICallStatus.none {
notifyAudioMessageRecordEvent(method: TUICore_RecordAudioMessageNotify_StartRecordAudioMessageSubKey,
errorCode: Int(TUICore_RecordAudioMessageNotifyError_StatusInCall), path: "")
return nil
}
if audioRecordInfo != nil {
notifyAudioMessageRecordEvent(method: TUICore_RecordAudioMessageNotify_StartRecordAudioMessageSubKey,
errorCode: Int(TUICore_RecordAudioMessageNotifyError_StatusIsAudioRecording), path: "")
return nil
}
requestRecordAuthorization { [weak self] granted in
guard let self = self else { return }
if granted {
if !self.requestAudioFocus() {
self.notifyAudioMessageRecordEvent(method: TUICore_RecordAudioMessageNotify_StartRecordAudioMessageSubKey,
errorCode: Int(TUICore_RecordAudioMessageNotifyError_RequestAudioFocusFailed), path: "")
} else {
self.audioRecordInfo = TUIAudioRecordInfo()
let pathKey = TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_PathKey
self.audioRecordInfo?.path = param[pathKey] as? String ?? ""
let sdkAppId = param[TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_SdkappidKey] as? NSNumber
if let sdkAppId = sdkAppId {
self.audioRecordInfo?.sdkAppId = sdkAppId.intValue
}
let signatureKey = TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_SignatureKey
self.audioRecordInfo?.signature = param[signatureKey] as? String ?? ""
TRTCCloud.sharedInstance().delegate = self
let audioVolumeEvaluateParams = TRTCAudioVolumeEvaluateParams()
audioVolumeEvaluateParams.interval = 500
TRTCCloud.sharedInstance().enableAudioVolumeEvaluation(true, with: audioVolumeEvaluateParams)
self.startRecordAudioMessage()
}
} else {
self.notifyAudioMessageRecordEvent(method: TUICore_RecordAudioMessageNotify_StartRecordAudioMessageSubKey,
errorCode: Int(TUICore_RecordAudioMessageNotifyError_MicPermissionRefused), path: "")
}
}
return nil
} else if method == TUICore_TUIAudioMessageRecordService_StopRecordAudioMessageMethod {
stopRecordAudioMessage()
}
return nil
}
func requestRecordAuthorization(response: @escaping (_ granted: Bool) -> Void) {
let session = AVAudioSession.sharedInstance()
let permission = session.recordPermission
if permission == .undetermined {
session.requestRecordPermission(response)
} else if permission == .granted {
response(true)
} else if permission == .denied {
response(false)
}
}
func startRecordAudioMessage() {
guard let audioRecordInfo = audioRecordInfo else { return }
let sdkAppIdKey = TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_SdkappidKey
let pathKey = TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_PathKey
let jsonParams: [String: Any] = ["api": "startRecordAudioMessage",
"params": ["key": audioRecordInfo.signature,
sdkAppIdKey: audioRecordInfo.sdkAppId,
pathKey: audioRecordInfo.path,] as [String : Any],]
guard let data = try? JSONSerialization.data(withJSONObject: jsonParams,
options: JSONSerialization.WritingOptions(rawValue: 0)) else { return }
guard let paramsString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as? String else { return }
TRTCCloud.sharedInstance().callExperimentalAPI(paramsString)
}
func stopRecordAudioMessage() {
guard let _ = audioRecordInfo else { return }
let jsonParams: [String: Any] = ["api": "stopRecordAudioMessage",
"params": [:] as [String : Any],]
guard let data = try? JSONSerialization.data(withJSONObject: jsonParams,
options: JSONSerialization.WritingOptions(rawValue: 0)) else { return }
guard let paramsString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as? String else { return }
TRTCCloud.sharedInstance().callExperimentalAPI(paramsString)
TRTCCloud.sharedInstance().enableAudioVolumeEvaluation(false, with: TRTCAudioVolumeEvaluateParams())
TRTCCloud.sharedInstance().stopLocalAudio()
audioRecordInfo = nil
let _ = abandonAudioFocus()
}
}
// MARK: TUICallObserver
extension TUIAudioMessageRecordService {
func onCallReceived(callerId: String, calleeIdList: [String], groupId: String?, callMediaType: TUICallMediaType, userData: String?) {
stopRecordAudioMessage()
}
}
// MARK: TRTCCloudDelegate
extension TUIAudioMessageRecordService {
func onError(_ errCode: TXLiteAVError, errMsg: String?, extInfo: [AnyHashable : Any]?) {
if errCode.rawValue == TUICore_RecordAudioMessageNotifyError_MicStartFail ||
errCode.rawValue == TUICore_RecordAudioMessageNotifyError_MicNotAuthorized ||
errCode.rawValue == TUICore_RecordAudioMessageNotifyError_MicSetParamFail ||
errCode.rawValue == TUICore_RecordAudioMessageNotifyError_MicOccupy {
notifyAudioMessageRecordEvent(method: TUICore_RecordAudioMessageNotify_StartRecordAudioMessageSubKey,
errorCode: Int(errCode.rawValue) , path: "")
}
}
func onLocalRecordBegin(_ errCode: Int, storagePath: String) {
if errCode == TUICore_RecordAudioMessageNotifyError_None {
TRTCCloud.sharedInstance().startLocalAudio(TRTCAudioQuality.speech)
}
let tempCode = convertErrorCode("onLocalRecordBegin", errorCode: errCode)
notifyAudioMessageRecordEvent(method: TUICore_RecordAudioMessageNotify_StartRecordAudioMessageSubKey,
errorCode: Int(tempCode), path: storagePath)
}
func onLocalRecordComplete(_ errCode: Int, storagePath: String) {
let tempCode = convertErrorCode("onLocalRecordComplete", errorCode: errCode)
notifyAudioMessageRecordEvent(method: TUICore_RecordAudioMessageNotify_StopRecordAudioMessageSubKey,
errorCode: Int(tempCode), path: storagePath)
}
func onUserVoiceVolume(_ userVolumes: [TRTCVolumeInfo], totalVolume: Int) {
for volumeInfo in userVolumes {
if volumeInfo.userId == nil || volumeInfo.userId?.isEmpty == true {
let param = [TUICore_RecordAudioMessageNotify_RecordAudioVoiceVolumeSubKey_VolumeKey: volumeInfo.volume,]
TUICore.notifyEvent(TUICore_RecordAudioMessageNotify,
subKey: TUICore_RecordAudioMessageNotify_RecordAudioVoiceVolumeSubKey,
object: nil, param: param)
break
}
}
}
}
// MARK: Private
extension TUIAudioMessageRecordService {
func requestAudioFocus() -> Bool {
let session = AVAudioSession.sharedInstance()
category = session.category
categoryOptions = session.categoryOptions
do {
try session.setCategory(.playAndRecord, options: .allowBluetooth)
try session.setActive(true)
return true
} catch {
return false
}
}
func abandonAudioFocus() -> Bool {
guard let category = category else { return false }
guard let categoryOptions = categoryOptions else { return false }
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(category, options: categoryOptions)
try session.setActive(false, options: .notifyOthersOnDeactivation)
return true
} catch {
return false
}
}
func notifyAudioMessageRecordEvent(method: String, errorCode: Int, path: String) {
let params: [String: Any] = ["method": method,
"errorCode": errorCode,
"path": path,]
guard let callback = callback else { return }
callback(errorCode, "", params)
}
func convertErrorCode(_ method: String?, errorCode: Int) -> Int32 {
var targetCode: Int32
switch errorCode {
case -1:
if let method = method, method == "onLocalRecordBegin" {
targetCode = TUICore_RecordAudioMessageNotifyError_RecordInitFailed
} else {
targetCode = TUICore_RecordAudioMessageNotifyError_RecordFailed
}
case -2:
targetCode = TUICore_RecordAudioMessageNotifyError_PathFormatNotSupport
case -3:
targetCode = TUICore_RecordAudioMessageNotifyError_NoMessageToRecord
case -4:
targetCode = TUICore_RecordAudioMessageNotifyError_SignatureError
case -5:
targetCode = TUICore_RecordAudioMessageNotifyError_SignatureExpired
default:
targetCode = TUICore_RecordAudioMessageNotifyError_None
}
return targetCode
}
}