[*] 会议
This commit is contained in:
20
README.md
20
README.md
@@ -44,34 +44,24 @@
|
|||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
```
|
```
|
||||||
|
|
||||||
## 内网穿透
|
|
||||||
|
|
||||||
请用公共STUN/TURN服务或者自行搭建coturn服务。
|
|
||||||
|
|
||||||
> 只有公网Mesh架构才需要真正的内网穿透
|
|
||||||
|
|
||||||
## 直播
|
## 直播
|
||||||
|
|
||||||
终端推流到服务端,由服务端分流。
|
终端推流到服务端,由服务端分流。
|
||||||
|
|
||||||
## 会议
|
## 会议
|
||||||
|
|
||||||
Mesh架构声音视频控制部分功能均在终端实现,同时不会实现终端录制、美颜、AI识别、变声、混音等等功能。
|
|
||||||
MCU/SFU声音视频控制在服务端实现,如果没有终端订阅并且没有录制是不会对终端进行拉流。
|
|
||||||
|
|
||||||
### Mesh
|
### Mesh
|
||||||
|
|
||||||
流媒体点对点连接,不经过服务端。
|
流媒体点对点连接,不经过服务端。
|
||||||
|
Mesh架构声音视频控制部分功能均在终端实现,同时不会实现录制、美颜、变声、混音等等功能。
|
||||||
|
|
||||||
|
> 需要使用STUN/TURN实现内网穿透(可以自己搭建coturn服务)
|
||||||
|
|
||||||
### MCU
|
### MCU
|
||||||
|
|
||||||
终端推流到服务端,由服务端分流并且混音。
|
终端推流到服务端,由服务端混音分流。
|
||||||
|
|
||||||
### SFU
|
### SFU
|
||||||
|
|
||||||
终端推流到服务端,由服务端分流没有混音。
|
终端推流到服务端,由服务端直接分流。
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
springdoc升级正式版本
|
|
||||||
springboot升级正式版本
|
|
||||||
33
docs/flow/README.md
Normal file
33
docs/flow/README.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 流程
|
||||||
|
|
||||||
|
## WebRTC流程
|
||||||
|
|
||||||
|
发起响应
|
||||||
|
|
||||||
|
## SFU流程
|
||||||
|
|
||||||
|
媒体流
|
||||||
|
|
||||||
|
## MCU流程
|
||||||
|
|
||||||
|
媒体流
|
||||||
|
|
||||||
|
## Mesh流程
|
||||||
|
|
||||||
|
媒体流
|
||||||
|
|
||||||
|
## 直播流程
|
||||||
|
|
||||||
|
创建直播,终端操作。
|
||||||
|
|
||||||
|
## 会议流程
|
||||||
|
|
||||||
|
创建会议,终端操作
|
||||||
|
|
||||||
|
## 终端流程
|
||||||
|
|
||||||
|
注册下线
|
||||||
|
|
||||||
|
## 媒体控制
|
||||||
|
|
||||||
|
拉流发布
|
||||||
BIN
docs/flow/WebRTC.png
Normal file
BIN
docs/flow/WebRTC.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -39,6 +39,9 @@ public class Meeting {
|
|||||||
*/
|
*/
|
||||||
public void addSn(String sn) {
|
public void addSn(String sn) {
|
||||||
synchronized (this.sns) {
|
synchronized (this.sns) {
|
||||||
|
if(this.sns.contains(sn)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.sns.add(sn);
|
this.sns.add(sn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.acgist.taoyao.boot.model.Message;
|
|||||||
import com.acgist.taoyao.meeting.Meeting;
|
import com.acgist.taoyao.meeting.Meeting;
|
||||||
import com.acgist.taoyao.meeting.MeetingManager;
|
import com.acgist.taoyao.meeting.MeetingManager;
|
||||||
import com.acgist.taoyao.signal.client.ClientSession;
|
import com.acgist.taoyao.signal.client.ClientSession;
|
||||||
import com.acgist.taoyao.signal.client.ClientSessionManager;
|
|
||||||
import com.acgist.taoyao.signal.event.meeting.MeetingCreateEvent;
|
import com.acgist.taoyao.signal.event.meeting.MeetingCreateEvent;
|
||||||
import com.acgist.taoyao.signal.listener.ApplicationListenerAdapter;
|
import com.acgist.taoyao.signal.listener.ApplicationListenerAdapter;
|
||||||
|
|
||||||
@@ -23,8 +22,6 @@ public class MeetingCreateListener extends ApplicationListenerAdapter<MeetingCre
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private MeetingManager meetingManager;
|
private MeetingManager meetingManager;
|
||||||
@Autowired
|
|
||||||
private ClientSessionManager clientSessionManager;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(MeetingCreateEvent event) {
|
public void onApplicationEvent(MeetingCreateEvent event) {
|
||||||
@@ -32,7 +29,6 @@ public class MeetingCreateListener extends ApplicationListenerAdapter<MeetingCre
|
|||||||
final Meeting meeting = this.meetingManager.create(session.sn());
|
final Meeting meeting = this.meetingManager.create(session.sn());
|
||||||
final Message message = event.getMessage();
|
final Message message = event.getMessage();
|
||||||
message.setBody(Map.of("id", meeting.getId()));
|
message.setBody(Map.of("id", meeting.getId()));
|
||||||
// 广播:不改ID触发创建终端事件回调
|
|
||||||
this.clientSessionManager.broadcast(message);
|
this.clientSessionManager.broadcast(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.acgist.taoyao.meeting.listener;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.acgist.taoyao.boot.model.Message;
|
||||||
|
import com.acgist.taoyao.meeting.Meeting;
|
||||||
|
import com.acgist.taoyao.meeting.MeetingManager;
|
||||||
|
import com.acgist.taoyao.signal.client.ClientSession;
|
||||||
|
import com.acgist.taoyao.signal.event.meeting.MeetingEnterEvent;
|
||||||
|
import com.acgist.taoyao.signal.listener.ApplicationListenerAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进入会议监听
|
||||||
|
*
|
||||||
|
* @author acgist
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class MeetingEnterListener extends ApplicationListenerAdapter<MeetingEnterEvent> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MeetingManager meetingManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(MeetingEnterEvent event) {
|
||||||
|
final Map<?, ?> body = event.getBody();
|
||||||
|
final ClientSession session = event.getSession();
|
||||||
|
final String sn = session.sn();
|
||||||
|
final String id = (String) body.get("id");
|
||||||
|
final Meeting meeting = this.meetingManager.meeting(id);
|
||||||
|
meeting.addSn(sn);
|
||||||
|
final Message message = event.getMessage();
|
||||||
|
message.setBody(Map.of(
|
||||||
|
"id", meeting.getId(),
|
||||||
|
"sn", sn
|
||||||
|
));
|
||||||
|
this.clientSessionManager.broadcast(sn, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/** 桃夭WebRTC终端核心功能 */
|
/** 桃夭WebRTC终端核心功能 */
|
||||||
/** 兼容 */
|
/** 兼容 */
|
||||||
const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
const RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
||||||
/** 默认音频配置 */
|
/** 默认音频配置 */
|
||||||
const defaultAudioConfig = {
|
const defaultAudioConfig = {
|
||||||
// 音量:0~1
|
// 音量:0~1
|
||||||
@@ -39,6 +39,21 @@ const defaultVideoConfig = {
|
|||||||
// 选摄像头:user|left|right|environment
|
// 选摄像头:user|left|right|environment
|
||||||
facingMode: 'environment'
|
facingMode: 'environment'
|
||||||
}
|
}
|
||||||
|
/** 默认RTCPeerConnection配置 */
|
||||||
|
const defaultRPCConfig = {
|
||||||
|
// ICE代理的服务器
|
||||||
|
// iceServers: null,
|
||||||
|
// 证书
|
||||||
|
// certificates: null,
|
||||||
|
// 传输通道绑定策略:balanced|max-compat|max-bundle
|
||||||
|
bundlePolicy: 'balanced',
|
||||||
|
// RTCP多路复用策略:require|negotiate
|
||||||
|
rtcpMuxPolicy: 'negotiate',
|
||||||
|
// ICE传输策略:all|relay
|
||||||
|
iceTransportPolicy: 'all'
|
||||||
|
// ICE候选个数
|
||||||
|
// iceCandidatePoolSize: 10
|
||||||
|
}
|
||||||
/** 信令配置 */
|
/** 信令配置 */
|
||||||
const signalConfig = {
|
const signalConfig = {
|
||||||
/** 当前终端SN */
|
/** 当前终端SN */
|
||||||
@@ -328,13 +343,9 @@ function TaoyaoClient(
|
|||||||
/** 设置媒体流 */
|
/** 设置媒体流 */
|
||||||
this.buildStream = async function(stream) {
|
this.buildStream = async function(stream) {
|
||||||
if(stream) {
|
if(stream) {
|
||||||
if ('srcObject' in this.video) {
|
|
||||||
this.video.srcObject = stream;
|
this.video.srcObject = stream;
|
||||||
} else {
|
|
||||||
this.video.src = URL.createObjectURL(stream);;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await this.play();
|
await this.play();
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
/** 设置音频流 */
|
/** 设置音频流 */
|
||||||
|
|||||||
@@ -62,9 +62,9 @@
|
|||||||
this.taoyao
|
this.taoyao
|
||||||
.checkDevice()
|
.checkDevice()
|
||||||
.buildChannel(this.callback)
|
.buildChannel(this.callback)
|
||||||
//.buildLocalMedia()
|
.buildLocalMedia()
|
||||||
//.then(stream => this.taoyao.buildLocalClient('local', stream))
|
.then(stream => this.taoyao.buildLocalClient('local', stream))
|
||||||
//.catch((e) => console.error('打开终端媒体失败', e));
|
.catch((e) => console.error('打开终端媒体失败', e));
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
#### 消息流程:终端->服务端-)终端
|
#### 消息流程:终端->服务端-)终端
|
||||||
|
|
||||||
终端注册成功响应同时[下发配置信令](#下发配置信令2004),并且广播[终端上线信令](#终端上线信令2002)。
|
终端注册成功以后响应同时[下发配置信令](#下发配置信令2004),并且广播[终端上线信令](#终端上线信令2002)。
|
||||||
|
|
||||||
### 终端关闭信令(2001)
|
### 终端关闭信令(2001)
|
||||||
|
|
||||||
@@ -119,11 +119,11 @@
|
|||||||
|
|
||||||
#### 消息流程:终端-)服务端
|
#### 消息流程:终端-)服务端
|
||||||
|
|
||||||
广播[终端下线信令](#终端下线信令2003),同时释放所有相关资源(信令通道、媒体通道等等)
|
终端关闭以后广播[终端下线信令](#终端下线信令2003),同时释放所有相关资源(信令通道、媒体通道等等)
|
||||||
|
|
||||||
### 终端上线信令(2002)
|
### 终端上线信令(2002)
|
||||||
|
|
||||||
服务端->终端:参考[终端注册信令](#终端注册信令)
|
#### 消息主体
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
@@ -131,9 +131,13 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 消息流程:服务端->终端
|
||||||
|
|
||||||
|
参考[终端注册信令](#终端注册信令2000)
|
||||||
|
|
||||||
### 终端下线信令(2003)
|
### 终端下线信令(2003)
|
||||||
|
|
||||||
服务端->终端:参考[终端关闭信令](#关闭信令)
|
#### 消息主体
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
@@ -141,9 +145,13 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 消息流程:服务端->终端
|
||||||
|
|
||||||
|
参考[终端关闭信令](#终端关闭信令2001)
|
||||||
|
|
||||||
### 下发配置信令(2004)
|
### 下发配置信令(2004)
|
||||||
|
|
||||||
服务端->终端:参考[注册信令](#注册信令)
|
#### 消息主体
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
@@ -153,56 +161,112 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 消息流程:服务端->终端
|
||||||
|
|
||||||
|
参考[终端注册信令](#终端注册信令2000)
|
||||||
|
|
||||||
### 心跳信令(2005)
|
### 心跳信令(2005)
|
||||||
|
|
||||||
心跳:响应
|
#### 消息主体
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# 请求
|
||||||
{
|
{
|
||||||
"signal": "信号强度",
|
"signal": "信号强度",
|
||||||
"battery": "电池电量"
|
"battery": "电池电量"
|
||||||
}
|
}
|
||||||
|
# 响应
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 消息流程:终端->服务端->终端
|
||||||
|
|
||||||
|
### 单播信令(2006)
|
||||||
|
|
||||||
|
#### 消息主体
|
||||||
|
|
||||||
|
```
|
||||||
|
# 请求
|
||||||
|
{
|
||||||
|
"to": "接收终端标识",
|
||||||
|
// 主体信息
|
||||||
|
}
|
||||||
|
# 转发
|
||||||
|
{
|
||||||
|
// 主体信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 消息流程:终端->服务端->终端
|
||||||
|
|
||||||
|
终端转发信令到指定的终端
|
||||||
|
|
||||||
|
### 广播信令(2007)
|
||||||
|
|
||||||
|
#### 消息主体
|
||||||
|
|
||||||
|
```
|
||||||
|
# 请求
|
||||||
|
{
|
||||||
|
// 主体信息
|
||||||
|
}
|
||||||
|
# 广播
|
||||||
|
{
|
||||||
|
// 主体信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 消息流程:终端->服务端-)终端
|
||||||
|
|
||||||
|
终端广播信令到所有的终端
|
||||||
|
|
||||||
|
### 终端状态信令(2998)
|
||||||
|
|
||||||
|
#### 消息主体
|
||||||
|
|
||||||
|
```
|
||||||
|
# 请求
|
||||||
|
{
|
||||||
|
"sn": "终端标识"
|
||||||
|
}
|
||||||
|
# 响应
|
||||||
|
{
|
||||||
|
"sn": "终端标识",
|
||||||
|
"ip": "IP地址",
|
||||||
|
"mac": "MAC地址",
|
||||||
|
"signal": "信号强度",
|
||||||
|
"battery": "电池电量"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 单播信令(2006)
|
#### 消息流程:终端->服务端->终端
|
||||||
|
|
||||||
发送到指定的终端:删除`to`字段
|
响应指定终端状态(如果没有指定终端标识默认查询自己)
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"to": "接收终端标识",
|
|
||||||
// 主体信息
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 广播信令(2007)
|
|
||||||
|
|
||||||
发送到所有的终端:排除自己
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
// 主体信息
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 终端状态信令(2998)
|
|
||||||
|
|
||||||
返回指定终端状态(如果没有指定终端标识默认查询自己)
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"sn": "终端标识"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 终端列表信令(2999)
|
### 终端列表信令(2999)
|
||||||
|
|
||||||
返回所有终端状态列表
|
#### 消息主体
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# 请求
|
||||||
{}
|
{}
|
||||||
|
# 响应
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"sn": "终端标识",
|
||||||
|
"ip": "IP地址",
|
||||||
|
"mac": "MAC地址",
|
||||||
|
"signal": "信号强度",
|
||||||
|
"battery": "电池电量"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 消息流程:终端->服务端->终端
|
||||||
|
|
||||||
|
响应所有终端状态列表
|
||||||
|
|
||||||
## 直播信令(3000~3999)
|
## 直播信令(3000~3999)
|
||||||
|
|
||||||
### 开启直播信令(3000)
|
### 开启直播信令(3000)
|
||||||
@@ -225,7 +289,7 @@
|
|||||||
{}
|
{}
|
||||||
----
|
----
|
||||||
{
|
{
|
||||||
"id": "会议标识"
|
"id": "会议标识",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -237,7 +301,23 @@
|
|||||||
|
|
||||||
### 进入会议信令(4002)
|
### 进入会议信令(4002)
|
||||||
|
|
||||||
广播
|
#### 消息主体
|
||||||
|
|
||||||
|
```
|
||||||
|
# 请求
|
||||||
|
{
|
||||||
|
"id": "会议标识"
|
||||||
|
}
|
||||||
|
# 广播
|
||||||
|
{
|
||||||
|
"id": "会议标识",
|
||||||
|
"sn": "终端标识"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 消息流程:终端->服务端-)终端
|
||||||
|
|
||||||
|
终端进入会议,广播通知其他终端。
|
||||||
|
|
||||||
### 离开会议信令(4003)
|
### 离开会议信令(4003)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.acgist.taoyao.signal.event.meeting;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.acgist.taoyao.boot.model.Message;
|
||||||
|
import com.acgist.taoyao.signal.client.ClientSession;
|
||||||
|
import com.acgist.taoyao.signal.event.ApplicationEventAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进入会议事件
|
||||||
|
*
|
||||||
|
* @author acgist
|
||||||
|
*/
|
||||||
|
public class MeetingEnterEvent extends ApplicationEventAdapter {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public MeetingEnterEvent(Map<?, ?> body, Message message, ClientSession session) {
|
||||||
|
super(body, message, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ import com.acgist.taoyao.signal.protocol.client.ClientOnlineProtocol;
|
|||||||
/**
|
/**
|
||||||
* 终端注册监听
|
* 终端注册监听
|
||||||
*
|
*
|
||||||
|
* TODO:如果已经在会议、直播中,自动推流。
|
||||||
|
*
|
||||||
* @author acgist
|
* @author acgist
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class ClientHeartbeatProtocol extends ProtocolMapAdapter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(String sn, Map<?, ?> body, Message message, ClientSession session) {
|
public void execute(String sn, Map<?, ?> body, Message message, ClientSession session) {
|
||||||
// 回应心跳
|
// 响应心跳
|
||||||
session.push(message.cloneWidthoutBody());
|
session.push(message.cloneWidthoutBody());
|
||||||
// 设置状态
|
// 设置状态
|
||||||
final ClientSessionStatus status = session.status();
|
final ClientSessionStatus status = session.status();
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
package com.acgist.taoyao.signal.protocol.meeting;
|
package com.acgist.taoyao.signal.protocol.meeting;
|
||||||
|
|
||||||
public class MeetingEnterProtocol {
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.acgist.taoyao.boot.model.Message;
|
||||||
|
import com.acgist.taoyao.signal.client.ClientSession;
|
||||||
|
import com.acgist.taoyao.signal.event.meeting.MeetingEnterEvent;
|
||||||
|
import com.acgist.taoyao.signal.protocol.ProtocolMapAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进入会议信令
|
||||||
|
*
|
||||||
|
* @author acgist
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class MeetingEnterProtocol extends ProtocolMapAdapter {
|
||||||
|
|
||||||
|
public static final Integer PID = 4002;
|
||||||
|
|
||||||
|
public MeetingEnterProtocol() {
|
||||||
|
super(PID, "进入会议信令");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(String sn, Map<?, ?> body, Message message, ClientSession session) {
|
||||||
|
this.publishEvent(new MeetingEnterEvent(body, message, session));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,4 +49,3 @@ SDP只是一种信息格式的描述标准,不属于传输协议,但是可
|
|||||||
|信令通道|自己实现|
|
|信令通道|自己实现|
|
||||||
|会话通道|SIP/SDP|
|
|会话通道|SIP/SDP|
|
||||||
|媒体通道|RTP/RTCP/SRTP/SRTCP|
|
|媒体通道|RTP/RTCP/SRTP/SRTCP|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user