[+] 重启终端、创建会议、进入会议
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user