This commit is contained in:
acgist
2022-11-19 23:41:55 +08:00
parent 7b5e41ff5a
commit f5bfe2cef9
9 changed files with 249 additions and 107 deletions

View File

@@ -26,5 +26,21 @@ public class Meeting {
*/ */
@Schema(title = "终端会话标识列表", description = "终端会话标识列表") @Schema(title = "终端会话标识列表", description = "终端会话标识列表")
private List<String> sns; private List<String> sns;
/**
* 创建终端标识
*/
@Schema(title = "创建终端标识", description = "创建终端标识")
private String creator;
/**
* 新增终端会话标识
*
* @param sn 终端会话标识
*/
public void addSn(String sn) {
synchronized (this.sns) {
this.sns.add(sn);
}
}
} }

View File

@@ -3,8 +3,11 @@ package com.acgist.taoyao.meeting;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.acgist.taoyao.boot.service.IdService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
@@ -16,6 +19,9 @@ import lombok.extern.slf4j.Slf4j;
@Service @Service
public class MeetingManager { public class MeetingManager {
@Autowired
private IdService idService;
/** /**
* 会议列表 * 会议列表
*/ */
@@ -50,4 +56,22 @@ public class MeetingManager {
return meeting == null ? List.of() : meeting.getSns(); return meeting == null ? List.of() : meeting.getSns();
} }
/**
* 创建会议
*
* @param sn 创建会议终端标识
*
* @return 会议信息
*/
public Meeting create(String sn) {
final Meeting meeting = new Meeting();
meeting.setId(this.idService.buildIdToString());
meeting.setSns(new CopyOnWriteArrayList<>());
meeting.setCreator(sn);
meeting.addSn(sn);
this.meetings.add(meeting);
log.info("创建会议:{}", meeting.getId());
return meeting;
}
} }

View File

@@ -6,32 +6,34 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.acgist.taoyao.boot.model.Message; import com.acgist.taoyao.boot.model.Message;
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;
import lombok.extern.slf4j.Slf4j;
/** /**
* 创建会议监听 * 创建会议监听
* *
* @author acgist * @author acgist
*/ */
@Slf4j
@Component @Component
public class MeetingCreateListener extends ApplicationListenerAdapter<MeetingCreateEvent> { public class MeetingCreateListener extends ApplicationListenerAdapter<MeetingCreateEvent> {
@Autowired @Autowired
private MeetingManager meetingManager; private MeetingManager meetingManager;
@Autowired
private ClientSessionManager clientSessionManager;
@Override @Override
public void onApplicationEvent(MeetingCreateEvent event) { public void onApplicationEvent(MeetingCreateEvent event) {
// this.meetingManager.create();
final ClientSession session = event.getSession(); final ClientSession session = event.getSession();
final Meeting meeting = this.meetingManager.create(session.sn());
final Message message = event.getMessage(); final Message message = event.getMessage();
message.setBody(Map.of("id", "1234")); message.setBody(Map.of("id", meeting.getId()));
session.push(message); // 广播不改ID触发创建终端事件回调
this.clientSessionManager.broadcast(message);
} }
} }

View File

