// // Agora.swift // SodaLive // // Created by klaus on 2023/08/14. // import Foundation import AgoraRtcKit import AgoraRtmKit final class Agora { static let shared = Agora() func initialize() { initRtcEngine() initRtmClient() } func deInit() { deInitRtcEngine() deInitRtmClient() } // MARK: RTC private var rtcEngine: AgoraRtcEngineKit? var rtcEngineDelegate: AgoraRtcEngineDelegate? func initRtcEngine() { rtcEngine = AgoraRtcEngineKit.sharedEngine(withAppId: AGORA_APP_ID, delegate: rtcEngineDelegate) rtcEngine?.setChannelProfile(.liveBroadcasting) rtcEngine?.enableAudio() rtcEngine?.enableAudioVolumeIndication(500, smooth: 3, reportVad: true) } func deInitRtcEngine() { if let rtcEngine = rtcEngine { rtcEngine.leaveChannel(nil) DispatchQueue.global(qos: .userInitiated).async { AgoraRtcEngineKit.destroy() } } rtcEngine = nil } func joinRtcChannel(rtcToken: String, channelName: String) { let userId = UserDefaults.int(forKey: .userId) rtcEngine?.joinChannel( byToken: rtcToken, channelId: channelName, info: nil, uid: UInt(userId), joinSuccess: nil ) rtcEngine?.setAudioProfile(.musicHighQualityStereo) rtcEngine?.setAudioScenario(.gameStreaming) } func setRole(role: AgoraClientRole) { self.rtcEngine?.setClientRole(role) } func mute(_ isMute: Bool) { rtcEngine?.muteLocalAudioStream(isMute) } func speakerMute(_ isMute: Bool) { rtcEngine?.muteAllRemoteAudioStreams(isMute) } func getRtcConnectionState() -> AgoraConnectionState { return rtcEngine!.getConnectionState() } // MARK: RTM private var rtmKit: AgoraRtmClientKit? private var roomChannelName: String? = nil // 상태 플래그: RTM 로그인 완료 여부 private var rtmLoggedIn: Bool = false // 상태 플래그: RTM 로그인 진행 중 여부 private var rtmLoginInProgress: Bool = false var rtmClientDelegate: AgoraRtmClientDelegate? func initRtmClient() { if rtmKit != nil { rtmKit?.logout() rtmKit?.destroy() rtmKit = nil } let userId = UserDefaults.int(forKey: .userId) let config = AgoraRtmClientConfig(appId: AGORA_APP_ID, userId: String(userId)) rtmKit = try? AgoraRtmClientKit(config, delegate: rtmClientDelegate) } func deInitRtmClient() { let userId = UserDefaults.int(forKey: .userId) let group = DispatchGroup() if let channel = roomChannelName { group.enter() rtmKit?.unsubscribe(channel) { [weak self] _, error in if let error = error { DEBUG_LOG("RTM unsubscribe fail - \(error.operation)") DEBUG_LOG("RTM unsubscribe fail - \(error.errorCode)") DEBUG_LOG("RTM unsubscribe fail - \(error.reason)") } else { DEBUG_LOG("RTM unsubscribe - \(channel)") self?.roomChannelName = nil } group.leave() } } group.enter() rtmKit?.unsubscribe("inbox_\(userId)") { _, error in if let error = error { DEBUG_LOG("RTM unsubscribe fail - \(error.operation)") DEBUG_LOG("RTM unsubscribe fail - \(error.errorCode)") DEBUG_LOG("RTM unsubscribe fail - \(error.reason)") } else { DEBUG_LOG("RTM unsubscribe - inbox_\(userId)") } group.leave() } group.notify(queue: .global(qos: .userInitiated)) { [weak self] in guard let self = self else { return } self.rtmKit?.logout() self.rtmKit?.destroy() self.rtmKit = nil self.rtmLoggedIn = false self.rtmLoginInProgress = false } } func rtmLogin( creatorId: Int, rtmToken: String, channelName: String, onConnectSuccess: @escaping (Bool) -> Void, onConnectFail: @escaping () -> Void ) { if rtmLoggedIn && roomChannelName == channelName { DEBUG_LOG("rtmLogin - already logged in and subscribed. skip") return } // 로그인 시도 중이면 재호출 방지 if (rtmLoginInProgress) { DEBUG_LOG("rtmLogin - already in progress. skip") return } roomChannelName = channelName func attemptLogin(_ attempt: Int) { rtmKit?.login(rtmToken) { [weak self] response, error in if let error = error { DEBUG_LOG("rtmClient login - fail (attempt=\(attempt)), \(error.reason)") if attempt < 4 { } else { self?.rtmLoginInProgress = false onConnectFail() } } else { DEBUG_LOG("rtmClient login - success (attempt=\(attempt))") // 로그인 성공 후 두 채널 구독 시도 self?.subscribeChannel( creatorId: creatorId, onConnectSuccess: onConnectSuccess, onConnectFail: onConnectFail ) } } } rtmLoginInProgress = true attemptLogin(1) } private func subscribeChannel( creatorId: Int, onConnectSuccess: @escaping (Bool) -> Void, onConnectFail: @escaping () -> Void ) { let targetRoom = roomChannelName if (targetRoom == nil) { DEBUG_LOG("subscribeChannel - roomChannelName is nil") onConnectFail() return } var completed = false var roomSubscribed = false var inboxSubscribed = false let userId = UserDefaults.int(forKey: .userId) func completeSuccessIfReady() { if (!completed && roomSubscribed && inboxSubscribed) { completed = true rtmLoggedIn = true rtmLoginInProgress = false DEBUG_LOG("RTM subscribe - both channels subscribed") if userId == creatorId { self.setRole(role: .broadcaster) } else { self.setRole(role: .audience) } onConnectSuccess(userId == creatorId) } } func failOnce(_ reason: String?) { if (!completed) { completed = true if let reason = reason { DEBUG_LOG("RTM subscribe failed: \(reason)") } else { DEBUG_LOG("RTM subscribe failed: nil") } onConnectFail() } } func subscribeRoom(_ attempt: Int) { DEBUG_LOG("RTM subscribe(room: \(targetRoom!)) attempt=\(attempt)") rtmKit?.subscribe(channelName: targetRoom!, option: nil) { _, error in if error != nil { DEBUG_LOG("RTM subscribe(room) failure at attempt=\(attempt) operation=\(error!.operation) reason=\(error!.reason) code=\(error!.errorCode)") if (attempt < 4) { subscribeRoom(attempt + 1) } else { failOnce("room subscribe failed after 3 retries (4 attempts)") } } else { DEBUG_LOG("RTM subscribe(room) success at attempt=\(attempt)") roomSubscribed = true completeSuccessIfReady() } } } func subscribeInbox(_ attempt: Int) { let inboxChannel = "inbox_\(userId)" DEBUG_LOG("RTM subscribe(inbox: \(inboxChannel)) attempt=\(attempt)") rtmKit?.subscribe(channelName: inboxChannel, option: nil) { _, error in if error != nil { DEBUG_LOG("RTM subscribe(inbox) failure at attempt=\(attempt) operation=\(error!.operation) reason=\(error!.reason) code=\(error!.errorCode)") if (attempt < 4) { subscribeInbox(attempt + 1) } else { failOnce("room subscribe failed after 3 retries (4 attempts)") } } else { DEBUG_LOG("RTM subscribe(inbox) success at attempt=\(attempt)") inboxSubscribed = true completeSuccessIfReady() } } } // 두 채널 구독을 병렬로 시도 subscribeRoom(1) subscribeInbox(1) } func sendMessageToPeer(peerId: String, rawMessage: Data, completion: AgoraRtmOperationBlock?) { rtmKit?.publish(channelName: "inbox_\(peerId)", data: rawMessage, option: nil, completion: completion) } func sendRawMessageToPeer(peerId: String, rawMessage: LiveRoomChatRawMessage, completion: AgoraRtmOperationBlock? = nil, fail: (() -> Void)? = nil) { let encoder = JSONEncoder() let jsonMessageData = try? encoder.encode(rawMessage) if let jsonMessageData = jsonMessageData { rtmKit?.publish(channelName: "inbox_\(peerId)", data: jsonMessageData, option: nil, completion: completion) } else { if let fail = fail { fail() } } } func sendMessageToGroup(textMessage: String, completion: @escaping AgoraRtmOperationBlock) { guard !textMessage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } guard let channelName = roomChannelName else { return } rtmKit?.publish(channelName: channelName, message: textMessage, option: nil, completion: completion) } func sendRawMessageToGroup(rawMessage: LiveRoomChatRawMessage, completion: AgoraRtmOperationBlock? = nil, fail: (() -> Void)? = nil) { let encoder = JSONEncoder() let jsonMessageData = try? encoder.encode(rawMessage) if let jsonMessageData = jsonMessageData, let channelName = roomChannelName { rtmKit?.publish(channelName: channelName, data: jsonMessageData, option: nil, completion: completion) } else { if let fail = fail { fail() } } } func isRtmLoggedIn() -> Bool { return rtmLoggedIn } }