This commit is contained in:
acgist
2023-02-08 21:31:09 +08:00
parent 7889d4f708
commit 50f80bee2d
164 changed files with 2023 additions and 1457 deletions

View File

@@ -0,0 +1,131 @@
/**
* 桃夭配置
*/
/**
* 信令配置
*/
const config = {
// 终端标识
sn: null,
// 终端名称
name: "taoyao-client-web",
// 终端版本
version: "1.0.0",
// 日志级别
logLevel: "DEBUG",
// 信令服务地址
host: "localhost",
port: "8888",
signal: function () {
return `wss://${this.host}:${this.port}/websocket.signal`;
},
};
/**
* 信令协议
*/
const protocol = {
// 当前索引
index: 100000,
// 最小索引
minIndex: 100000,
// 最大索引
maxIndex: 999999,
/**
* @returns 索引
*/
buildId: function () {
if (this.index++ >= this.maxIndex) {
this.index = this.minIndex;
}
return Date.now() + "" + this.index;
},
/**
* 生成信令消息
*
* @param {*} id ID
* @param {*} body 信令消息
* @param {*} signal 信令标识
*
* @returns 信令消息
*/
buildMessage: function (id, body, signal) {
let message = {
header: {
v: config.version,
id: id || this.buildId(),
sn: config.sn,
signal: signal,
},
body: body,
};
return message;
},
};
/**
* 默认音频配置
*/
const defaultAudioConfig = {
// 设备
// deviceId : '',
// 音量0~1
volume: 0.5,
// 延迟大小单位毫秒500毫秒以内较好
latency: 0.4,
// 采样数16
sampleSize: 16,
// 采样率8000|16000|32000|48000
sampleRate: 32000,
// 声道数量1|2
channelCount: 1,
// 是否开启自动增益true|false
autoGainControl: false,
// 是否开启降噪功能true|false
noiseSuppression: true,
// 是否开启回音消除true|false
echoCancellation: true,
// 消除回音方式system|browser
echoCancellationType: "system",
};
/**
* 默认视频配置
*/
const defaultVideoConfig = {
// 设备
// deviceId: '',
// 宽度
width: 1280,
// 高度
height: 720,
// 帧率
frameRate: 24,
// 选摄像头user|left|right|environment
facingMode: "environment",
};
/**
* 默认RTCPeerConnection配置
*/
const defaultRTCPeerConnectionConfig = {
// ICE代理的服务器
iceServers: null,
// 传输通道绑定策略balanced|max-compat|max-bundle
bundlePolicy: "balanced",
// RTCP多路复用策略require|negotiate
rtcpMuxPolicy: "require",
// ICE传输策略all|relay
iceTransportPolicy: "all",
// ICE候选个数
iceCandidatePoolSize: 8,
};
export {
config,
protocol,
defaultAudioConfig,
defaultVideoConfig,
defaultRTCPeerConnectionConfig,
};

View File

@@ -1,7 +1,2 @@
<!-- 本地终端 -->
<template>
</template>
<style scoped>
</style>
<template></template>

View File

@@ -0,0 +1,94 @@
/**
* 日志
*/
import moment from "moment";
import { config } from "./Config.js";
/**
* 日志
*/
class Logger {
constructor(prefix = "") {
if (prefix) {
this.name = this.name + " : " + prefix;
}
}
// 名称
name = config.name;
// 级别
level = ["DEBUG", "INFO", "WARN", "ERROR", "OFF"];
// 级别索引
levelIndex = this.level.indexOf(config.logLevel.toUpperCase());
/**
* debug
*
* @param {...any} args 参数
*
* @returns this
*/
debug(...args) {
return this.log(console.debug, "37m", "DEBUG", args);
}
/**
* info
*
* @param {...any} args 参数
*
* @returns this
*/
info(...args) {
return this.log(console.info, "32m", "INFO", args);
}
/**
* warn
*
* @param {...any} args 参数
*
* @returns this
*/
warn(...args) {
return this.log(console.warn, "33m", "WARN", args);
}
/**
* error
*
* @param {...any} args 参数
*
* @returns this
*/
error(...args) {
return this.log(console.error, "31m", "ERROR", args);
}
/**
* 日志
*
* @param {*} out 输出
* @param {*} color 颜色
* @param {*} level 级别
* @param {*} args 参数
*
* @returns this
*/
log(out, color, level, args) {
if (!args || this.level.indexOf(level) < this.levelIndex) {
return this;
}
if (args.length > 1 && args[0].length > 0) {
out(`\x1B[${color}${this.name} ${moment().format("yyyy-MM-DD HH:mm:ss")} : [${level.padEnd(5, " ")}] :\x1B[0m`, args);
} else if (args.length === 1 && args[0].length > 0) {
out(`\x1B[${color}${this.name} ${moment().format("yyyy-MM-DD HH:mm:ss")} : [${level.padEnd(5, " ")}] :\x1B[0m`, args);
} else {
// 其他情况直接输出换行
out("");
}
return this;
}
}
export { Logger };

View File

@@ -1,7 +1,2 @@
<!-- 远程终端 -->
<template>
</template>
<style scoped>
</style>
<template></template>

