This commit is contained in:
acgist
2023-03-14 07:53:32 +08:00
parent 72e4b34630
commit 0cd054dbb0
25 changed files with 242 additions and 203 deletions

View File

@@ -26,6 +26,10 @@
|taoyao-client-harmony|鸿蒙终端|鸿蒙智能终端接入|
|taoyao-signal-server|信令服务|终端信令控制|
## 证书
本地开发测试安装`docs/certs`中的`ca.crt`证书
## 部署
[部署文档](./docs/Deploy.md)

View File

@@ -1,7 +0,0 @@
# 测试
## 推流测试
```
ffmpeg
```

View File

@@ -19,7 +19,7 @@ http {
default_type application/octet-stream;
gzip on;
gzip_types text/xml text/css text/plain text/javascript image/gif image/png image/jpg image/webp image/jpeg image/x-icon image/svg+xml application/json application/javascript font/woff application/octet-stream application/vnd.ms-fontobject;
gzip_types font/woff text/xml text/css text/plain text/javascript image/gif image/png image/jpg image/webp image/jpeg image/x-icon image/svg+xml application/json application/javascript application/octet-stream application/vnd.ms-fontobject;
gzip_min_length 1k;
sendfile on;

View File

@@ -119,12 +119,12 @@ module.exports = {
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP || defaultTaoyaoHost || "127.0.0.1",
},
// TCP
// {
// protocol: "tcp",
// ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
// port: 44444,
// announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP || defaultTaoyaoHost || "127.0.0.1",
// },
{
protocol: "tcp",
ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
port: 44444,
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP || defaultTaoyaoHost || "127.0.0.1",
},
],
},
// WebRtcTransporthttps://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions

View File

