526 lines
13 KiB
JavaScript
526 lines
13 KiB
JavaScript
/**
|
||
* 桃夭
|
||
*/
|
||
import { Logger } from "./Logger.js";
|
||
import { TaoyaoClient } from "./TaoyaoClient.js";
|
||
import * as mediasoupClient from "mediasoup-client";
|
||
import {
|
||
config,
|
||
protocol,
|
||
defaultAudioConfig,
|
||
defaultVideoConfig,
|
||
} from "./Config.js";
|
||
|
||
// 日志
|
||
const logger = new Logger();
|
||
|
||
const PC_PROPRIETARY_CONSTRAINTS = {
|
||
optional : [ { googDscp: true } ]
|
||
};
|
||
|
||
/**
|
||
* 信令通道
|
||
*/
|
||
const signalChannel = {
|
||
// 桃夭
|
||
taoyao: null,
|
||
// 通道
|
||
channel: null,
|
||
// 地址
|
||
address: null,
|
||
// 回调
|
||
callback: null,
|
||
// 回调事件
|
||
callbackMapping: new Map(),
|
||
// 心跳时间
|
||
heartbeatTime: 30 * 1000,
|
||
// 心跳定时器
|
||
heartbeatTimer: null,
|
||
// 是否重连
|
||
reconnection: true,
|
||
// 重连定时器
|
||
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: 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);
|
||
self.channel.onopen = async function (e) {
|
||
logger.debug("打开信令通道", e);
|
||
// 注册终端
|
||
const battery = await navigator.getBattery();
|
||
self.push(
|
||
protocol.buildMessage("client::register", {
|
||
ip: "localhost",
|
||
clientId: config.clientId,
|
||
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 (self.reconnection) {
|
||
self.reconnect();
|
||
}
|
||
reject(e);
|
||
};
|
||
self.channel.onerror = function (e) {
|
||
logger.error("信令通道异常", self.channel, e);
|
||
if (self.reconnection) {
|
||
self.reconnect();
|
||
}
|
||
reject(e);
|
||
};
|
||
/**
|
||
* 回调策略:
|
||
* 1. 如果注册请求回调,同时执行结果返回true不再执行后面所有回调。
|
||
* 2. 如果注册全局回调,同时执行结果返回true不再执行后面所有回调。
|
||
* 3. 如果前面所有回调没有返回true执行默认回调。
|
||
*/
|
||
self.channel.onmessage = function (e) {
|
||
logger.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 (!done && 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 () {
|
||
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;
|
||
}
|
||
},
|
||
/**
|
||
* 异步请求
|
||
*
|
||
* @param {*} data 消息内容
|
||
* @param {*} callback 注册回调
|
||
*/
|
||
push: function (data, callback) {
|
||
// 注册回调
|
||
let self = this;
|
||
if (callback) {
|
||
self.callbackMapping.set(data.header.id, callback);
|
||
}
|
||
// 发送消息
|
||
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 () {
|
||
let self = this;
|
||
self.reconnection = false;
|
||
self.channel.close();
|
||
clearTimeout(self.heartbeatTimer);
|
||
},
|
||
/**
|
||
* 默认回调
|
||
*
|
||
* @param {*} data 消息内容
|
||
*/
|
||
defaultCallback: function (data) {
|
||
let self = this;
|
||
logger.debug("没有适配信令消息默认处理", data);
|
||
switch (data.header.signal) {
|
||
case "client::config":
|
||
self.defaultClientConfig(data);
|
||
break;
|
||
case "client::reboot":
|
||
self.defaultClientReboot(data);
|
||
break;
|
||
case "client::register":
|
||
logger.info("桃夭终端注册成功");
|
||
break;
|
||
case "platform::error":
|
||
logger.error("信令发生错误", data);
|
||
break;
|
||
}
|
||
},
|
||
/**
|
||
* 默认配置回调
|
||
*
|
||
* @param {*} data 消息内容
|
||
*/
|
||
defaultClientConfig: function (data) {
|
||
config.audio = { ...config.defaultAudioConfig, ...data.body.media.audio };
|
||
config.video = { ...config.defaultVideoConfig, ...data.body.media.video };
|
||
config.webrtc = data.body.webrtc;
|
||
config.mediaServerList = data.body.media.mediaServerList;
|
||
logger.info("终端配置", config.audio, config.video, config.webrtc, config.mediaServerList);
|
||
},
|
||
/**
|
||
* 默认终端重启回调
|
||
*
|
||
* @param {*} data 消息内容
|
||
*/
|
||
defaultClientReboot: function (data) {
|
||
logger.info("重启终端");
|
||
location.reload();
|
||
},
|
||
};
|
||
|
||
/**
|
||
* 桃夭
|
||
*/
|
||
class Taoyao {
|
||
// 发送信令
|
||
push;
|
||
// 房间ID
|
||
roomId;
|
||
// 请求信令
|
||
request;
|
||
// 本地视频
|
||
localVideo;
|
||
// 本地终端
|
||
localClient;
|
||
// 远程终端
|
||
remoteClients = new Map();
|
||
// 媒体通道
|
||
sendTransport;
|
||
recvTransport;
|
||
// 信令通道
|
||
signalChannel;
|
||
// 媒体设备
|
||
mediasoupDevice;
|
||
// 是否消费
|
||
consume = true;
|
||
// 是否生产
|
||
produce = true;
|
||
// 强制使用TCP
|
||
forceTcp = false;
|
||
// 使用数据通道
|
||
useDataChannel = true;
|
||
// 是否生产音频
|
||
audioProduce = true && this.produce;
|
||
// 是否生成视频
|
||
videoProduce = true && this.produce;
|
||
// 音频生产者
|
||
audioProducer;
|
||
// 视频生产者
|
||
videoProducer;
|
||
// 消费者
|
||
consumers = new Map();
|
||
// 数据消费者
|
||
dataConsumers = new Map();
|
||
|
||
constructor({
|
||
roomId,
|
||
peerId,
|
||
displayName,
|
||
device,
|
||
handlerName,
|
||
useSimulcast,
|
||
useSharingSimulcast,
|
||
forceTcp,
|
||
produce,
|
||
consume,
|
||
forceH264,
|
||
forceVP9,
|
||
svc,
|
||
datachannel,
|
||
externalVideo,
|
||
e2eKey,
|
||
consumerReplicas
|
||
}) {
|
||
this.roomId = roomId;
|
||
}
|
||
|
||
/**
|
||
* 打开信令通道
|
||
*
|
||
* @param {*} callback
|
||
*
|
||
* @returns
|
||
*/
|
||
buildSignal = 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);
|
||
};
|
||
/**
|
||
* 打开媒体通道
|
||
* TODO:共享 navigator.mediaDevices.getDisplayMedia();
|
||
*/
|
||
buildMedia = async function (roomId) {
|
||
let self = this;
|
||
// 释放资源
|
||
self.closeMedia();
|
||
self.mediasoupDevice = new mediasoupClient.Device();
|
||
const response = await self.request(protocol.buildMessage(
|
||
"router::rtp::capabilities",
|
||
{ roomId : roomId || self.roomId }
|
||
));
|
||
const routerRtpCapabilities = response.body;
|
||
self.mediasoupDevice.load({ routerRtpCapabilities });
|
||
self.produce();
|
||
};
|
||
/**
|
||
* 生产媒体
|
||
*/
|
||
produceMedia = async function() {
|
||
// 打开媒体:TODO:参数
|
||
const self = this;
|
||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video : true });
|
||
const audioTrack = stream.getAudioTracks()[0];
|
||
const videoTrack = stream.getVideoTracks()[0];
|
||
if(self.produce) {
|
||
const transportInfo = await self.request("webrtc::transport::create", {
|
||
forceTcp : self.forceTcp,
|
||
producing : true,
|
||
consuming : false,
|
||
sctpCapabilities : self.useDataChannel ? self.mediasoupDevice.sctpCapabilities : undefined
|
||
});
|
||
const {
|
||
id,
|
||
iceParameters,
|
||
iceCandidates,
|
||
dtlsParameters,
|
||
sctpParameters
|
||
} = transportInfo;
|
||
|
||
self.sendTransport = self.mediasoupDevice.createSendTransport(
|
||
{
|
||
id,
|
||
iceParameters,
|
||
iceCandidates,
|
||
dtlsParameters :
|
||
{
|
||
...dtlsParameters,
|
||
role : 'auto'
|
||
},
|
||
sctpParameters,
|
||
iceServers : [],
|
||
proprietaryConstraints : PC_PROPRIETARY_CONSTRAINTS,
|
||
additionalSettings :
|
||
// TODO:加密解密
|
||
{ encodedInsertableStreams: true }
|
||
});
|
||
|
||
self.sendTransport.on(
|
||
'connect', ({ dtlsParameters }, callback, errback) =>
|
||
{
|
||
self.request(
|
||
'webrtc::transport::connect',
|
||
{
|
||
transportId : self.sendTransport.id,
|
||
dtlsParameters
|
||
})
|
||
.then(callback)
|
||
.catch(errback);
|
||
});
|
||
|
||
self.sendTransport.on(
|
||
'produce', async ({ kind, rtpParameters, appData }, callback, errback) =>
|
||
{
|
||
try
|
||
{
|
||
const { id } = await self.request(
|
||
'produce',
|
||
{
|
||
transportId : self.sendTransport.id,
|
||
kind,
|
||
rtpParameters,
|
||
appData
|
||
});
|
||
|
||
callback({ id });
|
||
}
|
||
catch (error)
|
||
{
|
||
errback(error);
|
||
}
|
||
});
|
||
|
||
sef.sendTransport.on('producedata', async (
|
||
{
|
||
sctpStreamParameters,
|
||
label,
|
||
protocol,
|
||
appData
|
||
},
|
||
callback,
|
||
errback
|
||
) =>
|
||
{
|
||
logger.debug(
|
||
'"producedata" event: [sctpStreamParameters:%o, appData:%o]',
|
||
sctpStreamParameters, appData);
|
||
|
||
try
|
||
{
|
||
const { id } = await self.request(
|
||
'produceData',
|
||
{
|
||
transportId : self.sendTransport.id,
|
||
sctpStreamParameters,
|
||
label,
|
||
protocol,
|
||
appData
|
||
});
|
||
|
||
callback({ id });
|
||
}
|
||
catch (error)
|
||
{
|
||
errback(error);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
/**
|
||
* 关闭媒体
|
||
*/
|
||
closeMedia = function() {
|
||
let self = this;
|
||
if (self.sendTransport) {
|
||
self.sendTransport.close();
|
||
}
|
||
if (self.recvTransport) {
|
||
self.recvTransport.close();
|
||
}
|
||
}
|
||
/**
|
||
* 关闭
|
||
*/
|
||
close = function () {
|
||
let self = this;
|
||
self.closeMedia();
|
||
if (self.signalChannel) {
|
||
self.signalChannel.close();
|
||
}
|
||
};
|
||
}
|
||
|
||
export { Taoyao };
|