This commit is contained in:
hy001 2026-05-15 19:40:36 +08:00
parent 1d2961c09a
commit 3bbaa78528
14 changed files with 745 additions and 193 deletions

View File

@ -46,6 +46,8 @@ red-circle:
endpoint: ${LIKEI_TRTC_ENDPOINT}
region: ${LIKEI_TRTC_REGION:ap-singapore}
sdkAppId: ${LIKEI_TRTC_SDK_APP_ID}
userSigKey: ${LIKEI_TRTC_USER_SIG_KEY:${LIKEI_TRTC_SECRET_KEY}}
userSigExpireSeconds: ${LIKEI_TRTC_USER_SIG_EXPIRE_SECOND:7200}
tencet-im:
appId: ${LIKEI_IM_APP_ID}
key: ${LIKEI_IM_KEY}

View File

@ -0,0 +1,74 @@
package com.red.circle.common.business.dto.rtc;
import com.red.circle.framework.dto.ClientObject;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* RTC credential returned to compatible clients.
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class RtcCredentialCO extends ClientObject {
@Serial
private static final long serialVersionUID = 1L;
/**
* AGORA or TRTC.
*/
private String rtcProvider;
/**
* Legacy Agora token.
*/
private String agoraToken;
/**
* Agora channel name, currently the business room id.
*/
private String agoraChannelName;
/**
* Tencent TRTC sdkAppId.
*/
private Long trtcSdkAppId;
/**
* TRTC user id. Use business userId string.
*/
private String trtcUserId;
/**
* TRTC UserSig.
*/
private String trtcUserSig;
/**
* Numeric TRTC room id when available.
*/
private Long trtcRoomId;
/**
* String TRTC room id fallback.
*/
private String trtcStrRoomId;
/**
* TRTC role: 20 anchor, 21 audience.
*/
private Integer trtcRole;
/**
* UserSig lifetime in seconds.
*/
private Long trtcUserSigExpireSeconds;
/**
* UserSig expire timestamp in milliseconds.
*/
private Long trtcUserSigExpireAt;
}

View File

