This commit is contained in:
acgist
2023-02-11 22:11:51 +08:00
parent 50f80bee2d
commit 5f85dfccca
112 changed files with 1770 additions and 1213 deletions

View File

@@ -1,29 +1,3 @@
# client
# Web终端
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
注意部分`API`需要`localhost`或者`https`

View File

@@ -11,7 +11,9 @@
},
"dependencies": {
"vue": "^3.2.45",
"moment": "^2.29.4"
"moment": "^2.29.4",
"element-plus": "^2.2.29",
"mediasoup-client": "^3.6.77"
},
"devDependencies": {
"vite": "^4.0.0",

View File

@@ -1,26 +1,36 @@
<template></template>
<!-- 桃夭 -->
<template>
<SettingRoom :roomVisible="roomVisible" :taoyao="taoyao"></SettingRoom>
<SettingSignal :signalVisible="signalVisible" @connectSignal="connectSignal"></SettingSignal>
</template>
<script>
import { Logger } from "./components/Logger.js";
import { Taoyao } from "./components/Taoyao.js";
import SettingRoom from "./components/SettingRoom.vue";
import SettingSignal from "./components/SettingSignal.vue";
export default {
name: "taoyao",
name: "Taoyao",
data() {
return {
logger: null,
taoyao: null,
logger: {},
taoyao: {},
roomVisible: false,
signalVisible: true,
};
},
mounted() {
// 填写、密码、帐号
this.logger = new Logger();
this.taoyao = new Taoyao();
this.logger.info("桃夭终端启动");
this.taoyao.buildChannel(this.callback);
this.logger.info("桃夭终端开始启动");
},
beforeDestroy() {},
methods: {
connectSignal: function() {
let self = this;
self.signalVisible = false;
self.taoyao.buildChannel(self.callback);
},
/**
* 信令回调
*
@@ -30,10 +40,22 @@ export default {
*/
callback: function (data) {
let self = this;
switch (data.header.snail) {
switch (data.header.signal) {
case "client::config":
self.roomVisible = true;
break;
case "client::register":
if(data.code === '3401') {
self.signalVisible = true;
}
return true;
}
return false;
},
},
components: {
SettingRoom,
SettingSignal,
},
};
</script>

View File

@@ -1,5 +1,5 @@
@import "./base.css";
@import "./font.taoyao.css";
@import "./font.css";
#app {
max-width: 1280px;

View File

@@ -7,19 +7,27 @@
*/
const config = {
// 终端标识
sn: null,
sn: "taoyao",
// 信令服务地址
host: "localhost",
port: "8888",
// 终端名称
name: "taoyao-client-web",
// 终端版本
version: "1.0.0",
// 日志级别
logLevel: "DEBUG",
// 信令服务地址
host: "localhost",
port: "8888",
// 帐号密码
username: "taoyao",
password: "taoyao",
signal: function () {
return `wss://${this.host}:${this.port}/websocket.signal`;
},
// 媒体配置
audio: {},
video: {},
// WebRTC配置
webrtc: {},
};
/**
@@ -44,13 +52,13 @@ const protocol = {
/**
* 生成信令消息
*
* @param {*} id ID
* @param {*} body 信令消息
* @param {*} signal 信令标识
* @param {*} body 信令消息
* @param {*} id ID
*
* @returns 信令消息
*/
buildMessage: function (id, body, signal) {
buildMessage: function (signal, body = {}, id) {
let message = {
header: {
v: config.version,

View File

@@ -1,2 +1,23 @@
<!-- 本地终端 -->
<template></template>
<script>
import { defineComponent } from "@vue/composition-api";
export default defineComponent({
setup() {
// 本地视频
this._externalVideo = document.createElement("video");
this._externalVideo.controls = true;
this._externalVideo.muted = true;
this._externalVideo.loop = true;
this._externalVideo.setAttribute("playsinline", "");
this._externalVideo.src = EXTERNAL_VIDEO_SRC;
this._externalVideo
.play()
.catch((error) => logger.warn("externalVideo.play() failed:%o", error));
},
});
</script>

View File

@@ -0,0 +1,100 @@
<!-- 房间设置 -->
<template>
<el-dialog
v-model="localVisible"
@open="init"
width="30%"
:show-close="false"
center
>
<el-form ref="SettingRoomForm" :model="room">
<el-tabs v-model="activeName">
<el-tab-pane label="进入房间" name="enter">
<el-form-item label="房间标识">
<el-select v-model="room.id" placeholder="房间标识">
<el-option
v-for="value in rooms"
:key="value.id"
:label="value.name"
:value="value.id"
/>
</el-select>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="创建房间" name="create">
<el-form-item label="媒体服务">
<el-select v-model="room.mediasoup" placeholder="媒体服务">
<el-option
v-for="mediasoup in config.webrtc.mediasoupList"
:key="mediasoup.name"
:label="mediasoup.name"
:value="mediasoup.name"
/>
</el-select>
</el-form-item>
<el-form-item label="房间名称">
<el-input v-model="room.name" placeholder="房间名称" />
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item label="房间密码">
<el-input v-model="room.password" placeholder="房间密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="setting">设置</el-button>
</template>
</el-dialog>
</template>
<script>
import { config, protocol } from "./Config.js";
export default {
name: "SettingRoom",
data() {
return {
config,
room: {},
rooms: [],
activeName: "enter",
localVisible: false,
};
},
props: {
taoyao: {},
roomVisible: false,
},
watch: {
roomVisible() {
this.localVisible = this.roomVisible;
},
},
methods: {
async init() {
let response = await this.taoyao.request(
protocol.buildMessage("room::list")
);
this.rooms = response.body;
},
async setting() {
this.localVisible = false;
if (this.activeName === "enter") {
await this.taoyao.request(
protocol.buildMessage("room::enter", {
sn: config.sn,
...this.room,
})
);
} else {
await this.taoyao.request(
protocol.buildMessage("room::create", {
sn: config.sn,
...this.room,
})
);
}
},
},
};
</script>

View File

@@ -0,0 +1,53 @@
<!-- 终端设置 -->
<template>
<el-dialog v-model="localVisible" title="终端设置" width="30%" :show-close="false" center>
<el-form ref="SettingSignalForm">
<el-form-item label="终端名称">
<el-input v-model="config.sn" placeholder="终端名称" />
</el-form-item>
<el-form-item label="信令帐号">
<el-input v-model="config.username" placeholder="信令帐号" />
</el-form-item>
<el-form-item label="信令密码">
<el-input v-model="config.password" placeholder="信令密码" />
</el-form-item>
<el-form-item label="信令地址">
<el-input v-model="config.host" placeholder="信令地址" />
</el-form-item>
<el-form-item label="信令端口">
<el-input v-model="config.port" placeholder="信令端口" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="setting">设置</el-button>
</template>
</el-dialog>
</template>
<script>
import { config } from "./Config.js";
export default {
name: "SettingSignal",
data: () => {
return {
config,
localVisible: true,
};
},
props: {
signalVisible: true,
},
watch: {
signalVisible() {
this.localVisible = this.signalVisible;
},
},
methods: {
setting: function () {
this.localVisible = false;
this.$emit("connectSignal");
},
},
};
</script>

View File

@@ -3,14 +3,19 @@
*/
import { Logger } from "./Logger.js";
import { TaoyaoClient } from "./TaoyaoClient.js";
import { config, protocol, defaultAudioConfig, defaultVideoConfig } from "./Config.js";
import * as mediasoupClient from 'mediasoup-client';
import {
config,
protocol,
defaultAudioConfig,
defaultVideoConfig,
} from "./Config.js";
// 日志
const logger = new Logger();
/**
* 信令通道
* TODO获取IP/MAC/信号强度
*/
const signalChannel = {
// 桃夭
@@ -27,6 +32,8 @@ const signalChannel = {
heartbeatTime: 30 * 1000,
// 心跳定时器
heartbeatTimer: null,
// 是否重连
reconnection: true,
// 重连定时器
reconnectTimer: null,
// 防止重复重连
@@ -72,10 +79,11 @@ const signalChannel = {
*
* @returns Promise
*/
connect: function (address, callback, reconnection = true) {
connect: async function (address, callback, reconnection = true) {
let self = this;
self.address = address;
self.callback = callback;
self.reconnection = reconnection;
return new Promise((resolve, reject) => {
logger.debug("连接信令通道", address);
self.channel = new WebSocket(address);
@@ -85,8 +93,7 @@ const signalChannel = {
const battery = await navigator.getBattery();
self.push(
protocol.buildMessage("client::register", {
ip: null,
mac: null,
ip: 'localhost',
signal: 100,
battery: battery.level * 100,
charging: battery.charging,
@@ -103,14 +110,14 @@ const signalChannel = {
};
self.channel.onclose = function (e) {
logger.error("信令通道关闭", self.channel, e);
if (reconnection) {
if (self.reconnection) {
self.reconnect();
}
reject(e);
};
self.channel.onerror = function (e) {
logger.error("信令通道异常", self.channel, e);
if (reconnection) {
if (self.reconnection) {
self.reconnect();
}
reject(e);
@@ -122,7 +129,7 @@ const signalChannel = {
* 3. 如果前面所有回调没有返回true执行默认回调。
*/
self.channel.onmessage = function (e) {
console.debug("信令通道消息", e.data);
logger.debug("信令通道消息", e.data);
let done = false;
let data = JSON.parse(e.data);
// 请求回调
@@ -134,7 +141,7 @@ const signalChannel = {
}
}
// 全局回调
if (self.callback) {
if (!done && self.callback) {
done = self.callback(data);
}
// 默认回调
@@ -163,39 +170,67 @@ const signalChannel = {
}
// 打开定时重连
self.reconnectTimer = setTimeout(function () {
console.info("信令通道重连", self.address);
logger.info("信令通道重连", self.address);
self.connect(self.address, self.callback, true);
self.lockReconnect = false;
}, self.connectionTimeout);
if (self.connectionTimeout >= self.maxReconnectionDelay) {
self.connectionTimeout = self.maxReconnectionDelay;
} else {
self.connectionTimeout = self.connectionTimeout * self.reconnectionDelayGrowFactor;
self.connectionTimeout =
self.connectionTimeout * self.reconnectionDelayGrowFactor;
}
},
/**
* 发送消息
*
* 异步请求
*
* @param {*} data 消息内容
* @param {*} callback 注册回调
*/
push: function (data, callback) {
// 注册回调
if (data && callback) {
this.callbackMapping.set(data.header.id, callback);
let self = this;
if (callback) {
self.callbackMapping.set(data.header.id, callback);
}
// 发送消息
if (data && data.header) {
this.channel.send(JSON.stringify(data));
} else {
this.channel.send(data);
}
self.channel.send(JSON.stringify(data));
},
/**
* 同步请求
*
* @param {*} data 消息内容
*
* @returns Promise
*/
request: async function(data) {
let self = this;
return new Promise((resolve, reject) => {
let callback = false;
// 设置回调
self.callbackMapping.set(data.header.id, (response) => {
callback = true;
resolve(response);
return true;
});
// 发送请求
self.channel.send(JSON.stringify(data));
// 设置超时
setTimeout(() => {
if(!callback) {
reject("请求超时", data);
}
}, 5000);
});
},
/**
* 关闭通道
*/
close: function () {
clearTimeout(this.heartbeatTimer);
let self = this;
self.reconnection = false;
self.channel.close();
clearTimeout(self.heartbeatTimer);
},
/**
* 默认回调
@@ -203,10 +238,17 @@ const signalChannel = {
* @param {*} data 消息内容
*/
defaultCallback: function (data) {
console.debug("没有适配信令消息默认处理", data);
let self = this;
logger.debug("没有适配信令消息默认处理", data);
switch (data.header.signal) {
case "client::config":
self.defaultClientConfig(data);
break;
case "client::register":
logger.info("桃夭终端注册成功");
break;
case "platform::error":
console.error("信令发生错误", data);
logger.error("信令发生错误", data);
break;
}
},
@@ -216,24 +258,10 @@ const signalChannel = {
* @param {*} data 消息内容
*/
defaultClientConfig: function (data) {
let self = this;
// 配置终端
self.taoyao
.configMedia(data.body.media.audio, data.body.media.video)
.configWebrtc(data.body.webrtc);
// 打开媒体通道
let videoId = self.taoyao.videoId;
if (videoId) {
self.taoyao
.buildLocalMedia()
.then((stream) => {
self.taoyao.buildMediaChannel(videoId, stream);
})
.catch((e) => console.error("打开终端媒体失败", e));
console.debug("自动打开媒体通道", videoId);
} else {
console.debug("没有配置本地媒体信息跳过自动打开媒体通道");
}
config.webrtc = data.body.webrtc;
config.audio = { ...config.defaultAudioConfig, ...data.body.media.audio };
config.video = { ...config.defaultVideoConfig, ...data.body.media.video };
logger.info("终端配置", config.audio, config.video, config.webrtc);
},
/**
* 默认终端重启回调
@@ -241,7 +269,7 @@ const signalChannel = {
* @param {*} data 消息内容
*/
defaultClientReboot: function (data) {
console.info("重启终端");
logger.info("重启终端");
location.reload();
},
};
@@ -250,63 +278,89 @@ const signalChannel = {
* 桃夭
*/
class Taoyao {
// 发送信令
push = null;
// 请求信令
request = null;
// 本地视频
localVideo = null;
// 本地终端
localClient;
// 远程终端
remoteClientList;
// 设备状态
audioEnabled = true;
videoEnabled = true;
// 媒体配置
audioConfig = defaultAudioConfig;
videoConfig = defaultVideoConfig;
// 媒体通道
transSend;
transRecv;
// 发送信令
push = null;
// 信令通道
signalChannel = null;
sendTransport = null;
recvTransport = null;
// 信令通道
signalChannel = null;
// 媒体设备
mediasoupDevice = null;
// 是否消费
consume = true;
// 是否生产
produce = true;
// 是否生产音频
audioProduce = true && this.produce;
// 是否生成视频
videoProduce = true && this.produce;
// 音频生产者
audioProducer = null;
// 视频生产者
videoProducer = null;
// 消费者
consumers = new Map();
// 数据消费者
dataConsumers = new Map();
/**
* 媒体配置
*
* @param {*} audio
* @param {*} video
*
* @returns
*/
configMedia = function(audio = {}, video = {}) {
this.audioConfig = {...this.audioConfig, ...audio};
this.videoConfig = {...this.videoConfig, ...video};
console.debug('终端媒体配置', this.audioConfig, this.videoConfig);
return this;
};
/**
* WebRTC配置
*
* @param {*} config
*
* @returns
*/
configWebrtc = function(config = {}) {
return this;
};
/**
* 打开信令通道
*
* @param {*} callback
*
* @returns
*
* @param {*} callback
*
* @returns
*/
buildChannel = function(callback) {
signalChannel.taoyao = this;
this.signalChannel = signalChannel;
// 不能直接this.push = this.signalChannel.push这样导致this对象错误
this.push = function(data, pushCallback) {
this.signalChannel.push(data, pushCallback);
};
return this.signalChannel.connect(config.signal(), callback);
};
buildChannel = async function (callback) {
signalChannel.taoyao = this;
this.signalChannel = signalChannel;
// 不能直接this.push = this.signalChannel.push这样导致this对象错误
this.push = function (data, pushCallback) {
this.signalChannel.push(data, pushCallback);
};
this.request = async function(data) {
return await this.signalChannel.request(data);
}
return this.signalChannel.connect(config.signal(), callback);
};
/**
* 设置本地媒体
*/
buildLocal = function() {
new mediasoupClient.Device();
};
/**
* 打开媒体通道
*/
buildMediaTransport = function() {
let self = this;
// 释放资源
self.close();
}
/**
* 关闭
*/
close = function() {
let self = this;
if(self.sendTransport) {
self.sendTransport.close();
}
if(self.recvTransport) {
self.recvTransport.close();
}
if(self.signalChannel) {
self.signalChannel.close();
}
};
}
export { Taoyao };

View File

@@ -1,5 +1,9 @@
import { createApp } from "vue";
import App from "./App.vue";
import "./assets/main.css";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
createApp(App).mount("#app");
const app = createApp(App);
app.use(ElementPlus);
app.mount("#app");

View File

@@ -1,7 +1,6 @@
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath, URL } from "node:url";
export default defineConfig({
plugins: [vue()],