라이브룸 V2V 번역 자막 기능을 추가한다
라이브룸에서 진행자 언어와 기기 언어가 다를 때 자막 토글을 제공한다. 룸 정보 응답에 V2V 워커 토큰과 진행자 언어 코드를 포함한다. Agora V2V 에이전트 참여와 종료 API 연동을 추가한다.
This commit is contained in:
@@ -30,9 +30,11 @@ import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
@@ -109,8 +111,10 @@ import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity
|
||||
import kr.co.vividnext.sodalive.report.ProfileReportDialog
|
||||
import kr.co.vividnext.sodalive.report.ReportType
|
||||
import kr.co.vividnext.sodalive.report.UserReportDialog
|
||||
import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.random.Random
|
||||
@@ -135,6 +139,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
showLiveRoomUserProfileDialog(userId = userId)
|
||||
}
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private var rvChatBaseBottomMargin: Int? = null
|
||||
|
||||
private lateinit var agora: Agora
|
||||
private lateinit var roomDialog: LiveRoomDialog
|
||||
@@ -155,6 +160,25 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
// joinChannel 중복 호출 방지 플래그
|
||||
private var hasInvokedJoinChannel = false
|
||||
|
||||
private var v2vSourceLanguage: String? = null
|
||||
private var v2vTargetLanguage: String? = null
|
||||
private var isV2vAvailable = false
|
||||
private val v2vMessageCache = mutableMapOf<String, MutableList<V2vMessageChunk>>()
|
||||
private val v2vMessageCacheTimestamps = mutableMapOf<String, Long>()
|
||||
|
||||
private data class V2vMessageChunk(
|
||||
val partIdx: Int,
|
||||
val partSum: Int,
|
||||
val content: String
|
||||
)
|
||||
|
||||
private data class V2vChunkEnvelope(
|
||||
val messageId: String,
|
||||
val partIdx: Int,
|
||||
val partSum: Int,
|
||||
val content: String
|
||||
)
|
||||
|
||||
// region 채팅 금지
|
||||
private var isNoChatting = false
|
||||
private var remainingNoChattingTime = NO_CHATTING_TIME
|
||||
@@ -262,6 +286,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
cropper.cleanup()
|
||||
hideKeyboard {
|
||||
viewModel.quitRoom(roomId) {
|
||||
viewModel.stopV2vTranslation()
|
||||
SodaLiveService.stopService(this)
|
||||
agora.deInitAgoraEngine(rtmEventListener)
|
||||
RtmClient.release()
|
||||
@@ -515,6 +540,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
|
||||
binding.tvBgSwitch.setOnClickListener { viewModel.toggleBackgroundImage() }
|
||||
binding.tvSignatureSwitch.setOnClickListener { viewModel.toggleSignatureImage() }
|
||||
binding.tvV2vSignatureSwitch.setOnClickListener { toggleV2vCaption() }
|
||||
binding.llDonation.setOnClickListener {
|
||||
LiveRoomDonationRankingDialog(
|
||||
activity = this,
|
||||
@@ -723,6 +749,37 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isV2vCaptionOn.observe(this) { isOn ->
|
||||
if (isOn) {
|
||||
binding.tvV2vSignatureSwitch.text =
|
||||
getString(R.string.screen_live_room_v2v_signature_on_label)
|
||||
binding.tvV2vSignatureSwitch.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_3bb9f1
|
||||
)
|
||||
)
|
||||
binding.tvV2vSignatureSwitch
|
||||
.setBackgroundResource(R.drawable.bg_round_corner_5_3_transparent_3bb9f1)
|
||||
binding.tvV2vCaption.visibility = View.VISIBLE
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
} else {
|
||||
binding.tvV2vSignatureSwitch.text =
|
||||
getString(R.string.screen_live_room_v2v_signature_off_label)
|
||||
binding.tvV2vSignatureSwitch.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_eeeeee
|
||||
)
|
||||
)
|
||||
binding.tvV2vSignatureSwitch
|
||||
.setBackgroundResource(R.drawable.bg_round_corner_5_3_transparent_bbbbbb)
|
||||
binding.tvV2vCaption.text = ""
|
||||
binding.tvV2vCaption.visibility = View.GONE
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth)
|
||||
@@ -763,6 +820,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
|
||||
viewModel.roomInfoLiveData.observe(this) { response ->
|
||||
updateV2vAvailability(response)
|
||||
binding.ivShield.visibility = if (response.isAdult) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
@@ -1101,6 +1159,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
})
|
||||
rvChat.adapter = chatAdapter
|
||||
setupV2vCaptionOffset()
|
||||
rvChat.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (!rvChat.canScrollVertically(1)) {
|
||||
binding.tvNewChat.visibility = View.GONE
|
||||
@@ -1138,6 +1197,33 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
rvSpeakers.adapter = speakerListAdapter
|
||||
}
|
||||
|
||||
private fun setupV2vCaptionOffset() {
|
||||
binding.tvV2vCaption.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
}
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
}
|
||||
|
||||
private fun updateChatBottomMarginByV2vCaption() {
|
||||
val rvChat = binding.rvChat
|
||||
val layoutParams = rvChat.layoutParams as? ViewGroup.MarginLayoutParams ?: return
|
||||
val baseMargin = rvChatBaseBottomMargin ?: layoutParams.bottomMargin.also {
|
||||
rvChatBaseBottomMargin = it
|
||||
}
|
||||
|
||||
val captionHeight = if (binding.tvV2vCaption.visibility == View.VISIBLE) {
|
||||
binding.tvV2vCaption.height
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val targetBottomMargin = baseMargin + captionHeight
|
||||
|
||||
if (layoutParams.bottomMargin != targetBottomMargin) {
|
||||
layoutParams.bottomMargin = targetBottomMargin
|
||||
rvChat.layoutParams = layoutParams
|
||||
}
|
||||
}
|
||||
|
||||
private fun inviteSpeaker(peerId: Long) {
|
||||
agora.sendRawMessageToPeer(
|
||||
receiverUid = peerId.toString(),
|
||||
@@ -1487,6 +1573,17 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
Logger.e("onJoinChannelSuccess - uid: $uid, channel: $channel")
|
||||
}
|
||||
|
||||
override fun onStreamMessage(uid: Int, streamId: Int, data: ByteArray?) {
|
||||
super.onStreamMessage(uid, streamId, data)
|
||||
if (!isV2vAvailable || viewModel.isV2vCaptionOn.value != true) {
|
||||
return
|
||||
}
|
||||
|
||||
val rawMessage = data?.toString(Charsets.UTF_8)?.takeIf { it.isNotBlank() } ?: return
|
||||
Logger.d("v2v onStreamMessage: uid=$uid, streamId=$streamId, length=${rawMessage.length}")
|
||||
handleV2vStreamChunk(rawMessage)
|
||||
}
|
||||
|
||||
override fun onActiveSpeaker(uid: Int) {
|
||||
Logger.e("onActiveSpeaker - uid: $uid")
|
||||
super.onActiveSpeaker(uid)
|
||||
@@ -1814,6 +1911,194 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleV2vCaption() {
|
||||
if (!isV2vAvailable || !viewModel.isRoomInfoInitialized()) {
|
||||
return
|
||||
}
|
||||
|
||||
val isOn = viewModel.isV2vCaptionOn.value == true
|
||||
if (isOn) {
|
||||
viewModel.setV2vCaptionEnabled(false)
|
||||
viewModel.stopV2vTranslation()
|
||||
} else {
|
||||
val source = v2vSourceLanguage ?: return
|
||||
val target = v2vTargetLanguage ?: return
|
||||
viewModel.startV2vTranslation(
|
||||
roomInfo = viewModel.roomInfoResponse,
|
||||
sourceLanguage = source,
|
||||
targetLanguage = target,
|
||||
userId = SharedPreferenceManager.userId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateV2vAvailability(response: GetRoomInfoResponse) {
|
||||
val source = mapRoomLanguage(response.creatorLanguageCode)
|
||||
val target = mapDeviceLanguage(LanguageManager.getEffectiveLanguage(this))
|
||||
val available = !source.isNullOrBlank() && !target.isNullOrBlank() && source != target
|
||||
Logger.d(
|
||||
"v2v language mapping - creatorLanguageCode=${response.creatorLanguageCode}, " +
|
||||
"effectiveDeviceLanguage=${LanguageManager.getEffectiveLanguage(this)}, " +
|
||||
"source=$source, target=$target, available=$available"
|
||||
)
|
||||
|
||||
isV2vAvailable = available
|
||||
v2vSourceLanguage = if (available) source else null
|
||||
v2vTargetLanguage = if (available) target else null
|
||||
|
||||
binding.tvV2vSignatureSwitch.visibility = if (available) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (!available) {
|
||||
viewModel.setV2vCaptionEnabled(false)
|
||||
binding.tvV2vCaption.text = ""
|
||||
binding.tvV2vCaption.visibility = View.GONE
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
viewModel.stopV2vTranslation()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapRoomLanguage(languageCode: String?): String? {
|
||||
return when (languageCode?.trim()?.lowercase()) {
|
||||
"ko" -> "ko-KR"
|
||||
"en" -> "en-US"
|
||||
"ja" -> "ja-JP"
|
||||
else -> "ko-KR"
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapDeviceLanguage(languageCode: String): String? {
|
||||
return when (languageCode.lowercase()) {
|
||||
"ko" -> "ko-KR"
|
||||
"en" -> "en-US"
|
||||
"ja" -> "ja-JP"
|
||||
else -> "ja-JP"
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleV2vRawMessage(rawMessage: String) {
|
||||
Logger.d("v2v raw message before parse: $rawMessage")
|
||||
|
||||
try {
|
||||
val json = JSONObject(rawMessage)
|
||||
if (json.has("part_idx") && json.has("part_sum") && json.has("content")) {
|
||||
handleV2vChunk(json)
|
||||
return
|
||||
}
|
||||
handleV2vTranslation(json)
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to parse v2v message: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleV2vStreamChunk(rawMessage: String) {
|
||||
val chunk = parseV2vChunk(rawMessage)
|
||||
if (chunk == null) {
|
||||
handleV2vRawMessage(rawMessage)
|
||||
return
|
||||
}
|
||||
|
||||
clearExpiredV2vCache()
|
||||
|
||||
val chunks = v2vMessageCache.getOrPut(chunk.messageId) { mutableListOf() }
|
||||
if (chunks.none { it.partIdx == chunk.partIdx }) {
|
||||
chunks.add(V2vMessageChunk(chunk.partIdx, chunk.partSum, chunk.content))
|
||||
v2vMessageCacheTimestamps[chunk.messageId] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
if (chunks.size == chunk.partSum) {
|
||||
val fullContent = chunks.sortedBy { it.partIdx }
|
||||
.joinToString(separator = "") { it.content }
|
||||
v2vMessageCache.remove(chunk.messageId)
|
||||
v2vMessageCacheTimestamps.remove(chunk.messageId)
|
||||
val decoded = String(Base64.decode(fullContent, Base64.DEFAULT))
|
||||
handleV2vRawMessage(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseV2vChunk(rawMessage: String): V2vChunkEnvelope? {
|
||||
val parts = rawMessage.split("|", limit = 4)
|
||||
if (parts.size != 4) {
|
||||
return null
|
||||
}
|
||||
|
||||
val messageId = parts[0]
|
||||
val partIdx = parts[1].toIntOrNull()
|
||||
val partSum = parts[2].toIntOrNull()
|
||||
val content = parts[3]
|
||||
|
||||
if (messageId.isBlank() || partIdx == null || partSum == null || partIdx <= 0 || partSum <= 0 || content.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return V2vChunkEnvelope(
|
||||
messageId = messageId,
|
||||
partIdx = partIdx,
|
||||
partSum = partSum,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleV2vChunk(json: JSONObject) {
|
||||
val messageId = json.optString("message_id")
|
||||
val partIdx = json.optInt("part_idx", -1)
|
||||
val partSum = json.optInt("part_sum", -1)
|
||||
val content = json.optString("content")
|
||||
|
||||
if (messageId.isBlank() || partIdx < 0 || partSum <= 0 || content.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
clearExpiredV2vCache()
|
||||
|
||||
val chunks = v2vMessageCache.getOrPut(messageId) { mutableListOf() }
|
||||
if (chunks.none { it.partIdx == partIdx }) {
|
||||
chunks.add(V2vMessageChunk(partIdx, partSum, content))
|
||||
v2vMessageCacheTimestamps[messageId] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
if (chunks.size == partSum) {
|
||||
val fullContent = chunks.sortedBy { it.partIdx }
|
||||
.joinToString(separator = "") { it.content }
|
||||
v2vMessageCache.remove(messageId)
|
||||
v2vMessageCacheTimestamps.remove(messageId)
|
||||
val decoded = String(Base64.decode(fullContent, Base64.DEFAULT))
|
||||
handleV2vRawMessage(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearExpiredV2vCache() {
|
||||
val now = System.currentTimeMillis()
|
||||
val expiredKeys = v2vMessageCacheTimestamps
|
||||
.filter { now - it.value > 10_000 }
|
||||
.keys
|
||||
expiredKeys.forEach {
|
||||
v2vMessageCache.remove(it)
|
||||
v2vMessageCacheTimestamps.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleV2vTranslation(json: JSONObject) {
|
||||
val type = json.optString("object")
|
||||
if (type != "user.translation" && type != "agent.translation") {
|
||||
return
|
||||
}
|
||||
|
||||
val text = json.optString("text")
|
||||
if (text.isBlank() || viewModel.isV2vCaptionOn.value != true) {
|
||||
return
|
||||
}
|
||||
|
||||
handler.post {
|
||||
binding.tvV2vCaption.text = text
|
||||
binding.tvV2vCaption.visibility = View.VISIBLE
|
||||
updateChatBottomMarginByV2vCaption()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAgora() {
|
||||
agora = Agora(
|
||||
uid = SharedPreferenceManager.userId,
|
||||
|
||||
@@ -9,6 +9,14 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vAdvancedFeatures
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vAsr
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vJoinProperties
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vJoinRequest
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vParameters
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vRepository
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vTranslation
|
||||
import kr.co.vividnext.sodalive.agora.v2v.V2vTts
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
@@ -45,8 +53,11 @@ class LiveRoomViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val reportRepository: ReportRepository,
|
||||
private val rouletteRepository: RouletteRepository,
|
||||
private val userEventRepository: UserEventRepository
|
||||
private val userEventRepository: UserEventRepository,
|
||||
private val v2vRepository: V2vRepository
|
||||
) : BaseViewModel() {
|
||||
private val v2vPreset = "v2vt_base"
|
||||
|
||||
private val _roomInfoLiveData = MutableLiveData<GetRoomInfoResponse>()
|
||||
val roomInfoLiveData: LiveData<GetRoomInfoResponse>
|
||||
get() = _roomInfoLiveData
|
||||
@@ -99,6 +110,12 @@ class LiveRoomViewModel(
|
||||
val isSignatureOn: LiveData<Boolean>
|
||||
get() = _isSignatureOn
|
||||
|
||||
private var _isV2vCaptionOn = MutableLiveData(false)
|
||||
val isV2vCaptionOn: LiveData<Boolean>
|
||||
get() = _isV2vCaptionOn
|
||||
|
||||
private var v2vAgentId: String? = null
|
||||
|
||||
private val blockedMemberIdList: MutableList<Long> = mutableListOf()
|
||||
|
||||
// 메인 스레드 보장을 위한 Handler (postValue의 병합(coalescing) 이슈 방지 목적)
|
||||
@@ -275,6 +292,81 @@ class LiveRoomViewModel(
|
||||
blockedMemberIdList.add(memberId)
|
||||
}
|
||||
|
||||
fun setV2vCaptionEnabled(isEnabled: Boolean) {
|
||||
_isV2vCaptionOn.value = isEnabled
|
||||
}
|
||||
|
||||
fun startV2vTranslation(
|
||||
roomInfo: GetRoomInfoResponse,
|
||||
sourceLanguage: String,
|
||||
targetLanguage: String,
|
||||
userId: Long
|
||||
) {
|
||||
val agentRtcUid = "${userId}333"
|
||||
val request = V2vJoinRequest(
|
||||
name = "v2v-translation-agent-${System.currentTimeMillis()}",
|
||||
preset = v2vPreset,
|
||||
properties = V2vJoinProperties(
|
||||
channel = roomInfo.channelName,
|
||||
token = roomInfo.v2vWorkerToken,
|
||||
agentRtcUid = agentRtcUid,
|
||||
remoteRtcUids = listOf(roomInfo.creatorId.toString()),
|
||||
idleTimeout = 300,
|
||||
advancedFeatures = V2vAdvancedFeatures(enableRtm = false),
|
||||
parameters = V2vParameters(dataChannel = "datastream"),
|
||||
asr = V2vAsr(language = sourceLanguage),
|
||||
translation = V2vTranslation(language = targetLanguage),
|
||||
tts = V2vTts(enable = false)
|
||||
)
|
||||
)
|
||||
Logger.d(
|
||||
"v2v join request - roomId=${roomInfo.roomId}, creatorId=${roomInfo.creatorId}, " +
|
||||
"sourceLanguage=$sourceLanguage, targetLanguage=$targetLanguage"
|
||||
)
|
||||
|
||||
compositeDisposable.add(
|
||||
v2vRepository.join(request)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
v2vAgentId = it.agentId
|
||||
_isV2vCaptionOn.value = true
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.screen_live_room_unknown_error)
|
||||
)
|
||||
_isV2vCaptionOn.value = false
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun stopV2vTranslation() {
|
||||
val agentId = v2vAgentId ?: return
|
||||
compositeDisposable.add(
|
||||
v2vRepository.leave(agentId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
v2vAgentId = null
|
||||
_isV2vCaptionOn.value = false
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.screen_live_room_unknown_error)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun removeBlockedMember(memberId: Long) {
|
||||
blockedMemberIdList.remove(memberId)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ data class GetRoomInfoResponse(
|
||||
@SerializedName("channelName") val channelName: String,
|
||||
@SerializedName("rtcToken") val rtcToken: String,
|
||||
@SerializedName("rtmToken") val rtmToken: String,
|
||||
@SerializedName("v2vWorkerToken") val v2vWorkerToken: String,
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("creatorProfileUrl") val creatorProfileUrl: String,
|
||||
@@ -24,6 +25,7 @@ data class GetRoomInfoResponse(
|
||||
@SerializedName("managerList") val managerList: List<LiveRoomMember>,
|
||||
@SerializedName("donationRankingTop3UserIds") val donationRankingTop3UserIds: List<Long>,
|
||||
@SerializedName("menuPan") val menuPan: String,
|
||||
@SerializedName("creatorLanguageCode") val creatorLanguageCode: String?,
|
||||
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean,
|
||||
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
|
||||
@SerializedName("password") val password: String? = null
|
||||
|
||||
Reference in New Issue
Block a user