live-room(agora): rtm version 1.5.3 -> 2.2.6

This commit is contained in:
2025-10-29 01:55:30 +09:00
parent f08c481807
commit 1ca6d068d0
4 changed files with 864 additions and 655 deletions

View File

@@ -32,6 +32,33 @@ android {
includeInBundle = false includeInBundle = false
} }
packaging {
// JNI(.so) 관련
jniLibs {
// pickFirsts: 충돌 시 첫 파일만 채택
pickFirsts += ["**/libaosl.so"]
}
// 일반 리소스(META-INF 등) 관련
resources {
// pickFirsts: 충돌 시 첫 파일만 채택
pickFirsts += [
"META-INF/LICENSE.txt",
"META-INF/NOTICE*"
]
// 자주 쓰는 제외/병합 예시
excludes += [
"META-INF/DEPENDENCIES",
"META-INF/AL2.0",
"META-INF/LGPL2.1"
]
merges += [
"META-INF/services/**"
]
}
}
defaultConfig { defaultConfig {
applicationId "kr.co.vividnext.sodalive" applicationId "kr.co.vividnext.sodalive"
minSdk 23 minSdk 23
@@ -168,8 +195,8 @@ dependencies {
implementation "io.github.bootpay:android:4.4.3" implementation "io.github.bootpay:android:4.4.3"
// agora // agora
implementation "io.agora.rtc:voice-sdk:4.6.0" implementation "io.agora.rtc:voice-sdk:4.2.6"
implementation 'io.agora.rtm:rtm-sdk:1.5.3' implementation 'io.agora:agora-rtm:2.2.6'
// Glide // Glide
implementation 'com.github.bumptech.glide:glide:5.0.5' implementation 'com.github.bumptech.glide:glide:5.0.5'

View File

@@ -6,28 +6,31 @@ import io.agora.rtc2.Constants
import io.agora.rtc2.IRtcEngineEventHandler import io.agora.rtc2.IRtcEngineEventHandler
import io.agora.rtc2.RtcEngine import io.agora.rtc2.RtcEngine
import io.agora.rtm.ErrorInfo import io.agora.rtm.ErrorInfo
import io.agora.rtm.GetOnlineUsersOptions
import io.agora.rtm.GetOnlineUsersResult
import io.agora.rtm.PublishOptions
import io.agora.rtm.ResultCallback import io.agora.rtm.ResultCallback
import io.agora.rtm.RtmChannel
import io.agora.rtm.RtmChannelListener
import io.agora.rtm.RtmClient import io.agora.rtm.RtmClient
import io.agora.rtm.RtmClientListener import io.agora.rtm.RtmConfig
import io.agora.rtm.SendMessageOptions import io.agora.rtm.RtmConstants
import io.agora.rtm.RtmEventListener
import io.agora.rtm.SubscribeOptions
import kr.co.vividnext.sodalive.BuildConfig import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.live.room.LiveRoomRequestType import kr.co.vividnext.sodalive.live.room.LiveRoomRequestType
import kotlin.concurrent.thread import kotlin.concurrent.thread
class Agora( class Agora(
private val uid: Long,
private val context: Context, private val context: Context,
private val rtcEventHandler: IRtcEngineEventHandler, private val rtcEventHandler: IRtcEngineEventHandler,
private val rtmClientListener: RtmClientListener private val rtmEventListener: RtmEventListener
) { ) {
// RTM client instance // 상태 플래그: RTM 로그인 완료 여부
private var rtmClient: RtmClient? = null private var rtmLoggedIn: Boolean = false
// RTM channel instance // 상태 플래그: RTM 로그인 진행 중 여부
private var rtmChannel: RtmChannel? = null private var rtmLoginInProgress: Boolean = false
private var rtcEngine: RtcEngine? = null
init { init {
initAgoraEngine() initAgoraEngine()
@@ -42,19 +45,23 @@ class Agora(
} }
} }
fun deInitAgoraEngine() { fun deInitAgoraEngine(rtmEventListener: RtmEventListener) {
deInitRtcEngine() deInitRtcEngine()
deInitRtmChannelAndClient() deInitRtmClient(rtmEventListener)
} }
// region RtcEngine // region RtcEngine
private var rtcEngine: RtcEngine? = null
@Throws(Exception::class) @Throws(Exception::class)
private fun initRtcEngine() { private fun initRtcEngine() {
Logger.e("initRtcEngine")
rtcEngine = RtcEngine.create( rtcEngine = RtcEngine.create(
context, context,
BuildConfig.AGORA_APP_ID, BuildConfig.AGORA_APP_ID,
rtcEventHandler rtcEventHandler
) )
Logger.e("initRtcEngine - rtcEngine: ${rtcEngine != null}")
rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING) rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
rtcEngine!!.setAudioProfile( rtcEngine!!.setAudioProfile(
@@ -66,6 +73,15 @@ class Agora(
} }
fun joinRtcChannel(uid: Int, rtcToken: String, channelName: String) { fun joinRtcChannel(uid: Int, rtcToken: String, channelName: String) {
val state = rtcEngine?.connectionState
val isDisconnected = state == null || state == Constants.CONNECTION_STATE_DISCONNECTED
if (!isDisconnected) {
Logger.e("joinRtcChannel - skip (state=$state)")
return
}
Logger.e("joinRtcChannel - proceed (state=$state) uid=$uid channel=$channelName")
rtcEngine!!.joinChannel( rtcEngine!!.joinChannel(
rtcToken, rtcToken,
channelName, channelName,
@@ -90,6 +106,10 @@ class Agora(
return rtcEngine!!.connectionState return rtcEngine!!.connectionState
} }
fun isRtmLoggedIn(): Boolean {
return rtmLoggedIn
}
fun deInitRtcEngine() { fun deInitRtcEngine() {
if (rtcEngine != null) { if (rtcEngine != null) {
rtcEngine!!.leaveChannel() rtcEngine!!.leaveChannel()
@@ -103,64 +123,179 @@ class Agora(
// endregion // endregion
// region RtmClient // region RtmClient
private var rtmClient: RtmClient? = null
private var roomChannelName: String? = null
@Throws(Exception::class) @Throws(Exception::class)
private fun initRtmClient() { private fun initRtmClient() {
rtmClient = RtmClient.createInstance( val rtmConfig = RtmConfig.Builder(BuildConfig.AGORA_APP_ID, uid.toString())
context, .eventListener(rtmEventListener)
BuildConfig.AGORA_APP_ID, .build()
rtmClientListener
) rtmClient = RtmClient.create(rtmConfig)
} }
fun createRtmChannelAndLogin( fun rtmLogin(
uid: String,
rtmToken: String, rtmToken: String,
channelName: String, channelName: String,
rtmChannelListener: RtmChannelListener,
rtmChannelJoinSuccess: () -> Unit, rtmChannelJoinSuccess: () -> Unit,
rtmChannelJoinFail: () -> Unit rtmChannelJoinFail: () -> Unit
) { ) {
rtmChannel = rtmClient!!.createChannel(channelName, rtmChannelListener) // 이미 RTM 로그인 및 구독이 완료된 경우 재호출 방지
rtmClient!!.login( if (rtmLoggedIn && roomChannelName == channelName) {
rtmToken, Logger.e("rtmLogin - already logged in and subscribed. skip")
uid, return
object : ResultCallback<Void> { }
override fun onSuccess(p0: Void?) { // 로그인 시도 중이면 재호출 방지
rtmChannel!!.join(object : ResultCallback<Void> { if (rtmLoginInProgress) {
override fun onSuccess(p0: Void?) { Logger.e("rtmLogin - already in progress. skip")
Logger.e("rtmChannel join - onSuccess") return
rtmChannelJoinSuccess() }
}
override fun onFailure(p0: ErrorInfo?) { roomChannelName = channelName
fun attemptLogin(attempt: Int) {
rtmClient!!.login(
rtmToken,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("rtmClient login - success (attempt=$attempt)")
// 로그인 성공 후 두 채널 구독 시도
subscribeChannel(rtmChannelJoinSuccess, rtmChannelJoinFail)
}
override fun onFailure(p0: ErrorInfo?) {
Logger.e("rtmClient login - fail (attempt=$attempt), ${p0?.errorReason}")
if (attempt < 4) {
attemptLogin(attempt + 1)
} else {
rtmLoginInProgress = false
rtmChannelJoinFail() rtmChannelJoinFail()
} }
}) }
} }
)
}
override fun onFailure(p0: ErrorInfo?) { rtmLoginInProgress = true
} attemptLogin(1)
}
)
} }
fun inputChat(message: String) { private fun subscribeChannel(
val rtmMessage = rtmClient!!.createMessage() rtmChannelJoinSuccess: () -> Unit,
rtmMessage.text = message rtmChannelJoinFail: () -> Unit
) {
val targetRoom = roomChannelName
if (targetRoom == null) {
Logger.e("subscribeChannel - roomChannelName is null")
rtmChannelJoinFail()
return
}
rtmChannel!!.sendMessage( var completed = false
rtmMessage, var roomSubscribed = false
object : ResultCallback<Void?> { var inboxSubscribed = false
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
}
override fun onFailure(p0: ErrorInfo) { fun completeSuccessIfReady() {
Logger.e("sendMessage fail - ${p0.errorCode}") if (!completed && roomSubscribed && inboxSubscribed) {
Logger.e("sendMessage fail - ${p0.errorDescription}") completed = true
} rtmLoggedIn = true
rtmLoginInProgress = false
Logger.e("RTM subscribe - both channels subscribed")
rtmChannelJoinSuccess()
} }
) }
fun failOnce(reason: String?) {
if (!completed) {
completed = true
Logger.e("RTM subscribe failed: $reason")
rtmChannelJoinFail()
}
}
fun subscribeRoom(attempt: Int) {
val channelOptions = SubscribeOptions()
channelOptions.withMessage = true
channelOptions.withPresence = true
Logger.e("RTM subscribe(room: $targetRoom) attempt=$attempt")
rtmClient!!.subscribe(
targetRoom,
channelOptions,
object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM subscribe(room) success at attempt=$attempt")
roomSubscribed = true
completeSuccessIfReady()
}
override fun onFailure(errorInfo: ErrorInfo?) {
Logger.e("RTM subscribe(room) failure at attempt=$attempt reason=${errorInfo?.errorReason}")
if (attempt < 4) {
subscribeRoom(attempt + 1)
} else {
failOnce("room subscribe failed after 3 retries (4 attempts)")
}
}
}
)
}
fun subscribeInbox(attempt: Int) {
val inboxChannel = "inbox_$uid"
val inboxChannelOptions = SubscribeOptions()
inboxChannelOptions.withMessage = true
Logger.e("RTM subscribe(inbox: $inboxChannel) attempt=$attempt")
rtmClient!!.subscribe(
inboxChannel,
inboxChannelOptions,
object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM subscribe(inbox) success at attempt=$attempt")
inboxSubscribed = true
completeSuccessIfReady()
}
override fun onFailure(errorInfo: ErrorInfo?) {
Logger.e("RTM subscribe(inbox) failure at attempt=$attempt reason=${errorInfo?.errorReason}")
if (attempt < 4) {
subscribeInbox(attempt + 1)
} else {
failOnce("inbox subscribe failed after 3 retries (4 attempts)")
}
}
}
)
}
// 두 채널 구독을 병렬로 시도
subscribeRoom(1)
subscribeInbox(1)
}
fun inputChat(message: String, onFailure: () -> Unit) {
if (roomChannelName != null) {
val options = PublishOptions()
options.setChannelType(RtmConstants.RtmChannelType.MESSAGE)
rtmClient!!.publish(
roomChannelName!!,
message,
options,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorReason}")
}
}
)
} else {
Logger.e("inputChat - roomChannelName is null")
onFailure()
}
} }
fun sendRawMessageToGroup( fun sendRawMessageToGroup(
@@ -168,23 +303,30 @@ class Agora(
onSuccess: (() -> Unit)? = null, onSuccess: (() -> Unit)? = null,
onFailure: (() -> Unit)? = null onFailure: (() -> Unit)? = null
) { ) {
val message = rtmClient!!.createMessage() if (roomChannelName != null) {
message.rawMessage = rawMessage val options = PublishOptions()
rtmChannel!!.sendMessage( options.customType = "ByteArray"
message, rtmClient!!.publish(
object : ResultCallback<Void?> { roomChannelName!!,
override fun onSuccess(p0: Void?) { rawMessage,
Logger.e("sendMessage - onSuccess") options,
onSuccess?.invoke() object : ResultCallback<Void> {
} override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
onSuccess?.invoke()
}
override fun onFailure(p0: ErrorInfo) { override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}") Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorDescription}") Logger.e("sendMessage fail - ${p0.errorReason}")
onFailure?.invoke() onFailure?.invoke()
}
} }
} )
) } else {
Logger.e("inputChat - roomChannelName is null")
onFailure?.invoke()
}
} }
fun sendRawMessageToPeer( fun sendRawMessageToPeer(
@@ -193,34 +335,71 @@ class Agora(
rawMessage: ByteArray? = null, rawMessage: ByteArray? = null,
onSuccess: () -> Unit onSuccess: () -> Unit
) { ) {
val option = SendMessageOptions() if (roomChannelName != null) {
val message = rawMessage ?: requestType.toString().toByteArray()
val options = PublishOptions()
options.customType = "ByteArray"
rtmClient!!.publish(
"inbox_$receiverUid",
message,
options,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
onSuccess()
}
val message = rtmClient!!.createMessage() override fun onFailure(p0: ErrorInfo) {
message.rawMessage = rawMessage ?: requestType.toString().toByteArray() Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorReason}")
}
}
)
} else {
Logger.e("inputChat - roomChannelName is null")
}
}
rtmClient!!.sendMessageToPeer( fun deInitRtmClient(rtmEventListener: RtmEventListener) {
receiverUid, rtmClient?.removeEventListener(rtmEventListener)
message, rtmClient?.unsubscribe(roomChannelName, object : ResultCallback<Void> {
option, override fun onSuccess(responseInfo: Void?) {
object : ResultCallback<Void?> { Logger.e("RTM unsubscribe - $roomChannelName")
override fun onSuccess(aVoid: Void?) { roomChannelName = null
onSuccess() }
override fun onFailure(errorInfo: ErrorInfo) {
Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}")
Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}")
}
})
rtmClient?.unsubscribe(
"inbox_${SharedPreferenceManager.userId}",
object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM unsubscribe - inbox_${SharedPreferenceManager.userId}")
} }
override fun onFailure(errorInfo: ErrorInfo) { override fun onFailure(errorInfo: ErrorInfo) {
Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}")
Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}")
} }
})
rtmClient?.logout(object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM logout")
rtmClient = null
} }
)
}
fun rtmChannelIsNull(): Boolean { override fun onFailure(errorInfo: ErrorInfo) {
return rtmChannel == null Logger.e("RTM logout fail - ${errorInfo.errorCode}")
} Logger.e("RTM logout fail - ${errorInfo.errorReason}")
}
})
// 상태 리셋
rtmLoggedIn = false
rtmLoginInProgress = false
fun deInitRtmChannelAndClient() {
rtmChannel?.leave(null)
rtmChannel?.release()
rtmClient?.logout(null)
} }
// endregion // endregion
} }

View File

@@ -86,7 +86,7 @@
android:gravity="center" android:gravity="center"
android:paddingVertical="16dp" android:paddingVertical="16dp"
android:text="취소" android:text="취소"
android:textColor="@color/color_9970ff" android:textColor="@color/color_3bb9f1"
android:textSize="18.3sp" /> android:textSize="18.3sp" />
<TextView <TextView