@ -0,0 +1,21 @@
package com.red.circle.common.business.dto.rtc;
/**
* RTC provider used by voice rooms.
*/
public enum RtcProviderEnum {
AGORA,
TRTC;
public static RtcProviderEnum of(String provider) {
if (provider == null || provider.isBlank()) {
return null;
}
for (RtcProviderEnum value : values()) {
if (value.name().equalsIgnoreCase(provider.trim())) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,66 @@
package com.red.circle.common.business.dto.rtc;
/**
* Version based RTC compatibility rules.
*/
public final class RtcVersionSupport {
public static final String TRTC_MIN_VERSION = "1.4.0";
public static final String ROOM_PROVIDER_KEY_PREFIX = "RTC:ROOM:PROVIDER:";
public static final long ROOM_PROVIDER_TTL_SECONDS = 12 * 60 * 60L;
private RtcVersionSupport() {
}
public static RtcProviderEnum defaultProvider(String version) {
return compareVersion(version, TRTC_MIN_VERSION) <= 0
? RtcProviderEnum.AGORA
: RtcProviderEnum.TRTC;
}
public static String roomProviderKey(Object roomId) {
return ROOM_PROVIDER_KEY_PREFIX + roomId;
}
public static int compareVersion(String left, String right) {
int[] leftParts = parseVersion(left);
int[] rightParts = parseVersion(right);
int length = Math.max(leftParts.length, rightParts.length);
for (int i = 0; i < length; i++) {
int leftPart = i < leftParts.length ? leftParts[i] : 0;
int rightPart = i < rightParts.length ? rightParts[i] : 0;
if (leftPart != rightPart) {
return Integer.compare(leftPart, rightPart);
}
}
return 0;
}
private static int[] parseVersion(String version) {
if (version == null || version.isBlank()) {
return new int[]{0};
}
String normalized = version.trim().split("-", 2)[0];
String[] parts = normalized.split("\\.");
int[] values = new int[parts.length];
for (int i = 0; i < parts.length; i++) {
values[i] = parseVersionPart(parts[i]);
}
return values;
}
private static int parseVersionPart(String part) {
if (part == null || part.isBlank()) {
return 0;
}
String digits = part.replaceAll("[^0-9].*$", "");
if (digits.isBlank()) {
return 0;
}
try {
return Integer.parseInt(digits);
} catch (NumberFormatException ignored) {
return 0;
}
}
}

View File

@ -1,8 +1,9 @@
package com.red.circle.external.inner.endpoint.message.api;
import com.red.circle.external.inner.model.cmd.message.TrtcRemoveUserCmd;
import com.red.circle.external.inner.model.cmd.message.TrtcUserBlockedCmd;
import com.red.circle.framework.dto.ResultResponse;
package com.red.circle.external.inner.endpoint.message.api;
import com.red.circle.common.business.dto.rtc.RtcCredentialCO;
import com.red.circle.external.inner.model.cmd.message.TrtcRemoveUserCmd;
import com.red.circle.external.inner.model.cmd.message.TrtcUserBlockedCmd;
import com.red.circle.framework.dto.ResultResponse;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@ -50,10 +51,20 @@ public interface TrtcClientApi {
@RequestParam("type") String channelName
);
@GetMapping("/createAgoraTokenGoUp")
ResultResponse<String> createAgoraTokenGoUp(
@RequestParam("userId") Long userId,
@RequestParam("type") String channelName
);
}
@GetMapping("/createAgoraTokenGoUp")
ResultResponse<String> createAgoraTokenGoUp(
@RequestParam("userId") Long userId,
@RequestParam("type") String channelName
);
/**
* 创建 TRTC 入房凭证.
*/
@GetMapping("/createTrtcCredential")
ResultResponse<RtcCredentialCO> createTrtcCredential(
@RequestParam("userId") Long userId,
@RequestParam("roomIdentifier") String roomIdentifier,
@RequestParam(value = "publisher", required = false) Boolean publisher
);
}

View File

@ -1,15 +1,21 @@
package com.red.circle.external.adapter.app.imtrtc;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.red.circle.auth.inner.endpoint.AuthClient;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.component.instant.msg.agora.AgoraAuthentication;
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.im.account.ImAccountManagerService;
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.im.group.ImGroupAvManagerService;
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.trtc.TencentTrtcService;
import com.red.circle.component.instant.msg.tencet.service.result.OnlineStatusResult;
import com.red.circle.component.instant.msg.tencet.service.result.OnlineStatusResult.OnlineStatus;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.red.circle.auth.inner.endpoint.AuthClient;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.common.business.dto.rtc.RtcCredentialCO;
import com.red.circle.common.business.dto.rtc.RtcProviderEnum;
import com.red.circle.common.business.dto.rtc.RtcVersionSupport;
import com.red.circle.component.instant.msg.InstantMessageProperties;
import com.red.circle.component.instant.msg.agora.AgoraAuthentication;
import com.red.circle.component.instant.msg.tencet.props.TrtcTencetProperties;
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.im.account.ImAccountManagerService;
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.im.group.ImGroupAvManagerService;
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.trtc.TencentTrtcService;
import com.red.circle.component.instant.msg.tencet.service.result.OnlineStatusResult;
import com.red.circle.component.instant.msg.tencet.service.result.OnlineStatusResult.OnlineStatus;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.external.clienobject.TmpSigCO;
import com.red.circle.external.cmd.imtrtc.ImRoomRemoveUserCmd;
import com.red.circle.external.infra.database.redis.service.UserAgoraTokenCacheService;
@ -23,13 +29,15 @@ import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.regex.RegexConstant;
import com.red.circle.tool.core.text.StringPool;
import com.red.circle.other.inner.asserts.user.UserErrorCode;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import com.red.circle.other.inner.asserts.user.UserErrorCode;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.tencentyun.TLSSigAPIv2;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import io.agora.media.RtcTokenBuilder2;
import lombok.RequiredArgsConstructor;
@ -41,8 +49,9 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.util.StringUtils;
/**
* IM操作.
@ -62,14 +71,22 @@ public class ImRestController extends BaseController {
private final UserProfileClient userProfileClient;
private final TencentTrtcService tencentTrtcService;
private final AgoraAuthentication agoraAuthentication;
private final ImGroupAvManagerService imGroupAvManagerService;
private final ImAccountManagerService imAccountManagerService;
private final UserAgoraTokenCacheService userAgoraTokenCacheService;
@Value("${rtc.appId}")
private String appId;
@Value("${rtc.certificate}")
private String certificate;
private final ImGroupAvManagerService imGroupAvManagerService;
private final ImAccountManagerService imAccountManagerService;
private final UserAgoraTokenCacheService userAgoraTokenCacheService;
private final RedisService redisService;
private final InstantMessageProperties instantMessageProperties;
@Value("${rtc.appId}")
private String appId;
@Value("${rtc.certificate}")
private String certificate;
@Value("${red-circle.instant-message.tencet-trtc.userSigExpireSeconds:7200}")
private long trtcUserSigExpireSeconds;
@Value("${red-circle.instant-message.tencet-trtc.userSigKey:${red-circle.instant-message.tencet-trtc.secretKey:}}")
private String trtcUserSigKey;
/**
* 认证签名(腾讯).
@ -127,13 +144,16 @@ public class ImRestController extends BaseController {
* @eo.method get
* @eo.request-type formdata
*/
@GetMapping("/tmp/agora/sig")
public TmpSigCO userSig(Long userId, String channelName,Boolean isPublisher) {
log.warn("/tmp/agora/sig {}, {}, {}", userId, channelName, isPublisher);
if (isPublisher == null){
isPublisher = false;
}
String token;
@GetMapping("/tmp/agora/sig")
public TmpSigCO userSig(AppExtCommand cmd, Long userId, String channelName, Boolean isPublisher) {
log.warn("/tmp/agora/sig {}, {}, {}", userId, channelName, isPublisher);
if (isPublisher == null){
isPublisher = false;
}
RtcProviderEnum provider = resolveRoomProvider(channelName, currentVersion(cmd));
ResponseAssert.isTrue(CommonErrorCode.UPGRADE_VERSION,
!Objects.equals(RtcProviderEnum.TRTC, provider));
String token;
if (isPublisher){
token = userAgoraTokenCacheService.getUserAgoraToken(userId, Long.valueOf(channelName));
} else {
@ -156,36 +176,160 @@ public class ImRestController extends BaseController {
if (Objects.nonNull(json)) {
return getTmpSigByUserCredential(userId, token, json, gson);
} else {
return new TmpSigCO()
.setRtcToken(token)
.setNewServiceToken(authClient.createIfAbsentUserCredentialToken(userId).getBody());
}
}
/**
* 根据用户信息获取用户Servicetoken
} else {
RtcCredentialCO credential = createAgoraCredential(channelName, token);
return new TmpSigCO()
.setRtcToken(token)
.setRtcProvider(RtcProviderEnum.AGORA.name())
.setRtcCredential(credential)
.setNewServiceToken(authClient.createIfAbsentUserCredentialToken(userId).getBody());
}
}
/**
* RTC 凭证支持按客户端版本和房间级 provider Agora/TRTC 间分流.
*
* @param userId 用户id
* @param roomId 业务房间id作为房间级 provider 缓存 key
* @param channelName 历史 Agora channel可为空
* @param roomAccount TRTC 房间号优先使用
* @param isPublisher 是否主播/上麦
*/
@GetMapping("/tmp/rtc/sig")
public TmpSigCO rtcSig(
AppExtCommand cmd,
@RequestParam("userId") Long userId,
@RequestParam(value = "roomId", required = false) Long roomId,
@RequestParam(value = "channelName", required = false) String channelName,
@RequestParam(value = "roomAccount", required = false) String roomAccount,
@RequestParam(value = "isPublisher", required = false) Boolean isPublisher) {
String providerRoomKey = Objects.nonNull(roomId) ? roomId.toString() : channelName;
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
StringUtils.hasText(providerRoomKey));
RtcProviderEnum provider = resolveRoomProvider(providerRoomKey, currentVersion(cmd));
boolean publisher = Boolean.TRUE.equals(isPublisher);
if (Objects.equals(RtcProviderEnum.AGORA, provider)) {
String agoraChannelName = StringUtils.hasText(channelName) ? channelName : providerRoomKey;
String token = publisher
? createChannelPublisherToken(agoraChannelName, userId)
: createChannelSubscriberToken(agoraChannelName, userId);
RtcCredentialCO credential = createAgoraCredential(agoraChannelName, token);
return new TmpSigCO()
.setRtcToken(token)
.setRtcProvider(RtcProviderEnum.AGORA.name())
.setRtcCredential(credential)
.setNewServiceToken(authClient.createIfAbsentUserCredentialToken(userId).getBody());
}
String trtcRoomIdentifier = firstText(roomAccount, channelName, providerRoomKey);
RtcCredentialCO credential = createTrtcCredential(userId, trtcRoomIdentifier, publisher);
return new TmpSigCO()
.setRtcToken(credential.getTrtcUserSig())
.setRtcProvider(RtcProviderEnum.TRTC.name())
.setRtcCredential(credential)
.setNewServiceToken(authClient.createIfAbsentUserCredentialToken(userId).getBody());
}
/**
* 根据用户信息获取用户Servicetoken
*
* @param userId
* @param token
* @param json
* @param gson
* @return
*/
private TmpSigCO getTmpSigByUserCredential(Long userId, String token, String json, Gson gson) {
try {
UserCredential userCredential = gson.fromJson(json, UserCredential.class);
return new TmpSigCO()
.setRtcToken(token)
.setNewServiceToken(userCredential.token());
} catch (JsonSyntaxException e) {
log.error("/tmp/agora/sig类型转换错误", e);
return new TmpSigCO()
.setRtcToken(token)
.setNewServiceToken(authClient.createIfAbsentUserCredentialToken(userId).getBody());
}
}
* @return
*/
private TmpSigCO getTmpSigByUserCredential(Long userId, String token, String json, Gson gson) {
RtcCredentialCO credential = createAgoraCredential(null, token);
try {
UserCredential userCredential = gson.fromJson(json, UserCredential.class);
return new TmpSigCO()
.setRtcToken(token)
.setRtcProvider(RtcProviderEnum.AGORA.name())
.setRtcCredential(credential)
.setNewServiceToken(userCredential.token());
} catch (JsonSyntaxException e) {
log.error("/tmp/agora/sig类型转换错误", e);
return new TmpSigCO()
.setRtcToken(token)
.setRtcProvider(RtcProviderEnum.AGORA.name())
.setRtcCredential(credential)
.setNewServiceToken(authClient.createIfAbsentUserCredentialToken(userId).getBody());
}
}
private RtcProviderEnum resolveRoomProvider(String roomKey, String version) {
if (!StringUtils.hasText(roomKey)) {
return RtcVersionSupport.defaultProvider(version);
}
String cacheKey = RtcVersionSupport.roomProviderKey(roomKey.trim());
RtcProviderEnum cached = RtcProviderEnum.of(redisService.getString(cacheKey));
if (Objects.nonNull(cached)) {
return cached;
}
RtcProviderEnum provider = RtcVersionSupport.defaultProvider(version);
redisService.setIfAbsent(cacheKey, provider.name(),
RtcVersionSupport.ROOM_PROVIDER_TTL_SECONDS, TimeUnit.SECONDS);
RtcProviderEnum resolved = RtcProviderEnum.of(redisService.getString(cacheKey));
return Objects.nonNull(resolved) ? resolved : provider;
}
private String currentVersion(AppExtCommand cmd) {
return Optional.ofNullable(cmd)
.map(AppExtCommand::getReqAppIntel)
.map(reqAppIntel -> reqAppIntel.getVersion())
.orElse(null);
}
private RtcCredentialCO createAgoraCredential(String channelName, String token) {
return new RtcCredentialCO()
.setRtcProvider(RtcProviderEnum.AGORA.name())
.setAgoraChannelName(channelName)
.setAgoraToken(token);
}
private RtcCredentialCO createTrtcCredential(Long userId, String roomIdentifier,
boolean publisher) {
TrtcTencetProperties trtcProperties = instantMessageProperties.getTencetTrtc();
ResponseAssert.notNull(CommonErrorCode.CONFIGURATION_ERROR, trtcProperties);
ResponseAssert.notNull(CommonErrorCode.CONFIGURATION_ERROR, trtcProperties.getSdkAppId());
ResponseAssert.isTrue(CommonErrorCode.CONFIGURATION_ERROR, StringUtils.hasText(trtcUserSigKey));
String trtcUserId = userId.toString();
String userSig = new TLSSigAPIv2(trtcProperties.getSdkAppId(), trtcUserSigKey)
.genUserSig(trtcUserId, trtcUserSigExpireSeconds);
RtcCredentialCO credential = new RtcCredentialCO()
.setRtcProvider(RtcProviderEnum.TRTC.name())
.setTrtcSdkAppId(trtcProperties.getSdkAppId())
.setTrtcUserId(trtcUserId)
.setTrtcUserSig(userSig)
.setTrtcRole(publisher ? 20 : 21)
.setTrtcUserSigExpireSeconds(trtcUserSigExpireSeconds)
.setTrtcUserSigExpireAt(
System.currentTimeMillis() + trtcUserSigExpireSeconds * 1000L);
applyTrtcRoomIdentifier(credential, roomIdentifier);
return credential;
}
private void applyTrtcRoomIdentifier(RtcCredentialCO credential, String roomIdentifier) {
String normalizedRoomIdentifier = roomIdentifier.trim();
try {
credential.setTrtcRoomId(Long.valueOf(normalizedRoomIdentifier));
} catch (NumberFormatException ignored) {
credential.setTrtcStrRoomId(normalizedRoomIdentifier);
}
}
private String firstText(String... values) {
for (String value : values) {
if (StringUtils.hasText(value)) {
return value.trim();
}
}
return "";
}
/**

View File

@ -1,9 +1,10 @@
package com.red.circle.external.clienobject;
import com.red.circle.framework.dto.ClientObject;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
package com.red.circle.external.clienobject;
import com.red.circle.common.business.dto.rtc.RtcCredentialCO;
import com.red.circle.framework.dto.ClientObject;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 临时签名(兼容服务).
@ -20,8 +21,18 @@ public class TmpSigCO extends ClientObject {
*/
private String rtcToken;
/**
* 新服务token.
*/
private String newServiceToken;
}
/**
* 新服务token.
*/
private String newServiceToken;
/**
* RTC provider: AGORA or TRTC.
*/
private String rtcProvider;
/**
* Provider specific credential.
*/
private RtcCredentialCO rtcCredential;
}

View File

@ -1,8 +1,9 @@
package com.red.circle.external.inner.endpoint.message;
import com.red.circle.external.inner.endpoint.message.api.TrtcClientApi;
import com.red.circle.external.inner.model.cmd.message.TrtcRemoveUserCmd;
import com.red.circle.external.inner.model.cmd.message.TrtcUserBlockedCmd;
package com.red.circle.external.inner.endpoint.message;
import com.red.circle.common.business.dto.rtc.RtcCredentialCO;
import com.red.circle.external.inner.endpoint.message.api.TrtcClientApi;
import com.red.circle.external.inner.model.cmd.message.TrtcRemoveUserCmd;
import com.red.circle.external.inner.model.cmd.message.TrtcUserBlockedCmd;
import com.red.circle.external.inner.service.message.TrtcClientService;
import com.red.circle.framework.dto.ResultResponse;
import lombok.RequiredArgsConstructor;
@ -45,8 +46,15 @@ public class TrtcClientEndpoint implements TrtcClientApi {
}
@Override
public ResultResponse<String> createAgoraTokenGoUp(Long userId, String channelName) {
return ResultResponse.success(trtcClientService.createAgoraTokenGoUp(userId, channelName));
}
}
public ResultResponse<String> createAgoraTokenGoUp(Long userId, String channelName) {
return ResultResponse.success(trtcClientService.createAgoraTokenGoUp(userId, channelName));
}
@Override
public ResultResponse<RtcCredentialCO> createTrtcCredential(Long userId, String roomIdentifier,
Boolean publisher) {
return ResultResponse.success(
trtcClientService.createTrtcCredential(userId, roomIdentifier, publisher));
}
}

