修复麦位状态
This commit is contained in:
parent
e2e59eb1b2
commit
0fc752832e
@ -49,7 +49,7 @@ red-circle:
|
|||||||
noticeAccount: ${LIKEI_IM_NOTICE_ACCOUNT}
|
noticeAccount: ${LIKEI_IM_NOTICE_ACCOUNT}
|
||||||
newsletterAccount: ${LIKEI_IM_NEWSLETTER_ACCOUNT}
|
newsletterAccount: ${LIKEI_IM_NEWSLETTER_ACCOUNT}
|
||||||
agora:
|
agora:
|
||||||
appId: ${LIKEI_AGORA_APP_ID}
|
appId: ${LIKEI_RTC_APP_ID}
|
||||||
appCertificate: ${LIKEI_AGORA_APP_CERTIFICATE}
|
appCertificate: ${LIKEI_AGORA_APP_CERTIFICATE}
|
||||||
tokenExpireSecond: ${LIKEI_AGORA_TOKEN_EXPIRE_SECOND}
|
tokenExpireSecond: ${LIKEI_AGORA_TOKEN_EXPIRE_SECOND}
|
||||||
privilegeExpireSecond: ${LIKEI_AGORA_PRIVILEGE_EXPIRE_SECOND}
|
privilegeExpireSecond: ${LIKEI_AGORA_PRIVILEGE_EXPIRE_SECOND}
|
||||||
|
|||||||
@ -4,13 +4,15 @@ import com.red.circle.component.redis.service.RedisService;
|
|||||||
import com.red.circle.external.inner.endpoint.message.TrtcClient;
|
import com.red.circle.external.inner.endpoint.message.TrtcClient;
|
||||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||||
import com.red.circle.framework.core.response.CommonErrorCode;
|
import com.red.circle.framework.core.response.CommonErrorCode;
|
||||||
|
import com.red.circle.framework.dto.ResultResponse;
|
||||||
import com.red.circle.live.app.convertor.LiveMicAppConvertor;
|
import com.red.circle.live.app.convertor.LiveMicAppConvertor;
|
||||||
import com.red.circle.live.app.dto.clientobject.LiveMicrophoneCO;
|
import com.red.circle.live.app.dto.clientobject.LiveMicrophoneCO;
|
||||||
import com.red.circle.live.app.dto.cmd.MicUpDownCmd;
|
import com.red.circle.live.app.dto.cmd.MicUpDownCmd;
|
||||||
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
|
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
|
||||||
import com.red.circle.live.domain.live.dto.LiveMicrophoneNoticeDTO;
|
import com.red.circle.live.domain.live.dto.LiveMicrophoneNoticeDTO;
|
||||||
import com.red.circle.live.infra.database.cache.service.UserAgoraTokenCacheService;
|
import com.red.circle.live.infra.database.cache.service.LiveMicCacheService;
|
||||||
import com.red.circle.live.infra.database.mongo.service.LiveMicSettingService;
|
import com.red.circle.live.infra.database.cache.service.UserAgoraTokenCacheService;
|
||||||
|
import com.red.circle.live.infra.database.mongo.service.LiveMicSettingService;
|
||||||
import com.red.circle.live.infra.util.DateTimeAsiaRiyadhUtils;
|
import com.red.circle.live.infra.util.DateTimeAsiaRiyadhUtils;
|
||||||
import com.red.circle.live.inner.error.LiveMicErrorCode;
|
import com.red.circle.live.inner.error.LiveMicErrorCode;
|
||||||
import com.red.circle.other.inner.endpoint.live.RoomManagerClient;
|
import com.red.circle.other.inner.endpoint.live.RoomManagerClient;
|
||||||
@ -43,41 +45,95 @@ public class LiveMicGoUpCmdExe {
|
|||||||
private static final Logger log = LoggerFactory.getLogger(LiveMicGoUpCmdExe.class);
|
private static final Logger log = LoggerFactory.getLogger(LiveMicGoUpCmdExe.class);
|
||||||
private final LiveMicAppConvertor liveMicAppConvertor;
|
private final LiveMicAppConvertor liveMicAppConvertor;
|
||||||
private final LiveMicSettingService liveMicSettingService;
|
private final LiveMicSettingService liveMicSettingService;
|
||||||
private final LiveMicrophoneGateway liveMicrophoneGateway;
|
private final LiveMicrophoneGateway liveMicrophoneGateway;
|
||||||
private final TrtcClient trtcClient;
|
private final LiveMicCacheService liveMicCacheService;
|
||||||
private final UserAgoraTokenCacheService userAgoraTokenCacheService;
|
private final TrtcClient trtcClient;
|
||||||
|
private final UserAgoraTokenCacheService userAgoraTokenCacheService;
|
||||||
private final RedisService redisService;
|
private final RedisService redisService;
|
||||||
private final RoomDailyTaskClient roomDailyTaskClient;
|
private final RoomDailyTaskClient roomDailyTaskClient;
|
||||||
private final RoomManagerClient roomManagerClient;
|
private final RoomManagerClient roomManagerClient;
|
||||||
private final RedisTemplate redisTemplate;
|
private final RedisTemplate redisTemplate;
|
||||||
private final RoomMemberClient roomMemberClient;
|
private final RoomMemberClient roomMemberClient;
|
||||||
|
|
||||||
public LiveMicrophoneCO execute(MicUpDownCmd cmd) {
|
public LiveMicrophoneCO execute(MicUpDownCmd cmd) {
|
||||||
checkPermissions(cmd);
|
boolean targetMicAlreadySelf = false;
|
||||||
LiveMicrophoneNoticeDTO noticeDTO = liveMicrophoneGateway.goUp(
|
boolean goUpCompleted = false;
|
||||||
|
try {
|
||||||
|
checkPermissions(cmd);
|
||||||
|
targetMicAlreadySelf = checkTargetMicAvailableOrSelf(cmd);
|
||||||
|
String roomToken = createPublisherToken(cmd);
|
||||||
|
|
||||||
|
LiveMicrophoneNoticeDTO noticeDTO = liveMicrophoneGateway.goUp(
|
||||||
|
cmd.getRoomId(),
|
||||||
|
cmd.getMickIndex(),
|
||||||
|
cmd.requiredReqUserId()
|
||||||
|
);
|
||||||
|
goUpCompleted = true;
|
||||||
|
|
||||||
|
cachePublisherToken(cmd, roomToken);
|
||||||
|
LiveMicrophoneCO notice = returnMicrophoneCO(cmd, noticeDTO);
|
||||||
|
notice.setRoomToken(roomToken);
|
||||||
|
|
||||||
|
// 房主邀请成员上麦
|
||||||
|
handleRoomDailyTask(cmd);
|
||||||
|
return notice;
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
if (goUpCompleted) {
|
||||||
|
rollbackPublisherToken(cmd);
|
||||||
|
if (!targetMicAlreadySelf) {
|
||||||
|
rollbackMicSeat(cmd, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createPublisherToken(MicUpDownCmd cmd) {
|
||||||
|
ResultResponse<String> response = trtcClient.createAgoraTokenGoUp(
|
||||||
|
cmd.requiredReqUserId(),
|
||||||
|
cmd.getRoomId().toString()
|
||||||
|
);
|
||||||
|
String roomToken = Objects.nonNull(response) ? response.getBody() : null;
|
||||||
|
ResponseAssert.notNull(CommonErrorCode.THIRD_PARTY_SERVER_ERROR, roomToken);
|
||||||
|
return roomToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cachePublisherToken(MicUpDownCmd cmd, String roomToken) {
|
||||||
|
userAgoraTokenCacheService.remove(cmd.requiredReqUserId(), cmd.getRoomId());
|
||||||
|
userAgoraTokenCacheService.save(roomToken, cmd.requiredReqUserId(), cmd.getRoomId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkTargetMicAvailableOrSelf(MicUpDownCmd cmd) {
|
||||||
|
boolean targetMicAlreadySelf = liveMicCacheService.checkEqSelf(
|
||||||
cmd.getRoomId(),
|
cmd.getRoomId(),
|
||||||
cmd.getMickIndex(),
|
cmd.getMickIndex(),
|
||||||
cmd.requiredReqUserId()
|
cmd.requiredReqUserId()
|
||||||
);
|
);
|
||||||
LiveMicrophoneCO notice = returnMicrophoneCO(cmd, noticeDTO);
|
if (targetMicAlreadySelf) {
|
||||||
notice.setRoomToken(refreshPublisherToken(cmd));
|
return true;
|
||||||
//notice.setRoomToken(createAgoraTokenGoUp(cmd));
|
}
|
||||||
|
ResponseAssert.isTrue(LiveMicErrorCode.MICROPHONE_OCCUPIED,
|
||||||
// 房主邀请成员上麦
|
liveMicCacheService.checkAvailable(cmd.getRoomId(), cmd.getMickIndex()));
|
||||||
handleRoomDailyTask(cmd);
|
return false;
|
||||||
|
|
||||||
return notice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String refreshPublisherToken(MicUpDownCmd cmd) {
|
private void rollbackPublisherToken(MicUpDownCmd cmd) {
|
||||||
String roomToken = trtcClient.createAgoraTokenGoUp(
|
try {
|
||||||
cmd.getReqUserId(),
|
userAgoraTokenCacheService.remove(cmd.requiredReqUserId(), cmd.getRoomId());
|
||||||
cmd.getRoomId().toString()
|
} catch (RuntimeException rollbackException) {
|
||||||
).getBody();
|
log.warn("上麦失败回滚 publisher token 失败 roomId={}, userId={}",
|
||||||
ResponseAssert.notNull(CommonErrorCode.THIRD_PARTY_SERVER_ERROR, roomToken);
|
cmd.getRoomId(), cmd.requiredReqUserId(), rollbackException);
|
||||||
userAgoraTokenCacheService.remove(cmd.getReqUserId(), cmd.getRoomId());
|
}
|
||||||
userAgoraTokenCacheService.save(roomToken, cmd.getReqUserId(), cmd.getRoomId());
|
}
|
||||||
return roomToken;
|
|
||||||
|
private void rollbackMicSeat(MicUpDownCmd cmd, RuntimeException cause) {
|
||||||
|
try {
|
||||||
|
liveMicrophoneGateway.goDown(cmd.getRoomId(), cmd.getMickIndex(), cmd.requiredReqUserId());
|
||||||
|
} catch (RuntimeException rollbackException) {
|
||||||
|
log.warn("上麦失败回滚麦位失败 roomId={}, micIndex={}, userId={}, cause={}",
|
||||||
|
cmd.getRoomId(), cmd.getMickIndex(), cmd.requiredReqUserId(), cause.getMessage(),
|
||||||
|
rollbackException);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleRoomDailyTask(MicUpDownCmd cmd) {
|
private void handleRoomDailyTask(MicUpDownCmd cmd) {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import com.red.circle.component.redis.service.RedisService;
|
|||||||
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
|
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
|
||||||
import com.red.circle.framework.dto.ResultResponse;
|
import com.red.circle.framework.dto.ResultResponse;
|
||||||
import com.red.circle.live.app.dto.cmd.HeartbeatStatusEnum;
|
import com.red.circle.live.app.dto.cmd.HeartbeatStatusEnum;
|
||||||
|
import com.red.circle.live.app.service.AgoraMicMissingConfirmService;
|
||||||
import com.red.circle.live.app.service.AgoraMicStateCleanupService;
|
import com.red.circle.live.app.service.AgoraMicStateCleanupService;
|
||||||
import com.red.circle.live.app.service.AgoraRoomStateService;
|
import com.red.circle.live.app.service.AgoraRoomStateService;
|
||||||
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
|
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
|
||||||
@ -51,8 +52,7 @@ import java.util.stream.Collectors;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.annotation.Profile;
|
import org.springframework.context.annotation.Profile;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户心跳.
|
* 用户心跳.
|
||||||
@ -78,6 +78,7 @@ public class UserHeartbeatListener implements MessageListener {
|
|||||||
private final RedisService redisService;
|
private final RedisService redisService;
|
||||||
private final RoomMemberClient roomMemberClient;
|
private final RoomMemberClient roomMemberClient;
|
||||||
private final AgoraRoomStateService agoraRoomStateService;
|
private final AgoraRoomStateService agoraRoomStateService;
|
||||||
|
private final AgoraMicMissingConfirmService agoraMicMissingConfirmService;
|
||||||
private final AgoraMicStateCleanupService agoraMicStateCleanupService;
|
private final AgoraMicStateCleanupService agoraMicStateCleanupService;
|
||||||
|
|
||||||
private static final int HEARTBEAT_INTERVAL_SECONDS = 60;
|
private static final int HEARTBEAT_INTERVAL_SECONDS = 60;
|
||||||
@ -214,10 +215,11 @@ public class UserHeartbeatListener implements MessageListener {
|
|||||||
UserProfileDTO actualUserProfile = ensureUserProfile(userProfile);
|
UserProfileDTO actualUserProfile = ensureUserProfile(userProfile);
|
||||||
AgoraChannelStateDTO channelState = agoraRoomStateService.getChannelState(roomId);
|
AgoraChannelStateDTO channelState = agoraRoomStateService.getChannelState(roomId);
|
||||||
if (channelState.querySuccess() && !isUserInAgoraChannel(channelState, actualUserProfile)) {
|
if (channelState.querySuccess() && !isUserInAgoraChannel(channelState, actualUserProfile)) {
|
||||||
agoraMicStateCleanupService.cleanupUser(roomId, actualUserProfile.getId(),
|
handleAgoraUserMissing(roomId, actualUserProfile.getId(), channelState,
|
||||||
"heartbeat agora user missing");
|
"heartbeat agora user missing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
clearAgoraMissingAfterPresent(channelState, roomId, actualUserProfile.getId());
|
||||||
|
|
||||||
// 添加房间在线用户
|
// 添加房间在线用户
|
||||||
addRoomOnlineUser(actualUserProfile, roomProfile);
|
addRoomOnlineUser(actualUserProfile, roomProfile);
|
||||||
@ -314,10 +316,11 @@ public class UserHeartbeatListener implements MessageListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isUserInAgoraChannel(channelState, userProfile)) {
|
if (!isUserInAgoraChannel(channelState, userProfile)) {
|
||||||
agoraMicStateCleanupService.cleanupUser(roomProfile.getId(), userProfile.getId(),
|
handleAgoraUserMissing(roomProfile.getId(), userProfile.getId(), channelState,
|
||||||
"heartbeat agora user missing");
|
"heartbeat agora user missing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
clearAgoraMissingAfterPresent(channelState, roomProfile.getId(), userProfile.getId());
|
||||||
|
|
||||||
refreshMicActiveTimeWithoutAgoraCheck(event, roomProfile, liveMicrophone);
|
refreshMicActiveTimeWithoutAgoraCheck(event, roomProfile, liveMicrophone);
|
||||||
}
|
}
|
||||||
@ -370,4 +373,28 @@ public class UserHeartbeatListener implements MessageListener {
|
|||||||
&& channelState.getAudience().contains(agoraUid));
|
&& channelState.getAudience().contains(agoraUid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleAgoraUserMissing(Long roomId,
|
||||||
|
Long userId,
|
||||||
|
AgoraChannelStateDTO channelState,
|
||||||
|
String reason) {
|
||||||
|
if (!agoraMicMissingConfirmService.markMissingAndShouldCleanup(roomId, userId,
|
||||||
|
appendChannelStateReason(channelState, reason))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
agoraMicStateCleanupService.cleanupUser(roomId, userId, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAgoraMissingAfterPresent(AgoraChannelStateDTO channelState, Long roomId, Long userId) {
|
||||||
|
if (Objects.nonNull(channelState) && channelState.querySuccess()) {
|
||||||
|
agoraMicMissingConfirmService.clearMissing(roomId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String appendChannelStateReason(AgoraChannelStateDTO channelState, String reason) {
|
||||||
|
if (Objects.isNull(channelState)) {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
return reason + ", channelExist=" + channelState.channelExist();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.red.circle.live.app.scheduler;
|
|||||||
|
|
||||||
import com.red.circle.component.redis.service.RedisService;
|
import com.red.circle.component.redis.service.RedisService;
|
||||||
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
|
import com.red.circle.external.inner.model.dto.AgoraChannelStateDTO;
|
||||||
|
import com.red.circle.live.app.service.AgoraMicMissingConfirmService;
|
||||||
import com.red.circle.live.app.service.AgoraMicStateCleanupService;
|
import com.red.circle.live.app.service.AgoraMicStateCleanupService;
|
||||||
import com.red.circle.live.app.service.AgoraRoomStateService;
|
import com.red.circle.live.app.service.AgoraRoomStateService;
|
||||||
import com.red.circle.live.domain.live.LiveMicrophone;
|
import com.red.circle.live.domain.live.LiveMicrophone;
|
||||||
@ -35,6 +36,7 @@ public class AgoraMicStateReconcileTask {
|
|||||||
private final RedisTemplate<String, Object> redisTemplate;
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
private final LiveMicCacheService liveMicCacheService;
|
private final LiveMicCacheService liveMicCacheService;
|
||||||
private final AgoraRoomStateService agoraRoomStateService;
|
private final AgoraRoomStateService agoraRoomStateService;
|
||||||
|
private final AgoraMicMissingConfirmService agoraMicMissingConfirmService;
|
||||||
private final AgoraMicStateCleanupService agoraMicStateCleanupService;
|
private final AgoraMicStateCleanupService agoraMicStateCleanupService;
|
||||||
|
|
||||||
@Scheduled(fixedDelayString = "${red-circle.live.agora-mic-reconcile-delay-ms:30000}")
|
@Scheduled(fixedDelayString = "${red-circle.live.agora-mic-reconcile-delay-ms:30000}")
|
||||||
@ -62,13 +64,15 @@ public class AgoraMicStateReconcileTask {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.channelExist()) {
|
List<LiveMicrophone> microphones = liveMicCacheService.listLiveMicrophone(roomId);
|
||||||
agoraMicStateCleanupService.cleanupRoom(roomId, "reconcile agora channel not exist");
|
if (CollectionUtils.isEmpty(microphones)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<LiveMicrophone> microphones = liveMicCacheService.listLiveMicrophone(roomId);
|
if (!state.channelExist()) {
|
||||||
if (CollectionUtils.isEmpty(microphones)) {
|
for (LiveMicrophone microphone : microphones) {
|
||||||
|
cleanupMissingMicrophoneUser(roomId, microphone, state, "reconcile agora channel not exist");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,12 +89,30 @@ public class AgoraMicStateReconcileTask {
|
|||||||
}
|
}
|
||||||
Long agoraUid = DataTypeUtils.toLong(microphone.getUser().getAccount());
|
Long agoraUid = DataTypeUtils.toLong(microphone.getUser().getAccount());
|
||||||
if (Objects.isNull(agoraUid) || !channelUids.contains(agoraUid)) {
|
if (Objects.isNull(agoraUid) || !channelUids.contains(agoraUid)) {
|
||||||
agoraMicStateCleanupService.cleanupUser(roomId, microphone.getUser().getId(),
|
cleanupMissingMicrophoneUser(roomId, microphone, state, "reconcile agora user missing");
|
||||||
"reconcile agora user missing");
|
} else {
|
||||||
|
agoraMicMissingConfirmService.clearMissing(roomId, microphone.getUser().getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cleanupMissingMicrophoneUser(Long roomId,
|
||||||
|
LiveMicrophone microphone,
|
||||||
|
AgoraChannelStateDTO state,
|
||||||
|
String reason) {
|
||||||
|
if (Objects.isNull(microphone)
|
||||||
|
|| Objects.isNull(microphone.getUser())
|
||||||
|
|| Objects.isNull(microphone.getUser().getId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Long userId = microphone.getUser().getId();
|
||||||
|
if (!agoraMicMissingConfirmService.markMissingAndShouldCleanup(roomId, userId,
|
||||||
|
reason + ", channelExist=" + state.channelExist())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
agoraMicStateCleanupService.cleanupUser(roomId, userId, reason);
|
||||||
|
}
|
||||||
|
|
||||||
private Set<Long> scanSeatRoomIds() {
|
private Set<Long> scanSeatRoomIds() {
|
||||||
Set<Long> roomIds = new HashSet<>();
|
Set<Long> roomIds = new HashSet<>();
|
||||||
ScanOptions options = ScanOptions.scanOptions().match(SEAT_KEY_PREFIX + "*").count(200).build();
|
ScanOptions options = ScanOptions.scanOptions().match(SEAT_KEY_PREFIX + "*").count(200).build();
|
||||||
|
|||||||
@ -0,0 +1,102 @@
|
|||||||
|
package com.red.circle.live.app.service;
|
||||||
|
|
||||||
|
import com.red.circle.component.redis.service.RedisService;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 声网 missing 状态二次确认.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AgoraMicMissingConfirmService {
|
||||||
|
|
||||||
|
private static final String KEY_PREFIX = "agora:mic:missing:";
|
||||||
|
private static final int MISSING_TTL_SECONDS = 10;
|
||||||
|
private static final int CONFIRM_MISSING_COUNT = 2;
|
||||||
|
private static final long CONFIRM_MISSING_AFTER_MILLIS = 5_000L;
|
||||||
|
|
||||||
|
private final RedisService redisService;
|
||||||
|
|
||||||
|
public boolean markMissingAndShouldCleanup(Long roomId, Long userId, String reason) {
|
||||||
|
if (Objects.isNull(roomId) || Objects.isNull(userId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = missingKey(roomId, userId);
|
||||||
|
long nowMillis = System.currentTimeMillis();
|
||||||
|
MissingState nextState = MissingState.next(redisService.getString(key), nowMillis);
|
||||||
|
if (nextState.shouldCleanup(nowMillis)) {
|
||||||
|
redisService.delete(key);
|
||||||
|
log.warn("声网 missing 已二次确认,允许清理 roomId={}, userId={}, count={}, firstMissingAt={}, reason={}",
|
||||||
|
roomId, userId, nextState.count, nextState.firstMissingAtMillis, reason);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
redisService.setString(key, nextState.serialize(), MISSING_TTL_SECONDS, TimeUnit.SECONDS);
|
||||||
|
log.warn("声网 missing 首次缓冲,跳过本次清理和刷新 roomId={}, userId={}, count={}, firstMissingAt={}, reason={}",
|
||||||
|
roomId, userId, nextState.count, nextState.firstMissingAtMillis, reason);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearMissing(Long roomId, Long userId) {
|
||||||
|
if (Objects.isNull(roomId) || Objects.isNull(userId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisService.delete(missingKey(roomId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String missingKey(Long roomId, Long userId) {
|
||||||
|
return KEY_PREFIX + roomId + ":" + userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class MissingState {
|
||||||
|
|
||||||
|
private final int count;
|
||||||
|
private final long firstMissingAtMillis;
|
||||||
|
|
||||||
|
private MissingState(int count, long firstMissingAtMillis) {
|
||||||
|
this.count = count;
|
||||||
|
this.firstMissingAtMillis = firstMissingAtMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MissingState next(String rawValue, long nowMillis) {
|
||||||
|
MissingState current = parse(rawValue, nowMillis);
|
||||||
|
long firstMissingAtMillis = current.count <= 0 ? nowMillis : current.firstMissingAtMillis;
|
||||||
|
return new MissingState(current.count + 1, firstMissingAtMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean shouldCleanup(long nowMillis) {
|
||||||
|
return count >= CONFIRM_MISSING_COUNT
|
||||||
|
|| nowMillis - firstMissingAtMillis >= CONFIRM_MISSING_AFTER_MILLIS;
|
||||||
|
}
|
||||||
|
|
||||||
|
String serialize() {
|
||||||
|
return count + ":" + firstMissingAtMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MissingState parse(String rawValue, long nowMillis) {
|
||||||
|
if (Objects.isNull(rawValue) || rawValue.isBlank()) {
|
||||||
|
return new MissingState(0, nowMillis);
|
||||||
|
}
|
||||||
|
String[] parts = rawValue.split(":", 2);
|
||||||
|
if (parts.length != 2) {
|
||||||
|
return new MissingState(0, nowMillis);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int count = Integer.parseInt(parts[0]);
|
||||||
|
long firstMissingAtMillis = Long.parseLong(parts[1]);
|
||||||
|
if (count <= 0 || firstMissingAtMillis <= 0 || firstMissingAtMillis > nowMillis) {
|
||||||
|
return new MissingState(0, nowMillis);
|
||||||
|
}
|
||||||
|
return new MissingState(count, firstMissingAtMillis);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return new MissingState(0, nowMillis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,222 @@
|
|||||||
|
package com.red.circle.live.app.command.mic;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import com.red.circle.external.inner.endpoint.message.TrtcClient;
|
||||||
|
import com.red.circle.framework.dto.ResultResponse;
|
||||||
|
import com.red.circle.live.app.convertor.LiveMicAppConvertor;
|
||||||
|
import com.red.circle.live.app.dto.clientobject.LiveMicUserCO;
|
||||||
|
import com.red.circle.live.app.dto.clientobject.LiveMicrophoneCO;
|
||||||
|
import com.red.circle.live.app.dto.cmd.MicUpDownCmd;
|
||||||
|
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
|
||||||
|
import com.red.circle.live.domain.live.LiveMicUser;
|
||||||
|
import com.red.circle.live.domain.live.LiveMicrophone;
|
||||||
|
import com.red.circle.live.domain.live.dto.LiveMicrophoneDTO;
|
||||||
|
import com.red.circle.live.domain.live.dto.LiveMicrophoneNoticeDTO;
|
||||||
|
import com.red.circle.live.infra.database.cache.service.LiveMicCacheService;
|
||||||
|
import com.red.circle.live.infra.database.cache.service.UserAgoraTokenCacheService;
|
||||||
|
import com.red.circle.live.infra.database.mongo.entity.LiveMicSetting;
|
||||||
|
import com.red.circle.live.infra.database.mongo.entity.LiveMicSettingSeat;
|
||||||
|
import com.red.circle.live.infra.database.mongo.service.LiveMicSettingService;
|
||||||
|
import com.red.circle.other.inner.endpoint.live.RoomManagerClient;
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class LiveMicGoUpCmdExeTest {
|
||||||
|
|
||||||
|
private static final Long ROOM_ID = 1001L;
|
||||||
|
private static final Long USER_ID = 2002L;
|
||||||
|
private static final Integer MIC_INDEX = 1;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void executeShouldCreateTokenBeforeGoUpAndCacheAfterGoUp() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
|
||||||
|
LiveMicrophoneCO result = fixture.exe.execute(command());
|
||||||
|
|
||||||
|
assertEquals("publisher-token", result.getRoomToken());
|
||||||
|
assertEquals(List.of("checkEqSelf", "checkAvailable", "createToken", "goUp", "removeToken",
|
||||||
|
"saveToken", "convert", "getRoom"), fixture.calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void executeShouldRollbackMicAndTokenWhenCacheFailsAfterGoUp() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
RuntimeException cacheException = new RuntimeException("cache failed");
|
||||||
|
fixture.cacheException = cacheException;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fixture.exe.execute(command());
|
||||||
|
fail("Expected cache exception");
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
assertSame(cacheException, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(List.of("checkEqSelf", "checkAvailable", "createToken", "goUp", "removeToken",
|
||||||
|
"saveToken", "removeToken", "goDown"), fixture.calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MicUpDownCmd command() {
|
||||||
|
MicUpDownCmd cmd = new MicUpDownCmd()
|
||||||
|
.setRoomId(ROOM_ID)
|
||||||
|
.setMickIndex(MIC_INDEX);
|
||||||
|
cmd.setReqUserId(USER_ID);
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LiveMicrophoneNoticeDTO notice() {
|
||||||
|
return new LiveMicrophoneNoticeDTO()
|
||||||
|
.setMics(List.of(new LiveMicrophoneDTO().setRoomId(ROOM_ID).setMicIndex(MIC_INDEX)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> T proxy(Class<T> type, InvocationHandler handler) {
|
||||||
|
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[] {type},
|
||||||
|
(Object proxy, Method method, Object[] args) -> {
|
||||||
|
if (method.getDeclaringClass().equals(Object.class)) {
|
||||||
|
return method.invoke(handler, args);
|
||||||
|
}
|
||||||
|
return handler.invoke(proxy, method, args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Fixture {
|
||||||
|
|
||||||
|
private final List<String> calls = new ArrayList<>();
|
||||||
|
private RuntimeException cacheException;
|
||||||
|
|
||||||
|
private final LiveMicAppConvertor liveMicAppConvertor = new LiveMicAppConvertor() {
|
||||||
|
@Override
|
||||||
|
public List<LiveMicrophoneCO> toListLiveMicrophoneCO(List<LiveMicrophoneDTO> liveMicrophones) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LiveMicrophoneCO toLiveMicrophoneCO(LiveMicrophoneDTO dto) {
|
||||||
|
calls.add("convert");
|
||||||
|
return new LiveMicrophoneCO();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LiveMicUserCO toLiveMicUserCO(LiveMicUser liveMicUser) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final LiveMicSettingService liveMicSettingService = new LiveMicSettingService() {
|
||||||
|
@Override
|
||||||
|
public LiveMicSetting updateMicSize(Long roomId, int micSize) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMicSeat(Long roomId, LiveMicSettingSeat settingSeat) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LiveMicSettingSeat getMicSeat(Long roomId, Integer micIndex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LiveMicSettingSeat> listMicSeat(Long roomId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkLock(Long roomId, Integer micIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists(Long roomId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final LiveMicrophoneGateway liveMicrophoneGateway = proxy(LiveMicrophoneGateway.class,
|
||||||
|
(Object proxy, Method method, Object[] args) -> {
|
||||||
|
if ("goUp".equals(method.getName())) {
|
||||||
|
calls.add("goUp");
|
||||||
|
return notice();
|
||||||
|
}
|
||||||
|
if ("goDown".equals(method.getName())) {
|
||||||
|
calls.add("goDown");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.getName());
|
||||||
|
});
|
||||||
|
|
||||||
|
private final LiveMicCacheService liveMicCacheService = proxy(LiveMicCacheService.class,
|
||||||
|
(Object proxy, Method method, Object[] args) -> {
|
||||||
|
if ("checkEqSelf".equals(method.getName())) {
|
||||||
|
calls.add("checkEqSelf");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ("checkAvailable".equals(method.getName())) {
|
||||||
|
calls.add("checkAvailable");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.getName());
|
||||||
|
});
|
||||||
|
|
||||||
|
private final TrtcClient trtcClient = proxy(TrtcClient.class,
|
||||||
|
(Object proxy, Method method, Object[] args) -> {
|
||||||
|
if ("createAgoraTokenGoUp".equals(method.getName())) {
|
||||||
|
calls.add("createToken");
|
||||||
|
return ResultResponse.success("publisher-token");
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.getName());
|
||||||
|
});
|
||||||
|
|
||||||
|
private final UserAgoraTokenCacheService userAgoraTokenCacheService =
|
||||||
|
new UserAgoraTokenCacheService() {
|
||||||
|
@Override
|
||||||
|
public void remove(Long userId, Long roomId) {
|
||||||
|
calls.add("removeToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(String agoraToken, Long userId, Long roomId) {
|
||||||
|
calls.add("saveToken");
|
||||||
|
if (cacheException != null) {
|
||||||
|
throw cacheException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUserAgoraToken(Long userId, Long roomId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final RoomManagerClient roomManagerClient = proxy(RoomManagerClient.class,
|
||||||
|
(Object proxy, Method method, Object[] args) -> {
|
||||||
|
if ("getById".equals(method.getName())) {
|
||||||
|
calls.add("getRoom");
|
||||||
|
return ResultResponse.success(null);
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.getName());
|
||||||
|
});
|
||||||
|
|
||||||
|
private final LiveMicGoUpCmdExe exe = new LiveMicGoUpCmdExe(
|
||||||
|
liveMicAppConvertor,
|
||||||
|
liveMicSettingService,
|
||||||
|
liveMicrophoneGateway,
|
||||||
|
liveMicCacheService,
|
||||||
|
trtcClient,
|
||||||
|
userAgoraTokenCacheService,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
roomManagerClient,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.red.circle.live.app.service;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import com.red.circle.live.app.service.AgoraMicMissingConfirmService.MissingState;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class AgoraMicMissingConfirmServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstMissingShouldOnlyRecordState() {
|
||||||
|
MissingState state = MissingState.next(null, 1_000L);
|
||||||
|
|
||||||
|
assertFalse(state.shouldCleanup(1_000L));
|
||||||
|
assertEquals("1:1000", state.serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void secondConsecutiveMissingShouldCleanup() {
|
||||||
|
MissingState state = MissingState.next("1:1000", 2_000L);
|
||||||
|
|
||||||
|
assertTrue(state.shouldCleanup(2_000L));
|
||||||
|
assertEquals("2:1000", state.serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void delayedMissingShouldCleanupAfterConfirmWindow() {
|
||||||
|
MissingState state = MissingState.next("1:1000", 6_000L);
|
||||||
|
|
||||||
|
assertTrue(state.shouldCleanup(6_000L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void malformedStateShouldRestartConfirmWindow() {
|
||||||
|
MissingState state = MissingState.next("bad-value", 3_000L);
|
||||||
|
|
||||||
|
assertFalse(state.shouldCleanup(3_000L));
|
||||||
|
assertEquals("1:3000", state.serialize());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -164,6 +164,7 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
|
|||||||
micOpsLock(roomId, micIndex);
|
micOpsLock(roomId, micIndex);
|
||||||
// RLock lock = redissonClient.getLock(roomId.toString()+micIndex.toString()+"room");
|
// RLock lock = redissonClient.getLock(roomId.toString()+micIndex.toString()+"room");
|
||||||
// lock.lock();
|
// lock.lock();
|
||||||
|
boolean micUpdated = false;
|
||||||
try {
|
try {
|
||||||
// 验证用户是否真实存在
|
// 验证用户是否真实存在
|
||||||
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, userProfileClient.getByUserId(userId));
|
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, userProfileClient.getByUserId(userId));
|
||||||
@ -204,12 +205,24 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
|
|||||||
|
|
||||||
// 上麦克风
|
// 上麦克风
|
||||||
liveMicCacheService.goUp(roomId, micIndex, liveMicrophone);
|
liveMicCacheService.goUp(roomId, micIndex, liveMicrophone);
|
||||||
|
micUpdated = true;
|
||||||
|
|
||||||
// 清理活跃用户
|
// 清理活跃用户
|
||||||
liveMicCacheService.refreshNotActiveUser(roomId);
|
liveMicCacheService.refreshNotActiveUser(roomId);
|
||||||
|
|
||||||
// 发送通知
|
// 发送通知
|
||||||
return micUserChangeNotice(roomProfile);
|
return micUserChangeNotice(roomProfile);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
if (micUpdated) {
|
||||||
|
try {
|
||||||
|
liveMicCacheService.goDown(roomId, micIndex);
|
||||||
|
liveMicCacheService.refreshNotActiveUser(roomId);
|
||||||
|
} catch (RuntimeException rollbackException) {
|
||||||
|
log.warn("上麦写入后异常,回滚麦位失败 roomId={}, micIndex={}, userId={}",
|
||||||
|
roomId, micIndex, userId, rollbackException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
// lock.unlock();
|
// lock.unlock();
|
||||||
micOpsUnlock(roomId, micIndex);
|
micOpsUnlock(roomId, micIndex);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user