View File

@@ -0,0 +1,312 @@
/**
* 桃夭
*/
import { Logger } from "./Logger.js";
import { TaoyaoClient } from "./TaoyaoClient.js";
import { config, protocol, defaultAudioConfig, defaultVideoConfig } from "./Config.js";
// 日志
const logger = new Logger();
/**
* 信令通道
* TODO获取IP/MAC/信号强度
*/
const signalChannel = {
// 桃夭
taoyao: null,
// 通道
channel: null,
// 地址
address: null,
// 回调
callback: null,
// 回调事件
callbackMapping: new Map(),
// 心跳时间
heartbeatTime: 30 * 1000,
// 心跳定时器
heartbeatTimer: null,
// 重连定时器
reconnectTimer: null,
// 防止重复重连
lockReconnect: false,
// 当前重连时间
connectionTimeout: 5 * 1000,
// 最小重连时间
minReconnectionDelay: 5 * 1000,
// 最大重连时间
maxReconnectionDelay: 60 * 1000,
// 重连失败时间增长倍数
reconnectionDelayGrowFactor: 2,
/**
* 心跳
*/
heartbeat: function () {
let self = this;
if (self.heartbeatTimer) {
clearTimeout(self.heartbeatTimer);
}
self.heartbeatTimer = setTimeout(async function () {
if (self.channel && self.channel.readyState === WebSocket.OPEN) {
const battery = await navigator.getBattery();
self.push(
protocol.buildMessage("client::heartbeat", {
signal: 100,
battery: battery.level * 100,
charging: battery.charging,
})
);
self.heartbeat();
} else {
logger.warn("发送心跳失败", self.channel);
}
}, self.heartbeatTime);
},
/**
* 连接
*
* @param {*} address 地址
* @param {*} callback 回调
* @param {*} reconnection 是否重连
*
* @returns Promise
*/
connect: function (address, callback, reconnection = true) {
let self = this;
self.address = address;
self.callback = callback;
return new Promise((resolve, reject) => {
logger.debug("连接信令通道", address);
self.channel = new WebSocket(address);
self.channel.onopen = async function (e) {
logger.debug("打开信令通道", e);
// 注册终端
const battery = await navigator.getBattery();
self.push(
protocol.buildMessage("client::register", {
ip: null,
mac: null,
signal: 100,
battery: battery.level * 100,
charging: battery.charging,
username: config.username,
password: config.password,
})
);
// 重置时间
self.connectionTimeout = self.minReconnectionDelay;
// 开始心跳
self.heartbeat();
// 成功回调
resolve(e);
};
self.channel.onclose = function (e) {
logger.error("信令通道关闭", self.channel, e);
if (reconnection) {
self.reconnect();
}
reject(e);
};
self.channel.onerror = function (e) {
logger.error("信令通道异常", self.channel, e);
if (reconnection) {
self.reconnect();
}
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);
}
// 默认回调
if (!done) {
self.defaultCallback(data);
}
};
});
},
/**
* 重连
*/
reconnect: function () {
let self = this;
if (self.lockReconnect) {
return;
}
self.lockReconnect = true;
// 关闭旧的通道
if (self.channel && self.channel.readyState === WebSocket.OPEN) {
self.channel.close();
self.channel = null;
}
if (self.reconnectTimer) {
clearTimeout(self.reconnectTimer);
}
// 打开定时重连
self.reconnectTimer = setTimeout(function () {
console.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;
}
},
/**
* 发送消息
*
* @param {*} data 消息内容
* @param {*} callback 注册回调
*/
push: function (data, callback) {
// 注册回调
if (data && callback) {
this.callbackMapping.set(data.header.id, callback);
}
// 发送消息
if (data && data.header) {
this.channel.send(JSON.stringify(data));
} else {
this.channel.send(data);
}
},
/**
* 关闭通道
*/
close: function () {
clearTimeout(this.heartbeatTimer);
},
/**
* 默认回调
*
* @param {*} data 消息内容
*/
defaultCallback: function (data) {
console.debug("没有适配信令消息默认处理", data);
switch (data.header.signal) {
case "platform::error":
console.error("信令发生错误", data);
break;
}
},
/**
* 默认配置回调
*
* @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("没有配置本地媒体信息跳过自动打开媒体通道");
}
},
/**
* 默认终端重启回调
*
* @param {*} data 消息内容
*/
defaultClientReboot: function (data) {
console.info("重启终端");
location.reload();
},
};
/**
* 桃夭
*/
class Taoyao {
// 本地终端
localClient;
// 远程终端
remoteClientList;
// 设备状态
audioEnabled = true;
videoEnabled = true;
// 媒体配置
audioConfig = defaultAudioConfig;
videoConfig = defaultVideoConfig;
// 媒体通道
transSend;
transRecv;
// 发送信令
push = null;
// 信令通道
signalChannel = null;
/**
* 媒体配置
*
* @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
*/
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);
};
}
export { Taoyao };

View File

@@ -0,0 +1,6 @@
/**
* 桃夭终端
*/
class TaoyaoClient {}
export { TaoyaoClient };