Commit a807bfcf by hewei

Merge branch '1.4_rtc' into '1.4'

1.4 rtc

See merge request !3
parents dd30d6d8 9c7c59d3
package io.geekidea.springbootplus.test;
import cn.hutool.core.codec.Base64;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* 读取系统文件目录 生成apns字符串
*/
public class ApnsTest {
@Test
public void test() throws Exception {
File file = new File("/Users/giaogiao/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/f31cd9e6d7da1d15c57c40575f5c85db/Message/MessageTemp/e1180914825140f051a87348bda5cbb5/File/hipro_test_push.p12");
InputStream certificate = new FileInputStream(file);
String encode = Base64.encode(certificate);
}
}
......@@ -6,6 +6,8 @@ import com.turo.pushy.apns.PushType;
import com.wecloud.im.ws.sender.IosPush;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
......@@ -29,17 +31,22 @@ public class IosApnsBase64Test {
// * @param sound rtc= "call.caf"; 否则为default
Map<String, Object> customProperty = new HashMap<String, Object>(10);
String apnsCertificatePath = "frogsell_push_dev.p12";
String deviceToken = "27c93ca84bbf17d9ff8eb05df0576ac49822db2ae1c02aa0afea83b5c3861276";
// String apnsCertificatePath = "frogsell_push_dev.p12";
String deviceToken = "5b761f954efe7493de0bc751942e1a8355853771b66a512f5687ca05e7335e99";
String alertTitle = "你好333";
String alertBody = "hi333";
int badge = 1;
String topicBundleId = "com.jdw.frogsell";
String topicBundleId = "com.xteng.Hibro";
boolean contentAvailable = false;
InputStream certificate = IosPush.getApnsCertificate(apnsCertificatePath);
String encode = Base64.encode(certificate);
// InputStream certificate = IosPush.getApnsCertificate(apnsCertificatePath);
// String encode = Base64.encode(certificate);
File file = new File("/Users/giaogiao/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/f31cd9e6d7da1d15c57c40575f5c85db/Message/MessageTemp/e1180914825140f051a87348bda5cbb5/File/hipro_test_push.p12");
InputStream in = new FileInputStream(file);
String encode = Base64.encode(in);
// 解码
byte[] decode = Base64.decode(encode);
......
package com.wecloud.im.controller;
import com.wecloud.im.param.CreateRtcChannelResult;
import com.wecloud.im.param.add.CreateRtcChannelParam;
import com.wecloud.im.param.rtc.CandidateForwardParam;
import com.wecloud.im.param.rtc.CreateRtcChannelParam;
import com.wecloud.im.param.rtc.CreateRtcChannelResult;
import com.wecloud.im.param.rtc.JoinRtcChannelParam;
import com.wecloud.im.param.rtc.LeaveRtcChannelParam;
import com.wecloud.im.param.rtc.RejectRtcChannelParam;
import com.wecloud.im.param.rtc.SdpForwardParam;
import com.wecloud.rtc.service.RtcService;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.common.controller.BaseController;
......@@ -35,37 +40,41 @@ public class ImRtcController extends BaseController {
@PostMapping("/createAndCall")
@ApiOperation(value = "创建频道,并邀请客户端加入", notes = "创建频道,并邀请客户端加入")
public ApiResult<CreateRtcChannelResult> createAndCall(@RequestBody CreateRtcChannelParam createRtcChannelParam) throws Exception {
CreateRtcChannelResult createRtcChannelResult = rtcService.create(createRtcChannelParam);
return ApiResult.ok(createRtcChannelResult);
return rtcService.createAndCall(createRtcChannelParam);
}
public ApiResult<Boolean> join() {
return ApiResult.result(true);
@PostMapping("/join")
@ApiOperation(value = "同意进入频道", notes = "")
public ApiResult<Boolean> join(@RequestBody JoinRtcChannelParam joinRtcChannelParam) {
return rtcService.join(joinRtcChannelParam);
}
public ApiResult<Boolean> reject() {
return ApiResult.result(true);
@PostMapping("/reject")
@ApiOperation(value = "拒接进入频道", notes = "")
public ApiResult<Boolean> reject(@RequestBody RejectRtcChannelParam rejectRtcChannelParam) {
return rtcService.reject(rejectRtcChannelParam);
}
public ApiResult<Boolean> leave() {
return ApiResult.result(true);
@PostMapping("/leave")
@ApiOperation(value = "主动挂断(离开频道)", notes = "")
public ApiResult<Boolean> leave(@RequestBody LeaveRtcChannelParam leaveRtcChannelParam) {
return rtcService.leave(leaveRtcChannelParam);
}
public ApiResult<Boolean> sdpForword() {
return ApiResult.result(true);
@PostMapping("/sdpForward")
@ApiOperation(value = "SDP数据转发", notes = "")
public ApiResult<Boolean> sdpForward(@RequestBody SdpForwardParam sdpForwardParam) {
return rtcService.sdpForward(sdpForwardParam);
}
public ApiResult<Boolean> candidateForword() {
return ApiResult.result(true);
@PostMapping("/candidateForward")
@ApiOperation(value = "candidate候选者数据转发", notes = "")
public ApiResult<Boolean> candidateForward(@RequestBody CandidateForwardParam candidateForwardParam) {
return rtcService.candidateForward(candidateForwardParam);
}
......
......@@ -32,7 +32,7 @@ public class SignController extends BaseController {
* 根据客户方生成签名字符串 验证通过则下发token
*/
@PostMapping("/get")
@ApiOperation(value = "获取sign(仅试使用)", notes = "生成签名测试,在生产环境中,此步骤需要第三方应用的服务端进行生成")
@ApiOperation(value = "获取sign(仅提供测试调试使用)", notes = "生成签名测试,在生产环境中,此步骤需要第三方应用的服务端进行生成")
public String get(@RequestBody GetSignParam getSignParam) throws Exception {
return new MD5().digestHex(getSignParam.getTimestamp() + getSignParam.getClientId() + getSignParam.getAppKey() + getSignParam.getAppSecret());
......
......@@ -44,7 +44,7 @@ public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> {
* allIdleTime—状态为IdleState的IdleStateEvent。在一定时间内不读不写会触发ALL_IDLE。指定0禁用。
* unit—readerIdleTime、writeIdleTime和allIdleTime的时间单位
*/
pipeline.addLast(new IdleStateHandler(15, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
}
}
......@@ -4,12 +4,14 @@ import cn.hutool.core.thread.ThreadFactoryBuilder;
import com.wecloud.im.ws.model.WsConstants;
import com.wecloud.im.ws.receive.ReadWsData;
import com.wecloud.im.ws.service.MangerChannelService;
import com.wecloud.rtc.service.RtcService;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
......@@ -35,6 +37,9 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram
@Resource
private ReadWsData readWsData;
@Autowired
private RtcService rtcService;
@Resource
private MangerChannelService mangerChannelService;
......@@ -170,9 +175,16 @@ public class WsReadHandler extends SimpleChannelInboundHandler<TextWebSocketFram
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
String appKey = ctx.channel().attr(MangerChannelService.APP_KEY).get();
String clientId = ctx.channel().attr(MangerChannelService.CLIENT_ID).get();
String userIdByChannel = mangerChannelService.getInfoByChannel(ctx);
log.info("uid:" + userIdByChannel + "," + "handlerRemoved" + ",channelId:" + ctx.channel().id().asLongText());
// 关掉连接
ctx.close();
// rtc清空缓存
rtcService.clientOffline(appKey, clientId);
}
}
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "CandidateForwardParam")
public class CandidateForwardParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("频道id")
private Long channelId;
/**
* 转发的候选者数据
*/
private String candidateData;
}
package com.wecloud.im.param.add;
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
......@@ -27,12 +27,9 @@ public class CreateRtcChannelParam implements Serializable {
private String type;
@ApiModelProperty(value = "绑定的会话id,可选", required = false)
private String toConversation;
private Long conversationId;
@ApiModelProperty(value = "接收方展示的系统推送内容,可", required = false, example = "{" +
" \"title\":\"xxx正在邀请你视频通话\"," +
" \"subTitle\":\"点击接听\"" +
" }")
@ApiModelProperty(value = "接收方展示的系统推送内容,可", required = false)
private String push;
@ApiModelProperty(value = "是否需要给对方发系统通知", required = true)
......
package com.wecloud.im.param;
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
......
package com.wecloud.im.param.add;
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
......@@ -7,13 +7,13 @@ import lombok.Data;
import java.io.Serializable;
/**
* 创建频道请求参数
* 加入频道请求参数
*
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "CreateRtcChannelParam")
@ApiModel(value = "JoinRtcChannelParam")
public class JoinRtcChannelParam implements Serializable {
private static final long serialVersionUID = 1L;
......
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 退出频道 请求
*
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "LeaveRtcChannelParam")
public class LeaveRtcChannelParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("频道id")
private Long channelId;
}
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 拒接加入频道 请求
*
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "RejectRtcChannelParam")
public class RejectRtcChannelParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("频道id")
private Long channelId;
}
package com.wecloud.im.param.rtc;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* @author wei
* @since 2021-04-29
*/
@Data
@ApiModel(value = "SdpForwardParam")
public class SdpForwardParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("频道id")
private Long channelId;
@ApiModelProperty("sdp转发的数据")
private String sdpData;
@ApiModelProperty("sdp类型: Offer或Answer")
private String sdpType;
}
......@@ -16,7 +16,7 @@ import com.wecloud.im.service.ImConversationMembersService;
import com.wecloud.im.service.ImInboxService;
import com.wecloud.im.service.ImMessageService;
import com.wecloud.im.ws.enums.WsResponseCmdEnum;
import com.wecloud.im.ws.model.ResponseModel;
import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.service.WriteDataService;
import io.geekidea.springbootplus.framework.common.api.ApiCode;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
......@@ -205,7 +205,7 @@ public class ImInboxServiceImpl extends BaseServiceImpl<ImInboxMapper, ImInbox>
continue;
}
// 向接收方推送
ResponseModel<ImMessageOnlineSend> responseModel = new ResponseModel<>();
WsResponseModel<ImMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.ONLINE_EVENT_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
......
......@@ -8,7 +8,7 @@ package com.wecloud.im.ws.enums;
public enum WsResponseCmdEnum {
/**
* 下发单人在线RTC事件
* 下发在线RTC事件
*/
SINGLE_RTC_MSG(4),
......@@ -34,20 +34,6 @@ public enum WsResponseCmdEnum {
this.cmdCode = uriCode;
}
/**
* 根据uriCode获取
*
* @param uriCode
* @return
*/
public static WsResponseCmdEnum getByCode(int uriCode) {
for (WsResponseCmdEnum wsResponsePathEnum : values()) {
if (wsResponsePathEnum.getCmdCode() == uriCode) {
return wsResponsePathEnum;
}
}
return null;
}
public int getCmdCode() {
return cmdCode;
......
package com.wecloud.im.ws.enums;
/**
* @Description webrtc响应类型
* @Author hewei hwei1233@163.com
* @Date 2019-12-05
*/
public enum WsRtcResponseSubCmdEnum {
// --- 服务端响应
/**
* 接收到RTC邀请
*/
RTC_CALL(1),
/**
* 用户状态更新事件(用户加入频道)
*/
CLIENT_JOIN(2),
/**
* 用户状态更新事件(用户退出频道)
*/
CLIENT_LEAVE(3),
/**
* 用户状态更新事件(用户拒接邀请,不同意进入频道)
*/
CLIENT_REJECT(4),
/**
* SDP数据转发
*/
SDP_FORWARD(5),
/**
* candidate候选者数据转发
*/
CANDIDATE_FORWARD(6);
private final int cmdCode;
WsRtcResponseSubCmdEnum(int cmdCode) {
this.cmdCode = cmdCode;
}
public int getCmdCode() {
return cmdCode;
}
}
......@@ -12,7 +12,7 @@ import java.io.Serializable;
*/
@Data
@Accessors(chain = true)
public class ResponseModel<T> implements Serializable {
public class WsResponseModel<T> implements Serializable {
/**
* 枚举类WsResponseCmdEnum 请求uri的编码
......
package com.wecloud.im.ws.service;
import com.wecloud.im.ws.model.ResponseModel;
import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.model.request.ReceiveModel;
import io.geekidea.springbootplus.framework.common.api.ApiCode;
......@@ -47,7 +47,7 @@ public interface WriteDataService {
*
* @param responseModel
*/
void write(ResponseModel responseModel, String toAppKey, String toClientId);
void write(WsResponseModel responseModel, String toAppKey, String toClientId);
}
......@@ -310,22 +310,22 @@ public class MangerChannelServiceImpl implements MangerChannelService {
NioSocketChannel nioSocketChannel = get(toAppKey, toClientId);
if (null == nioSocketChannel) {
// userCache.offline(toAppKey + toClientId);
if (log.isDebugEnabled()) {
// if (log.isDebugEnabled()) {
log.info("writeData连接为空:" + toAppKey + toClientId + "," + msg);
}
// }
return false;
}
// 判断连接是否断开
if (nioSocketChannel.isShutdown()) {
if (log.isDebugEnabled()) {
// if (log.isDebugEnabled()) {
log.info("writeData连接断开:" + toAppKey + toClientId + "," + msg + ",\nchannelId:" + nioSocketChannel.id().asLongText());
}
// }
return false;
}
if (log.isDebugEnabled()) {
// if (log.isDebugEnabled()) {
log.info("writeData:" + toAppKey + "," + toClientId + "," + msg + ",\nchannelId:" + nioSocketChannel.id().asLongText());
}
// }
ChannelFuture channelFuture = nioSocketChannel.writeAndFlush(new TextWebSocketFrame(msg));
channelFuture.addListener(
......
......@@ -3,7 +3,7 @@ package com.wecloud.im.ws.service.impl;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.wecloud.im.ws.model.ResponseModel;
import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.model.WsConstants;
import com.wecloud.im.ws.model.request.ReceiveModel;
import com.wecloud.im.ws.service.MangerChannelService;
......@@ -62,7 +62,7 @@ public class WriteDataServiceImpl implements WriteDataService {
@Override
public void dataAndStatus(ReceiveModel receiveModel, ApiCode apiCode, Object data, String toAppKey, String toClientId) {
ApiResult<Boolean> apiResult = ApiResult.result(apiCode);
ResponseModel responseModel = new ResponseModel();
WsResponseModel responseModel = new WsResponseModel();
responseModel.setMsg(apiResult.getMessage());
responseModel.setCmd(receiveModel.getCmd());
responseModel.setReqId(receiveModel.getReqId());
......@@ -72,7 +72,7 @@ public class WriteDataServiceImpl implements WriteDataService {
}
@Override
public void write(ResponseModel responseModel, String toAppKey, String toClientId) {
public void write(WsResponseModel responseModel, String toAppKey, String toClientId) {
WRITE_TASK_THREAD_POOL_EXECUTOR.execute(
() -> {
......
......@@ -19,7 +19,7 @@ import com.wecloud.im.service.ImMessageService;
import com.wecloud.im.ws.annotation.CmdTypeAnnotation;
import com.wecloud.im.ws.enums.WsRequestCmdEnum;
import com.wecloud.im.ws.enums.WsResponseCmdEnum;
import com.wecloud.im.ws.model.ResponseModel;
import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.model.request.ReceiveModel;
import com.wecloud.im.ws.sender.SystemPush;
import com.wecloud.im.ws.service.WriteDataService;
......@@ -172,7 +172,7 @@ public class ImChatConcrete extends ImCmdAbstract {
}
// 向接收方推送
ResponseModel<ImMessageOnlineSend> responseModel = new ResponseModel<>();
WsResponseModel<ImMessageOnlineSend> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.ONLINE_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
......@@ -186,7 +186,7 @@ public class ImChatConcrete extends ImCmdAbstract {
}
// 响应发送方消息id等信息
ResponseModel<HashMap<String, Long>> responseModel = new ResponseModel<>();
WsResponseModel<HashMap<String, Long>> responseModel = new WsResponseModel<>();
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCmd(WsResponseCmdEnum.RES.getCmdCode());
responseModel.setCode(result.getCode());
......@@ -233,7 +233,7 @@ public class ImChatConcrete extends ImCmdAbstract {
log.info("被对方拉黑了");
// 响应发送方
ResponseModel<HashMap<String, Long>> responseModel = new ResponseModel<>();
WsResponseModel<HashMap<String, Long>> responseModel = new WsResponseModel<>();
ApiResult<Boolean> result = ApiResult.result(ApiCode.IS_BE_BLACK);
responseModel.setCmd(WsResponseCmdEnum.RES.getCmdCode());
responseModel.setCode(result.getCode());
......@@ -248,7 +248,7 @@ public class ImChatConcrete extends ImCmdAbstract {
if (black) {
log.info("你把对方拉黑了");
// 响应发送方
ResponseModel<HashMap<String, Long>> responseModel = new ResponseModel<>();
WsResponseModel<HashMap<String, Long>> responseModel = new WsResponseModel<>();
ApiResult<Boolean> result = ApiResult.result(ApiCode.IS_TO_BLACK);
responseModel.setCmd(WsResponseCmdEnum.RES.getCmdCode());
responseModel.setCode(result.getCode());
......
......@@ -14,7 +14,7 @@ import com.wecloud.im.ws.model.request.ReceiveModel;
import com.wecloud.im.ws.sender.SystemPush;
import com.wecloud.im.ws.service.WriteDataService;
import com.wecloud.im.ws.strategy.ImCmdAbstract;
import com.wecloud.rtc.SubCmd;
import com.wecloud.rtc.entity.RtcSubCmd;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -59,31 +59,31 @@ public class SingleRtcConcrete extends ImCmdAbstract {
public void process(ReceiveModel receiveModel, ChannelHandlerContext ctx, String data, String appKey, String clientId) throws JsonProcessingException {
// 指令判空
if (receiveModel.getData().get(SubCmd.SUB_CMD) == null) {
if (receiveModel.getData().get(RtcSubCmd.SUB_CMD) == null) {
return;
}
String cmd = receiveModel.getData().get(SubCmd.SUB_CMD).toString();
String cmd = receiveModel.getData().get(RtcSubCmd.SUB_CMD).toString();
switch (cmd) {
//创建频道
case SubCmd.CREATE:
break;
//加入频道
case SubCmd.JOIN:
break;
//拒绝加入频道
case SubCmd.REJECT:
break;
//SDP数据转发
case SubCmd.SDP:
break;
//主动挂断(离开频道)
case SubCmd.LEAVE:
break;
// //创建频道
// case RtcSubCmd.CREATE:
// break;
//
// //加入频道
// case RtcSubCmd.JOIN:
// break;
//
// //拒绝加入频道
// case RtcSubCmd.REJECT:
// break;
//
// //SDP数据转发
// case RtcSubCmd.SDP:
// break;
//
// //主动挂断(离开频道)
// case RtcSubCmd.LEAVE:
// break;
}
......
package com.wecloud.rtc;
import java.io.Serializable;
public class SubCmd implements Serializable {
/**
* subCmd子类型指令码
*/
public static final String SUB_CMD = "subCmd";
/**
* 创建频道
*/
public static final String CREATE = "create";
/**
* 加入频道
*/
public static final String JOIN = "join";
/**
* 拒绝加入频道
*/
public static final String REJECT = "reject";
/**
* 主动挂断(离开频道)
*/
public static final String LEAVE = "leave";
// --- 服务端响应
/**
* 接收到RTC邀请
*/
public static final String RTC_CALL = "rtcCall";
/**
* 用户状态更新事件(用户加入,用户退出,用户拒接邀请)
*/
public static final String CLIENT_EVENT = "clientEvent";
/**
* 流状态更新(切换音频 切换视频)
*/
public static final String TYPE_EVENT = "typeEvent";
/**
* 状态更新(网络断开,挂断)
*/
public static final String STATUS_EVENT = "statusEvent";
/**
* SDP数据转发
*/
public static final String SDP = "SDP";
/**
* 忙线
*/
public static final String BUSY = "busy";
}
package com.wecloud.rtc.entity;
import java.io.Serializable;
public class RtcSubCmd implements Serializable {
/**
* subCmd子类型指令码
*/
public static final String SUB_CMD = "subCmd";
}
package com.wecloud.rtc.entity;
package com.wecloud.rtc.entity.redis;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
......@@ -12,8 +12,11 @@ import java.io.Serializable;
public class RtcChannelInfo implements Serializable {
@ApiModelProperty("当前房主")
String owner;
private String owner;
private String appKey;
// private String appId;
@ApiModelProperty("创建时间")
Long createTimestamp;
private Long createTimestamp;
}
package com.wecloud.rtc.entity;
package com.wecloud.rtc.entity.redis;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
......@@ -12,11 +12,13 @@ import java.io.Serializable;
public class RtcJoinUser implements Serializable {
@ApiModelProperty("客户端")
String clientId;
private String clientId;
@ApiModelProperty("加入时间")
Long createTimestamp;
private Long createTimestamp;
String sdp;
private String sdpData;
private String sdpType;
}
package com.wecloud.rtc;
package com.wecloud.rtc.entity.redis;
import java.io.Serializable;
public class RtcRedisKey implements Serializable {
/**
* 维护频道信息
* 维护频道信息 (kv)
*/
public static final String RTC_CHANNEL_INFO = "rci:%s";
public static final String RTC_CHANNEL_INFO = "r:ci:%s";
/**
* 维护所有用户当前在线的频道ID
* 维护用户当前在线的频道ID ( kv)
* user_join_channel = ujc
* rcu:clientA = 10001
* rcu:clientB = 10001
* rcu:clientC = 10002
* rcu:clientD = 10003
*/
public static final String USER_JOIN_CHANNEL = "ujc:%s";
public static final String USER_JOIN_CHANNEL = "r:ujc:%s";
/**
* 维护频道中存在的用户
* <p>
* redis Key (set 集合):
* 维护频道中存在的用户 (set 集合):
* rtc_channel_users = rcu
* rcu:10001 = clientA , clientB
* rcu:10002 = clientC
* rcu:10003 = clientD
*/
public static final String RTC_CHANNEL_USERS = "rcu:%s";
public static final String RTC_CHANNEL_USERS = "r:cu:%s";
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcCallResponse extends RtcSubDataBase implements Serializable {
private String type;
private Long conversationId;
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcCandidateForwardResponse extends RtcSubDataBase implements Serializable {
/**
* 转发的候选者数据
*/
private String candidateData;
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcClientJoinResponse extends RtcSubDataBase implements Serializable {
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcClientLeaveResponse extends RtcSubDataBase implements Serializable {
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcClientRejectResponse extends RtcSubDataBase implements Serializable {
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import java.io.Serializable;
/**
* webRtc websocket下发数据封装类
*
* @param <T>
*/
@Data
public class RtcResponseBase<T> implements Serializable {
/**
* 子指令
*/
private Integer subCmd;
/**
* 根据不同子指令 不同的实体
*/
private T subData;
/**
* 自定义拓展字段
*/
private String attrs;
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
public class RtcSdpForwardResponse extends RtcSubDataBase implements Serializable {
/**
* channelId : 1234263457652
* clientId : 7657567
* sdpData : xxxxxxxxxxxxxxxx
* sdpType : Offer/Answer
*/
private String sdpData;
private String sdpType;
}
package com.wecloud.rtc.entity.response;
import lombok.Data;
import java.io.Serializable;
@Data
public class RtcSubDataBase implements Serializable {
private Long channelId;
private String clientId;
private Long timestamp;
}
package com.wecloud.rtc.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.wecloud.rtc.entity.redis.RtcChannelInfo;
import java.util.List;
......@@ -9,7 +10,32 @@ import java.util.List;
*/
public interface MangerRtcCacheService {
boolean isEmpty(String appKey, String clientId, Long rtcChannelId);
/**
* 移除频道信息
*
* @param rtcChannelId
* @return
*/
boolean delChannelInfo(Long rtcChannelId);
/**
* 频道中客户端是否为空
*
* @param rtcChannelId
* @return
*/
boolean channelIsEmpty(Long rtcChannelId);
/**
* 获取频道信息
*
* @param rtcChannelId
* @return
* @throws JsonProcessingException
*/
RtcChannelInfo getRtcChannelInfo(Long rtcChannelId) throws JsonProcessingException;
/**
* 创建一个频道
......@@ -28,17 +54,17 @@ public interface MangerRtcCacheService {
/**
* 退出频道
*/
void remove(String appKey, String clientId, Long rtcChannelId);
void leave(String appKey, String clientId, Long rtcChannelId);
/**
* 根据频道ID获取频道内所有client
*/
List<String> getClientListByRtcChannelId(Long rtcChannelId);
// /**
// * 根据客户端ID获取该客户端加入的频道ID
// */
// Long getRtcChannelIdListByClientId(String appKey, String clientId);
/**
* 根据客户端ID获取该客户端加入的频道ID
*/
Long getRtcChannelIdListByClientId(String appKey, String clientId);
/**
* 获取客户端忙线/空闲状态
......
package com.wecloud.rtc.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.wecloud.im.param.CreateRtcChannelResult;
import com.wecloud.im.param.add.CreateRtcChannelParam;
import com.wecloud.im.param.rtc.CandidateForwardParam;
import com.wecloud.im.param.rtc.CreateRtcChannelParam;
import com.wecloud.im.param.rtc.CreateRtcChannelResult;
import com.wecloud.im.param.rtc.JoinRtcChannelParam;
import com.wecloud.im.param.rtc.LeaveRtcChannelParam;
import com.wecloud.im.param.rtc.RejectRtcChannelParam;
import com.wecloud.im.param.rtc.SdpForwardParam;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
/**
* 管理rtc频道
*/
public interface RtcService {
/**
* 客户端离线
*/
void clientOffline(String appKey, String clientId);
/**
* 创建一个频道
* 创建一个频道,并向接收方发送系统推送
*/
CreateRtcChannelResult create(CreateRtcChannelParam createRtcChannelParam) throws JsonProcessingException;
ApiResult<CreateRtcChannelResult> createAndCall(CreateRtcChannelParam createRtcChannelParam) throws JsonProcessingException;
/**
* 加入频道
*/
void join();
ApiResult<Boolean> join(JoinRtcChannelParam joinRtcChannelParam);
/**
* 拒接加入频道
*/
void reject();
ApiResult<Boolean> reject(RejectRtcChannelParam rejectRtcChannelParam);
/**
* 退出频道
*/
void leave();
ApiResult<Boolean> leave(LeaveRtcChannelParam leaveRtcChannelParam);
void sdpForword();
/**
* SDP数据转发
*
* @param sdpForwardParam
* @return
*/
ApiResult<Boolean> sdpForward(SdpForwardParam sdpForwardParam);
void candidateForword();
/**
* candidate候选者数据转发
*
* @param candidateForwardParam
* @return
*/
ApiResult<Boolean> candidateForward(CandidateForwardParam candidateForwardParam);
}
package com.wecloud.rtc.service;
import com.wecloud.rtc.entity.response.RtcCallResponse;
import com.wecloud.rtc.entity.response.RtcCandidateForwardResponse;
import com.wecloud.rtc.entity.response.RtcClientJoinResponse;
import com.wecloud.rtc.entity.response.RtcClientLeaveResponse;
import com.wecloud.rtc.entity.response.RtcClientRejectResponse;
import com.wecloud.rtc.entity.response.RtcSdpForwardResponse;
/**
* WebRtc webSocket下发指令数据
*/
public interface WsRtcWrite {
/**
* 接收到RTC邀请
*/
void rtcCall(RtcCallResponse rtcCallResponse, String toAppKey, String toClientId);
/**
* 用户状态更新事件(用户加入频道)
*/
void clientJoin(RtcClientJoinResponse rtcClientJoinResponse, String toAppKey, String toClientId);
/**
* 用户状态更新事件(用户退出频道)
*/
void clientLeave(RtcClientLeaveResponse rtcClientLeaveResponse, String toAppKey, String toClientId);
/**
* 用户状态更新事件(用户拒接邀请;不同意进入频道)
*/
void clientReject(RtcClientRejectResponse rtcClientRejectResponse, String toAppKey, String toClientId);
/**
* SDP数据转发
*/
void sdpForward(RtcSdpForwardResponse rtcSdpForwardResponse, String toAppKey, String toClientId);
/**
* candidate候选者数据转发
*/
void candidateForward(RtcCandidateForwardResponse rtcCandidateForwardResponse, String toAppKey, String toClientId);
}
......@@ -3,9 +3,10 @@ package com.wecloud.rtc.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.wecloud.im.ws.utils.RedisUtils;
import com.wecloud.rtc.entity.RtcChannelInfo;
import com.wecloud.rtc.RtcRedisKey;
import com.wecloud.rtc.entity.redis.RtcChannelInfo;
import com.wecloud.rtc.entity.redis.RtcRedisKey;
import com.wecloud.rtc.service.MangerRtcCacheService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
......@@ -22,29 +23,54 @@ public class MangerRtcCacheServiceImpl implements MangerRtcCacheService {
@Override
public boolean isEmpty(String appKey, String clientId, Long rtcChannelId) {
public boolean delChannelInfo(Long rtcChannelId) {
String channelKey = String.format(RtcRedisKey.RTC_CHANNEL_INFO, rtcChannelId);
redisUtils.delKey(channelKey);
return true;
}
@Override
public boolean channelIsEmpty(Long rtcChannelId) {
List<String> clientListByRtcChannelId = getClientListByRtcChannelId(rtcChannelId);
// 移除自己
clientListByRtcChannelId.remove(appKey + clientId);
// // 移除自己
// clientListByRtcChannelId.remove(appKey + clientId);
return clientListByRtcChannelId.isEmpty();
}
@Override
public RtcChannelInfo getRtcChannelInfo(Long rtcChannelId) throws JsonProcessingException {
// 频道信息
String channelKey = String.format(RtcRedisKey.RTC_CHANNEL_INFO, rtcChannelId);
String value = redisUtils.getKey(channelKey);
if (StringUtils.isBlank(value)) {
return null;
}
JsonMapper jsonMapper = new JsonMapper();
return jsonMapper.readValue(value, RtcChannelInfo.class);
}
@Override
public void create(String appKey, String clientId, Long rtcChannelId) throws JsonProcessingException {
// --- 频道信息
RtcChannelInfo rtcChannelInfo = new RtcChannelInfo();
rtcChannelInfo.setAppKey(appKey);
// rtcChannelInfo.setAppId("");
//当前房主
rtcChannelInfo.setOwner(appKey + clientId);
rtcChannelInfo.setOwner(clientId);
//创建时间
rtcChannelInfo.setCreateTimestamp(new Date().getTime());
String rtcChannelInfoJson = new JsonMapper().writeValueAsString(rtcChannelInfo);
// --- 保存频道信息
redisUtils.setKey(RtcRedisKey.RTC_CHANNEL_INFO, rtcChannelInfoJson);
String channelKey = String.format(RtcRedisKey.RTC_CHANNEL_INFO, rtcChannelId);
redisUtils.setKey(channelKey, rtcChannelInfoJson);
//用户当前在线的频道ID
String userJoinChannelKey = String.format(RtcRedisKey.USER_JOIN_CHANNEL, appKey + clientId);
......@@ -52,7 +78,7 @@ public class MangerRtcCacheServiceImpl implements MangerRtcCacheService {
//频道中存在的用户
String rtcChannelUsers = String.format(RtcRedisKey.RTC_CHANNEL_USERS, rtcChannelId);
redisUtils.addForSet(rtcChannelUsers, appKey + clientId);
redisUtils.addForSet(rtcChannelUsers, clientId);
}
......@@ -65,12 +91,12 @@ public class MangerRtcCacheServiceImpl implements MangerRtcCacheService {
//频道中存在的用户
String rtcChannelUsers = String.format(RtcRedisKey.RTC_CHANNEL_USERS, rtcChannelId);
redisUtils.addForSet(rtcChannelUsers, appKey + clientId);
redisUtils.addForSet(rtcChannelUsers, clientId);
}
@Override
public void remove(String appKey, String clientId, Long rtcChannelId) {
public void leave(String appKey, String clientId, Long rtcChannelId) {
//用户当前在线的频道ID
String userJoinChannelKey = String.format(RtcRedisKey.USER_JOIN_CHANNEL, appKey + clientId);
......@@ -78,7 +104,7 @@ public class MangerRtcCacheServiceImpl implements MangerRtcCacheService {
//频道中存在的用户
String rtcChannelUsers = String.format(RtcRedisKey.RTC_CHANNEL_USERS, rtcChannelId);
redisUtils.removeForSet(rtcChannelUsers, appKey + clientId);
redisUtils.removeForSet(rtcChannelUsers, clientId);
}
@Override
......@@ -94,15 +120,17 @@ public class MangerRtcCacheServiceImpl implements MangerRtcCacheService {
}
// @Override
// public Long getRtcChannelIdListByClientId(String appKey, String clientId) {
//
// //用户当前在线的频道ID
// String userJoinChannelKey = String.format(RtcRedisKey.USER_JOIN_CHANNEL, appKey + clientId);
// String key = redisUtils.getKey(userJoinChannelKey);
//
// return Long.valueOf(key);
// }
@Override
public Long getRtcChannelIdListByClientId(String appKey, String clientId) {
//用户当前在线的频道ID
String userJoinChannelKey = String.format(RtcRedisKey.USER_JOIN_CHANNEL, appKey + clientId);
String key = redisUtils.getKey(userJoinChannelKey);
if (StringUtils.isBlank(key)) {
return null;
}
return Long.valueOf(key);
}
@Override
public boolean getBusyStatus(String appKey, String clientId) {
......
......@@ -3,22 +3,44 @@ package com.wecloud.rtc.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.wecloud.im.entity.ImApplication;
import com.wecloud.im.entity.ImClient;
import com.wecloud.im.param.CreateRtcChannelResult;
import com.wecloud.im.param.add.CreateRtcChannelParam;
import com.wecloud.im.param.rtc.CandidateForwardParam;
import com.wecloud.im.param.rtc.CreateRtcChannelParam;
import com.wecloud.im.param.rtc.CreateRtcChannelResult;
import com.wecloud.im.param.rtc.JoinRtcChannelParam;
import com.wecloud.im.param.rtc.LeaveRtcChannelParam;
import com.wecloud.im.param.rtc.RejectRtcChannelParam;
import com.wecloud.im.param.rtc.SdpForwardParam;
import com.wecloud.im.service.ImApplicationService;
import com.wecloud.im.service.ImClientService;
import com.wecloud.im.ws.service.MangerChannelService;
import com.wecloud.rtc.entity.response.RtcCallResponse;
import com.wecloud.rtc.entity.response.RtcCandidateForwardResponse;
import com.wecloud.rtc.entity.response.RtcClientJoinResponse;
import com.wecloud.rtc.entity.response.RtcClientLeaveResponse;
import com.wecloud.rtc.entity.response.RtcClientRejectResponse;
import com.wecloud.rtc.entity.response.RtcSdpForwardResponse;
import com.wecloud.rtc.service.MangerRtcCacheService;
import com.wecloud.rtc.service.RtcService;
import com.wecloud.rtc.service.WsRtcWrite;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import io.geekidea.springbootplus.framework.shiro.util.SnowflakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Slf4j
@Service
public class RtcServiceImpl implements RtcService {
@Autowired
private ImApplicationService imApplicationService;
@Autowired
private WsRtcWrite wsRtcWrite;
/**
* redis缓存
*/
......@@ -28,50 +50,238 @@ public class RtcServiceImpl implements RtcService {
@Autowired
private ImClientService imClientService;
@Autowired
private MangerChannelService mangerChannelService;
@Override
public CreateRtcChannelResult create(CreateRtcChannelParam createRtcChannelParam) throws JsonProcessingException {
public void clientOffline(String appKey, String clientId) {
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getOneByAppKey(appKey);
ImClient client = imClientService.getCacheImClient(imApplication.getId(), clientId);
// 获取该客户端加入的频道ID
Long listByClientId = mangerRtcCacheService.getRtcChannelIdListByClientId(imApplication.getAppKey(), client.getClientId());
if (listByClientId == null) {
return;
}
LeaveRtcChannelParam leaveRtcChannelParam = new LeaveRtcChannelParam();
leaveRtcChannelParam.setChannelId(listByClientId);
// websocket离线逻辑 服务端踢出频道
this.leave(leaveRtcChannelParam, client, imApplication);
}
@Override
public ApiResult<CreateRtcChannelResult> createAndCall(CreateRtcChannelParam createRtcChannelParam) throws JsonProcessingException {
ImClient client = imClientService.getCurentClient();
Long rtcChannelId = SnowflakeUtil.getId();
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
// 判断发起方必须在线
boolean onlineStatus = mangerChannelService.getOnlineStatus(imApplication.getAppKey(), client.getClientId());
if (!onlineStatus) {
log.info("发起方必须在线" + imApplication.getAppKey() + client.getClientId());
ApiResult.fail();
}
// 添加缓存
mangerRtcCacheService.create(imApplication.getAppKey(), client.getClientId(), rtcChannelId);
CreateRtcChannelResult createRtcChannelResult = new CreateRtcChannelResult();
createRtcChannelResult.setChannelId(rtcChannelId);
// ws向接收方发送通知
RtcCallResponse rtcCallResponse = new RtcCallResponse();
rtcCallResponse.setType(createRtcChannelParam.getType());
// rtcCallResponse.setConversationId(createRtcChannelParam.getConversationId());
rtcCallResponse.setChannelId(rtcChannelId);
rtcCallResponse.setClientId(client.getClientId());
rtcCallResponse.setTimestamp(new Date().getTime());
wsRtcWrite.rtcCall(rtcCallResponse, imApplication.getAppKey(), createRtcChannelParam.getToClient());
// 像对方发送邀请
// TODO 待开发 下发安卓和ios系统推送
return createRtcChannelResult;
return ApiResult.ok(createRtcChannelResult);
}
@Override
public void join() {
public ApiResult<Boolean> join(JoinRtcChannelParam joinRtcChannelParam) {
ImClient client = imClientService.getCurentClient();
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
// 修改缓存
mangerRtcCacheService.join(imApplication.getAppKey(), client.getClientId(), joinRtcChannelParam.getChannelId());
//获取频道内所有client
List<String> clientListByRtcChannelId = mangerRtcCacheService.getClientListByRtcChannelId(joinRtcChannelParam.getChannelId());
// 移除自己
clientListByRtcChannelId.remove(client.getClientId());
for (String toClientId : clientListByRtcChannelId) {
// ws向接收方发送通知
RtcClientJoinResponse rtcSdpForwardResponse = new RtcClientJoinResponse();
rtcSdpForwardResponse.setChannelId(joinRtcChannelParam.getChannelId());
rtcSdpForwardResponse.setClientId(client.getClientId());
rtcSdpForwardResponse.setTimestamp(new Date().getTime());
wsRtcWrite.clientJoin(rtcSdpForwardResponse, imApplication.getAppKey(), toClientId);
}
return ApiResult.ok(true);
}
@Override
public void reject() {
public ApiResult<Boolean> reject(RejectRtcChannelParam rejectRtcChannelParam) {
ImClient client = imClientService.getCurentClient();
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
//获取频道内所有client
List<String> clientListByRtcChannelId = mangerRtcCacheService.getClientListByRtcChannelId(rejectRtcChannelParam.getChannelId());
// 移除自己
clientListByRtcChannelId.remove(client.getClientId());
for (String toClientId : clientListByRtcChannelId) {
// ws向接收方发送通知
RtcClientRejectResponse rtcClientRejectResponse = new RtcClientRejectResponse();
rtcClientRejectResponse.setChannelId(rejectRtcChannelParam.getChannelId());
rtcClientRejectResponse.setClientId(client.getClientId());
rtcClientRejectResponse.setTimestamp(new Date().getTime());
wsRtcWrite.clientReject(rtcClientRejectResponse, imApplication.getAppKey(), toClientId);
}
return ApiResult.ok(true);
}
@Override
public void leave() {
public ApiResult<Boolean> leave(LeaveRtcChannelParam leaveRtcChannelParam) {
ImClient client = imClientService.getCurentClient();
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
this.leave(leaveRtcChannelParam, client, imApplication);
return ApiResult.ok(true);
}
private void leave(LeaveRtcChannelParam leaveRtcChannelParam, ImClient client, ImApplication imApplication) {
// 修改缓存
mangerRtcCacheService.leave(imApplication.getAppKey(), client.getClientId(), leaveRtcChannelParam.getChannelId());
//获取频道内所有client
List<String> clientListByRtcChannelId = mangerRtcCacheService.getClientListByRtcChannelId(leaveRtcChannelParam.getChannelId());
// 移除自己
clientListByRtcChannelId.remove(client.getClientId());
for (String toClientId : clientListByRtcChannelId) {
// ws向接收方发送通知
RtcClientLeaveResponse rtcClientLeaveResponse = new RtcClientLeaveResponse();
rtcClientLeaveResponse.setChannelId(leaveRtcChannelParam.getChannelId());
rtcClientLeaveResponse.setClientId(client.getClientId());
rtcClientLeaveResponse.setTimestamp(new Date().getTime());
wsRtcWrite.clientLeave(rtcClientLeaveResponse, imApplication.getAppKey(), toClientId);
}
// 判断频道内是否无其他人了
if (mangerRtcCacheService.channelIsEmpty(leaveRtcChannelParam.getChannelId())) {
// 移除频道信息
mangerRtcCacheService.delChannelInfo(leaveRtcChannelParam.getChannelId());
}
}
@Override
public void sdpForword() {
public ApiResult<Boolean> sdpForward(SdpForwardParam sdpForwardParam) {
ImClient client = imClientService.getCurentClient();
Long rtcChannelId = SnowflakeUtil.getId();
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
// 判断发起方必须在线
boolean onlineStatus = mangerChannelService.getOnlineStatus(imApplication.getAppKey(), client.getClientId());
if (!onlineStatus) {
log.info("发起方必须在线" + imApplication.getAppKey() + client.getClientId());
ApiResult.fail();
}
CreateRtcChannelResult createRtcChannelResult = new CreateRtcChannelResult();
createRtcChannelResult.setChannelId(rtcChannelId);
//获取频道内所有client
List<String> clientListByRtcChannelId = mangerRtcCacheService.getClientListByRtcChannelId(sdpForwardParam.getChannelId());
// 移除自己
clientListByRtcChannelId.remove(client.getClientId());
for (String toClientId : clientListByRtcChannelId) {
// ws向接收方发送通知
RtcSdpForwardResponse rtcSdpForwardResponse = new RtcSdpForwardResponse();
rtcSdpForwardResponse.setSdpData(sdpForwardParam.getSdpData());
rtcSdpForwardResponse.setSdpType(sdpForwardParam.getSdpType());
rtcSdpForwardResponse.setChannelId(rtcChannelId);
rtcSdpForwardResponse.setClientId(client.getClientId());
rtcSdpForwardResponse.setTimestamp(new Date().getTime());
wsRtcWrite.sdpForward(rtcSdpForwardResponse, imApplication.getAppKey(), toClientId);
}
return ApiResult.ok(true);
}
@Override
public void candidateForword() {
public ApiResult<Boolean> candidateForward(CandidateForwardParam candidateForwardParam) {
ImClient client = imClientService.getCurentClient();
Long rtcChannelId = SnowflakeUtil.getId();
// 根据appKey查询appid
ImApplication imApplication = imApplicationService.getById(client.getFkAppid());
// 判断发起方必须在线
boolean onlineStatus = mangerChannelService.getOnlineStatus(imApplication.getAppKey(), client.getClientId());
if (!onlineStatus) {
log.info("发起方必须在线" + imApplication.getAppKey() + client.getClientId());
ApiResult.fail();
}
CreateRtcChannelResult createRtcChannelResult = new CreateRtcChannelResult();
createRtcChannelResult.setChannelId(rtcChannelId);
//获取频道内所有client
List<String> clientListByRtcChannelId = mangerRtcCacheService.getClientListByRtcChannelId(candidateForwardParam.getChannelId());
// 移除自己
clientListByRtcChannelId.remove(client.getClientId());
for (String toClientId : clientListByRtcChannelId) {
// ws向接收方发送通知
RtcCandidateForwardResponse rtcCandidateForwardResponse = new RtcCandidateForwardResponse();
rtcCandidateForwardResponse.setCandidateData(candidateForwardParam.getCandidateData());
rtcCandidateForwardResponse.setChannelId(rtcChannelId);
rtcCandidateForwardResponse.setClientId(client.getClientId());
rtcCandidateForwardResponse.setTimestamp(new Date().getTime());
wsRtcWrite.candidateForward(rtcCandidateForwardResponse, imApplication.getAppKey(), toClientId);
}
return ApiResult.ok(true);
}
}
package com.wecloud.rtc.service.impl;
import com.wecloud.im.ws.enums.WsResponseCmdEnum;
import com.wecloud.im.ws.enums.WsRtcResponseSubCmdEnum;
import com.wecloud.im.ws.model.WsResponseModel;
import com.wecloud.im.ws.service.WriteDataService;
import com.wecloud.rtc.entity.response.RtcCallResponse;
import com.wecloud.rtc.entity.response.RtcCandidateForwardResponse;
import com.wecloud.rtc.entity.response.RtcClientJoinResponse;
import com.wecloud.rtc.entity.response.RtcClientLeaveResponse;
import com.wecloud.rtc.entity.response.RtcClientRejectResponse;
import com.wecloud.rtc.entity.response.RtcResponseBase;
import com.wecloud.rtc.entity.response.RtcSdpForwardResponse;
import com.wecloud.rtc.service.WsRtcWrite;
import io.geekidea.springbootplus.framework.common.api.ApiCode;
import io.geekidea.springbootplus.framework.common.api.ApiResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class WsRtcWriteImpl implements WsRtcWrite {
// private static final JsonMapper JSON_MAPPER = new JsonMapper();
@Autowired
private WriteDataService writeDataService;
@Override
public void rtcCall(RtcCallResponse rtcCallResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcCallResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.RTC_CALL.getCmdCode());
rtcResponseBase.setSubData(rtcCallResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcCallResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
@Override
public void clientJoin(RtcClientJoinResponse rtcClientJoinResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcClientJoinResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.CLIENT_JOIN.getCmdCode());
rtcResponseBase.setSubData(rtcClientJoinResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcClientJoinResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
@Override
public void clientLeave(RtcClientLeaveResponse rtcClientLeaveResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcClientLeaveResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.CLIENT_LEAVE.getCmdCode());
rtcResponseBase.setSubData(rtcClientLeaveResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcClientLeaveResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
@Override
public void clientReject(RtcClientRejectResponse rtcClientRejectResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcClientRejectResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.CLIENT_REJECT.getCmdCode());
rtcResponseBase.setSubData(rtcClientRejectResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcClientRejectResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
@Override
public void sdpForward(RtcSdpForwardResponse rtcSdpForwardResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcSdpForwardResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.SDP_FORWARD.getCmdCode());
rtcResponseBase.setSubData(rtcSdpForwardResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcSdpForwardResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
@Override
public void candidateForward(RtcCandidateForwardResponse rtcCandidateForwardResponse, String toAppKey, String toClientId) {
RtcResponseBase<RtcCandidateForwardResponse> rtcResponseBase = new RtcResponseBase<>();
rtcResponseBase.setSubCmd(WsRtcResponseSubCmdEnum.CANDIDATE_FORWARD.getCmdCode());
rtcResponseBase.setSubData(rtcCandidateForwardResponse);
// rtcResponseBase.setAttrs(rtcCallResponse.get);
// 向接收方推送
WsResponseModel<RtcResponseBase<RtcCandidateForwardResponse>> responseModel = new WsResponseModel<>();
responseModel.setCmd(WsResponseCmdEnum.SINGLE_RTC_MSG.getCmdCode());
ApiResult<Boolean> result = ApiResult.result(ApiCode.SUCCESS);
responseModel.setCode(result.getCode());
responseModel.setMsg(result.getMessage());
responseModel.setData(rtcResponseBase);
responseModel.setReqId(null);
writeDataService.write(responseModel, toAppKey, toClientId);
}
}
......@@ -14,22 +14,41 @@ spring-boot-plus:
response-log-format: false
#spring:
# datasource:
# url: jdbc:mysql://127.0.0.1:3306/wecloud_im?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
# username: web
# password: axT8knPN5hAP
#
# # Redis配置
# redis:
# database: 0
# host: 127.0.0.1
# password: JH86uc53r8Ca
# port: 6379
# cloud:
# nacos:
# discovery:
# server-addr: localhost:8848
# 国内IM测试外网
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/wecloud_im?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
username: web
password: axT8knPN5hAP
username: root
password: temple123456
# Redis配置
redis:
database: 0
host: 127.0.0.1
password: JH86uc53r8Ca
password: temple123456
port: 6379
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 打印SQL语句和结果集,本地开发环境可开启,线上注释掉
mybatis-plus:
configuration:
......
......@@ -8,4 +8,4 @@ ps aux|grep bootstrap-2.0|awk '{print $2}'|xargs kill -9
# 启动新jar,
nohup java -jar bootstrap-2.0.jar &>/dev/null &
echo "success"
echo "run success"
#! /bin/shell
# 国内IM集成版测试环境
# sudo 超级权限启动, 否则无法创建log日志文件夹报错
# 停止服务
ps aux|grep bootstrap-2.0|awk '{print $2}'|xargs kill -9
#/data0/java_projects_jenkins/
# 启动新jar,
#nohup java -jar bootstrap-2.0.jar &>/dev/null &
nohup java -jar \
-Dspring.datasource.url="jdbc:mysql://127.0.0.1:3306/wecloud_im?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true" \
-Dspring.datasource.username=root \
-Dspring.datasource.password=temple123456 \
-Dspring.redis.database=0 \
-Dspring.redis.host=127.0.0.1 \
-Dspring.redis.password=temple123456 \
-Dspring.redis.port=6379 \
bootstrap-2.0-test.jar &>/dev/null &
echo "run success"
# wecloud-im 客户端对接文档
# wecloud-im 客户端对接文档
[TOC]
## 生产环境
ws.im199.com
websocket连接示例
```
wss://ws.im199.com/ws?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJjbGllbnRJZCI6ImFiY2QxIiwiaXNzIjoid2VjbG91ZF9pbSIsImFwcEtleSI6IkpLdE5IZnJWVXdzaGF4ek4iLCJleHAiOjE2MjgzMjMxNDMsImlhdCI6MTYyMzEzOTE0MywianRpIjoiNWU3NzU5ZjM2ODQ3NDFiMzg4MGEyYjkwMjQ0OWZjZmYifQ.CC-iuGjNwQLH4VxFI2wZEPuP4AGabOUOiRh9snp3IB4
```
_______
## 核心概念说明
### clientId、用户和登录
即时通讯服务中的每一个终端称为一个「Client」。Client 拥有一个在应用内唯一标识自己的 ID(`clientId`)。这个 ID 由应用自己定义,必须是 **由任意英文字母、数字、半角下划线与半角短横线组成,可以纯数字,不超过 64个字符的字符串组成**。在大部分场合,Client 都可以对应到应用中的某个「用户」,但是并不是只有真的用户才能作为「Client」,你完全可以把一个探测器当成一个「Client」,把它收集到的数据通过即时通讯服务广播给更多「人」。
要使用即时通讯服务,每一个终端设备需要首先建立与即时通讯云端的 WebSocket 长连接,并使用唯一的 `clientId` 来加入即时通讯服务,我们把这一过程称为「登录」。请注意这里的登录仅仅指客户端登录即时通讯服务,与应用层面的用户账户注册登录是不一样的。
### 对话(Conversation)
用户登录之后,与其他人进行消息沟通,即为开启了一个「对话(Conversation)」。在即时通讯服务中,「对话」包含了沟通的用户群体(成员),也是所有消息依托的媒介:消息都是由某一个 Client 发往一个「对话」。终端用户在开始聊天之前,需要先创建或者加入一个对话,然后再邀请其他人进来(可选),之后所有参与者在这个对话内进行交流。
### 已读/已接收
**已接收**为客户端接收到该消息,已经保存到本地缓存中,用户还未打开会话查看该条消息的情况. 服务器将不会再次下发已接收状态的离线消息,未接收的消息会在拉取离线消息时返回
**已读**为客户端已经查看该消息
## 客户端登陆api
<img src="https://tva1.sinaimg.cn/large/008i3skNly1gqlielspokj30s80xe0vv.jpg" alt="image-20210517140904878" style="zoom:67%;" />
1. appKey, appSecret为im云下发给客户方安全保护密钥,不建议保存在前端客户端,
2. 客户前端需提供获取sign的接口, sign 由MD5{timestamp + clientId + appKey + appSecret},其中clientId由后端生成
3. 前端拿到sign后,调用验证sign接口进行获取token
4. websocket连接初始化需要带上token即可连接成功
### 生成sign示例
java示例代码 ,供客户应用后端参考
```java
private void getSign(String timestemp, String clientId, String appKey, String appSecret) {
String sign = new MD5().digestHex(timestemp + clientId + appKey + appSecret);
}
public static void main(String[] args) {
String clientId = "client_123123";
String appKey = "elLwpel1gWCHDqZy";
String appSecret = "68809bb5a9077a83631aeb0b17b5965d6b2302faf2ab3737";
String timestemp = String.valueOf(new Date().getTime());
getSign(timestemp, clientId, appKey, appSecret);
}
```
后端需要响应参数示例:
```
{
"timestamp": "1628838135066",
"clientId": "client_3334444",
"appKey": "D13ug9jsWbJbeVx1",
"sign": "c15a886fe4114dba2c8f078369e6bec9"
}
```
### sign
### 连接websocket
ws://localhost:8899/ws?token=xxxxxx
示例:
```
ws://localhost:8899/ws?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJjbGllbnRJZCI6ImhhaGFoXzMwIiwiaXNzIjoid2VjbG91ZF9pbSIsImFwcEtleSI6ImVsTHdwZWwxZ1dDSERxWnkiLCJleHAiOjE2MjkwOTY0MzksImlhdCI6MTYyMDQ1NjQzOSwianRpIjoiNDA1YzE3MWM2Njc5NGJmMDllNGRjZDdhNzA0ZjY3YTgifQ.7g2J_0q9UnuWszpuapSJUXJEwVevvI8Rm2Srg3594Lk
```
## 请求CMD指令说明
- 1: 发送消息
- 2: RTC
## 响应CMD指令说明
1响应用户请求
2下发在线用户消息
3下发在线事件类型消息
401:无权限 token失效
## 消息type说明
- 文本消息 -1
- 图像消息 -2
- 音频消息 -3
- 视频消息 -4
- 位置消息 -5
- 文件消息 -6
- ### 事件类型type (xx表示为某客户端)
- xx邀请xx加入会话 -1007
- xx被xx移出会话 -1008
- xx已接收某消息 -1009
- xx已读某条消息 -1010
- 你被xx拉入新会话 -1011
以上类型均使用负数,所有正数留给自定义扩展类型使用,0 作为「没有类型」被保留起来。
## code错误码
- 401 用户token无效
## websocket客户端请求参数说明
**reqId** 为每次客户端通过websocket通道发送请求的唯一request Id, 用来标识每次请求,方便定位问题,以及用于绑定服务端响应对此请求request id的数据.
websocket像服务端发送数据不同于http请求接口.
http发送请求 会一个请求对应一个响应,客户端接收也知道该响应数据是针对哪个请求的.
websocket是异步的 有可能你很快速的发送了几条消息,服务器响应的数据不一定是按你发送的消息顺序响应, 所以你每个请求要自己定一个id, 服务器就算乱序响应数据,客户端也能知道响应的数据对应哪个请求
**cmd** 为请求指令
**data** 为数据,所有需要发送的数据都在此参数下
**data内**的参数: **toConversation**(会话id)与**type**(消息类型)为固定的,其它参数名与参数值皆可由使用方自定义
## websocket服务器响应参数说明
**reqId** 一个请求唯一的id,服务端响应时,会将对应的id与数据一同返回
**cmd** 为响应指令
![image-20210521104503728](https://tva1.sinaimg.cn/large/008i3skNly1gqpurt4tgnj30py0dc3zd.jpg)
## 消息收发流程
### 1. 客户端发送文本消息
```json
{
"reqId":"123123123",
"cmd":1,
"data":{
"push":{
"title":"收到一条新消息",
"subTitle":"发给12312123213这是一123个纯文本消息,发给12312123213这是一123个纯文本消息发给12312123213这是一123个纯文本消息"
},
"diyAbcd":"aaaa自已定义字段的值",
"toConversation":1402147846261706752,
"type":-1,
"text":"发给12312123213这是一123个纯文本消息,发给12312123213这是一123个纯文本消息发给12312123213这是一123个纯文本消息",
"attrs":{
"a":"attrs 阿道夫123123是用来213存储用户自定义的一些键值对,ttrs 阿道夫123123是用来213存储用户自定义的一些键值对",
"b":"attrs 阿道夫123123是用来213存储用户自定义的一些键值对,ttrs 阿道夫123123是用来213存储用户自定义的一些键值对"
}
}
}
```
data里面的参数 除toConversation与type为固定的,其它参数皆可由使用方自定义
push为客户端自定义系统推送内容
### 2. 发送成功服务端响应给发送者
```json
{
"cmd":2,
"code":200,
"msg":"成功",
"data":{
"msgId":"1394207796915998720"
},
"reqId":"123123123"
}
```
### 3. 接收方收到在线消息
```json
{
"cmd":1,
"code":200,
"msg":"成功",
"data":{
"msgId":"1394207796915998720",
"createTime":1621240016587,
"withdrawTime":null,
"sender":"hahah_30",
"content":{
"text":"发给12312123213这是一123个纯文本消息,发给12312123213这是一123个纯文本消息发给12312123213这是一123个纯文本消息",
"type":-1,
"attrs":{
"a":"attrs 阿道夫123123是用来213存储用户自定义的一些键值对,ttrs 阿道夫123123是用来213存储用户自定义的一些键值对",
"b":"attrs 阿道夫123123是用来213存储用户自定义的一些键值对,ttrs 阿道夫123123是用来213存储用户自定义的一些键值对"
}
},
"withdraw":false,
"event":false,
"system":false,
"at":null,
"conversationId":"1394188055950266368"
},
"reqId":null
}
```
### 4.接收方收到,需要回执
已接收回执需参考API对接文档
## 发送图片
```json
{
"reqId":"123123123",
"cmd":1,
"data":{
"toConversation":1394188055950266368,
"type":-2,
"file": {
"url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25", // 必要参数
"objId": "54699d87e4b0a56c64f470a4", // 文件对应的AVFile.objectId
"metaData": {
"name": "IMG_20141223.jpeg", // 图像的名称
"format": "png", // 图像的格式
"height": 768, // 单位:像素
"width": 1024, // 单位:像素
"size": 18 // 单位:b
}
},
"attrs":{
"a":"attrs 存储用户自定义的一些键值对,ttrs 阿道夫123123是用来213存储用户自定义的一些键值对",
"b":"attrs 阿道夫123123是用来213存储用户自定义的一些键值对,ttrs 阿道夫123123是用来213存储用户自定义的一些键值对"
}
}
}
```
上面是完整的例子,如果只想简单的发送图像 URL:
```json
{
"reqId":"123123123",
"cmd":1,
"data":{
"toConversation":1394188055950266368,
"type":-2,
"file": {
"url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25", // 必要参数
"objId": "54699d87e4b0a56c64f470a4", // 文件对应的AVFile.objectId
"metaData": {
"name": "IMG_20141223.jpeg", // 图像的名称
"format": "png", // 图像的格式
"height": 768, // 单位:像素
"width": 1024, // 单位:像素
"size": 18 // 单位:b
}
}
}
```
## 其他富媒体消息
如视频 位置 音频 发送红包 好友验证等等
可参考:https://leancloud.cn/docs/realtime_v2.html#hash939050100
```json
{
"reqId":"123123123",
"cmd":1,
"data":{
"toConversation":1394188055950266368,
"type":由应用自定义,
"应用自定义参数": 应用自定义值
}
}
```
## 客户端在线接收事件类型消息
### 事件类型type (xx表示为某客户端)
- xx邀请xx加入会话 -1007
- xx被xx移出会话 -1008
- xx已接收某消息 -1009
- xx已读某条消息 -1010
- 你被xx拉入新会话 -1011
### 服务端在线下发 某消息*已接收*状态
receiverId:接收方客户端id
conversationId:会话id
type为-1009 : 某消息已接收状态
msgId: 消息id;
该event事件消息类型不需要回执,不会保存进离线,仅下发给当前在线会话客户端.
```json
{
"cmd":3,
"code":200,
"msg":"成功",
"data":{
"msgId":1427109835333308416,
"createTime":1629086007054,
"withdrawTime":null,
"sender":"aaaaa1",
"content":{
"receiverId":"aaaaa1",
"type":"-1009"
},
"withdraw":false,
"event":true,
"system":false,
"at":null,
"conversationId":1427109730563788800
},
"reqId":null
}
```
### 服务端在线下发 某消息*已读*状态
receiverId:接收方客户端id
conversationId:会话id
type为-1010 : 某消息已读状态
msgId: 消息id;
该event事件消息类型不需要回执,不会保存进离线,仅下发给当前在线会话客户端.
```json
{
"cmd":3,
"code":200,
"msg":"成功",
"data":{
"msgId":1427109835333308416,
"createTime":1629086007084,
"withdrawTime":null,
"sender":"aaaaa1",
"content":{
"receiverId":"aaaaa1",
"type":"-1010"
},
"withdraw":false,
"event":true,
"system":false,
"at":null,
"conversationId":1427109730563788800
},
"reqId":null
}
```
# API 文档
**简介**:wecloud_im API Documents
**HOST**:https://ws.im199.com/
[TOC]
# token相关
## 验证sign,并返回token
**接口地址**:`/api/token/verify`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:根据客户方生成签名字符串 验证通过则下发token
**请求示例**:
```javascript
{
"timestamp": "",
"clientId": "",
"appKey": "",
"sign": ""
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| --------------------- | -------------------- | ------ | -------- | ------------- | ------------- |
| imTokenVerify | imTokenVerify | body | true | ImTokenVerify | ImTokenVerify |
| &emsp;&emsp;timestamp | 时间戳 | | false | string | |
| &emsp;&emsp;clientId | client客户端id | | false | string | |
| &emsp;&emsp;appKey | appkey | | false | string | |
| &emsp;&emsp;sign | 签名sign | | false | string | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------------------ |
| 200 | OK | ApiResult«TokenVo» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| ---------------------- | -------------------------------------- | -------------- | -------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | TokenVo | TokenVo |
| &emsp;&emsp;token | | string | |
| &emsp;&emsp;id | 客户端id | integer(int64) | |
| &emsp;&emsp;attributes | 可选 自定义属性,供开发者扩展使用。 | string | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": {
"token": "",
"id": 0,
"attributes": ""
}
}
```
# 会话表
## 创建会话
**接口地址**:`/api/conversation/create`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:后台可配置:两个客户端如果已经创建过会话,是否重复创建会话
**请求示例**:
```javascript
{
"name": "",
"attributes": {},
"clientIds": []
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| ---------------------- | --------------------------------------------------------- | ------ | -------- | -------------------- | -------------------- |
| imConversationCreate | imConversationCreate | body | true | ImConversationCreate | ImConversationCreate |
| &emsp;&emsp;name | 可选 对话的名字,可为群组命名。 | | false | string | |
| &emsp;&emsp;attributes | json格式,可选 自定义属性,供开发者扩展使用。 | | false | object | |
| &emsp;&emsp;clientIds | 可选 邀请加入会话的客户端,如创建单聊,则填入对方的clientId | | false | array | string |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | --------------------------------- |
| 200 | OK | ApiResult«ImConversationCreateVo» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------------- | -------- | ---------------------- | ---------------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | ImConversationCreateVo | ImConversationCreateVo |
| &emsp;&emsp;id | 会话id | integer(int64) | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": {
"id": 0
}
}
```
## 批量修改单向隐藏或显示会话
**接口地址**:`/api/conversation/displayUpdate`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:拉取会话列表不展示已隐藏状态的会话,云端聊天记录不删除;假设有A和B两个用户,A删会话,B还能发; 如果B发了消息,A这边要重新把会话显示出来,并能显示之前的聊天记录
**请求示例**:
```javascript
{
"conversationIds": [],
"displayStatus": 0
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| --------------------------- | --------------------------- | ------ | -------- | -------------------- | -------------------- |
| imConversationDisplayUpdate | imConversationDisplayUpdate | body | true | 修改是否单向隐藏会话 | 修改是否单向隐藏会话 |
| &emsp;&emsp;conversationIds | 会话id | | false | array | integer |
| &emsp;&emsp;displayStatus | 显示状态 1显示 0不显示 | | false | integer(int32) | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------------------ |
| 200 | OK | ApiResult«boolean» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | -------------- | -------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | boolean | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": true
}
```
## 查询加入的会话列表
**接口地址**:`/api/conversation/getList`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:查询用户加入的非隐藏状态的会话列表 与每个会话的未读条数 成员
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| -------- | -------------------- | ------ | -------- | -------- | ------ |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------------------------------------- |
| 200 | OK | ApiResult«List«MyConversationListVo»» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| ---------------------------------------- | -------------------------------------------------- | ----------------- | -------------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | array | MyConversationListVo |
| &emsp;&emsp;id | 会话id | integer(int64) | |
| &emsp;&emsp;createTime | 创建时间 | string(date-time) | |
| &emsp;&emsp;creator | 创建者客户端id | string | |
| &emsp;&emsp;name | 可选 对话的名字,可为群组命名。 | string | |
| &emsp;&emsp;attributes | 可选 自定义属性,供开发者扩展使用。 | object | |
| &emsp;&emsp;system | 可选 对话类型标志,是否是系统对话,后面会说明。 | boolean | |
| &emsp;&emsp;msgNotReadCount | 未读消息条数 | integer(int64) | |
| &emsp;&emsp;members | 成员 | string | |
| &emsp;&emsp;lastMsg | 会话最后一条消息 | OfflineMsgDto | OfflineMsgDto |
| &emsp;&emsp;&emsp;&emsp;msgId | 消息id | | false |
| &emsp;&emsp;&emsp;&emsp;createTime | 创建时间 | | false |
| &emsp;&emsp;&emsp;&emsp;withdrawTime | 撤回时间 | | false |
| &emsp;&emsp;&emsp;&emsp;sender | 发送者客户端id | | false |
| &emsp;&emsp;&emsp;&emsp;content | 内容 | | false |
| &emsp;&emsp;&emsp;&emsp;withdraw | 0未撤回; 1已撤回 | | false |
| &emsp;&emsp;&emsp;&emsp;event | 0非事件; 1为事件 | | false |
| &emsp;&emsp;&emsp;&emsp;system | 0非系统通知; 1为系统通知 | | false |
| &emsp;&emsp;&emsp;&emsp;at | at他人,传入客户端id数组 | | false |
| &emsp;&emsp;&emsp;&emsp;notReadCount | 未读人数统计,全部人已读为0 | | false |
| &emsp;&emsp;&emsp;&emsp;notReceiverCount | 未接收人数统计,全部人已接收为0 | | false |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": [
{
"id": 0,
"createTime": "",
"creator": "",
"name": "",
"attributes": {},
"system": true,
"msgNotReadCount": 0,
"members": "",
"lastMsg": {
"msgId": 0,
"createTime": "",
"withdrawTime": "",
"sender": "",
"content": "",
"withdraw": true,
"event": true,
"system": true,
"at": "",
"notReadCount": 0,
"notReceiverCount": 0
}
}
]
}
```
# 消息存储表
## 查询某个会话历史消息分页列表
**接口地址**:`/api/imMessage/getHistoryMsg`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求示例**:
```javascript
{
"conversationId": 0,
"pageIndex": 1,
"pageSorts": [
{
"column": "",
"asc": true
}
],
"pageSize": 10,
"keyword": ""
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| ------------------------------ | ------------------------- | ------ | -------- | ------------------------- | ------------------------- |
| imHistoryMessagePageParam | imHistoryMessagePageParam | body | true | ImHistoryMessagePageParam | ImHistoryMessagePageParam |
| &emsp;&emsp;conversationId | 会话id | | false | integer(int64) | |
| &emsp;&emsp;pageIndex | 页码,默认为1 | | false | integer(int64) | |
| &emsp;&emsp;pageSorts | 排序 | | false | array | OrderItem |
| &emsp;&emsp;&emsp;&emsp;column | | | false | string | |
| &emsp;&emsp;&emsp;&emsp;asc | | | false | boolean | |
| &emsp;&emsp;pageSize | 页大小,默认为10 | | false | integer(int64) | |
| &emsp;&emsp;keyword | 搜索字符串 | | false | string | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | -------------------------------------- |
| 200 | OK | ApiResult«分页结果对象«OfflineMsgDto»» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| ---------------------------------------- | ------------------------------ | --------------------------- | --------------------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | 分页结果对象«OfflineMsgDto» | 分页结果对象«OfflineMsgDto» |
| &emsp;&emsp;total | 总行数 | integer(int64) | |
| &emsp;&emsp;records | 数据列表 | array | OfflineMsgDto |
| &emsp;&emsp;&emsp;&emsp;msgId | 消息id | | false |
| &emsp;&emsp;&emsp;&emsp;createTime | 创建时间 | | false |
| &emsp;&emsp;&emsp;&emsp;withdrawTime | 撤回时间 | | false |
| &emsp;&emsp;&emsp;&emsp;sender | 发送者客户端id | | false |
| &emsp;&emsp;&emsp;&emsp;content | 内容 | | false |
| &emsp;&emsp;&emsp;&emsp;withdraw | 0未撤回; 1已撤回 | | false |
| &emsp;&emsp;&emsp;&emsp;event | 0非事件; 1为事件 | | false |
| &emsp;&emsp;&emsp;&emsp;system | 0非系统通知; 1为系统通知 | | false |
| &emsp;&emsp;&emsp;&emsp;at | at他人,传入客户端id数组 | | false |
| &emsp;&emsp;&emsp;&emsp;notReadCount | 未读人数统计,全部人已读为0 | | false |
| &emsp;&emsp;&emsp;&emsp;notReceiverCount | 未接收人数统计,全部人已接收为0 | | false |
| &emsp;&emsp;pageIndex | 页码 | integer(int64) | |
| &emsp;&emsp;pageSize | 页大小 | integer(int64) | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": {
"total": 0,
"records": [
{
"msgId": 0,
"createTime": "",
"withdrawTime": "",
"sender": "",
"content": "",
"withdraw": true,
"event": true,
"system": true,
"at": "",
"notReadCount": 0,
"notReceiverCount": 0
}
],
"pageIndex": 0,
"pageSize": 0
}
}
```
## 离线消息列表
**接口地址**:`/api/imMessage/offlineList`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| -------- | -------------------- | ------ | -------- | -------- | ------ |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | --------------------------------------- |
| 200 | OK | ApiResult«List«ImMessageOfflineListVo»» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| ---------------------------------------- | ------------------------------ | -------------- | ---------------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | array | ImMessageOfflineListVo |
| &emsp;&emsp;conversationId | 会话id | integer(int64) | |
| &emsp;&emsp;msgList | 消息列表 | array | OfflineMsgDto |
| &emsp;&emsp;&emsp;&emsp;msgId | 消息id | | false |
| &emsp;&emsp;&emsp;&emsp;createTime | 创建时间 | | false |
| &emsp;&emsp;&emsp;&emsp;withdrawTime | 撤回时间 | | false |
| &emsp;&emsp;&emsp;&emsp;sender | 发送者客户端id | | false |
| &emsp;&emsp;&emsp;&emsp;content | 内容 | | false |
| &emsp;&emsp;&emsp;&emsp;withdraw | 0未撤回; 1已撤回 | | false |
| &emsp;&emsp;&emsp;&emsp;event | 0非事件; 1为事件 | | false |
| &emsp;&emsp;&emsp;&emsp;system | 0非系统通知; 1为系统通知 | | false |
| &emsp;&emsp;&emsp;&emsp;at | at他人,传入客户端id数组 | | false |
| &emsp;&emsp;&emsp;&emsp;notReadCount | 未读人数统计,全部人已读为0 | | false |
| &emsp;&emsp;&emsp;&emsp;notReceiverCount | 未接收人数统计,全部人已接收为0 | | false |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": [
{
"conversationId": 0,
"msgList": [
{
"msgId": 0,
"createTime": "",
"withdrawTime": "",
"sender": "",
"content": "",
"withdraw": true,
"event": true,
"system": true,
"at": "",
"notReadCount": 0,
"notReceiverCount": 0
}
]
}
]
}
```
## 修改消息体
**接口地址**:`/api/imMessage/updateMsgById`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:只能修改客户端自己发送的消息
**请求示例**:
```javascript
{
"msgId": 0,
"content": {}
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| ------------------- | -------------------- | ------ | -------- | -------------- | ----------- |
| imMsgUpdate | imMsgUpdate | body | true | ImMsgUpdate | ImMsgUpdate |
| &emsp;&emsp;msgId | 消息id | | false | integer(int64) | |
| &emsp;&emsp;content | 内容 | | false | object | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------------------ |
| 200 | OK | ApiResult«boolean» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | -------------- | -------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | boolean | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": true
}
```
## 消息撤回
**接口地址**:`/api/imMessage/withdraw`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:只能撤回客户端自己发送的消息
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| -------- | -------------------- | ------ | -------- | -------------- | ------ |
| msgId | msgId | query | true | integer(int64) | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------------------ |
| 200 | OK | ApiResult«boolean» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | -------------- | -------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | boolean | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": true
}
```
# 消息收件箱表
## 消息修改为已读状态
**接口地址**:`/api/imInbox/msgReadUpdate`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求示例**:
```javascript
{
"msgIds": []
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| --------------------- | ------------------------------------------------------------ | ------ | -------- | --------------------- | --------------------- |
| imMsgReadStatusUpdate | imMsgReadStatusUpdate | body | true | ImMsgReadStatusUpdate | ImMsgReadStatusUpdate |
| &emsp;&emsp;msgIds | 消息id数组,可以传入单个或多个, 如接收离线消息列表时可以批量修改 则传入多个 | | false | array | integer |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------------------ |
| 200 | OK | ApiResult«boolean» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | -------------- | -------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | boolean | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": true
}
```
## 消息修改为已接收状态
**接口地址**:`/api/imInbox/msgReceivedUpdate`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求示例**:
```javascript
{
"msgIds": [],
"readStatus": true
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| ---------------------- | ------------------------------------------------------------ | ------ | -------- | ------------------------- | ------------------------- |
| imMsgReceivedUpdate | imMsgReceivedUpdate | body | true | ImMsgReceivedStatusUpdate | ImMsgReceivedStatusUpdate |
| &emsp;&emsp;msgIds | 消息id数组,可以传入单个或多个, 如接收离线消息列表时可以批量修改 则传入多个 | | false | array | integer |
| &emsp;&emsp;readStatus | 是否同时修改为已读状态 | | false | boolean | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------------------ |
| 200 | OK | ApiResult«boolean» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | -------------- | -------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | boolean | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": true
}
```
# 终端
## 添加或修改推送设备信息(每次请求都会覆盖之前的数据)
**接口地址**:`/api/imClient/addDeviceInfo`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求示例**:
```javascript
{
"valid": 0,
"deviceType": 0,
"deviceToken": ""
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| ----------------------- | -------------------------------- | ------ | -------- | ---------------------- | ---------------------- |
| imClientDevice | imClientDevice | body | true | 添加或修改推送设备信息 | 添加或修改推送设备信息 |
| &emsp;&emsp;valid | 设备不想收到推送提醒, 1想, 0不想 | | false | integer(int32) | |
| &emsp;&emsp;deviceType | 设备类型1:ios; 2:android | | false | integer(int32) | |
| &emsp;&emsp;deviceToken | 设备推送token | | false | string | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------------------ |
| 200 | OK | ApiResult«boolean» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | -------------- | -------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | boolean | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": true
}
```
## 获取用户在线状态(批量)
**接口地址**:`/api/imClient/onlineStatus`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求示例**:
```javascript
{
"clientIds": []
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| --------------------- | -------------------- | ------ | -------- | -------------------- | -------------------- |
| getOnlineStatusParam | getOnlineStatusParam | body | true | GetOnlineStatusParam | GetOnlineStatusParam |
| &emsp;&emsp;clientIds | 客户端ID | | false | array | string |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | --------------------------------- |
| 200 | OK | ApiResult«List«ImOnlineStatusVo»» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------------------- | ----------------------- | -------------- | ---------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | array | ImOnlineStatusVo |
| &emsp;&emsp;status | true:在线, false 不在线 | boolean | |
| &emsp;&emsp;clientId | | string | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": [
{
"status": true,
"clientId": ""
}
]
}
```
# 获取sign(Demo)
## 获取sign(仅测试使用)
**接口地址**:`/api/signDemo/get`
**请求方式**:`GET`
**请求数据类型**:`*`
**响应数据类型**:`*/*`
**接口描述**:生成签名
**请求示例**:
```javascript
{
"timestamp": "",
"clientId": "",
"appKey": "",
"appSecret": ""
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| --------------------- | -------------------- | ------ | -------- | ------------ | ------------ |
| getSignParam | getSignParam | body | true | GetSignParam | GetSignParam |
| &emsp;&emsp;timestamp | 时间戳 | | false | string | |
| &emsp;&emsp;clientId | client客户端id | | false | string | |
| &emsp;&emsp;appKey | appKey | | false | string | |
| &emsp;&emsp;appSecret | 密钥 | | false | string | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------ |
| 200 | OK | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
暂无
**响应示例**:
```javascript
```
# 黑名单
## 拉入黑名单
**接口地址**:`/api/ClientBlacklist/add`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求示例**:
```javascript
{
"clientIdBePrevent": ""
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| ----------------------------- | ----------------------- | ------ | -------- | ----------------------- | ----------------------- |
| imClientBlacklistUpdate | imClientBlacklistUpdate | body | true | ImClientBlacklistUpdate | ImClientBlacklistUpdate |
| &emsp;&emsp;clientIdBePrevent | 被拉黑者id | | false | string | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------------------ |
| 200 | OK | ApiResult«boolean» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | -------------- | -------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | boolean | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": true
}
```
## 移出黑名单
**接口地址**:`/api/ClientBlacklist/delete`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求示例**:
```javascript
{
"clientIdBePrevent": ""
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| ----------------------------- | ----------------------- | ------ | -------- | ----------------------- | ----------------------- |
| imClientBlacklistUpdate | imClientBlacklistUpdate | body | true | ImClientBlacklistUpdate | ImClientBlacklistUpdate |
| &emsp;&emsp;clientIdBePrevent | 被拉黑者id | | false | string | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ------------------ |
| 200 | OK | ApiResult«boolean» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | -------------- | -------------- |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | boolean | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": true
}
```
## 黑名单分页列表
**接口地址**:`/api/ClientBlacklist/getPageList`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求示例**:
```javascript
{
"pageIndex": 1,
"pageSorts": [
{
"column": "",
"asc": true
}
],
"pageSize": 10,
"keyword": ""
}
```
**请求参数**:
| 参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
| ------------------------------ | -------------------------- | ------ | -------- | -------------- | -------------- |
| imClientBlacklistPageParam | imClientBlacklistPageParam | body | true | 黑名单分页参数 | 黑名单分页参数 |
| &emsp;&emsp;pageIndex | 页码,默认为1 | | false | integer(int64) | |
| &emsp;&emsp;pageSorts | 排序 | | false | array | OrderItem |
| &emsp;&emsp;&emsp;&emsp;column | | | false | string | |
| &emsp;&emsp;&emsp;&emsp;asc | | | false | boolean | |
| &emsp;&emsp;pageSize | 页大小,默认为10 | | false | integer(int64) | |
| &emsp;&emsp;keyword | 搜索字符串 | | false | string | |
| token | Token Request Header | header | false | string | |
**响应状态**:
| 状态码 | 说明 | schema |
| ------ | ------------ | ----------------------------------------------------- |
| 200 | OK | ApiResult«分页结果对象«ImClientBlacklistQueryVo对象»» |
| 201 | Created | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Not Found | |
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| ----------------------------------------- | ---------- | ------------------------------------------ | ------------------------------------------ |
| code | | integer(int32) | integer(int32) |
| message | | string | |
| data | | 分页结果对象«ImClientBlacklistQueryVo对象» | 分页结果对象«ImClientBlacklistQueryVo对象» |
| &emsp;&emsp;total | 总行数 | integer(int64) | |
| &emsp;&emsp;records | 数据列表 | array | ImClientBlacklistQueryVo对象 |
| &emsp;&emsp;&emsp;&emsp;clientIdBePrevent | 被拉黑者id | | false |
| &emsp;&emsp;pageIndex | 页码 | integer(int64) | |
| &emsp;&emsp;pageSize | 页大小 | integer(int64) | |
**响应示例**:
```javascript
{
"code": 0,
"message": "",
"data": {
"total": 0,
"records": [
{
"clientIdBePrevent": ""
}
],
"pageIndex": 0,
"pageSize": 0
}
}
```
\ No newline at end of file
# wecloud-RTC音视频客户端信令对接文档
# wecloud-RTC音视频客户端信令对接文档
......@@ -19,42 +19,54 @@
4. 一个频道可以由通话发起者绑定到会话ID,"挂断","未接听"等状态会同步到会话, 未绑定将不同步到会话(可选)
5. 连接websocket时带上client类型, 如web,安卓,ios
client需要监听频道内 状态更新(房间断开,挂断)、用户状态更新(用户加入, 用户退出)、流状态更新(切换音频 切换视频)
6.如果接收方未在线时收到音视频通话邀请,在规定时间内发起方还在等待中,发起方重新连接可以收到音视频通话的离线信令
```
## 流程图
### 不绑定到会话 示例一
![双人WebRTC流程图-不绑定会话](https://tva1.sinaimg.cn/large/008i3skNly1gvftfayr8ij60u016wtcx02.jpg)
![image-20211018180059906](https://tva1.sinaimg.cn/large/008i3skNly1gvjmbmw0wcj60u016877z02.jpg)
## 指令码说明
## subCmd指令码说明
#### **subCmd**子类型指令码
##### 客户端**请求**指令列表:
**create**:创建频道
**join**: 加入频道
**SDP**:SDP数据转发
**reject**:拒绝加入频道
**leave**:主动挂断(离开频道)
枚举类:
```
/**
* 接收到RTC邀请
*/
RTC_CALL(1),
##### 服务端**响应**指令列表:
/**
* 用户状态更新事件(用户加入频道)
*/
CLIENT_JOIN(2),
**rtcCall**:接收到RTC邀请
**clientEvent** : 用户状态更新事件(用户加入,用户退出,用户拒接邀请)
**typeEvent**:流状态更新(切换音频 切换视频)
**statusEvent**:状态更新(网络断开,挂断)
**SDP**:SDP数据转发
**busy**:忙线
/**
* 用户状态更新事件(用户退出频道)
*/
CLIENT_LEAVE(3),
/**
* 用户状态更新事件(用户拒接邀请,不同意进入频道)
*/
CLIENT_REJECT(4),
/**
* SDP数据转发
*/
SDP_FORWARD(5),
/**
* candidate候选者数据转发
*/
CANDIDATE_FORWARD(6);
```
## 创建频道 发起RTC音视频通话 (http)
......@@ -70,14 +82,14 @@ client需要监听频道内 状态更新(房间断开,挂断)、用户状态更
{
"cmd":4,
"data":{
"subCmd":"rtcCall",
"subCmd":"1",
"subData":{
"type":"video",
"toConversation":null,
"conversationId":null,
"channelId":1234263457652,
"sender":"client_1010"
"timestamp":113123123,
"clientId":"client_1010"
},
"diyParam自定义字段":"aaaa自已定义字段的值",
"attrs":{
"a":"示例: 用户自定义的一些键值对",
"b":"存储用户自定义的一些键值对"
......@@ -92,12 +104,12 @@ client需要监听频道内 状态更新(房间断开,挂断)、用户状态更
| ---- | -------- | -- | ------ |
| cmd | String | 否 | 指令码 |
| attrs | Object | 是 | 自定义拓展字段 |
| toConversation | Long | 否 | 绑定的会话id |
| diyParam自定义字段 | Object | 是 | 自定义拓展字段 |
| conversationId | Long | 否 | 绑定的会话id |
| subCmd | String | 否 | 子类型指令 |
| sender | String | 否 | 发起通话的客户端ID |
| clientId | String | 否 | 发起通话的客户端ID |
| channelId | Long | 否 | 由服务端创建的频道id |
| type | String | 否 | 类型: "video" 或 "voice" |
| timestamp | Timestamp | 否 | 频道创建时间戳 |
## 同意加入频道(http)
......@@ -112,10 +124,9 @@ client需要监听频道内 状态更新(房间断开,挂断)、用户状态更
{
"cmd":4,
"data":{
"subCmd":"clientEvent",
"subCmd":"2",
"subData":{
"channelId":1234263457652,
"type":"join",
"clientId":7657567,
}
}
......@@ -124,32 +135,8 @@ client需要监听频道内 状态更新(房间断开,挂断)、用户状态更
## 拒绝加入频道 http
client接收方向服务端请求数据:
```json
{
"reqId":"123",
"cmd":3,
"data":{
"subCmd":"reject",
"diyParam自定义字段":"aaaa自已定义字段的值",
"channelId":1234263457652,
"attrs":{}
}
}
```
**说明:**
**参数描述**
见http接口文档
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ---- | -------- | -- | ------ |
| cmd | String | 否 | 指令码 |
| attrs | Object | 是 | 自定义拓展字段 |
| toConversation | Long | 否 | 会话id |
| diyParam自定义字段 | Object | 是 | 自定义拓展字段 |
| subCmd | String | 否 | 子类型 |
## 有Client拒绝加入频道(ws下发)
......@@ -159,17 +146,18 @@ client接收方向服务端请求数据:
{
"cmd":4,
"data":{
"subCmd":"clientEvent",
"subCmd":4,
"subData":{
"channelId":1234263457652,
"type":"reject",
"clientId":7657567,
}
}
}
```
## 流媒体描述信息SDP转发
## SDP转发
流媒体描述信息
(服务端仅负责转发)(candidate,anser,offer)
......@@ -183,52 +171,89 @@ client接收方向服务端请求数据:
{
"cmd":4,
"data":{
"subCmd":"sdp",
"subCmd":5,
"subData":{
"channelId":1234263457652,
"clientId":7657567,
"sdpData":"xxxxxxxxxxxxxxxx",
"diyParam自定义字段":"aaaa自已定义字段的值",
"sdpType":"Offer/Answer"
},
"attrs":{}
}
}
}
```
## 主动挂断(离开频道) http
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ---- | -------- | -- | ------ |
| cmd | String | 否 | 指令码 |
| attrs | Object | 是 | 自定义拓展字段 |
| channelId | Long | 否 | 频道id |
| diyParam自定义字段 | Object | 是 | 自定义拓展字段 |
| subCmd | String | 否 | 子指令 |
| sdpData | String | 否 | sdp转发的数据 |
| sdpType | String | 否 | sdp类型: Offer或Answer |
client主动离开频道
见http接口文档
## 有client离开频道(ws下发)
## Candidate转发
服务端向频道内其他client响应数据:
候选人信息
(服务端仅负责转发)(candidate,anser,offer)
### client上传Candidate (http接口)
见http接口文档
### client接收Candidate (ws下发)
```json
{
"cmd":4,
"data":{
"subCmd":"clientEvent",
"subCmd":6,
"subData":{
"channelId":1234263457652,
"type":"leave",
"clientId":7657567,
}
"candidateData":"xxxxxxxxxxxxxxxx",
},
"attrs":{}
}
}
```
## 查询忙线状态(对方正在通话中) http
**参数描述**
| 字段名 | 字段类型 | 是否可空 | 说明 |
| ---- | -------- | -- | ------ |
| cmd | String | 否 | 指令码 |
| attrs | Object | 是 | 自定义拓展字段 |
| channelId | Long | 否 | 频道id |
| diyParam自定义字段 | Object | 是 | 自定义拓展字段 |
| subCmd | String | 否 | 子指令 |
| candidateData | String | 否 | 转发的候选者数据 |
服务端响应给发起方
## 主动挂断(离开频道) http
见http接口文档
## 有client离开频道(ws下发)
服务端向频道内其他client响应数据:
```json
{
"reqId":"555111-ad-afd12",
"cmd":5,
"cmd":4,
"data":{
"subCmd":"busy",
"subCmd":3,
"subData":{
"channelId":1234263457652,
"clientId":7657567
......@@ -237,6 +262,11 @@ client主动离开频道
}
```
## 查询忙线状态(对方正在通话中) http
见http接口文档
## 断线重连
重新join进频道即可重连
......@@ -247,7 +277,7 @@ client主动离开频道
## 视频/音频切换
## 查询对方是否离开
## 查询对方是否离开频道
# wecloud-im 即时通讯云对接文档
# wecloud-im 即时通讯云对接文档
[TOC]
## 使用场景
即时通讯服务现在已经被广泛使用**在应用内社交、工作协同、客服系统、超大型赛事和电视直播、以及游戏状态同步**等多种业务场景之中。
即时通讯是主要**解决产品内聊天**即时通信(Instant Messaging)、实时数据同步等需求,其设计上的主要目标是:
- **支持为现有应用快速加入多种通讯能力**
我们很多客户的产品都已经达到一个比较稳定的形态,即时通讯只是其中一个锦上添花的功能,所以如何和现有系统无缝集成,是我们设计上一个重要的出发点。即时通讯服务可以在应用账户系统独立的情况下,快速接入并稳定安全地运行。
我们支持了多种典型通讯场景,提供了UI 库和脚手架来帮助开发者快速接入。并且考虑到业务运行环境,我们提供了支持的 SDK。
- **强大的自定义机制满足业务各种扩展需求**
我们默认支持了**文本、图片、音视频、地理位置**等多种类型的消息收发,同时也允许产品开发者来扩展自己的消息类型和 UI 样式。并且在基本功能之外,我们也支持更多高阶需求,例如消息撤回与修改、@ 成员提醒、暂态消息、「已读」回执、离线推送、敏感内容过滤等等与消息收发相关的功能在这里都可以得到满足。
- **安全和权限控制**
我们始终把系统安全性放在首要位置,客户端与云端使用 WebSocket 全双工通讯,全程 TLS 加密传输。在用户登录和操作权限的控制上,我们专门设计了第三方操作签名的机制,让应用层在快速接入的同时,也可以实时、完整地控制用户在即时通讯系统内的所有活动。在群聊和开放聊天室等业务场景里,我们也提供了成员角色管理和黑名单的机制,以满足产品运营管理的多样需求。
- **最大限度降低客户的生产运维成本**
我们提供专业的技术支持服务,富有经验的资深工程师 7 × 24 小时对接,以帮助开发者快速、有效地完成产品集成,缩短开发周期。此外在产品运营阶段,让开发者彻底摆脱后端系统日常的运维细节和突发的软硬件故障处理,也不用关心用户量和流量的变化。帮助客户在享受高品质技术服务的同时,也可以最大限度降低生产运维成本,并且以更快的速度推进产品迭代,是我们始终追求的目标。
## 功能和特性
即时通讯服务提供的主要功能有:
- **基本聊天功能**,包括:
- 支持多种聊天场景。除了普通的单聊、群聊
- 用户之间可以发送多种多样的消息,如文本、图片、语音、音视频、地理位置等,以及更多的应用层自定义消息。
- 聊天消息自动保存在云端,可选支持永久消息存储,支持各种复杂的查找和翻页方式。
- **特殊的消息收发需求**。除了普通的消息收发之外,我们还支持:
- 带有提醒功能的 **@ 消息**(如微信里面的 @ 某人)
- 消息的 **撤回和修改**
- 消息送达和对方已读的 **回执通知**
- 群聊里面为了避免过于干扰,允许用户开启 **消息免打扰** 开关
- 在消息接收方离线时,自动转为 **系统推送通知,ios和安卓**(Push Notification)
- **安全控制**
任何终端用户要开启即时通讯服务,只需要提供一个唯一标识自己的 `clientId` 即可,不需要传入任何用户数据,以保证信息不被泄露,这种与产品自有账户系统解耦合的方式,带来了集成的便利,也可以促使通讯服务商专注做好底层的「信使」角色。 同时我们也提供 **第三方鉴权** 的机制,通过在聊天流程中加入开发者服务器签名授权这一环节,来确保通讯操作的安全。 而且,即时通讯 SDK 与云端是 WebSocket 全双工通讯,且全程使用 TLS 安全加密。
- **强大的业务扩展能力**
对于很多典型的需求,我们提供了默认的实现,而为了支持业务的多样性和特殊性,我们也提供了丰富的扩展机制:
- 为了和产品自有用户系统进行对接,我们提供了第三方操作鉴权的扩展接口,确保在用户登录、创建/加入/退出对话群组、以及拉取聊天记录时,所有操作都得到了授权。
- 同时我们还支持开发者对消息传递的过程进行 **hook 处理**,在消息到达云端但是还没有投递之前和投递之后,分别完成自定义的处理逻辑,例如过滤掉竞品的品牌,以及自定义离线推送消息,等等。
- 我们也支持通过简单的 **web hook** 来完成云端和应用后端的消息同步。
- 在提供移动端的 SDK 之外,我们还提供了 REST API,以帮助产品在可信环境下更好地实现业务处理。
**我们相信灵活性和扩展性也是云服务的核心竞争力。**
## 核心概念说明
### clientId、用户和登录
即时通讯服务中的每一个终端称为一个「Client」。Client 拥有一个在应用内唯一标识自己的 ID(`clientId`)。这个 ID 由应用自己定义,必须是 **由任意英文字母、数字、半角下划线与半角短横线组成,可以纯数字,不超过 64个字符的字符串组成**。在大部分场合,Client 都可以对应到应用中的某个「用户」,但是并不是只有真的用户才能作为「Client」,你完全可以把一个探测器当成一个「Client」,把它收集到的数据通过即时通讯服务广播给更多「人」。
要使用即时通讯服务,每一个终端设备需要首先建立与即时通讯云服务端的 WebSocket 长连接,并使用唯一的 `clientId` 来加入即时通讯服务,我们把这一过程称为「登录」。请注意这里的登录仅仅指客户端登录即时通讯服务,与应用层面的用户账户注册登录是不一样的。
### appKey与appSecret
appKey为应用在蔚可云的唯一标识, appSecret为安全密钥, 均由蔚可云控制台或联系客服下发,给应用接入方安全验证密钥,生产环境appSecret不建议保存在接入方前端客户端, appKey与appSecret总是成对出现和使用
### 生成sign签名
sign 由接入方后端生成, 进行加签的参数: MD5{timestamp + clientId + appKey + appSecret}
timestamp:毫秒时间戳
clientId:由后端生成
**在生产环境中,你需要自行部署服务器签发 sign**
### 对话(Conversation)
用户登录之后,与其他人(client)进行消息沟通,即为开启了一个「对话(Conversation)」。在即时通讯服务中,「对话」包含了沟通的用户群体(成员),也是所有消息依托的媒介:消息都是由某一个 Client 发往一个「对话」。终端用户在开始聊天之前,需要先创建或者加入一个对话,然后再邀请其他人进来(可选),之后所有参与者在这个对话内进行交流。
### 已读/已接收
**已接收**为客户端接收到该消息,已经保存到本地缓存中,用户还未打开会话查看该条消息的情况. 服务器将不会再次下发已接收状态的离线消息,未接收的消息会在拉取离线消息时返回
**已读**为客户端已经查看该消息
## 业务场景的需求
在解释对话类型之前,我们先列举一下即时通讯可能的使用场景。
- 单聊/私聊
就是两个 Client 之间的对话,公开与否(能否让其他人看到这个对话存在)由应用层自己控制。通常的业务场景里它是私密的,并且加入新的成员之后,会切换成新的群聊(当然,也可以依然不离开当前对话,这一点还是由应用层来决定), 可选两个client之间只能存在一个会话。
- 群聊
就是两个(含)以上 Client 之间的对话,通常可以添加和删除成员,并且会赋予群聊一个名字,例如「家人群」、「朋友群」、「部门同事群」等等。随着成员的减少,群聊也可能只有两个甚至一个成员(成员的多少并不是区分群聊和单聊的关键)。群聊能否公开(譬如支持名字搜索),由应用自己决定。
## 客户端连接流程
![wc_im_client连接流程-Page-1](https://tva1.sinaimg.cn/large/008i3skNly1gvv3zjcuajj30zt0u0div.jpg)
1. appKey, appSecret为蔚可云下发给客户方安全保护密钥,不建议保存在前端客户端;
2. 第三方应用服务端需提供获取sign的接口, sign 由MD5{timestamp + clientId + appKey + appSecret},其中clientId由后端生成;
3. 前端拿到sign后,调用验证sign接口进行获取token;
4. websocket连接初始化需要带上token即可连接成功;
5. sign 需要在你的应用服务端生成;
### 第三方应用后端生成sign接口示例
本文展示如何在服务端部署一个 sign 生成器。
**java**示例代码 ,供客户应用后端参考
```java
import org.springframework.util.DigestUtils;
private void getSign(String timestamp, String clientId, String appKey, String appSecret) {
String data = timestamp + clientId + appKey + appSecret;
String sign = DigestUtils.md5DigestAsHex(data.getBytes());
}
public static void main(String[] args) {
String clientId = "client_123123";
String appKey = "elLwpel1gWCHDqZy";
String appSecret = "68809bb5a9077a83631aeb0b17b5965d6b2302faf2ab3737";
String timestamp = String.valueOf(new Date().getTime());
getSign(timestamp, clientId, appKey, appSecret);
}
```
第三方应用后端**必须**要响应给前端的参数:
```json
{
"timestamp": "1628838135066",
"clientId": "client_3334444",
"appKey": "D13ug9jsWbJbeVx1",
"sign": "c15a886fe4114dba2c8f078369e6bec9"
}
```
# wecloud-im 视频通话云对接文档
# wecloud-im 视频通话云对接文档
[TOC]
## 产品概述
集成视频 SDK,实现高清流畅视频通话。
视频通话 SDK 可实现一对一视频通话,同时具备纯语音通话和视频通话功能。
**即将推出多对多视频通话**
## 使用场景
##### 视频聊天
支持 1 对 1 视频通话,适用于视频聊天、视频客服、远程医疗、金融双录、远程定损等场景
##### 在线教育
视频面对面教学,真实还原线下教学场景,支持 1v1 教学
##### 视频客服
支持 1v1 专属 VIP 视频客服,助力用户服务升级,提供更优质的服务体验
**即将推出多对多视频通话**
## 文档描述
此文档为一对一音视频通话技术对接文档
由于视频通话基于wecloud-im聊天服务,**对接视频通话前,需要先对接wecloud-im聊天服务**
## 核心概念说明
### 频道
两个client加入到同一个"频道"进行音视频通话
......@@ -6,11 +6,9 @@ String appSecret = "a5e619003868258e0f7c5b5821ea00fb6b2302faf2ab3737";
--
clientA1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJjbGllbnRJZCI6ImNsaWVudEExIiwiaXNzIjoid2VjbG91ZF9pbSIsImFwcEtleSI6IlFOdFAzRWp0THcyNmVrdDAiLCJleHAiOjE2ODQwMDA2NjIsImlhdCI6MTYzNDAyMTY1NCwianRpIjoiYzBiMDExOWNmYzE5NDk1YjgzYWU5YjQ3ZmFlZmM5ZTMifQ.2d_oQT-KwYmSOVZ7zXiuBTB8zRA4H8UgP2m_cMerGHE
--
aaaaa2
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJjbGllbnRJZCI6ImFhYWFhMiIsImlzcyI6IndlY2xvdWRfaW0iLCJhcHBLZXkiOiJRTnRQM0VqdEx3MjZla3QwIiwiZXhwIjoxNjg0MDAwNzAwLCJpYXQiOjE2MzQwMjE2OTIsImp0aSI6IjFhY2RhNWEzNGI3NjQwZTA4MDBlMWNiMTRhNTBmMWI5In0.m18ZspfoKDx_RjrBJ07o5CP1nSaLAMEwKmSUvh94ilc
--
......@@ -98,7 +96,7 @@ mysql
utf8mb4_unicode_ci
## 建库语句
CREATE DATABASE IF NOT EXISTS wecloud_im DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE DATABASE IF NOT EXISTS wecloud_im DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
## 单人RTC设计思路 , 2021年10月08日 周五
......@@ -256,4 +254,55 @@ ws
若房间内只剩一个人 ,下发只剩一个人的通知 并要求仅剩的客户端上传offer的SDP 与candidate, 使原发起方成为"房主"来等待接收方来应答"answer"
原发起方来重连时, 调用join接口,则返回另一端的offer的SDP,同时上传自己的answer SDP
\ No newline at end of file
原发起方来重连时, 调用join接口,则返回另一端的offer的SDP,同时上传自己的answer SDP
##webrtc测试时https的问题
https://blog.csdn.net/sxc1989/article/details/81291320
在chorme中需要在https的情况下才能使用webrtc,在没有正式证书情况下, 可采用以下几种方式。
mac
用命令行启动chrome
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --unsafely-treat-insecure-origin-as-secure="http://192.168.1.250:5500" --user-data-dir=/Users/giaogiao/Downloads/chr
win
右击chrome游览器图标,选择“属性”,在目标后面加入如下内容:
--unsafely-treat-insecure-origin-as-secure="http://ip:port" --user-data-dir="C:/temp"
1
其中ip、port为服务端地址,–user-data-dir提供一个本地路径即可。
国内IM测试 和正式部署在同一台服务器了. 测试外网启动 传递参数
nohup java -jar -Dnetty.port=9001 -Dserver.port=9002 bootstrap-2.0-test.jar &>/dev/null &
docker run -p 6479:6379 -d --restart=always --name redis6Test redis:6 --appendonly yes --requirepass "axT8knPN5hAP"
docker run -p 3406:3306 --name mysql57Test --restart always -e MYSQL_ROOT_PASSWORD=JH86uc53r8Ca -d mysql:5.7
#资讯项目 本地开发ios的推送证书
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
com.xteng.Hibro 123456
#资讯项目 测试外网和正式的ios的推送证书
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
com.xteng.Hibro 123456
国内IM集成版
正式环境
121.37.208.9
域名
测试外网
用探路者原来的空闲服务器
探路者_测试_华为_121.37.234.35
域名:
imapitest.wecloud.cn
web示例:
imwebtest.wecloud.cn
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment