Files
taoyao/taoyao-client-web/src/components/Taoyao.js
acgist 129c36ed80 [+] 整体架构调整
生产者接入完成
媒体作为信令服务的终端注册
2023-02-25 13:31:57 +08:00

847 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 桃夭
*/
import { TaoyaoClient } from "./TaoyaoClient.js";
import * as mediasoupClient from "mediasoup-client";
import {
protocol,
defaultAudioConfig,
defaultVideoConfig,
defaultRTCPeerConnectionConfig,
} from "./Config.js";
/**
* 信令通道
*/
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,
/**
* 心跳
*/
heartbeat() {
const 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();
// TODO信号强度
self.push(
protocol.buildMessage("client::heartbeat", {
signal: 100,
battery: battery.level * 100,
charging: battery.charging,
})
);
self.heartbeat();
} else {
console.warn("发送心跳失败:", self.address);
}
}, self.heartbeatTime);
},
/**
* 连接
*
* @param {*} address 地址
* @param {*} callback 回调
* @param {*} reconnection 是否重连
*
* @returns Promise
*/
async connect(address, callback, reconnection = true) {
const self = this;
if (self.channel && self.channel.readyState === WebSocket.OPEN) {
return new Promise((resolve, reject) => {
resolve(self.channel);
});
}
self.address = address;
self.callback = callback;
self.reconnection = reconnection;
return new Promise((resolve, reject) => {
console.debug("连接信令通道:", self.address);
self.channel = new WebSocket(self.address);
self.channel.onopen = async function () {
console.debug("打开信令通道:", self.address);
// 注册终端
// TODO信号强度
const battery = await navigator.getBattery();
self.push(
protocol.buildMessage("client::register", {
name: "桃夭Web",
clientId: self.taoyao.clientId,
clientType: "WEB",
signal: 100,
battery: battery.level * 100,
charging: battery.charging,
username: self.taoyao.username,
password: self.taoyao.password,
})
);
// 重置时间
self.connectionTimeout = self.minReconnectionDelay;
// 开始心跳
self.heartbeat();
// 成功回调
resolve(self.channel);
};
self.channel.onclose = async function () {
console.warn("信令通道关闭:", self.channel);
if (self.reconnection) {
self.reconnect();
}
// 不要失败回调
};
self.channel.onerror = async function (e) {
console.error("信令通道异常:", self.channel, e);
if (self.reconnection) {
self.reconnect();
}
// 不要失败回调
};
/**
* 回调策略:
* 1. 如果注册请求回调同时执行结果返回true不再执行后面所有回调。
* 2. 如果注册全局回调同时执行结果返回true不再执行后面所有回调。
* 3. 如果前面所有回调没有返回true执行默认回调。
*/
self.channel.onmessage = function (e) {
console.debug("信令通道消息:", e.data);
let done = false;
const message = JSON.parse(e.data);
// 请求回调
if (self.callbackMapping.has(message.header.id)) {
try {
done = self.callbackMapping.get(message.header.id)(message);
} finally {
self.callbackMapping.delete(message.header.id);
}
}
// 全局回调
if (!done && self.callback) {
done = self.callback(message);
}
// 默认回调
if (!done) {
self.defaultCallback(message);
}
};
});
},
/**
* 重连
*/
reconnect() {
const self = this;
if (
self.lockReconnect ||
(self.channel && self.channel.readyState === WebSocket.OPEN)
) {
return;
}
self.lockReconnect = true;
if (self.reconnectTimer) {
clearTimeout(self.reconnectTimer);
}
// 定时重连
self.reconnectTimer = setTimeout(function () {
console.info("信令通道重连:", self.address);
self.connect(self.address, self.callback, self.reconnection);
self.lockReconnect = false;
}, self.connectionTimeout);
self.connectionTimeout = Math.min(
self.connectionTimeout + self.minReconnectionDelay,
self.maxReconnectionDelay
);
},
/**
* 异步请求
*
* @param {*} message 消息
* @param {*} callback 回调
*/
push(message, callback) {
const self = this;
// 注册回调
if (callback) {
self.callbackMapping.set(message.header.id, callback);
}
// 发送消息
try {
self.channel.send(JSON.stringify(message));
} catch (error) {
console.error("推送消息异常:", message, error);
}
},
/**
* 同步请求
*
* @param {*} message 消息
*
* @returns Promise
*/
async request(message) {
const self = this;
return new Promise((resolve, reject) => {
let done = false;
// 注册回调
self.callbackMapping.set(message.header.id, (response) => {
resolve(response);
done = true;
// 返回true不要继续执行回调
return true;
});
// 发送消息
try {
self.channel.send(JSON.stringify(message));
} catch (error) {
console.error("请求消息异常:", message, error);
}
// 设置超时
setTimeout(() => {
if (!done) {
self.callbackMapping.delete(message.header.id);
reject("请求超时", message);
}
}, 5000);
});
},
/**
* 关闭通道
*/
close() {
const self = this;
self.reconnection = false;
self.channel.close();
clearTimeout(self.heartbeatTimer);
clearTimeout(self.reconnectTimer);
},
/**
* 默认回调
*
* @param {*} message 消息
*/
defaultCallback(message) {
let self = this;
console.debug("没有适配信令消息执行默认处理", message);
switch (message.header.signal) {
case "client::config":
self.defaultClientConfig(message);
break;
case "client::reboot":
self.defaultClientReboot(message);
break;
case "client::shutdown":
self.defaultClientShutdown(message);
break;
case "client::register":
console.info("终端注册成功");
break;
case "platform::error":
self.callbackError(message);
break;
}
},
/**
* 配置默认回调
*
* @param {*} message 消息
*/
defaultClientConfig(message) {
const self = this;
self.taoyao.audio = { ...defaultAudioConfig, ...message.body.media.audio };
self.taoyao.video = { ...defaultVideoConfig, ...message.body.media.video };
self.taoyao.webrtc = message.body.webrtc;
console.debug(
"终端配置",
self.taoyao.audio,
self.taoyao.video,
self.taoyao.webrtc
);
},
/**
* 终端重启默认回调
*
* @param {*} message 消息
*/
defaultClientReboot(message) {
console.info("重启终端");
location.reload();
},
/**
* 终端重启默认回调
*
* @param {*} message 消息
*/
defaultClientShutdown(message) {
console.info("关闭终端");
window.close();
},
};
/**
* 桃夭
*/
class Taoyao {
// 房间标识
roomId;
// 终端标识
clientId;
// 信令地址
host;
// 信令端口
port;
// 信令帐号
username;
// 信令密码
password;
// 回调事件
callback;
// 音频媒体配置
audio;
// 视频媒体配置
video;
// WebRTC配置
webrtc;
// 发送信令
push;
// 请求信令
request;
// 本地终端
localClient;
// 远程终端
remoteClients = new Map();
// 信令通道
signalChannel;
// 发送媒体通道
sendTransport;
// 接收媒体通道
recvTransport;
// 媒体设备
mediasoupDevice;
// 是否消费
consume;
// 是否生产
produce;
// 是否生产音频
audioProduce;
// 是否生成视频
videoProduce;
// 强制使用TCP
forceTcp;
// 使用数据通道
useDataChannel;
// 音频生产者
audioProducer;
// 视频生产者
videoProducer;
// 数据生产者
dataChannnelProducer;
// 媒体消费者
consumers = new Map();
// 数据消费者
dataConsumers = new Map();
constructor({
roomId,
clientId,
host,
port,
username,
password,
consume = true,
produce = true,
audioProduce = true,
videoProduce = true,
forceTcp = false,
useDataChannel = true,
}) {
this.roomId = roomId;
this.clientId = clientId;
this.host = host;
this.port = port;
this.username = username;
this.password = password;
this.consume = consume;
this.produce = produce;
this.audioProduce = produce && audioProduce;
this.videoProduce = produce && videoProduce;
this.forceTcp = forceTcp;
this.useDataChannel = useDataChannel;
}
/**
* 打开信令通道
*
* @param {*} callback
*
* @returns
*/
async buildSignal(callback) {
const self = this;
self.callback = callback;
self.signalChannel = signalChannel;
signalChannel.taoyao = self;
// 不能直接this.push = this.signalChannel.push这样导致this对象错误
self.push = function (data, pushCallback) {
self.signalChannel.push(data, pushCallback);
};
self.request = async function (data) {
return await self.signalChannel.request(data);
};
return self.signalChannel.connect(
`wss://${self.host}:${self.port}/websocket.signal`,
callback
);
}
/**
* 错误回调
*/
callbackError(message, error) {
const self = this;
if (!self.callback) {
console.warn("没有注册回调:", message, error);
}
// 错误回调
self.callback(
protocol.buildMessage("platform::error", { message }, -9999),
error
);
}
/**
* 创建房间
*/
async create(room) {
const self = this;
const response = await self.request(
protocol.buildMessage("room::create", room)
);
return response.body;
}
async enter(roomId) {
const self = this;
if (!roomId) {
this.callbackError("无效房间");
return;
}
self.roomId = roomId;
self.mediasoupDevice = new mediasoupClient.Device();
const response = await self.request(
protocol.buildMessage("media::router::rtp::capabilities", {
roomId: self.roomId,
})
);
const routerRtpCapabilities = response.body.rtpCapabilities;
await self.mediasoupDevice.load({ routerRtpCapabilities });
await self.request(
protocol.buildMessage("room::enter", {
roomId: roomId,
rtpCapabilities: self.consume
? self.mediasoupDevice.rtpCapabilities
: undefined,
sctpCapabilities:
self.consume && self.useDataChannel
? self.mediasoupDevice.sctpCapabilities
: undefined,
})
);
}
/**
* TODO共享 navigator.mediaDevices.getDisplayMedia();
* 生产媒体
* TODO验证API试试修改媒体
* audioTrack.getSettings
* audioTrack.getCapabilities
* audioTrack.applyCapabilities
*/
async produceMedia() {
const self = this;
if (!self.roomId) {
this.callbackError("无效房间");
return;
}
// 检查设备
self.checkDevice();
// 释放资源
self.closeMedia();
// TODO暂时不知道为什么
{
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioTrack = stream.getAudioTracks()[0];
audioTrack.enabled = false;
setTimeout(() => audioTrack.stop(), 120000);
}
if (self.produce) {
const response = await self.request(
protocol.buildMessage("media::transport::webrtc::create", {
roomId: self.roomId,
forceTcp: self.forceTcp,
producing: true,
consuming: false,
sctpCapabilities: self.useDataChannel
? self.mediasoupDevice.sctpCapabilities
: undefined,
})
);
const {
transportId,
iceParameters,
iceCandidates,
dtlsParameters,
sctpParameters,
} = response.body;
self.sendTransport = self.mediasoupDevice.createSendTransport({
id: transportId,
iceCandidates,
iceParameters,
dtlsParameters: {
...dtlsParameters,
role: "auto",
},
sctpParameters,
// TODO:iceservers
iceServers: [],
// Google配置
proprietaryConstraints: {
optional: [{ googDscp: true }],
},
additionalSettings:
// TODO加密解密
{ encodedInsertableStreams: false },
});
self.sendTransport.on(
"connect",
({ dtlsParameters }, callback, errback) => {
self
.request(
protocol.buildMessage("media::transport::webrtc::connect", {
roomId: self.roomId,
transportId: self.sendTransport.id,
dtlsParameters,
})
)
.then(callback)
.catch(errback);
}
);
self.sendTransport.on(
"produce",
async ({ kind, appData, rtpParameters }, callback, errback) => {
try {
const { producerId } = await self.request(
protocol.buildMessage("media::produce", {
kind,
roomId: self.roomId,
appData,
transportId: self.sendTransport.id,
rtpParameters,
})
);
callback({ id: producerId });
} catch (error) {
errback(error);
}
}
);
self.sendTransport.on(
"producedata",
async (
{ label, protocol, appData, sctpStreamParameters },
callback,
errback
) => {
try {
const { id } = await self.request(
protocol.buildMessage("media::produceData", {
label,
appData,
protocol,
transportId: self.sendTransport.id,
sctpStreamParameters,
})
);
callback({ id });
} catch (error) {
errback(error);
}
}
);
}
if (this.consume) {
const self = this;
const response = await self.request(
protocol.buildMessage("media::transport::webrtc::create", {
roomId: self.roomId,
forceTcp: self.forceTcp,
producing: false,
consuming: true,
sctpCapabilities: self.useDataChannel
? self.mediasoupDevice.sctpCapabilities
: undefined,
})
);
const {
transportId,
iceCandidates,
iceParameters,
dtlsParameters,
sctpParameters,
} = response.body;
self.recvTransport = self.mediasoupDevice.createRecvTransport({
id: transportId,
iceParameters,
iceCandidates,
dtlsParameters: {
...dtlsParameters,
// Remote DTLS role. We know it's always 'auto' by default so, if
// we want, we can force local WebRTC transport to be 'client' by
// indicating 'server' here and vice-versa.
role: "auto",
},
iceServers: [],
sctpParameters,
additionalSettings: {
// TODO加密解密
encodedInsertableStreams: false,
},
});
self.recvTransport.on(
"connect",
(
{ dtlsParameters },
callback,
errback // eslint-disable-line no-shadow
) => {
self
.request(
protocol.buildMessage("media::transport::webrtc::connect", {
transportId: this.recvTransport.id,
dtlsParameters,
})
)
.then(callback)
.catch(errback);
}
);
}
this.produceAudio();
this.produceVideo();
}
/**
* 生产音频
* TODO重复点击
*/
async produceAudio() {
const self = this;
if (self.audioProduce && self.mediasoupDevice.canProduce("audio")) {
if (this.audioProducer) {
return;
}
let track;
try {
console.debug("打开麦克风");
// TODO设置配置
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
track = stream.getAudioTracks()[0];
this.audioProducer = await this.sendTransport.produce({
track,
codecOptions: {
opusStereo: 1,
opusDtx: 1,
},
// NOTE: for testing codec selection.
// codec : this._mediasoupDevice.rtpCapabilities.codecs
// .find((codec) => codec.mimeType.toLowerCase() === 'audio/pcma')
});
// TODO加密解密
// if (this._e2eKey && e2e.isSupported()) {
// e2e.setupSenderTransform(this._micProducer.rtpSender);
// }
this.audioProducer.on("transportclose", () => {
this.audioProducer = null;
});
this.audioProducer.on("trackended", () => {
console.warn("audio producer trackended", this.audioProducer);
this.closeAudioProducer().catch(() => {});
});
} catch (error) {
self.callbackError("麦克风打开异常", error);
if (track) {
track.stop();
}
}
} else {
self.callbackError("麦克风打开失败");
}
}
async closeAudioProducer() {
console.debug("closeAudioProducer()");
if (!this.audioProducer) {
return;
}
this.audioProducer.close();
try {
await this.request(
protocol.buildMessage("media::producer::close", {
producerId: this.audioProducer.id,
})
);
} catch (error) {
console.error("关闭麦克风异常", error);
}
this.audioProducer = null;
}
async pauseAudioProducer() {
console.debug("静音麦克风");
this.audioProducer.pause();
try {
await this.request(
protocol.buildMessage("media::producer::pause", {
producerId: this.audioProducer.id,
})
);
} catch (error) {
console.error("静音麦克风异常", error);
// TODO异常调用回调
}
}
async resumeAudioProducer() {
console.debug("恢复麦克风");
this.audioProducer.resume();
try {
await this.request(
protocol.buildMessage("media::producer::resume", {
producerId: this.audioProducer.id,
})
);
} catch (error) {
console.error("恢复麦克风异常", error);
}
}
/**
* 生产视频
* TODO重复点击
*/
async produceVideo() {}
/**
* 验证设备
*/
async checkDevice() {
const self = this;
if (
self.produce &&
navigator.mediaDevices &&
navigator.mediaDevices.getUserMedia &&
navigator.mediaDevices.enumerateDevices
) {
let audioEnabled = false;
let videoEnabled = false;
(await navigator.mediaDevices.enumerateDevices()).forEach((v) => {
console.debug("终端媒体设备", v, v.kind, v.label);
switch (v.kind) {
case "audioinput":
audioEnabled = true;
break;
case "videoinput":
videoEnabled = true;
break;
default:
console.debug("没有适配设备", v.kind, v.label);
break;
}
});
if (!audioEnabled && self.audioProduce) {
self.callbackError("没有音频媒体设备");
}
if (!videoEnabled && self.videoProduce) {
self.callbackError("没有视频媒体设备");
}
} else {
self.callbackError("没有媒体设备");
}
}
async restartIce() {
const self = this;
try {
if (self.sendTransport) {
const response = await self.request("media::ice::restart", {
roomId: self.roomId,
transportId: self.sendTransport.id,
});
const iceParameters = response.data.iceParameters;
await self.sendTransport.restartIce({ iceParameters });
}
if (self.recvTransport) {
const response = await self.request("media::ice::restart", {
roomId: self.roomId,
transportId: self.recvTransport.id,
});
const iceParameters = response.data.iceParameters;
await self.recvTransport.restartIce({ iceParameters });
}
} catch (error) {
self.callbackError("重启ICE失败", 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 };