// // 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 } }