// // TUICallKitImpl.swift // TUICallKit // // Created by vincepzhang on 2023/1/4. // import Foundation import TUICore import UIKit import TUICallEngine #if canImport(TXLiteAVSDK_TRTC) import TXLiteAVSDK_TRTC #elseif canImport(TXLiteAVSDK_Professional) import TXLiteAVSDK_Professional #endif class TUICallKitImpl: TUICallKit { static let instance = TUICallKitImpl() let selfUserCallStatusObserver = Observer() let callingVibratorFeature = CallingVibratorFeature() let callingBellFeature = CallingBellFeature() override init() { super.init() registerNotifications() registerObserveState() } deinit { CallEngineManager.instance.removeObserver(TUICallState.instance) NotificationCenter.default.removeObserver(self) TUICallState.instance.selfUser.value.callStatus.removeObserver(selfUserCallStatusObserver) } // MARK: Implementation of external interface for TUICallKit override func setSelfInfo(nickname: String, avatar: String, succ: @escaping TUICallSucc, fail: @escaping TUICallFail) { CallEngineManager.instance.setSelfInfo(nickname: nickname, avatar: avatar) { succ() } fail: { code, message in fail(code,message) } } override func call(userId: String, callMediaType: TUICallMediaType) { call(userId: userId, callMediaType: callMediaType, params: getCallParams()) { } fail: { errCode, errMessage in } } override func call(userId: String, callMediaType: TUICallMediaType, params: TUICallParams, succ: @escaping TUICallSucc, fail: @escaping TUICallFail) { if TUILogin.getUserID() == nil { fail(ERROR_INIT_FAIL, "call failed, please login") return } if userId.count <= 0 || userId == TUILogin.getUserID() { fail(ERROR_PARAM_INVALID, "call failed, invalid params 'userId'") return } if WindowManager.instance.isFloating { fail(ERROR_PARAM_INVALID, "call failed, Unable to restart the call") TUITool.makeToast(TUICallKitLocalize(key: "TUICallKit.UnableToRestartTheCall")) return } if callMediaType == .unknown { fail(ERROR_PARAM_INVALID, "call failed, callMediaType is Unknown") return } if TUICallKitCommon.checkAuthorizationStatusIsDenied(mediaType: callMediaType) { showAuthorizationAlert(mediaType: callMediaType) fail(ERROR_PARAM_INVALID, "call failed, authorization status is denied") return } CallEngineManager.instance.call(userId: userId, callMediaType: callMediaType, params: params) { succ() } fail: { [weak self] code, message in guard let self = self else { return } self.handleAbilityFailErrorMessage(code: code, message: message) fail(code, message) } } override func groupCall(groupId: String, userIdList: [String], callMediaType: TUICallMediaType) { groupCall(groupId: groupId, userIdList: userIdList, callMediaType: callMediaType, params: getCallParams()) { } fail: { code, message in } } override func groupCall(groupId: String, userIdList: [String], callMediaType: TUICallMediaType, params: TUICallParams, succ: @escaping TUICallSucc, fail: @escaping TUICallFail) { if TUILogin.getUserID() == nil { fail(ERROR_INIT_FAIL, "call failed, please login") return } let userIdList = userIdList.filter { $0 != TUILogin.getUserID() } if userIdList.isEmpty { fail(ERROR_PARAM_INVALID, "call failed, invalid params 'userIdList'") return } if userIdList.count >= MAX_USER { fail(ERROR_PARAM_INVALID, "groupCall failed, currently supports call with up to 9 people") TUITool.makeToast(TUICallKitLocalize(key: "TUICallKit.User.Exceed.Limit")) return } if WindowManager.instance.isFloating { fail(ERROR_PARAM_INVALID, "call failed, Unable to restart the call") TUITool.makeToast(TUICallKitLocalize(key: "TUICallKit.UnableToRestartTheCall")) return } if callMediaType == .unknown { fail(ERROR_PARAM_INVALID, "call failed, callMediaType is Unknown") return } if TUICallKitCommon.checkAuthorizationStatusIsDenied(mediaType: callMediaType) { showAuthorizationAlert(mediaType: callMediaType) fail(ERROR_PARAM_INVALID, "call failed, authorization status is denied") return } CallEngineManager.instance.groupCall(groupId: groupId, userIdList: userIdList, callMediaType: callMediaType, params: params) { succ() } fail: { [weak self] code, message in guard let self = self else { return } self.handleAbilityFailErrorMessage(code: code, message: message) fail(code,message) } } override func joinInGroupCall(roomId: TUIRoomId, groupId: String, callMediaType: TUICallMediaType) { if TUICallKitCommon.checkAuthorizationStatusIsDenied(mediaType: callMediaType) { showAuthorizationAlert(mediaType: callMediaType) return } CallEngineManager.instance.joinInGroupCall(roomId: roomId, groupId: groupId, callMediaType: callMediaType) { } fail: { [weak self] code, message in guard let self = self else { return } self.handleAbilityFailErrorMessage(code: code, message: message) } } override func setCallingBell(filePath: String) { if filePath.hasPrefix("http") { let session = URLSession.shared guard let url = URL(string: filePath) else { return } let downloadTask = session.downloadTask(with: url) { location, response, error in if error != nil { return } if location != nil { if let oldBellFilePath = UserDefaults.standard.object(forKey: TUI_CALLING_BELL_KEY) as? String { do { try FileManager.default.removeItem(atPath: oldBellFilePath) } catch let error { debugPrint("FileManager Error: \(error)") } } guard let location = location else { return } guard let dstDocPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).last else { return } let dstPath = dstDocPath + "/" + location.lastPathComponent do { try FileManager.default.moveItem(at: location, to: URL(fileURLWithPath: dstPath)) } catch let error { debugPrint("FileManager Error: \(error)") } UserDefaults.standard.set(dstPath, forKey: TUI_CALLING_BELL_KEY) UserDefaults.standard.synchronize() } } downloadTask.resume() } else { UserDefaults.standard.set(filePath, forKey: TUI_CALLING_BELL_KEY) UserDefaults.standard.synchronize() } } override func enableMuteMode(enable: Bool) { UserDefaults.standard.set(enable, forKey: ENABLE_MUTEMODE_USERDEFAULT) TUICallState.instance.enableMuteMode = enable } override func enableFloatWindow(enable: Bool) { TUICallState.instance.enableFloatWindow = enable } override func enableCustomViewRoute(enable: Bool) { } override func getCallViewController() -> UIViewController { if let callWindowVC = WindowManager.instance.callWindow.rootViewController { return callWindowVC } if let floatingWindowVC = WindowManager.instance.floatWindow.rootViewController { return floatingWindowVC } return UIViewController() } override func enableVirtualBackground (enable: Bool) { CallEngineManager.instance.reportOnlineLog(enable) TUICallState.instance.showVirtualBackgroundButton = enable } override func enableIncomingBanner (enable: Bool) { TUICallState.instance.enableIncomingBanner = enable } } // MARK: Internal interface for TUICallKit private extension TUICallKitImpl { func registerNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(loginSuccessNotification), name: NSNotification.Name.TUILoginSuccess, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(logoutSuccessNotification), name: NSNotification.Name.TUILogoutSuccess, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(showViewControllerNotification), name: NSNotification.Name(Constants.EVENT_SHOW_TUICALLKIT_VIEWCONTROLLER), object: nil) } @objc func loginSuccessNotification(noti: Notification) { initEngine() initState() CallEngineManager.instance.addObserver(TUICallState.instance) CallEngineManager.instance.setFramework() CallEngineManager.instance.setExcludeFromHistoryMessage() } @objc func logoutSuccessNotification(noti: Notification) { CallEngineManager.instance.hangup() TUICallEngine.destroyInstance() TUICallState.instance.cleanState() } @objc func showViewControllerNotification(noti: Notification) { CallEngineManager.instance.setAudioPlaybackDevice(device: TUICallState.instance.audioDevice.value) WindowManager.instance.showCallWindow(false) } func initEngine() { CallEngineManager.instance.initEngine(sdkAppId:TUILogin.getSdkAppID(), userId: TUILogin.getUserID() ?? "", userSig: TUILogin.getUserSig() ?? "") {} fail: { Int32errCode, errMessage in } let videoEncoderParams = TUIVideoEncoderParams() videoEncoderParams.resolution = ._640_360 videoEncoderParams.resolutionMode = .portrait CallEngineManager.instance.setVideoEncoderParams(params: videoEncoderParams) {} fail: { Int32errCode, errMessage in } let videoRenderParams = TUIVideoRenderParams() videoRenderParams.fillMode = .fill videoRenderParams.rotation = ._0 CallEngineManager.instance.setVideoRenderParams(userId: TUILogin.getUserID() ?? "", params: videoRenderParams) {} fail: { Int32errCode, errMessage in } let beauty = CallEngineManager.instance.getTRTCCloudInstance().getBeautyManager() beauty.setBeautyStyle(.nature) beauty.setBeautyLevel(6.0) } func initState() { CallEngineManager.instance.addObserver(TUICallState.instance) User.getSelfUserInfo(response: { selfUser in TUICallState.instance.selfUser.value.id.value = selfUser.id.value TUICallState.instance.selfUser.value.nickname.value = selfUser.nickname.value TUICallState.instance.selfUser.value.avatar.value = selfUser.avatar.value }) } func registerObserveState() { TUICallState.instance.selfUser.value.callStatus.addObserver(selfUserCallStatusObserver, closure: { newValue, _ in if TUICallState.instance.selfUser.value.callRole.value != TUICallRole.none { if TUICallState.instance.selfUser.value.callStatus.value == TUICallStatus.waiting { TUICallState.instance.audioDevice.value = TUIAudioPlaybackDevice.earpiece CallEngineManager.instance.setAudioPlaybackDevice(device: TUIAudioPlaybackDevice.earpiece) WindowManager.instance.showCallWindow(TUICallState.instance.enableIncomingBanner) } else if TUICallState.instance.selfUser.value.callStatus.value == TUICallStatus.accept && !WindowManager.instance.isFloating { WindowManager.instance.showCallWindow(false) } } else { if TUICallState.instance.selfUser.value.callStatus.value == TUICallStatus.none { WindowManager.instance.closeCallWindow() WindowManager.instance.closeFloatWindow() } } }) } func showAuthorizationAlert(mediaType: TUICallMediaType) { let statusVideo: AVAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) var deniedType: AuthorizationDeniedType = AuthorizationDeniedType.audio if mediaType == .video && statusVideo == .denied { deniedType = .video } TUICallKitCommon.showAuthorizationAlert(deniedType: deniedType) { CallEngineManager.instance.hangup() } cancelHandler: { CallEngineManager.instance.hangup() } } func getCallParams() -> TUICallParams { let offlinePushInfo = OfflinePushInfoConfig.createOfflinePushInfo() let callParams = TUICallParams() callParams.offlinePushInfo = offlinePushInfo callParams.timeout = TUI_CALLKIT_SIGNALING_MAX_TIME return callParams } func convertCallKitError(code: Int32, message: String?) -> String { var errorMessage: String? = message if code == ERROR_PACKAGE_NOT_PURCHASED { errorMessage = TUICallKitLocalize(key: "TUICallKit.purchased") } else if code == ERROR_PACKAGE_NOT_SUPPORTED { errorMessage = TUICallKitLocalize(key: "TUICallKit.support") } else if code == ERR_SVR_MSG_IN_PEER_BLACKLIST.rawValue { errorMessage = TUICallKitLocalize(key: "TUICallKit.ErrorInPeerBlacklist") } else if code == ERROR_INIT_FAIL { errorMessage = TUICallKitLocalize(key: "TUICallKit.ErrorInvalidLogin") } else if code == ERROR_PARAM_INVALID { errorMessage = TUICallKitLocalize(key: "TUICallKit.ErrorParameterInvalid") } else if code == ERROR_REQUEST_REFUSED { errorMessage = TUICallKitLocalize(key: "TUICallKit.ErrorRequestRefused") } else if code == ERROR_REQUEST_REPEATED { errorMessage = TUICallKitLocalize(key: "TUICallKit.ErrorRequestRepeated") } else if code == ERROR_SCENE_NOT_SUPPORTED { errorMessage = TUICallKitLocalize(key: "TUICallKit.ErrorSceneNotSupport") } return errorMessage ?? "" } func handleAbilityFailErrorMessage(code: Int32, message: String?) { let errorMessage = TUITool.convertIMError(Int(code), msg: convertCallKitError(code: code, message: message)) TUITool.makeToast(errorMessage ?? "", duration: 4) } }