라이브룸 V2V 번역 자막 기능을 추가한다
라이브룸에서 진행자 언어와 기기 언어가 다를 때 자막 토글을 제공한다. 룸 정보 응답에 V2V 워커 토큰과 진행자 언어 코드를 포함한다. Agora V2V 에이전트 참여와 종료 API 연동을 추가한다
This commit is contained in:
@@ -34,6 +34,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||
private var agora: Agora = Agora.shared
|
||||
|
||||
private let repository = LiveRepository()
|
||||
private let v2vRepository: V2VRepository
|
||||
private let userRepository = UserRepository()
|
||||
private let reportRepository = ReportRepository()
|
||||
private let rouletteRepository = RouletteRepository()
|
||||
@@ -55,6 +56,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||
|
||||
@Published var isLoadingLikeHeart = false
|
||||
@Published var isLoading = false
|
||||
@Published var isV2VLoading = false
|
||||
@Published var errorMessage = ""
|
||||
@Published var reportMessage = ""
|
||||
@Published var isShowReportPopup = false
|
||||
@@ -283,9 +285,33 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||
private var blockedMemberIdList = Set<Int>()
|
||||
|
||||
private var hasInvokedJoinChannel = false
|
||||
private var v2vMessageAssembler = V2vMessageAssembler()
|
||||
private var v2vAgentId: String?
|
||||
private var v2vSourceLanguage: String?
|
||||
private var v2vTargetLanguage: String?
|
||||
private var isV2VJoinInProgress = false
|
||||
private var isV2VLeaveInProgress = false
|
||||
|
||||
@Published var isV2VAvailable = false
|
||||
@Published var isV2VCaptionOn = false
|
||||
@Published var v2vCaptionText = ""
|
||||
|
||||
// 로컬 BIG_HEART 발신자: 원격 물 채움 연출 억제 플래그
|
||||
private var suppressNextRemoteWaterFill = false
|
||||
|
||||
init(v2vRepository: V2VRepository = V2VRepositoryImpl()) {
|
||||
self.v2vRepository = v2vRepository
|
||||
super.init()
|
||||
}
|
||||
|
||||
private var isV2VJoined: Bool {
|
||||
v2vAgentId != nil
|
||||
}
|
||||
|
||||
func stopV2VTranslationIfJoined(clearCaptionText: Bool = true) {
|
||||
guard isV2VJoined else { return }
|
||||
stopV2VTranslation(clearCaptionText: clearCaptionText)
|
||||
}
|
||||
|
||||
func getBlockedMemberIdList() {
|
||||
userRepository.getBlockedMemberIdList()
|
||||
@@ -332,8 +358,149 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
private func deInitAgoraEngine() {
|
||||
stopV2VTranslationIfJoined()
|
||||
agora.deInit()
|
||||
}
|
||||
|
||||
func updateV2VAvailability(roomInfo: GetRoomInfoResponse?, deviceLanguage: String? = nil) {
|
||||
guard let roomInfo = roomInfo else {
|
||||
disableV2VIfNeeded()
|
||||
return
|
||||
}
|
||||
|
||||
let source = V2vLanguageMapper.mapToAgoraLanguage(roomInfo.creatorLanguageCode)
|
||||
let target = V2vLanguageMapper.mapToAgoraLanguage(deviceLanguage ?? V2vAppLanguageResolver.currentLanguageCode())
|
||||
let available = source != nil && target != nil && source != target
|
||||
|
||||
v2vSourceLanguage = source
|
||||
v2vTargetLanguage = target
|
||||
isV2VAvailable = available
|
||||
|
||||
if !available {
|
||||
disableV2VIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func toggleV2VCaption() {
|
||||
guard isV2VAvailable else { return }
|
||||
|
||||
if isV2VCaptionOn {
|
||||
stopV2VTranslation()
|
||||
} else {
|
||||
isV2VCaptionOn = true
|
||||
startV2VTranslation()
|
||||
}
|
||||
}
|
||||
|
||||
func startV2VTranslation() {
|
||||
guard !isV2VJoinInProgress,
|
||||
!isV2VLeaveInProgress,
|
||||
isV2VAvailable,
|
||||
v2vAgentId == nil,
|
||||
let roomInfo = liveRoomInfo,
|
||||
let sourceLanguage = v2vSourceLanguage,
|
||||
let targetLanguage = v2vTargetLanguage else {
|
||||
isV2VCaptionOn = false
|
||||
return
|
||||
}
|
||||
|
||||
isV2VJoinInProgress = true
|
||||
isV2VLoading = true
|
||||
|
||||
let request = V2VJoinRequest(
|
||||
roomInfo: roomInfo,
|
||||
sourceLanguage: sourceLanguage,
|
||||
targetLanguage: targetLanguage
|
||||
)
|
||||
|
||||
v2vRepository.join(request: request)
|
||||
.sink { [weak self] completion in
|
||||
guard let self else { return }
|
||||
self.isV2VJoinInProgress = false
|
||||
self.isV2VLoading = self.isV2VLeaveInProgress
|
||||
|
||||
if case .failure(let error) = completion {
|
||||
ERROR_LOG("[V2V] join failed: \(error)")
|
||||
self.isV2VCaptionOn = false
|
||||
self.errorMessage = error.userMessage
|
||||
self.isShowErrorPopup = true
|
||||
}
|
||||
} receiveValue: { [weak self] agentId in
|
||||
guard let self else { return }
|
||||
self.v2vAgentId = agentId
|
||||
DEBUG_LOG("[V2V] join success. agentId=\(agentId)")
|
||||
|
||||
if self.isV2VAvailable {
|
||||
self.isV2VCaptionOn = true
|
||||
} else {
|
||||
self.stopV2VTranslation()
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func stopV2VTranslation(clearCaptionText: Bool = true) {
|
||||
if clearCaptionText {
|
||||
v2vCaptionText = ""
|
||||
}
|
||||
isV2VCaptionOn = false
|
||||
v2vMessageAssembler.reset()
|
||||
|
||||
guard !isV2VLeaveInProgress,
|
||||
!isV2VJoinInProgress else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let agentId = v2vAgentId else {
|
||||
return
|
||||
}
|
||||
|
||||
DEBUG_LOG("[V2V] leave start. agentId=\(agentId)")
|
||||
|
||||
isV2VLeaveInProgress = true
|
||||
isV2VLoading = true
|
||||
|
||||
v2vRepository.leave(agentId: agentId)
|
||||
.sink { [weak self] completion in
|
||||
guard let self else { return }
|
||||
self.isV2VLeaveInProgress = false
|
||||
self.isV2VLoading = self.isV2VJoinInProgress
|
||||
|
||||
switch completion {
|
||||
case .finished:
|
||||
self.v2vAgentId = nil
|
||||
self.isV2VCaptionOn = false
|
||||
DEBUG_LOG("[V2V] leave success")
|
||||
|
||||
case .failure(let error):
|
||||
ERROR_LOG("[V2V] leave failed: \(error)")
|
||||
self.errorMessage = error.userMessage
|
||||
self.isShowErrorPopup = true
|
||||
}
|
||||
} receiveValue: { _ in }
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func handleV2VIncomingData(_ data: Data) {
|
||||
guard isV2VAvailable, isV2VCaptionOn, isV2VJoined else { return }
|
||||
|
||||
guard let parsedText = v2vMessageAssembler.consume(data: data) else {
|
||||
return
|
||||
}
|
||||
|
||||
let normalized = parsedText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !normalized.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
DEBUG_LOG("[V2V] subtitle received. text=\(normalized)")
|
||||
v2vCaptionText = normalized
|
||||
}
|
||||
|
||||
private func disableV2VIfNeeded() {
|
||||
isV2VAvailable = false
|
||||
stopV2VTranslation(clearCaptionText: true)
|
||||
}
|
||||
|
||||
func agoraConnectSuccess(isManager: Bool) {
|
||||
self.isLoading = false
|
||||
@@ -360,6 +527,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
func quitRoom() {
|
||||
stopV2VTranslationIfJoined()
|
||||
isLoading = true
|
||||
|
||||
if let index = muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) {
|
||||
@@ -424,6 +592,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.liveRoomInfo = data
|
||||
self.updateV2VAvailability(roomInfo: data)
|
||||
|
||||
if self.coverImageUrl != data.coverImageUrl {
|
||||
self.coverImageUrl = data.coverImageUrl
|
||||
@@ -2497,6 +2666,7 @@ extension LiveRoomViewModel: AgoraRtcEngineDelegate {
|
||||
DispatchQueue.main.async {[unowned self] in
|
||||
if uid == UInt(self.liveRoomInfo!.creatorId) {
|
||||
// 라이브 종료
|
||||
self.stopV2VTranslationIfJoined()
|
||||
self.liveRoomInfo = nil
|
||||
self.errorMessage = "라이브가 종료되었습니다."
|
||||
self.isShowErrorPopup = true
|
||||
@@ -2506,6 +2676,16 @@ extension LiveRoomViewModel: AgoraRtcEngineDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rtcEngine(_ engine: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self,
|
||||
self.isV2VJoined,
|
||||
self.isV2VCaptionOn else { return }
|
||||
DEBUG_LOG("[V2V] rtc stream message received uid=\(uid), streamId=\(streamId), size=\(data.count)")
|
||||
self.handleV2VIncomingData(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LiveRoomViewModel: AgoraRtmClientDelegate {
|
||||
@@ -2521,6 +2701,11 @@ extension LiveRoomViewModel: AgoraRtmClientDelegate {
|
||||
let rawMessageString = String(data: message, encoding: .utf8)
|
||||
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
if self.isV2VJoined && self.isV2VCaptionOn {
|
||||
DEBUG_LOG("[V2V] rtm raw message received size=\(message.count)")
|
||||
self.handleV2VIncomingData(message)
|
||||
}
|
||||
|
||||
if rawMessageString == LiveRoomRequestType.CHANGE_LISTENER.rawValue {
|
||||
self.setListener()
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user