@@ -452,8 +452,8 @@ class Taoyao {
case "media::transport::close":
this.mediaTransportClose(message, body);
break;
case "media::transport::plain::in":
me.mediaTransportPlainIn(message, body);
case "media::transport::plain":
me.mediaTransportPlain(message, body);
break;
case "media::transport::webrtc::connect":
me.mediaTransportWebrtcConnect(message, body);
@@ -629,8 +629,8 @@ class Taoyao {
producer.on("videoorientationchange", (videoOrientation) => {
console.info("producer videoorientationchange", producer.id, videoOrientation);
self.push(protocol.buildMessage("media::video::orientation::change", {
...videoOrientation,
roomId: roomId,
...videoOrientation
}));
});
// await producer.enableTraceEvent([ 'rtp', 'keyframe', 'nack', 'pli', 'fir' ]);
@@ -821,6 +821,7 @@ class Taoyao {
});
consumer.on("producerpause", () => {
console.info("consumer producerpause", consumer.id);
// consumer.pause();
this.push(
protocol.buildMessage("media::consumer::pause", {
roomId: roomId,
@@ -830,6 +831,7 @@ class Taoyao {
});
consumer.on("producerresume", () => {
console.info("consumer producerresume", consumer.id);
// consumer.resume();
this.push(
protocol.buildMessage("media::consumer::resume", {
roomId: roomId,
@@ -1261,16 +1263,21 @@ class Taoyao {
* @param {*} message 消息
* @param {*} body 消息主体
*/
async mediaTransportPlainIn(message, body) {
async mediaTransportPlain(message, body) {
const me = this;
const { roomId, rtcpMux, comedia, clientId } = body;
const { roomId, rtcpMux, comedia, clientId, enableSctp, numSctpStreams, enableSrtp, srtpCryptoSuite } = body;
const plainTransportOptions = {
...config.mediasoup.plainTransportOptions,
rtcpMux: rtcpMux,
comedia: comedia,
...config.mediasoup.plainTransportOptions,
enableSctp: enableSctp || Boolean(numSctpStreams),
numSctpStreams: numSctpStreams || 0,
enableSrtp: enableSrtp,
srtpCryptoSuite: srtpCryptoSuite,
};
const room = this.rooms.get(roomId);
const transport = await room.mediasoupRouter.createPlainTransport(plainTransportOptions);
me.transportEvent("plain", roomId, transport);
transport.clientId = clientId;
room.transports.set(transport.id, transport);
console.info(transport.tuple)
@@ -1336,8 +1343,35 @@ class Taoyao {
...webRtcTransportOptions,
webRtcServer: room.webRtcServer,
});
me.transportEvent("webrtc", roomId, transport);
transport.clientId = clientId;
// 通用事件
room.transports.set(transport.id, transport);
message.body = {
roomId: roomId,
transportId: transport.id,
iceCandidates: transport.iceCandidates,
iceParameters: transport.iceParameters,
dtlsParameters: transport.dtlsParameters,
sctpParameters: transport.sctpParameters,
};
self.push(message);
const { maxIncomingBitrate } = config.mediasoup.webRtcTransportOptions;
// If set, apply max incoming bitrate limit.
if (maxIncomingBitrate) {
try {
await transport.setMaxIncomingBitrate(maxIncomingBitrate);
} catch (error) {}
}
}
/**
* 通道事件
*
* @param {*} type 类型webrtc|plain|pipe|direct
* @param {*} roomId 房间ID
* @param {*} transport 通道
*/
transportEvent(type, roomId, transport) {
/********************* 通用通道事件 *********************/
transport.on("routerclose", () => {
console.info("transport routerclose", transport.id);
transport.close();
@@ -1377,6 +1411,7 @@ class Taoyao {
// });
// transport.observer.on("trace", fn(trace));
/********************* webRtcTransport通道事件 *********************/
if("webrtc" === type) {
// transport.on("icestatechange", (iceState) => {
// console.info("transport icestatechange", transport.id, iceState);
// });
@@ -1393,34 +1428,24 @@ class Taoyao {
// transport.observer.on("iceselectedtuplechange", fn(iceSelectedTuple));
// transport.observer.on("dtlsstatechange", fn(dtlsState));
// transport.observer.on("sctpstatechange", fn(sctpState));
}
/********************* plainTransport通道事件 *********************/
if("plain" === type) {
// transport.on("tuple", fn(tuple));
// transport.on("rtcptuple", fn(rtcpTuple));
// transport.on("sctpstatechange", fn(sctpState));
// transport.observer.on("tuple", fn(tuple));
// transport.observer.on("rtcptuple", fn(rtcpTuple));
// transport.observer.on("sctpstatechange", fn(sctpState));
}
/********************* pipeTransport通道事件 *********************/
if("pipe" === type) {
// transport.on("sctpstatechange", fn(sctpState));
// transport.observer.on("sctpstatechange", fn(sctpState));
}
/********************* directTransport通道事件 *********************/
if("rtcp" === type) {
// transport.on("rtcp", fn(rtcpPacket));
room.transports.set(transport.id, transport);
message.body = {
roomId: roomId,
transportId: transport.id,
iceCandidates: transport.iceCandidates,
iceParameters: transport.iceParameters,
dtlsParameters: transport.dtlsParameters,
sctpParameters: transport.sctpParameters,
};
self.push(message);
const { maxIncomingBitrate } = config.mediasoup.webRtcTransportOptions;
// If set, apply max incoming bitrate limit.
if (maxIncomingBitrate) {
try {
await transport.setMaxIncomingBitrate(maxIncomingBitrate);
} catch (error) {}
}
}
/**

View File

@@ -1,13 +1,7 @@
# Web终端
注意部分`API`需要`localhost`或者`https`
## Web终端
* [mediasoup-client源码](https://github.com/versatica/mediasoup-client)
* [mediasoup-client文档](https://mediasoup.org/documentation/v3/mediasoup-client)
* [mediasoup-client接口](https://mediasoup.org/documentation/v3/mediasoup-client/api)
## TODO
按钮:选择房间、媒体服务、开关

View File

@@ -21,8 +21,9 @@ const protocol = {
* @returns 索引
*/
buildId() {
if (++this.index > this.maxIndex) {
this.index = 0;
const me = this;
if (++me.index > me.maxIndex) {
me.index = 0;
}
const date = new Date();
return (
@@ -30,8 +31,8 @@ const protocol = {
1000000000000 * date.getHours() +
10000000000 * date.getMinutes() +
100000000 * date.getSeconds() +
1000 * this.clientIndex +
this.index
1000 * me.clientIndex +
me.index
);
},
/**
@@ -2010,23 +2011,25 @@ class Taoyao extends RemoteClient {
*/
closeMedia() {
let me = this;
if (me.sendTransport) {
me.sendTransport.close();
}
if (me.recvTransport) {
me.recvTransport.close();
}
if(me.audioTrack) {
me.audioTrack.stop();
}
if(me.videoTrack) {
me.videoTrack.stop();
}
if (me.sendTransport) {
me.sendTransport.close();
}
if (me.recvTransport) {
me.recvTransport.close();
}
me.sendTransport = null;
me.recvTransport = null;
me.dataProducer = null;
me.audioProducer = null;
me.videoProducer = null;
me.dataProducer = null;
me.consumers.clear();
me.dataConsumers.clear();
}
/**
* 关闭资源

View File

@@ -38,6 +38,8 @@
<outputDirectory>config</outputDirectory>
<includes>
<include>*.jks</include>
<include>*.p12</include>
<include>*.pfx</include>
</includes>
</fileSet>
<fileSet>

View File

@@ -38,6 +38,8 @@
<outputDirectory>config</outputDirectory>
<includes>
<include>*.jks</include>
<include>*.p12</include>
<include>*.pfx</include>
</includes>
</fileSet>
<fileSet>

View File

@@ -175,7 +175,9 @@
<configuration>
<!-- Jar配置独立config目录 -->
<excludes>
<exclude>*.jks</exclude>
<include>*.jks</include>
<include>*.p12</include>
<include>*.pfx</include>
<exclude>*.yml</exclude>
<exclude>*.properties</exclude>
</excludes>

View File

@@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
/**
* 配置
@@ -24,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "配置", description = "配置管理")
@RestController
@RequestMapping("/config")
@RequiredArgsConstructor
public class ConfigController {
private final MediaProperties mediaProperties;
@@ -31,18 +33,6 @@ public class ConfigController {
private final SocketProperties socketProperties;
private final WebrtcProperties webrtcProperties;
public ConfigController(
MediaProperties mediaProperties,
CameraProperties cameraProperties,
SocketProperties socketProperties,
WebrtcProperties webrtcProperties
) {
this.mediaProperties = mediaProperties;
this.cameraProperties = cameraProperties;
this.socketProperties = socketProperties;
this.webrtcProperties = webrtcProperties;
}
@Operation(summary = "媒体配置", description = "媒体配置")
@GetMapping("/media")
@ApiResponse(content = @Content(schema = @Schema(implementation = MediaProperties.class)))

View File

@@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
/**
* 控制
@@ -37,6 +38,7 @@ import jakarta.validation.constraints.NotNull;
@Validated
@RestController
@RequestMapping("/control")
@RequiredArgsConstructor
public class ControlController {
private final ControlAiProtocol controlAiProtocol;
@@ -49,28 +51,6 @@ public class ControlController {
private final ControlConfigAudioProtocol controlConfigAudioProtocol;
private final ControlConfigVideoProtocol controlConfigVideoProtocol;
public ControlController(
ControlAiProtocol controlAiProtocol,
ControlPtzProtocol controlPtzProtocol,
ControlBellProtocol controlBellProtocol,
ControlBeautyProtocol controlBeautyProtocol,
ControlRecordProtocol controlRecordProtocol,
ControlWatermarkProtocol controlWatermarkProtocol,
ControlPhotographProtocol controlPhotographProtocol,
ControlConfigAudioProtocol controlConfigAudioProtocol,
ControlConfigVideoProtocol controlConfigVideoProtocol
) {
this.controlAiProtocol = controlAiProtocol;
this.controlPtzProtocol = controlPtzProtocol;
this.controlBellProtocol = controlBellProtocol;
this.controlBeautyProtocol = controlBeautyProtocol;
this.controlRecordProtocol = controlRecordProtocol;
this.controlWatermarkProtocol = controlWatermarkProtocol;
this.controlPhotographProtocol = controlPhotographProtocol;
this.controlConfigAudioProtocol = controlConfigAudioProtocol;
this.controlConfigVideoProtocol = controlConfigVideoProtocol;
}
@Operation(summary = "AI识别", description = "AI识别控制")
@GetMapping("/ai/{clientId}")
public Message ai(@PathVariable String clientId, @Valid AiProperties aiProperties) {

View File

@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.boot.model.MessageCode;
import com.acgist.taoyao.signal.protocol.platform.PlatformRebootProtocol;
import com.acgist.taoyao.signal.protocol.platform.PlatformShutdownProtocol;
@@ -38,6 +39,9 @@ public class PlatformController {
@Operation(summary = "重启平台", description = "重启平台")
@GetMapping("/reboot")
public Message platformReboot() {
if(this.platformRebootProtocol == null) {
return Message.fail(MessageCode.CODE_3406, "功能没有开启");
}
this.platformRebootProtocol.execute();
return Message.success();
}
@@ -45,6 +49,9 @@ public class PlatformController {
@Operation(summary = "关闭平台", description = "关闭平台")
@GetMapping("/shutdown")
public Message platformShutdown() {
if(this.platformShutdownProtocol == null) {
return Message.fail(MessageCode.CODE_3406, "功能没有开启");
}
this.platformShutdownProtocol.execute();
return Message.success();
}

View File

@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.boot.model.MessageCode;
import com.acgist.taoyao.signal.protocol.system.SystemRebootProtocol;
import com.acgist.taoyao.signal.protocol.system.SystemShutdownProtocol;
@@ -38,6 +39,9 @@ public class SystemController {
@Operation(summary = "重启系统", description = "重启系统")
@GetMapping("/reboot")
public Message systemReboot() {
if(this.systemRebootProtocol == null) {
return Message.fail(MessageCode.CODE_3406, "功能没有开启");
}
this.systemRebootProtocol.execute();
return Message.success();
}
@@ -45,6 +49,9 @@ public class SystemController {
@Operation(summary = "关闭系统", description = "关闭系统")
@GetMapping("/shutdown")
public Message systemShutdown() {
if(this.systemShutdownProtocol == null) {
return Message.fail(MessageCode.CODE_3406, "功能没有开启");
}
this.systemShutdownProtocol.execute();
return Message.success();
}

View File

@@ -80,7 +80,7 @@ public class SocketSignalTest {
}
""";
// {"header":{"v":"1.0.0","id":1215310510002009,"signal":"room::enter"},"body":{"roomId":"8260e615-3081-4bfc-96a8-574f4dd780d9"}}
// {"header":{"v":"1.0.0","id":1215310510002010,"signal":"media::transport::plain::in"},"body":{"roomId":"8260e615-3081-4bfc-96a8-574f4dd780d9","rtcpMux":false,"comedia":true}}
// {"header":{"v":"1.0.0","id":1215310510002010,"signal":"media::transport::plain"},"body":{"roomId":"8260e615-3081-4bfc-96a8-574f4dd780d9","rtcpMux":false,"comedia":true}}
// {"header":{"v":"1.0.0","id":1215375110006012,"signal":"media::produce"},"body":{"kind":"video","roomId":"8260e615-3081-4bfc-96a8-574f4dd780d9","transportId":"14dc9307-bf9c-4442-a9ad-ce6a97623ef4","appData":{},"rtpParameters":{"codecs":[{"mimeType":"video/vp8","clockRate":90000,"payloadType":102,"rtcpFeedback":[]}],"encodings":[{"ssrc":123123}]}}}
// ffplay -protocol_whitelist "file,udp,rtp" taoyao.sdp
// ffmpeg -re -i video.mp4 -vcodec copy -map 0:0 -f rtp rtp://localhost:6666 > taoyao.sdp

View File

@@ -1,6 +1,5 @@
package com.acgist.taoyao.signal.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -13,6 +12,7 @@ import com.acgist.taoyao.signal.event.EventPublisher;
import com.acgist.taoyao.signal.protocol.ProtocolManager;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
/**
* 信令自动配置
@@ -20,13 +20,13 @@ import jakarta.annotation.PostConstruct;
* @author acgist
*/
@AutoConfiguration
@RequiredArgsConstructor
@EnableConfigurationProperties({
CameraProperties.class
})
public class SignalAutoConfiguration {
@Autowired
private ApplicationContext applicationContext;
private final ApplicationContext applicationContext;
@PostConstruct
public void init() {

View File

@@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
/**
* 终端
@@ -26,6 +27,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "终端", description = "终端管理")
@RestController
@RequestMapping("/client")
@RequiredArgsConstructor
public class ClientController {
private final ClientManager clientManager;
@@ -33,16 +35,6 @@ public class ClientController {
private final ClientRebootProtocol clientRebootProtocol;
private final ClientShutdownProtocol clientShutdownProtocol;
public ClientController(
ClientManager clientManager, ClientWakeupProtocol clientWakeupProtocol,
ClientRebootProtocol clientRebootProtocol, ClientShutdownProtocol clientShutdownProtocol
) {
this.clientManager = clientManager;
this.clientWakeupProtocol = clientWakeupProtocol;
this.clientRebootProtocol = clientRebootProtocol;
this.clientShutdownProtocol = clientShutdownProtocol;
}
@Operation(summary = "终端列表", description = "终端列表")
@GetMapping("/list")
@ApiResponse(content = @Content(schema = @Schema(implementation = ClientStatus.class)))

View File

@@ -15,6 +15,7 @@ import com.acgist.taoyao.signal.protocol.Protocol;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
@@ -26,14 +27,11 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
@RequestMapping("/protocol")
@RequiredArgsConstructor
public class ProtocolController {
private final ApplicationContext applicationContext;
public ProtocolController(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Operation(summary = "信令列表", description = "信令列表Markdown")
@GetMapping("/list")
public String list() {

View File

@@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
/**
* 房间
@@ -29,14 +30,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Validated
@RestController
@RequestMapping("/room")
@RequiredArgsConstructor
public class RoomController {
private final RoomManager roomManager;
public RoomController(RoomManager roomManager) {
this.roomManager = roomManager;
}
@Operation(summary = "房间信息", description = "房间信息")
@GetMapping("/log")
public Message log() {

View File

@@ -5,7 +5,7 @@ package com.acgist.taoyao.signal.party.media;
*
* @author acgist
*/
public enum RouteType {
public enum RouterType {
// 对讲只有两个人之间的媒体相互路由
ONE_TO_ONE,

View File

@@ -67,11 +67,16 @@ public class MediaConsumeProtocol extends ProtocolRoomAdapter implements Applica
final ClientWrapper produceClientWrapper = producer.getProducerClient();
room.getClients().values().stream()
.filter(v -> v != produceClientWrapper)
.filter(v -> v.getRecvTransport() != null)
.filter(v -> v.getSubscribeType().canConsume(producer))
.forEach(v -> this.consume(room, v, producer, this.build()));
} else if(event.getClientWrapper() != null) {
// 创建WebRTC消费通道消费其他终端
final ClientWrapper consumeClientWrapper = event.getClientWrapper();
if(consumeClientWrapper.getRecvTransport() == null) {
log.debug("没有消费通道:{}", consumeClientWrapper.getClientId());
return;
}
room.getClients().values().stream()
.filter(v -> v != consumeClientWrapper)
.flatMap(v -> v.getProducers().values().stream())

View File

@@ -0,0 +1,34 @@
package com.acgist.taoyao.signal.protocol.media;
import java.util.Map;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.client.Client;
import com.acgist.taoyao.signal.client.ClientType;
import com.acgist.taoyao.signal.party.media.Room;
import com.acgist.taoyao.signal.protocol.ProtocolRoomAdapter;
/**
* 设置路由类型信令
* 注意:不会添加移除消费者生产者,只会暂停恢复操作。
*
* @author acgist
*/
public class MediaSetRouterTypeProtocol extends ProtocolRoomAdapter {
public static final String SIGNAL = "media::set::router::type";
public MediaSetRouterTypeProtocol() {
super("设置路由类型信令", SIGNAL);
}
@Override
public void execute(String clientId, ClientType clientType, Room room, Client client, Client mediaClient, Message message, Map<String, Object> body) {
if(clientType.mediaClient()) {
// TODO路由类型
} else {
this.logNoAdapter(clientType);
}
}
}

View File

@@ -1,10 +0,0 @@
package com.acgist.taoyao.signal.protocol.media;
/**
* 创建RTP输出通道信令
*
* @author acgist
*/
public class MediaTransportPlainOutProtocol {
}

View File

@@ -22,7 +22,10 @@ import lombok.extern.slf4j.Slf4j;
/**
* 创建RTP输入通道信令
* TODOsrtp
* 注意
* 3. ffmpeg不支持rtcpMux
* 2. comedia必须开启srtp功能
* 1. 如果关闭comedia不会自动升级双向通道
*
* @author acgist
*/
@@ -34,14 +37,21 @@ import lombok.extern.slf4j.Slf4j;
"roomId": "房间ID",
"rtcpMux": RTP和RTCP端口复用true|false,
"comedia": 自动终端端口true|false,
"enableSctp": 是否开启sctptrue|false,
"numSctpStreams": sctp数量,
"enableSrtp": 是否开启srtptrue|false,
"srtpCryptoSuite": {
"cryptoSuite": "算法AEAD_AES_256_GCM|AEAD_AES_128_GCM|AES_CM_128_HMAC_SHA1_80|AES_CM_128_HMAC_SHA1_32",
"keyBase64": "密钥"
}
}
"""
)
public class MediaTransportPlainInProtocol extends ProtocolRoomAdapter {
public class MediaTransportPlainProtocol extends ProtocolRoomAdapter {
public static final String SIGNAL = "media::transport::plain::in";
public static final String SIGNAL = "media::transport::plain";
public MediaTransportPlainInProtocol() {
public MediaTransportPlainProtocol() {
super("创建RTP输入通道信令", SIGNAL);
}
@@ -65,6 +75,7 @@ public class MediaTransportPlainInProtocol extends ProtocolRoomAdapter {
log.warn("发送通道已经存在:{}", transportId);
}
clientWrapper.setSendTransport(sendTransport);
// TODO双向队列
// 拷贝属性
sendTransport.copy(responseBody);
client.push(response);

View File

@@ -40,6 +40,8 @@ public class RoomExpelProtocol extends ProtocolRoomAdapter {
if(clientType.mediaClient()) {
final String expelClientId = MapUtils.get(body, Constant.CLIENT_ID);
room.unicast(expelClientId, message);
// 如果需要强制提出
// room.leave(this.clientManager.clients(expelClientId));
} else {
this.logNoAdapter(clientType);
}