@@ -34,6 +34,6 @@ input::-webkit-calendar-picker-indicator{color:#1155AA;background:none;}
.taoyao .meeting > .video video{width:100%;height:100%;} .taoyao .meeting > .video video{width:100%;height:100%;}
.taoyao .meeting .handler{position:absolute;bottom:0rem;text-align:center;width:100%;background:rgba(0,0,0,0.2);padding:0.2rem 0;} .taoyao .meeting .handler{position:absolute;bottom:0rem;text-align:center;width:100%;background:rgba(0,0,0,0.2);padding:0.2rem 0;}
.taoyao .meeting .handler a{color:#fff;} .taoyao .meeting .handler a{color:#fff;}
.taoyao .meeting .handler a.record:hover{color:#060!important;} .taoyao .meeting .handler a:hover{color:#060!important;}
.taoyao .meeting .handler a.expel:hover{color:#C00!important;}
.taoyao .meeting .handler a.record.active{color:#C00;} .taoyao .meeting .handler a.record.active{color:#C00;}
.taoyao .meeting .handler a.kick:hover{color:#C00;}

View File

@@ -1,9 +0,0 @@
/** 桃夭WebRTC终端应用功能 */
// 样式操作
function classSwitch(e, className) {
if(e.className.indexOf(className) >= 0) {
e.className = e.className.replace(className, '').replace(/(^\s)|(\s$)/g, "");
} else {
e.className = e.className + ' ' + className;
}
}

View File

@@ -277,6 +277,70 @@ const signalChannel = {
} }
} }
}; };
/** 终端 */
function TaoyaoClient(
sn
) {
/** 终端标识 */
this.sn = sn;
/** 视频对象 */
this.video = null;
/** 媒体状态 */
this.audioStatus = true;
this.videoStatus = true;
this.recordStatus = false;
/** 媒体信息 */
this.audioStreamId = null;
this.videoStreamId = null;
/** 播放视频 */
this.play = async function() {
await this.video.play();
return this;
};
/** 重新加载 */
this.load = function() {
this.video.load();
return this;
}
/** 暂停视频 */
this.pause = function() {
this.video.pause();
return this;
};
/** 关闭视频 */
this.close = function() {
this.video.close();
return this;
};
/** 设置视频对象 */
this.buildVideo = async function(videoId, stream) {
if(!this.video) {
this.video = document.getElementById(videoId);
}
await this.buildStream(stream);
return this;
};
/** 设置媒体流 */
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;
};
/** 设置音频流 */
this.buildAudioStream = function() {
};
/** 设置视频流 */
this.buildVideoStream = function() {
};
}
/** 桃夭 */ /** 桃夭 */
function Taoyao( function Taoyao(
webSocket, webSocket,
@@ -288,26 +352,20 @@ function Taoyao(
this.webSocket = webSocket; this.webSocket = webSocket;
/** IceServer地址 */ /** IceServer地址 */
this.iceServer = iceServer; this.iceServer = iceServer;
/** 媒体状态 */
this.audioStatus = true;
this.videoStatus = true;
/** 设备状态 */ /** 设备状态 */
this.audioEnabled = true; this.audioEnabled = true;
this.videoEnabled = true; this.videoEnabled = true;
/** 媒体信息 */
this.audioStreamId = null;
this.videoStreamId = null;
/** 媒体配置 */ /** 媒体配置 */
this.audioConfig = audioConfig || defaultAudioConfig; this.audioConfig = audioConfig || defaultAudioConfig;
this.videoConfig = videoConfig || defaultVideoConfig; this.videoConfig = videoConfig || defaultVideoConfig;
/** 本地视频 */
this.localVideo = null;
/** 终端媒体 */
this.clientMedia = {};
/** 信令通道 */
this.signalChannel = null;
/** 发送信令 */ /** 发送信令 */
this.push = null; this.push = null;
/** 本地终端 */
this.localClient = null;
/** 远程终端 */
this.remoteClient = [];
/** 信令通道 */
this.signalChannel = null;
/** 检查设备 */ /** 检查设备 */
this.checkDevice = function() { this.checkDevice = function() {
let self = this; let self = this;
@@ -394,15 +452,10 @@ function Taoyao(
} }
}); });
}; };
/** 设置本地媒体 */ /** 设置本地终端 */
this.localMedia = async function(localVideoId, stream) { this.buildLocalClient = async function(localVideoId, stream) {
this.localVideo = document.getElementById(localVideoId); this.localClient = new TaoyaoClient(signalConfig.sn);
if ('srcObject' in this.localVideo) { await this.localClient.buildVideo(localVideoId, stream);
this.localVideo.srcObject = stream;
} else {
this.localVideo.src = URL.createObjectURL(stream);;
}
await this.localVideo.play();
}; };
/** 关闭:关闭媒体 */ /** 关闭:关闭媒体 */
this.close = function() { this.close = function() {

View File

@@ -5,91 +5,128 @@
<title>会议</title> <title>会议</title>
<link rel="stylesheet" type="text/css" href="./css/font.min.css" /> <link rel="stylesheet" type="text/css" href="./css/font.min.css" />
<link rel="stylesheet" type="text/css" href="./css/style.css" /> <link rel="stylesheet" type="text/css" href="./css/style.css" />
<script type="text/javascript" src="./javascript/app.js"></script> <script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
<script type="text/javascript" src="./javascript/taoyao.js"></script> <script type="text/javascript" src="./javascript/taoyao.js"></script>
</head> </head>
<body> <body>
<div class="taoyao"> <div class="taoyao" id="app">
<div class="handler"> <div class="handler">
<a class="create icon-svg" title="创建房间" onclick="create(this)"></a> <a class="create icon-svg" title="创建房间" @click="create"></a>
<a class="invite icon-address-book" title="邀请房间" onclick="invite"></a> <a class="invite icon-address-book" title="邀请房间" @click="invite"></a>
<a class="enter icon-enter" title="进入房间" onclick="enter"></a> <a class="enter icon-enter" title="进入房间" @click="enter"></a>
<a class="leave icon-exit" title="离开房间" onclick="leave"></a> <a class="leave icon-exit" title="离开房间" @click="leave"></a>
<a class="close icon-switch" title="关闭房间" onclick="close"></a> <a class="close icon-switch" title="关闭房间" @click="close"></a>
</div> </div>
<div class="list" id="list"> <div class="list">
<div class="meeting me"> <div class="meeting me">
<div class="video"> <div class="video">
<video id="local"></video> <video id="local"></video>
</div> </div>
<div class="handler"> <div class="handler">
<a class="audio icon-volume-medium" title="音频状态" onclick="audio"></a> <a class="audio icon-volume-medium" title="音频状态" @click="audioSelf"></a>
<a class="video icon-play2" title="视频状态" onclick="video"></a> <a class="video icon-play2" title="视频状态" @click="videoSelf"></a>
<a class="record icon-radio-checked" title="录制视频" onclick="record(this)"></a> <a class="record icon-radio-checked" title="录制视频" @click="recordSelf"></a>
<a class="kick icon-cancel-circle" title="踢出房间" onclick="kick"></a> </div>
</div>
<div class="meeting" v-for="client in this.clients" :key="client.sn">
<div class="video">
<video v-bind:id="client.sn"></video>
</div>
<div class="handler">
<a class="audio" title="音频状态" v-bind:class="client.audio?'icon-volume-medium':'icon-volume-mute2'" @click="audio(client.sn)"></a>
<a class="video" title="视频状态" v-bind:class="client.video?'icon-play2':'icon-stop'" @click="video(client.sn)"></a>
<a class="record icon-radio-checked" title="录制视频" v-bind:class="client.record?'active':''" @click="record(client.sn)"></a>
<a class="expel icon-cancel-circle" title="踢出房间" @click="expel(client.sn)"></a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
const list = document.getElementById('list'); const vue = new Vue({
const template = ` el: "#app",
<div class="video"> data: {
<video></video> clients: [
</div> {sn:"1", audio: true, video: true, record: false},
<div class="handler"> {sn:"2", audio: true, video: true, record: false},
<a class="audio icon-volume-medium" title="音频状态"></a> {sn:"3", audio: true, video: true, record: false}
<a class="video icon-play2" title="视频状态"></a> ],
<a class="record icon-radio-checked" title="录制视频"></a> taoyao: null,
<a class="kick icon-cancel-circle" title="踢出房间"></a> meetingId: null
</div> },
`; mounted() {
for(let i = 0; i < 10; i++) { if(signalConfig.sn) {
const child = document.createElement('div'); // TODO修改sn
child.className = 'meeting';
child.innerHTML = template;
list.appendChild(child);
} }
const taoyao = new Taoyao("wss://localhost:8888/websocket.signal"); this.taoyao = new Taoyao("wss://localhost:8888/websocket.signal");
// 检查设备 // 检查设备
taoyao this.taoyao
.checkDevice() .checkDevice()
.buildChannel(callback) .buildChannel(this.callback)
//.buildLocalMedia() //.buildLocalMedia()
//.then(stream => taoyao.localMedia('local', stream)) //.then(stream => this.taoyao.buildLocalClient('local', stream))
//.catch((e) => alert('获取终端媒体失败' + e)); //.catch((e) => console.error('打开终端媒体失败', e));
// 信令回调 },
function callback(data) { beforeDestroy() {
},
methods: {
// 信令回调返回true表示已经处理
callback: function(data) {
switch(data.header.pid) { switch(data.header.pid) {
case signalProtocol.client.heartbeat: case signalProtocol.client.heartbeat:
// 心跳 // 心跳
return true; return true;
} }
return false; return false;
} },
// 创建房间 // 创建会议
function create() { create: function(event) {
taoyao.createMeeting(data => { let self = this;
this.taoyao.createMeeting(data => {
self.meetingId = data.body.id;
}); });
} },
// 进入房间 // 返回终端
function enter() { client: function(sn) {
} return this.clients.filter(v => v.sn === sn)[0];
// 声音控制 },
function audio() { // 会议邀请
} invite: function(sn) {
// 视频控制 },
function video() { // 进入会议
} enter: function(sn) {
},
// 离开会议
leave: function(sn) {
},
// 关闭会议
close: function(sn) {
},
// 控制音频
audio: function(sn) {
this.client(sn).audio = !this.client(sn).audio;
},
// 控制视频
video: function(sn) {
this.client(sn).video = !this.client(sn).video;
},
// 录制视频 // 录制视频
function record(e) { record: function(sn) {
taoyao.push(signalProtocol.buildProtocol(signalConfig.sn, signalProtocol.client.heartbeat), () => { this.client(sn).record = !this.client(sn).record;
classSwitch(e, 'active'); },
});
}
// 踢出会议 // 踢出会议
function kick() { expel: function(sn) {
},
// 控制音频
audioSelf: function(sn) {
},
// 控制视频
videoSelf: function(sn) {
},
// 录制视频
recordSelf: function(sn) {
} }
}
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -184,6 +184,12 @@
### 创建会议信令4000 ### 创建会议信令4000
终端->服务端
```
{}
```
### 关闭会议信令4001 ### 关闭会议信令4001
释放资源、广播广播 释放资源、广播广播
@@ -243,3 +249,14 @@ MCU/SFU模式有效
### 开启录像5006 ### 开启录像5006
### 停止录像5007 ### 停止录像5007
### 配置媒体5008
配置订阅媒体:码率、帧率、分辨率等等
### IceCandidate
### Offer
### Answer

View File

@@ -3,6 +3,8 @@ package com.acgist.taoyao.signal.media;
/** /**
* 终端媒体操作 * 终端媒体操作
* *
* TODO注意暂停心跳防止端口关闭
*
* @author acgist * @author acgist
*/ */
public interface ClientMediaHandler { public interface ClientMediaHandler {