View File

@ -1,7 +1,8 @@
package com.red.circle.external.inner.service.message;
import com.red.circle.external.inner.model.cmd.message.TrtcRemoveUserCmd;
import com.red.circle.external.inner.model.cmd.message.TrtcUserBlockedCmd;
package com.red.circle.external.inner.service.message;
import com.red.circle.common.business.dto.rtc.RtcCredentialCO;
import com.red.circle.external.inner.model.cmd.message.TrtcRemoveUserCmd;
import com.red.circle.external.inner.model.cmd.message.TrtcUserBlockedCmd;
/**
* trtc 服务.
@ -33,8 +34,13 @@ public interface TrtcClientService {
/**
* 创建声网令牌.
*/
String createAgoraToken(Long userId, String channelName);
String createAgoraTokenGoUp(Long userId, String channelName);
}
String createAgoraToken(Long userId, String channelName);
String createAgoraTokenGoUp(Long userId, String channelName);
/**
* 创建 TRTC 入房凭证.
*/
RtcCredentialCO createTrtcCredential(Long userId, String roomIdentifier, Boolean publisher);
}

View File

@ -1,17 +1,23 @@
package com.red.circle.external.inner.service.message.impl;
import com.red.circle.component.instant.msg.agora.AgoraAuthentication;
import com.red.circle.component.instant.msg.agora.AgoraProperties;
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.trtc.TencentTrtcService;
import com.red.circle.external.inner.model.cmd.message.TrtcRemoveUserCmd;
import com.red.circle.external.inner.model.cmd.message.TrtcUserBlockedCmd;
import com.red.circle.external.inner.service.message.TrtcClientService;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import io.agora.media.RtcTokenBuilder2;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
package com.red.circle.external.inner.service.message.impl;
import com.red.circle.common.business.dto.rtc.RtcCredentialCO;
import com.red.circle.common.business.dto.rtc.RtcProviderEnum;
import com.red.circle.component.instant.msg.InstantMessageProperties;
import com.red.circle.component.instant.msg.tencet.props.TrtcTencetProperties;
import com.red.circle.component.instant.msg.tencet.service.endpoint.api.trtc.TencentTrtcService;
import com.red.circle.external.inner.model.cmd.message.TrtcRemoveUserCmd;
import com.red.circle.external.inner.model.cmd.message.TrtcUserBlockedCmd;
import com.red.circle.external.inner.service.message.TrtcClientService;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.tencentyun.TLSSigAPIv2;
import io.agora.media.RtcTokenBuilder2;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* trtc 服务.
@ -22,17 +28,22 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class TrtcClientServiceImpl implements TrtcClientService {
private final UserProfileClient userProfileClient;
private final TencentTrtcService tencentTrtcService;
// private final AgoraAuthentication agoraAuthentication;
// private final AgoraProperties agoraProperties;
@Value("${rtc.appId}")
private String appId;
@Value("${rtc.certificate}")
private String certificate;
private final UserProfileClient userProfileClient;
private final TencentTrtcService tencentTrtcService;
private final InstantMessageProperties instantMessageProperties;
@Value("${rtc.appId}")
private String appId;
@Value("${rtc.certificate}")
private String certificate;
@Value("${red-circle.instant-message.tencet-trtc.userSigExpireSeconds:7200}")
private long trtcUserSigExpireSeconds;
@Value("${red-circle.instant-message.tencet-trtc.userSigKey:${red-circle.instant-message.tencet-trtc.secretKey:}}")
private String trtcUserSigKey;
@Override
public boolean removeUser(TrtcRemoveUserCmd cmd) {
return tencentTrtcService.removeUser(cmd.getRoomId(),
@ -61,10 +72,50 @@ public class TrtcClientServiceImpl implements TrtcClientService {
}
@Override
public String createAgoraTokenGoUp(Long userId, String channelName) {
UserProfileDTO userProfile = userProfileClient.getByUserId(userId).getBody();
RtcTokenBuilder2 var3 = new RtcTokenBuilder2();
return var3.buildTokenWithUid(appId, certificate, channelName, Integer.parseInt(userProfile.getAccount()), RtcTokenBuilder2.Role.ROLE_PUBLISHER, 21600, 21600);
}
}
public String createAgoraTokenGoUp(Long userId, String channelName) {
UserProfileDTO userProfile = userProfileClient.getByUserId(userId).getBody();
RtcTokenBuilder2 var3 = new RtcTokenBuilder2();
return var3.buildTokenWithUid(appId, certificate, channelName, Integer.parseInt(userProfile.getAccount()), RtcTokenBuilder2.Role.ROLE_PUBLISHER, 21600, 21600);
}
@Override
public RtcCredentialCO createTrtcCredential(Long userId, String roomIdentifier,
Boolean publisher) {
ResponseAssert.notNull(CommonErrorCode.REQUIRED_FIELDS_CANNOT_BE_IGNORED, userId);
ResponseAssert.isTrue(CommonErrorCode.REQUIRED_FIELDS_CANNOT_BE_IGNORED,
StringUtils.hasText(roomIdentifier));
TrtcTencetProperties trtcProperties = instantMessageProperties.getTencetTrtc();
ResponseAssert.notNull(CommonErrorCode.CONFIGURATION_ERROR, trtcProperties);
ResponseAssert.notNull(CommonErrorCode.CONFIGURATION_ERROR, trtcProperties.getSdkAppId());
ResponseAssert.isTrue(CommonErrorCode.CONFIGURATION_ERROR, StringUtils.hasText(trtcUserSigKey));
String trtcUserId = userId.toString();
String userSig = new TLSSigAPIv2(
trtcProperties.getSdkAppId(),
trtcUserSigKey
).genUserSig(trtcUserId, trtcUserSigExpireSeconds);
RtcCredentialCO credential = new RtcCredentialCO()
.setRtcProvider(RtcProviderEnum.TRTC.name())
.setTrtcSdkAppId(trtcProperties.getSdkAppId())
.setTrtcUserId(trtcUserId)
.setTrtcUserSig(userSig)
.setTrtcRole(Boolean.TRUE.equals(publisher) ? 20 : 21)
.setTrtcUserSigExpireSeconds(trtcUserSigExpireSeconds)
.setTrtcUserSigExpireAt(
System.currentTimeMillis() + trtcUserSigExpireSeconds * 1000L);
applyTrtcRoomIdentifier(credential, roomIdentifier);
return credential;
}
private void applyTrtcRoomIdentifier(RtcCredentialCO credential, String roomIdentifier) {
String normalizedRoomIdentifier = roomIdentifier.trim();
try {
credential.setTrtcRoomId(Long.valueOf(normalizedRoomIdentifier));
} catch (NumberFormatException ignored) {
credential.setTrtcStrRoomId(normalizedRoomIdentifier);
}
}
}

View File

@ -1,6 +1,9 @@
package com.red.circle.live.app.command.mic;
import com.red.circle.component.redis.service.RedisService;
package com.red.circle.live.app.command.mic;
import com.red.circle.common.business.dto.rtc.RtcCredentialCO;
import com.red.circle.common.business.dto.rtc.RtcProviderEnum;
import com.red.circle.common.business.dto.rtc.RtcVersionSupport;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.external.inner.endpoint.message.TrtcClient;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
@ -29,7 +32,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
@ -52,16 +56,25 @@ public class LiveMicGoUpCmdExe {
private final RedisService redisService;
private final RoomDailyTaskClient roomDailyTaskClient;
private final RoomManagerClient roomManagerClient;
private final RedisTemplate redisTemplate;
private final RoomMemberClient roomMemberClient;
private final RedisTemplate redisTemplate;
private final RoomMemberClient roomMemberClient;
public LiveMicrophoneCO execute(MicUpDownCmd cmd) {
boolean targetMicAlreadySelf = false;
boolean goUpCompleted = false;
RtcProviderEnum rtcProvider = null;
try {
checkPermissions(cmd);
targetMicAlreadySelf = checkTargetMicAvailableOrSelf(cmd);
String roomToken = createPublisherToken(cmd);
rtcProvider = resolveRoomProvider(cmd);
String roomToken = null;
RtcCredentialCO rtcCredential;
if (Objects.equals(RtcProviderEnum.AGORA, rtcProvider)) {
roomToken = createPublisherToken(cmd);
rtcCredential = createAgoraCredential(cmd, roomToken);
} else {
rtcCredential = createTrtcCredential(cmd, true);
}
LiveMicrophoneNoticeDTO noticeDTO = liveMicrophoneGateway.goUp(
cmd.getRoomId(),
@ -70,16 +83,22 @@ public class LiveMicGoUpCmdExe {
);
goUpCompleted = true;
cachePublisherToken(cmd, roomToken);
if (Objects.equals(RtcProviderEnum.AGORA, rtcProvider)) {
cachePublisherToken(cmd, roomToken);
}
LiveMicrophoneCO notice = returnMicrophoneCO(cmd, noticeDTO);
notice.setRoomToken(roomToken);
notice.setRoomToken(roomToken)
.setRtcProvider(rtcProvider.name())
.setRtcCredential(rtcCredential);
// 房主邀请成员上麦
handleRoomDailyTask(cmd);
return notice;
} catch (RuntimeException e) {
if (goUpCompleted) {
rollbackPublisherToken(cmd);
if (Objects.equals(RtcProviderEnum.AGORA, rtcProvider)) {
rollbackPublisherToken(cmd);
}
if (!targetMicAlreadySelf) {
rollbackMicSeat(cmd, e);
}
@ -103,6 +122,60 @@ public class LiveMicGoUpCmdExe {
userAgoraTokenCacheService.save(roomToken, cmd.requiredReqUserId(), cmd.getRoomId());
}
private RtcProviderEnum resolveRoomProvider(MicUpDownCmd cmd) {
if (Objects.isNull(redisService)) {
return RtcVersionSupport.defaultProvider(currentVersion(cmd));
}
String cacheKey = RtcVersionSupport.roomProviderKey(cmd.getRoomId());
RtcProviderEnum requestedProvider = RtcVersionSupport.defaultProvider(currentVersion(cmd));
RtcProviderEnum cachedProvider = RtcProviderEnum.of(redisService.getString(cacheKey));
if (Objects.nonNull(cachedProvider)) {
ResponseAssert.isTrue(CommonErrorCode.UPGRADE_VERSION,
!Objects.equals(RtcProviderEnum.AGORA, requestedProvider)
|| !Objects.equals(RtcProviderEnum.TRTC, cachedProvider));
return cachedProvider;
}
redisService.setIfAbsent(cacheKey, requestedProvider.name(),
RtcVersionSupport.ROOM_PROVIDER_TTL_SECONDS, TimeUnit.SECONDS);
RtcProviderEnum resolvedProvider = RtcProviderEnum.of(redisService.getString(cacheKey));
return Objects.nonNull(resolvedProvider) ? resolvedProvider : requestedProvider;
}
private String currentVersion(MicUpDownCmd cmd) {
return Optional.ofNullable(cmd)
.map(MicUpDownCmd::getReqAppIntel)
.map(reqAppIntel -> reqAppIntel.getVersion())
.orElse(null);
}
private RtcCredentialCO createAgoraCredential(MicUpDownCmd cmd, String token) {
return new RtcCredentialCO()
.setRtcProvider(RtcProviderEnum.AGORA.name())
.setAgoraChannelName(cmd.getRoomId().toString())
.setAgoraToken(token);
}
private RtcCredentialCO createTrtcCredential(MicUpDownCmd cmd, boolean publisher) {
ResultResponse<RtcCredentialCO> response = trtcClient.createTrtcCredential(
cmd.requiredReqUserId(),
resolveTrtcRoomIdentifier(cmd),
publisher
);
RtcCredentialCO credential = Objects.nonNull(response) ? response.getBody() : null;
ResponseAssert.notNull(CommonErrorCode.THIRD_PARTY_SERVER_ERROR, credential);
return credential;
}
private String resolveTrtcRoomIdentifier(MicUpDownCmd cmd) {
ResultResponse<RoomProfileDTO> response = roomManagerClient.getById(cmd.getRoomId());
RoomProfileDTO roomProfile = Objects.nonNull(response) ? response.getBody() : null;
if (Objects.nonNull(roomProfile) && Objects.nonNull(roomProfile.getRoomAccount())
&& !roomProfile.getRoomAccount().isBlank()) {
return roomProfile.getRoomAccount();
}
return cmd.getRoomId().toString();
}
private boolean checkTargetMicAvailableOrSelf(MicUpDownCmd cmd) {
boolean targetMicAlreadySelf = liveMicCacheService.checkEqSelf(
cmd.getRoomId(),

View File

@ -1,10 +1,11 @@
package com.red.circle.live.app.dto.clientobject;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.framework.dto.ClientObject;
import java.io.Serial;
import lombok.Data;
package com.red.circle.live.app.dto.clientobject;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.common.business.dto.rtc.RtcCredentialCO;
import com.red.circle.framework.dto.ClientObject;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@ -47,9 +48,19 @@ public class LiveMicrophoneCO extends ClientObject {
*/
private LiveMicUserCO user;
/**
* 上麦token信息
*/
private String roomToken;
}
/**
* 上麦token信息
*/
private String roomToken;
/**
* RTC provider: AGORA or TRTC.
*/
private String rtcProvider;
/**
* RTC credential for clients that support provider negotiation.
*/
private RtcCredentialCO rtcCredential;
}

View File

@ -1,6 +1,9 @@
package com.red.circle.other.app.command.room;
package com.red.circle.other.app.command.room;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.common.business.dto.rtc.RtcCredentialCO;
import com.red.circle.common.business.dto.rtc.RtcProviderEnum;
import com.red.circle.common.business.dto.rtc.RtcVersionSupport;
import com.red.circle.external.inner.endpoint.message.ImGroupClient;
import com.red.circle.external.inner.endpoint.message.TrtcClient;
import com.red.circle.external.inner.model.cmd.message.CreateGroupCmd;
@ -121,13 +124,21 @@ public class RoomEnterCmdExe {
);
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, ownUserProfile);
//String token = createAgoraToken(cmd);
String token = userAgoraTokenCacheService.getUserAgoraToken(cmd.getReqUserId(), cmd.getRoomId());
if (Objects.isNull(token)) {
token = trtcClient.createAgoraToken(cmd.getReqUserId(), cmd.getRoomId().toString()).getBody();
log.warn("进房间token{}", token);
userAgoraTokenCacheService.save(token, cmd.getReqUserId(), cmd.getRoomId());
}
RtcProviderEnum rtcProvider = resolveRoomProvider(cmd);
String token = null;
RtcCredentialCO rtcCredential;
if (Objects.equals(RtcProviderEnum.AGORA, rtcProvider)) {
token = userAgoraTokenCacheService.getUserAgoraToken(cmd.getReqUserId(), cmd.getRoomId());
if (Objects.isNull(token)) {
token = trtcClient.createAgoraToken(cmd.getReqUserId(), cmd.getRoomId().toString())
.getBody();
log.warn("进房间token{}", token);
userAgoraTokenCacheService.save(token, cmd.getReqUserId(), cmd.getRoomId());
}
rtcCredential = createAgoraCredential(cmd, token);
} else {
rtcCredential = createTrtcCredential(cmd, manager, false);
}
try {
// 无法操作错误归属
@ -159,10 +170,12 @@ public class RoomEnterCmdExe {
.setEntrants(
new EntryRoomEntrantsCO()
.setRoles(roomManagerCacheService.getRoomUserRoles(cmd.getReqUserId(),
cmd.getRoomId()))
.setLevel(getUserLevel(cmd.requireReqSysOriginEnum(), cmd.getReqUserId()))
.setNobleVipAbility(listUserPropsVipActualEquity(cmd))
.setRoomToken(token)
cmd.getRoomId()))
.setLevel(getUserLevel(cmd.requireReqSysOriginEnum(), cmd.getReqUserId()))
.setNobleVipAbility(listUserPropsVipActualEquity(cmd))
.setRoomToken(token)
.setRtcProvider(rtcProvider.name())
.setRtcCredential(rtcCredential)
);
}
@ -210,9 +223,9 @@ public class RoomEnterCmdExe {
* @param cmd
* @return
*/
private String createAgoraToken(RoomEntryCmd cmd) {
ExecutorService executor = Executors.newSingleThreadExecutor();
AtomicReference<String> agoraToken = new AtomicReference<>("BaHrIypJdFWqwWtoZLToRucRyoGrVgYCONBFvGynmlJsevFYsWBfEIcaWUUGijQBjycTtmSvkmBINoSltJMmgiPeBnkaWErixaxg");
private String createAgoraToken(RoomEntryCmd cmd) {
ExecutorService executor = Executors.newSingleThreadExecutor();
AtomicReference<String> agoraToken = new AtomicReference<>("BaHrIypJdFWqwWtoZLToRucRyoGrVgYCONBFvGynmlJsevFYsWBfEIcaWUUGijQBjycTtmSvkmBINoSltJMmgiPeBnkaWErixaxg");
Future<?> future = executor.submit(() -> {
agoraToken.set(trtcClient.createAgoraToken(cmd.getReqUserId(), cmd.getRoomId().toString()).getBody());
});
@ -227,13 +240,63 @@ public class RoomEnterCmdExe {
log.error("上麦获取声网token其他异常: {}", e.getMessage());
}
executor.shutdown();
return agoraToken.get();
}
private List<PropsVipActualEquityEnum> listUserPropsVipActualEquity(RoomEntryCmd cmd) {
return userProfileGateway.listUserActualEquityEnum(cmd.requiredReqUserId());
}
executor.shutdown();
return agoraToken.get();
}
private RtcProviderEnum resolveRoomProvider(RoomEntryCmd cmd) {
String cacheKey = RtcVersionSupport.roomProviderKey(cmd.getRoomId());
RtcProviderEnum requestedProvider = RtcVersionSupport.defaultProvider(currentVersion(cmd));
RtcProviderEnum cachedProvider = RtcProviderEnum.of(redisService.getString(cacheKey));
if (Objects.nonNull(cachedProvider)) {
ResponseAssert.isTrue(CommonErrorCode.UPGRADE_VERSION,
!Objects.equals(RtcProviderEnum.AGORA, requestedProvider)
|| !Objects.equals(RtcProviderEnum.TRTC, cachedProvider));
return cachedProvider;
}
redisService.setIfAbsent(cacheKey, requestedProvider.name(),
RtcVersionSupport.ROOM_PROVIDER_TTL_SECONDS, TimeUnit.SECONDS);
RtcProviderEnum resolvedProvider = RtcProviderEnum.of(redisService.getString(cacheKey));
return Objects.nonNull(resolvedProvider) ? resolvedProvider : requestedProvider;
}
private String currentVersion(RoomEntryCmd cmd) {
return Optional.ofNullable(cmd)
.map(RoomEntryCmd::getReqAppIntel)
.map(reqAppIntel -> reqAppIntel.getVersion())
.orElse(null);
}
private RtcCredentialCO createAgoraCredential(RoomEntryCmd cmd, String token) {
return new RtcCredentialCO()
.setRtcProvider(RtcProviderEnum.AGORA.name())
.setAgoraChannelName(cmd.getRoomId().toString())
.setAgoraToken(token);
}
private RtcCredentialCO createTrtcCredential(RoomEntryCmd cmd, RoomProfileManager manager,
boolean publisher) {
ResultResponse<RtcCredentialCO> response = trtcClient.createTrtcCredential(
cmd.getReqUserId(),
resolveTrtcRoomIdentifier(cmd, manager),
publisher
);
RtcCredentialCO credential = Objects.nonNull(response) ? response.getBody() : null;
ResponseAssert.notNull(CommonErrorCode.THIRD_PARTY_SERVER_ERROR, credential);
return credential;
}
private String resolveTrtcRoomIdentifier(RoomEntryCmd cmd, RoomProfileManager manager) {
if (Objects.nonNull(manager) && Objects.nonNull(manager.getRoomAccount())
&& !manager.getRoomAccount().isBlank()) {
return manager.getRoomAccount();
}
return cmd.getRoomId().toString();
}
private List<PropsVipActualEquityEnum> listUserPropsVipActualEquity(RoomEntryCmd cmd) {
return userProfileGateway.listUserActualEquityEnum(cmd.requiredReqUserId());
}
private WealthAndCharmLevelDTO getUserLevel(SysOriginPlatformEnum sysOrigin, Long userId) {
return Optional.ofNullable(

View File

@ -1,10 +1,11 @@
package com.red.circle.other.app.dto.clientobject.room;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.red.circle.framework.dto.ClientObject;
import com.red.circle.other.inner.enums.live.RoomUserRolesEnum;
import com.red.circle.other.inner.enums.material.PropsVipActualEquityEnum;
import com.red.circle.other.inner.model.dto.user.WealthAndCharmLevelDTO;
package com.red.circle.other.app.dto.clientobject.room;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.red.circle.common.business.dto.rtc.RtcCredentialCO;
import com.red.circle.framework.dto.ClientObject;
import com.red.circle.other.inner.enums.live.RoomUserRolesEnum;
import com.red.circle.other.inner.enums.material.PropsVipActualEquityEnum;
import com.red.circle.other.inner.model.dto.user.WealthAndCharmLevelDTO;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -36,9 +37,19 @@ public class EntryRoomEntrantsCO extends ClientObject {
*/
private List<PropsVipActualEquityEnum> nobleVipAbility;
/**
* 进入房间token信息
*/
private String roomToken;
}
/**
* 进入房间token信息
*/
private String roomToken;
/**
* RTC provider: AGORA or TRTC.
*/
private String rtcProvider;
/**
* RTC credential for clients that support provider negotiation.
*/
private RtcCredentialCO rtcCredential;
}