[+] 重启终端、创建会议、进入会议

This commit is contained in:
acgist
2022-11-25 22:18:01 +08:00
parent 46130cc15b
commit 0f339f4aea
64 changed files with 1159 additions and 359 deletions

View File

@@ -1,6 +1,7 @@
/** 桃夭WebRTC终端核心功能 */
/** 兼容 */
const RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
const RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
/** 默认音频配置 */
const defaultAudioConfig = {
// 音量0~1
@@ -42,7 +43,7 @@ const defaultVideoConfig = {
/** 默认RTCPeerConnection配置 */
const defaultRPCConfig = {
// ICE代理的服务器
// iceServers: null,
iceServers: null,
// 传输通道绑定策略balanced|max-compat|max-bundle
bundlePolicy: 'balanced',
// RTCP多路复用策略require|negotiate
@@ -78,9 +79,15 @@ const signalProtocol = {
config: 2004,
/** 心跳 */
heartbeat: 2005,
/** 重启终端 */
reboot: 2997,
},
/** 会议信令 */
meeting: {
/** 创建会议信令 */
create: 4000,
/** 进入会议信令 */
enter: 4002,
},
/** 平台信令 */
platform: {
@@ -208,11 +215,25 @@ const signalChannel = {
}
reject(e);
};
/**
* 回调策略:
* 1. 如果注册请求回调同时执行结果返回true不再执行后面所有回调。
* 2. 如果注册全局回调同时执行结果返回true不再执行后面所有回调。
* 3. 如果前面所有回调没有返回true执行默认回调。
*/
self.channel.onmessage = function(e) {
console.debug('信令通道消息', e.data);
let done = false;
let data = JSON.parse(e.data);
// 注册回调
// 请求回调
if(self.callbackMapping.has(data.header.id)) {
try {
done = self.callbackMapping.get(data.header.id)(data);
} finally {
self.callbackMapping.delete(data.header.id);
}
}
// 全局回调
if(self.callback) {
done = self.callback(data);
}
@@ -220,11 +241,6 @@ const signalChannel = {
if(!done) {
self.defaultCallback(data);
}
// 请求回调
if(self.callbackMapping.has(data.header.id)) {
self.callbackMapping.get(data.header.id)();
self.callbackMapping.delete(data.header.id);
}
};
});
},
@@ -277,22 +293,42 @@ const signalChannel = {
console.debug('没有适配信令消息默认处理', data);
switch(data.header.pid) {
case signalProtocol.client.register:
console.debug('终端注册成功');
break;
case signalProtocol.client.config:
if(this.taoyao) {
this.taoyao
.configMedia(data.body.media)
.configWebrtc(data.body.webrtc);
}
this.defaultClientConfig(data);
break;
case signalProtocol.client.heartbeat:
console.debug('心跳');
break;
case signalProtocol.client.reboot:
this.defaultClientReboot(data);
break;
case signalProtocol.meeting.create:
break;
case signalProtocol.meeting.enter:
break;
case signalProtocol.platform.error:
console.error('信令发生错误', data);
break;
}
},
/** 终端默认回调 */
defaultClientConfig: function(data) {
this.taoyao
.configMedia(data.body.media)
.configWebrtc(data.body.webrtc);
},
defaultClientReboot: function(data) {
console.info('重启终端');
location.reload();
},
/** 默认媒体回调 */
defaultMediaSubscribe: function(data) {
},
/** 会议默认回调 */
defaultMeetingEnter: function(data) {
this.taoyao
.mediaSubscribe(data.body.sn);
}
};
/** 终端 */
@@ -304,16 +340,13 @@ function TaoyaoClient(
/** 视频对象 */
this.video = null;
/** 媒体信息 */
this.stream = null;
this.audioTrack = null;
this.videoTrack = null;
/** 媒体状态 */
this.audioStatus = true;
this.videoStatus = true;
/** 录制状态 */
this.audioStatus = false;
this.videoStatus = false;
this.recordStatus = false;
/** 媒体信息 */
this.audioStreamId = null;
this.videoStreamId = null;
/** 播放视频 */
this.play = async function() {
await this.video.play();
@@ -345,35 +378,48 @@ function TaoyaoClient(
/** 设置媒体流 */
this.buildStream = async function(stream) {
if(stream) {
this.stream = stream;
this.video.srcObject = stream;
let audioTrack = stream.getAudioTracks();
let videoTrack = stream.getVideoTracks();
if(audioTrack && audioTrack.length) {
this.audioTrack = audioTrack;
this.audioStatus = true;
}
if(videoTrack && videoTrack.length) {
this.videoTrack = videoTrack;
this.videoStatus = true;
}
console.debug('设置媒体流', this.stream, this.audioTrack, this.videoTrack);
await this.play();
}
return this;
};
/** 设置音频流 */
this.buildAudioStream = function() {
this.buildAudioTrack = function() {
// 关闭旧的
// 创建新的
};
/** 设置视频流 */
this.buildVideoStream = function() {
this.buildVideoTrack = function() {
// 关闭旧的
// 创建新的
};
}
/** 桃夭 */
function Taoyao(
webSocket,
iceServer,
audioConfig,
videoConfig
webSocket
) {
/** WebRTC配置 */
this.webrtc = null;
/** WebSocket地址 */
this.webSocket = webSocket;
/** IceServer地址 */
this.iceServer = iceServer;
/** 设备状态 */
this.audioEnabled = true;
this.videoEnabled = true;
/** 媒体配置 */
this.audioConfig = audioConfig || defaultAudioConfig;
this.videoConfig = videoConfig || defaultVideoConfig;
this.audioConfig = defaultAudioConfig;
this.videoConfig = defaultVideoConfig;
/** 发送信令 */
this.push = null;
/** 本地终端 */
@@ -438,26 +484,27 @@ function Taoyao(
};
/** WebRTC配置 */
this.configWebrtc = function(config = {}) {
this.webSocket = config.signalAddress;
this.iceServer = config.stun;
console.debug('WebRTC配置', this.webSocket, this.iceServer);
this.webrtc = config;
this.webSocket = this.webrtc.signalAddress;
defaultRPCConfig.iceServers = this.webrtc.stun.map(v => ({'urls': v}));
console.debug('WebRTC配置', this.webrtc, defaultRPCConfig);
return this;
};
/** 打开信令通道 */
this.buildChannel = function(callback) {
signalChannel.taoyao = this;
this.signalChannel = signalChannel;
this.signalChannel.connect(this.webSocket, callback);
// 不能直接this.push = this.signalChannel.push这样导致this对象错误
this.push = function(data, callback) {
this.signalChannel.push(data, callback)
};
return this;
return this.signalChannel.connect(this.webSocket, callback);
};
/** 打开本地媒体 */
this.buildLocalMedia = function() {
console.debug('打开终端媒体', this.audioConfig, this.videoConfig);
let self = this;
this.checkDevice();
return new Promise((resolve, reject) => {
navigator.mediaDevices.getUserMedia({
audio: self.audioConfig,
@@ -465,13 +512,21 @@ function Taoyao(
})
.then(resolve)
.catch(reject);
// 兼容旧版
// navigator.getUserMedia({
// audio: self.audioConfig,
// video: self.videoConfig
// }, resolve, reject);
});
};
/** 设置本地终端 */
this.buildLocalClient = async function(localVideoId, stream) {
this.localClient = new TaoyaoClient(signalConfig.sn);
await this.localClient.buildVideo(localVideoId, stream);
};
/** 远程终端过滤 */
this.remoteClientFilter = function(sn) {
let array = this.remoteClient.filter(v => v.sn === sn);
if(array.length <= 0) {
return null;
}
return this.remoteClient.filter(v => v.sn === sn)[0];
}
/** 关闭:关闭媒体 */
this.close = function() {
// TODO释放资源
@@ -479,9 +534,96 @@ function Taoyao(
/** 关机:关闭媒体、关闭信令 */
this.shutdown = function() {
this.close();
};
/** 打开媒体通道 */
this.buildMediaChannel = async function(localVideoId, stream) {
// 本地视频
this.localClient = new TaoyaoClient(signalConfig.sn);
await this.localClient.buildVideo(localVideoId, stream);
// 本地通道
this.localMediaChannel = new RTCPeerConnection(defaultRPCConfig);
if(this.localClient.audioTrack) {
this.localClient.audioTrack.forEach(v => this.localMediaChannel.addTrack(v, this.localClient.stream));
}
if(this.localClient.videoTrack) {
this.localClient.videoTrack.forEach(v => this.localMediaChannel.addTrack(v, this.localClient.stream));
}
this.localMediaChannel.ontrack = this.localMediaChannelTrack;
this.localMediaChannel.ondatachannel = this.localMediaChannelDataChannel;
this.localMediaChannel.onicecandidate = this.localMediaChannelIceCandidate;
// 远程通道
this.remoteMediaChannel = new RTCPeerConnection(defaultRPCConfig);
this.remoteMediaChannel.ontrack = this.remoteMediaChannelTrack;
this.remoteMediaChannel.ondatachannel = this.remoteMediaChannelDataChannel;
this.remoteMediaChannel.onicecandidate = this.remoteMediaChannelIceCandidate;
return this;
};
/** 本地 */
this.localMediaChannelTrack = function() {
};
this.localMediaChannelDataChannel = function(channel) {
channel.onopen = function() {
console.debug('DataChannel Open');
}
channel.onmessage = function(data) {
console.debug('DataChannel Message', data);
}
channel.onclose = function() {
console.debug('DataChannel Close');
}
channel.onerror = function(e) {
console.debug('DataChannel Error', e);
}
};
this.localMediaChannelIceCandidate = function() {
};
/** 远程 */
this.localMediaChannelTrack = function() {
};
this.localMediaChannelDataChannel = function(channel) {
channel.onopen = function() {
console.debug('DataChannel Open');
}
channel.onmessage = function(data) {
console.debug('DataChannel Message', data);
}
channel.onclose = function() {
console.debug('DataChannel Close');
}
channel.onerror = function(e) {
console.debug('DataChannel Error', e);
}
};
this.localMediaChannelIceCandidate = function() {
};
/** 媒体信令 */
this.mediaSubscribe = function(sn, callback) {
let self = this;
if(self.webrtc.model === 'MESH') {
self.localMediaChannel.createOffer().then(description => {
console.debug('Local Create Offer', description);
self.localMediaChannel.setLocalDescription(description);
});
}
};
/** 会议信令 */
this.meetingCreate = function(callback) {
let self = this;
self.push(signalProtocol.buildProtocol(
signalConfig.sn,
signalProtocol.meeting.create,
), callback);
}
/** 媒体 */
/** 视频 */
this.meetingEnter = function(id, callback) {
let self = this;
self.push(signalProtocol.buildProtocol(
signalConfig.sn,
signalProtocol.meeting.enter,
{
id: id
}
), callback);
};
};
/*
var peer;

View File

@@ -11,11 +11,11 @@
<body>
<div class="taoyao" id="app">
<div class="handler">
<a class="create icon-make-group" title="创建房间" @click="create"></a>
<a class="invite icon-address-book" title="邀请房间" @click="invite"></a>
<a class="enter icon-enter" title="进入房间" @click="enter"></a>
<a class="leave icon-exit" title="离开房间" @click="leave"></a>
<a class="close icon-switch" title="关闭房间" @click="close"></a>
<a class="create icon-make-group" title="创建会议" @click="create"></a>
<a class="invite icon-address-book" title="邀请会议" @click="invite"></a>
<a class="enter icon-enter" title="进入会议" @click="enter"></a>
<a class="leave icon-exit" title="离开会议" @click="leave"></a>
<a class="close icon-switch" title="关闭会议" @click="close"></a>
</div>
<div class="list">
<div class="meeting me">
@@ -36,7 +36,7 @@
<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>
<a class="expel icon-cancel-circle" title="踢出会议" @click="expel(client.sn)"></a>
</div>
</div>
</div>
@@ -57,14 +57,18 @@
if(signalConfig.sn) {
// TODO修改sn
}
let self = this;
this.taoyao = new Taoyao("wss://localhost:8888/websocket.signal");
// 检查设备
// 打开信令通道
this.taoyao
.checkDevice()
.buildChannel(this.callback)
.buildLocalMedia()
.then(stream => this.taoyao.buildLocalClient('local', stream))
.catch((e) => console.error('打开终端媒体失败', e));
.buildChannel(self.callback)
.then(e => console.debug('连接成功'));
// 打开媒体通道
this.taoyao.buildLocalMedia()
.then(stream => {
self.taoyao.buildMediaChannel('local', stream);
})
.catch(e => console.error('打开终端媒体失败', e));
},
beforeDestroy() {
},
@@ -81,19 +85,21 @@
// 创建会议
create: function(event) {
let self = this;
this.taoyao.createMeeting(data => {
self.meetingId = data.body.id;
this.taoyao.meetingCreate(data => {
console.log(data)
self.taoyao.meetingEnter(data.body.id);
return true;
});
},
// 返回终端
client: function(sn) {
return this.clients.filter(v => v.sn === sn)[0];
},
// 会议邀请
invite: function(sn) {
},
// 进入会议
enter: function(sn) {
let id = prompt('房间标识');
if(id) {
this.taoyao.meetingEnter(id);
}
},
// 离开会议
leave: function(sn) {

View File

@@ -3,12 +3,16 @@ package com.acgist.taoyao.signal;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.net.http.WebSocket;
import java.util.concurrent.CountDownLatch;
import org.junit.jupiter.api.Test;
import com.acgist.taoyao.main.TaoyaoApplication;
import com.acgist.taoyao.test.annotation.TaoyaoTest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@TaoyaoTest(classes = TaoyaoApplication.class)
class SignalTest {
@@ -18,9 +22,37 @@ class SignalTest {
final WebSocket clientB = WebSocketClient.build("wss://localhost:8888/websocket.signal", "clientB");
clientA.sendText("""
{"header":{"pid":1000,"v":"1.0.0","id":"1","sn":"clientA"},"body":{}}
""", true).join();
""", true).join();
assertNotNull(clientA);
assertNotNull(clientB);
}
@Test
void testThread() throws InterruptedException {
final int total = 1000;
final CountDownLatch count = new CountDownLatch(total);
final WebSocket clientA = WebSocketClient.build("wss://localhost:8888/websocket.signal", "clientA", count);
final long aTime = System.currentTimeMillis();
for (int index = 0; index < total; index++) {
clientA.sendText("""
{"header":{"pid":2999,"v":"1.0.0","id":"1","sn":"clientA"},"body":{}}
""", true).join();
}
// final ExecutorService executor = Executors.newFixedThreadPool(10);
// for (int index = 0; index < total; index++) {
// executor.execute(() -> {
// synchronized (clientA) {
// clientA.sendText("""
// {"header":{"pid":2999,"v":"1.0.0","id":"1","sn":"clientA"},"body":{}}
// """, true).join();
// }
// });
// }
count.await();
final long zTime = System.currentTimeMillis();
log.info("执行时间:{}", zTime - aTime);
Thread.sleep(1000);
assertNotNull(clientA);
}
}

View File

@@ -10,6 +10,7 @@ import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
@@ -21,6 +22,10 @@ import lombok.extern.slf4j.Slf4j;
public class WebSocketClient {
public static final WebSocket build(String uri, String sn) throws InterruptedException {
return build(uri, sn, null);
}
public static final WebSocket build(String uri, String sn, CountDownLatch count) throws InterruptedException {
final Object lock = new Object();
try {
return HttpClient
@@ -32,7 +37,7 @@ public class WebSocketClient {
@Override
public void onOpen(WebSocket webSocket) {
webSocket.sendText(String.format("""
{"header":{"pid":2000,"v":"1.0.0","id":"1","sn":"%s"},"body":{"username":"taoyao","password":"taoyao"}}
{"header":{"pid":2000,"v":"1.0.0","id":"1","sn":"%s"},"body":{"username":"taoyao","password":"taoyao","ip":"127.0.0.1","mac":"00:00:00:00:00:00"}}
""", sn), true);
Listener.super.onOpen(webSocket);
}
@@ -41,7 +46,12 @@ public class WebSocketClient {
synchronized (lock) {
lock.notifyAll();
}
log.info("收到WebSocket消息{}", data);
if(count == null) {
log.debug("收到WebSocket消息{}", data);
} else {
count.countDown();
log.debug("收到WebSocket消息{}-{}", count.getCount(), data);
}
return Listener.super.onText(webSocket, data, last);
}
})