[*] 会议
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架构声音视频控制部分功能均在终端实现,同时不会实现录制、美颜、变声、混音等等功能。
|
||||
|
||||
> 需要使用STUN/TURN实现内网穿透(可以自己搭建coturn服务)
|
||||
|
||||
### MCU
|
||||
|
||||
终端推流到服务端,由服务端分流并且混音。
|
||||
终端推流到服务端,由服务端混音分流。
|
||||
|
||||
### 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) {
|
||||
synchronized (this.sns) {
|
||||
if(this.sns.contains(sn)) {
|
||||
return;
|
||||
}
|
||||
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.MeetingManager;
|
||||
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.listener.ApplicationListenerAdapter;
|
||||
|
||||
@@ -23,8 +22,6 @@ public class MeetingCreateListener extends ApplicationListenerAdapter<MeetingCre
|
||||
|
||||
@Autowired
|
||||
private MeetingManager meetingManager;
|
||||
@Autowired
|
||||
private ClientSessionManager clientSessionManager;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(MeetingCreateEvent event) {
|
||||
@@ -32,7 +29,6 @@ public class MeetingCreateListener extends ApplicationListenerAdapter<MeetingCre
|
||||
final Meeting meeting = this.meetingManager.create(session.sn());
|
||||
final Message message = event.getMessage();
|
||||
message.setBody(Map.of("id", meeting.getId()));
|
||||
// 广播:不改ID触发创建终端事件回调
|
||||
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终端核心功能 */
|
||||
/** 兼容 */
|
||||
const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
||||
const RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
||||
/** 默认音频配置 */
|
||||
const defaultAudioConfig = {
|
||||
// 音量:0~1
|
||||
@@ -39,6 +39,21 @@ const defaultVideoConfig = {
|
||||
// 选摄像头:user|left|right|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 = {
|
||||
/** 当前终端SN */
|
||||
@@ -328,13 +343,9 @@ function TaoyaoClient(
|
||||
/** 设置媒体流 */
|
||||
this.buildStream = async function(stream) {
|
||||
if(stream) {
|
||||
if ('srcObject' in this.video) {
|
||||
this.video.srcObject = stream;
|
||||
} else {
|
||||
this.video.src = URL.createObjectURL(stream);;
|
||||
}
|
||||
}
|
||||
await this.play();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
/** 设置音频流 */
|
||||
|
||||
@@ -62,9 +62,9 @@
|
||||
this.taoyao
|
||||
.checkDevice()
|
||||
.buildChannel(this.callback)
|
||||
//.buildLocalMedia()
|
||||
//.then(stream => this.taoyao.buildLocalClient('local', stream))
|
||||
//.catch((e) => console.error('打开终端媒体失败', e));
|
||||
.buildLocalMedia()
|
||||
.then(stream => this.taoyao.buildLocalClient('local', stream))
|
||||
.catch((e) => console.error('打开终端媒体失败', e));
|
||||
},
|
||||
beforeDestroy() {
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
#### 消息流程:终端->服务端-)终端
|
||||
|
||||
终端注册成功响应同时[下发配置信令](#下发配置信令2004),并且广播[终端上线信令](#终端上线信令2002)。
|
||||
终端注册成功以后响应同时[下发配置信令](#下发配置信令2004),并且广播[终端上线信令](#终端上线信令2002)。
|
||||
|
||||
### 终端关闭信令(2001)
|
||||
|
||||
@@ -119,11 +119,11 @@
|
||||
|
||||
#### 消息流程:终端-)服务端
|
||||
|
||||
广播[终端下线信令](#终端下线信令2003),同时释放所有相关资源(信令通道、媒体通道等等)
|
||||
终端关闭以后广播[终端下线信令](#终端下线信令2003),同时释放所有相关资源(信令通道、媒体通道等等)
|
||||
|
||||
### 终端上线信令(2002)
|
||||
|
||||
服务端->终端:参考[终端注册信令](#终端注册信令)
|
||||
#### 消息主体
|
||||
|
||||
```
|
||||
{
|
||||
@@ -131,9 +131,13 @@
|
||||
}
|
||||
```
|
||||
|
||||
#### 消息流程:服务端->终端
|
||||
|
||||
参考[终端注册信令](#终端注册信令2000)
|
||||
|
||||
### 终端下线信令(2003)
|
||||
|
||||
服务端->终端:参考[终端关闭信令](#关闭信令)
|
||||
#### 消息主体
|
||||
|
||||
```
|
||||
{
|
||||
@@ -141,9 +145,13 @@
|
||||
}
|
||||
```
|
||||
|
||||
#### 消息流程:服务端->终端
|
||||
|
||||
参考[终端关闭信令](#终端关闭信令2001)
|
||||
|
||||
### 下发配置信令(2004)
|
||||
|
||||
服务端->终端:参考[注册信令](#注册信令)
|
||||
#### 消息主体
|
||||
|
||||
```
|
||||
{
|
||||
@@ -153,56 +161,112 @@
|
||||
}
|
||||
```
|
||||
|
||||
#### 消息流程:服务端->终端
|
||||
|
||||
参考[终端注册信令](#终端注册信令2000)
|
||||
|
||||
### 心跳信令(2005)
|
||||
|
||||
心跳:响应
|
||||
#### 消息主体
|
||||
|
||||
```
|
||||
# 请求
|
||||
{
|
||||
"signal": "信号强度",
|
||||
"battery": "电池电量"
|
||||
}
|
||||
# 响应
|
||||
{}
|
||||
```
|
||||
|
||||
#### 消息流程:终端->服务端->终端
|
||||
|
||||
### 单播信令(2006)
|
||||
|
||||
#### 消息主体
|
||||
|
||||
```
|
||||
# 请求
|
||||
{
|
||||
"to": "接收终端标识",
|
||||
// 主体信息
|
||||
}
|
||||
# 转发
|
||||
{
|
||||
// 主体信息
|
||||
}
|
||||
```
|
||||
|
||||
#### 消息流程:终端->服务端->终端
|
||||
|
||||
终端转发信令到指定的终端
|
||||
|
||||
### 广播信令(2007)
|
||||
|
||||
#### 消息主体
|
||||
|
||||
```
|
||||
# 请求
|
||||
{
|
||||
// 主体信息
|
||||
}
|
||||
# 广播
|
||||
{
|
||||
// 主体信息
|
||||
}
|
||||
```
|
||||
|
||||
#### 消息流程:终端->服务端-)终端
|
||||
|
||||
终端广播信令到所有的终端
|
||||
|
||||
### 终端状态信令(2998)
|
||||
|
||||
#### 消息主体
|
||||
|
||||
```
|
||||
# 请求
|
||||
{
|
||||
"sn": "终端标识"
|
||||
}
|
||||
# 响应
|
||||
{
|
||||
"sn": "终端标识",
|
||||
"ip": "IP地址",
|
||||
"mac": "MAC地址",
|
||||
"signal": "信号强度",
|
||||
"battery": "电池电量"
|
||||
}
|
||||
```
|
||||
|
||||
### 单播信令(2006)
|
||||
#### 消息流程:终端->服务端->终端
|
||||
|
||||
发送到指定的终端:删除`to`字段
|
||||
|
||||
```
|
||||
{
|
||||
"to": "接收终端标识",
|
||||
// 主体信息
|
||||
}
|
||||
```
|
||||
|
||||
### 广播信令(2007)
|
||||
|
||||
发送到所有的终端:排除自己
|
||||
|
||||
```
|
||||
{
|
||||
// 主体信息
|
||||
}
|
||||
```
|
||||
|
||||
### 终端状态信令(2998)
|
||||
|
||||
返回指定终端状态(如果没有指定终端标识默认查询自己)
|
||||
|
||||
```
|
||||
{
|
||||
"sn": "终端标识"
|
||||
}
|
||||
```
|
||||
响应指定终端状态(如果没有指定终端标识默认查询自己)
|
||||
|
||||
### 终端列表信令(2999)
|
||||
|
||||
返回所有终端状态列表
|
||||
#### 消息主体
|
||||
|
||||
```
|
||||
# 请求
|
||||
{}
|
||||
# 响应
|
||||
[
|
||||
{
|
||||
"sn": "终端标识",
|
||||
"ip": "IP地址",
|
||||
"mac": "MAC地址",
|
||||
"signal": "信号强度",
|
||||
"battery": "电池电量"
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
#### 消息流程:终端->服务端->终端
|
||||
|
||||
响应所有终端状态列表
|
||||
|
||||
## 直播信令(3000~3999)
|
||||
|
||||
### 开启直播信令(3000)
|
||||
@@ -225,7 +289,7 @@
|
||||
{}
|
||||
----
|
||||
{
|
||||
"id": "会议标识"
|
||||
"id": "会议标识",
|
||||
}
|
||||
```
|
||||
|
||||
@@ -237,7 +301,23 @@
|
||||
|
||||
### 进入会议信令(4002)
|
||||
|
||||
广播
|
||||
#### 消息主体
|
||||
|
||||
```
|
||||
# 请求
|
||||
{
|
||||
"id": "会议标识"
|
||||
}
|
||||
# 广播
|
||||
{
|
||||
"id": "会议标识",
|
||||
"sn": "终端标识"
|
||||
}
|
||||
```
|
||||
|
||||
#### 消息流程:终端->服务端-)终端
|
||||
|
||||
终端进入会议,广播通知其他终端。
|
||||
|
||||
### 离开会议信令(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
|
||||
*/
|
||||
@Component
|
||||
|
||||
@@ -26,7 +26,7 @@ public class ClientHeartbeatProtocol extends ProtocolMapAdapter {
|
||||
|
||||
@Override
|
||||
public void execute(String sn, Map<?, ?> body, Message message, ClientSession session) {
|
||||
// 回应心跳
|
||||
// 响应心跳
|
||||
session.push(message.cloneWidthoutBody());
|
||||
// 设置状态
|
||||
final ClientSessionStatus status = session.status();
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
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|
|
||||
|媒体通道|RTP/RTCP/SRTP/SRTCP|
|
||||
|
||||
|
||||
Reference in New Issue
Block a user