[+] media::transport::webrtc::connect
This commit is contained in:
@@ -1,40 +1,5 @@
|
||||
/**
|
||||
* 桃夭配置
|
||||
*/
|
||||
|
||||
/**
|
||||
* 信令配置
|
||||
* TODO:合并到taoyao
|
||||
*/
|
||||
const config = {
|
||||
// 终端标识
|
||||
clientId: "taoyao",
|
||||
// 信令服务地址
|
||||
host: "localhost",
|
||||
port: "8888",
|
||||
// 终端名称
|
||||
name: "taoyao-client-web",
|
||||
// 终端版本
|
||||
version: "1.0.0",
|
||||
// 日志级别
|
||||
logLevel: "DEBUG",
|
||||
// 帐号密码
|
||||
username: "taoyao",
|
||||
password: "taoyao",
|
||||
signal: function () {
|
||||
return `wss://${this.host}:${this.port}/websocket.signal`;
|
||||
},
|
||||
// 媒体配置
|
||||
audio: {},
|
||||
video: {},
|
||||
// WebRTC配置
|
||||
webrtc: {},
|
||||
// 媒体服务配置
|
||||
mediaServerList: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* 信令协议
|
||||
* 信令
|
||||
*/
|
||||
const protocol = {
|
||||
// 当前索引
|
||||
@@ -46,25 +11,28 @@ const protocol = {
|
||||
/**
|
||||
* @returns 索引
|
||||
*/
|
||||
buildId: function () {
|
||||
if (this.index++ >= this.maxIndex) {
|
||||
this.index = this.minIndex;
|
||||
buildId() {
|
||||
const self = this;
|
||||
if (self.index++ >= self.maxIndex) {
|
||||
self.index = self.minIndex;
|
||||
}
|
||||
return Date.now() + "" + this.index;
|
||||
return Date.now() + "" + self.index;
|
||||
},
|
||||
/**
|
||||
* 生成信令消息
|
||||
*
|
||||
* @param {*} signal 信令标识
|
||||
* @param {*} body 信令消息
|
||||
* @param {*} id ID
|
||||
* @param {*} body 消息主体
|
||||
* @param {*} id 消息标识
|
||||
* @param {*} v 消息版本
|
||||
*
|
||||
* @returns 信令消息
|
||||
*/
|
||||
buildMessage: function (signal, body = {}, id) {
|
||||
let message = {
|
||||
buildMessage(signal, body = {}, id, v) {
|
||||
if (!signal) {
|
||||
throw new Error("信令标识缺失");
|
||||
}
|
||||
const message = {
|
||||
header: {
|
||||
v: config.version,
|
||||
v: v || "1.0.0",
|
||||
id: id || this.buildId(),
|
||||
signal: signal,
|
||||
},
|
||||
@@ -75,7 +43,7 @@ const protocol = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认音频配置
|
||||
* 音频默认配置
|
||||
*/
|
||||
const defaultAudioConfig = {
|
||||
// 设备
|
||||
@@ -87,7 +55,7 @@ const defaultAudioConfig = {
|
||||
// 采样数:16
|
||||
sampleSize: 16,
|
||||
// 采样率:8000|16000|32000|48000
|
||||
sampleRate: 32000,
|
||||
sampleRate: 48000,
|
||||
// 声道数量:1|2
|
||||
channelCount: 1,
|
||||
// 是否开启自动增益:true|false
|
||||
@@ -101,15 +69,15 @@ const defaultAudioConfig = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认视频配置
|
||||
* 视频默认配置
|
||||
*/
|
||||
const defaultVideoConfig = {
|
||||
// 设备
|
||||
// deviceId: '',
|
||||
// 宽度
|
||||
width: 1280,
|
||||
width: { min: 720, ideal: 1280, max: 4096 },
|
||||
// 高度
|
||||
height: 720,
|
||||
height: { min: 480, ideal: 720, max: 2160 },
|
||||
// 帧率
|
||||
frameRate: 24,
|
||||
// 选摄像头:user|left|right|environment
|
||||
@@ -117,11 +85,11 @@ const defaultVideoConfig = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认RTCPeerConnection配置
|
||||
* RTCPeerConnection默认配置
|
||||
*/
|
||||
const defaultRTCPeerConnectionConfig = {
|
||||
// ICE代理的服务器
|
||||
iceServers: null,
|
||||
iceServers: [],
|
||||
// 传输通道绑定策略:balanced|max-compat|max-bundle
|
||||
bundlePolicy: "balanced",
|
||||
// RTCP多路复用策略:require|negotiate
|
||||
@@ -133,7 +101,6 @@ const defaultRTCPeerConnectionConfig = {
|
||||
};
|
||||
|
||||
export {
|
||||
config,
|
||||
protocol,
|
||||
defaultAudioConfig,
|
||||
defaultVideoConfig,
|
||||
|
||||
@@ -15,9 +15,10 @@ export default defineComponent({
|
||||
this._externalVideo.setAttribute("playsinline", "");
|
||||
this._externalVideo.src = EXTERNAL_VIDEO_SRC;
|
||||
|
||||
// TODO:关闭摄像头、视频、音频
|
||||
this._externalVideo
|
||||
.play()
|
||||
.catch((error) => logger.warn("externalVideo.play() failed:%o", error));
|
||||
.catch((error) => console.warn("externalVideo.play() failed:%o", error));
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* 日志
|
||||
*/
|
||||
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 };
|
||||
@@ -1,34 +1,35 @@
|
||||
<!-- 房间设置 -->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="localVisible"
|
||||
@open="init"
|
||||
width="30%"
|
||||
:show-close="false"
|
||||
center
|
||||
width="30%"
|
||||
title="房间设置"
|
||||
@open="init"
|
||||
:show-close="false"
|
||||
v-model="localVisible"
|
||||
>
|
||||
<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-select v-model="room.roomId" placeholder="房间标识">
|
||||
<el-option
|
||||
v-for="value in rooms"
|
||||
:key="value.id"
|
||||
:key="value.roomId"
|
||||
:label="value.name"
|
||||
:value="value.id"
|
||||
:value="value.roomId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="创建房间" name="create">
|
||||
<el-form-item label="媒体服务">
|
||||
<el-select v-model="room.mediaId" placeholder="媒体服务">
|
||||
<el-select v-model="room.mediaId" placeholder="媒体服务标识">
|
||||
<el-option
|
||||
v-for="value in config.mediaServerList"
|
||||
:key="value.name"
|
||||
:label="value.name"
|
||||
:value="value.name"
|
||||
v-for="value in taoyao.mediaServerList"
|
||||
:key="value.mediaId"
|
||||
:label="value.mediaId"
|
||||
:value="value.mediaId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -42,19 +43,23 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="setting">设置</el-button>
|
||||
<el-button type="primary" @click="enter" v-if="activeName === 'enter'"
|
||||
>进入</el-button
|
||||
>
|
||||
<el-button type="primary" @click="create" v-if="activeName === 'create'"
|
||||
>创建</el-button
|
||||
>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { config, protocol } from "./Config.js";
|
||||
import { protocol } from "./Config.js";
|
||||
|
||||
export default {
|
||||
name: "SettingRoom",
|
||||
data() {
|
||||
return {
|
||||
config,
|
||||
room: {},
|
||||
rooms: [],
|
||||
activeName: "enter",
|
||||
@@ -72,30 +77,28 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
let response = await this.taoyao.request(
|
||||
const response = await this.taoyao.request(
|
||||
protocol.buildMessage("room::list")
|
||||
);
|
||||
this.rooms = response.body;
|
||||
},
|
||||
async setting() {
|
||||
let roomId;
|
||||
async enter() {
|
||||
await this.taoyao.request(
|
||||
protocol.buildMessage("room::enter", {
|
||||
...this.room,
|
||||
})
|
||||
);
|
||||
this.localVisible = false;
|
||||
if (this.activeName === "enter") {
|
||||
roomId = this.room.id;
|
||||
await this.taoyao.request(
|
||||
protocol.buildMessage("room::enter", {
|
||||
...this.room,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
let response = await this.taoyao.request(
|
||||
protocol.buildMessage("room::create", {
|
||||
...this.room,
|
||||
})
|
||||
);
|
||||
roomId = response.body.id;
|
||||
}
|
||||
this.$emit("buildMedia", roomId);
|
||||
this.$emit("buildMedia", this.room.roomId);
|
||||
},
|
||||
async create() {
|
||||
const response = await this.taoyao.request(
|
||||
protocol.buildMessage("room::create", {
|
||||
...this.room,
|
||||
})
|
||||
);
|
||||
this.localVisible = false;
|
||||
this.$emit("buildMedia", response.body.roomId);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,37 +1,47 @@
|
||||
<!-- 终端设置 -->
|
||||
<template>
|
||||
<el-dialog v-model="localVisible" title="终端设置" width="30%" :show-close="false" center>
|
||||
<el-dialog
|
||||
center
|
||||
width="30%"
|
||||
title="终端设置"
|
||||
:show-close="false"
|
||||
v-model="localVisible"
|
||||
>
|
||||
<el-form ref="SettingSignalForm">
|
||||
<el-form-item label="终端名称">
|
||||
<el-input v-model="config.clientId" 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-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>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="setting">设置</el-button>
|
||||
<el-button type="primary" @click="connect">连接</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { config } from "./Config.js";
|
||||
|
||||
export default {
|
||||
name: "SettingSignal",
|
||||
data: () => {
|
||||
data() {
|
||||
return {
|
||||
config,
|
||||
config: {
|
||||
clientId: "taoyao",
|
||||
host: "localhost",
|
||||
port: 8888,
|
||||
username: "taoyao",
|
||||
password: "taoyao",
|
||||
},
|
||||
localVisible: true,
|
||||
};
|
||||
},
|
||||
@@ -44,9 +54,9 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setting: function () {
|
||||
connect() {
|
||||
this.localVisible = false;
|
||||
this.$emit("buildSignal");
|
||||
this.$emit("buildSignal", this.config);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
/**
|
||||
* 桃夭
|
||||
*/
|
||||
import { Logger } from "./Logger.js";
|
||||
import { TaoyaoClient } from "./TaoyaoClient.js";
|
||||
import * as mediasoupClient from "mediasoup-client";
|
||||
import {
|
||||
config,
|
||||
protocol,
|
||||
defaultAudioConfig,
|
||||
defaultVideoConfig,
|
||||
defaultRTCPeerConnectionConfig,
|
||||
} from "./Config.js";
|
||||
|
||||
// 日志
|
||||
const logger = new Logger();
|
||||
|
||||
const PC_PROPRIETARY_CONSTRAINTS = {
|
||||
optional : [ { googDscp: true } ]
|
||||
};
|
||||
|
||||
/**
|
||||
* 信令通道
|
||||
*/
|
||||
@@ -48,13 +40,11 @@ const signalChannel = {
|
||||
minReconnectionDelay: 5 * 1000,
|
||||
// 最大重连时间
|
||||
maxReconnectionDelay: 60 * 1000,
|
||||
// 重连失败时间增长倍数
|
||||
reconnectionDelayGrowFactor: 2,
|
||||
/**
|
||||
* 心跳
|
||||
*/
|
||||
heartbeat: function () {
|
||||
let self = this;
|
||||
heartbeat() {
|
||||
const self = this;
|
||||
if (self.heartbeatTimer) {
|
||||
clearTimeout(self.heartbeatTimer);
|
||||
}
|
||||
@@ -70,7 +60,7 @@ const signalChannel = {
|
||||
);
|
||||
self.heartbeat();
|
||||
} else {
|
||||
logger.warn("发送心跳失败", self.channel);
|
||||
console.warn("发送心跳失败", self.channel);
|
||||
}
|
||||
}, self.heartbeatTime);
|
||||
},
|
||||
@@ -83,27 +73,30 @@ const signalChannel = {
|
||||
*
|
||||
* @returns Promise
|
||||
*/
|
||||
connect: async function (address, callback, reconnection = true) {
|
||||
let self = this;
|
||||
async connect(address, callback, reconnection = true) {
|
||||
const self = this;
|
||||
if (self.channel && self.channel.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
self.address = address;
|
||||
self.callback = callback;
|
||||
self.reconnection = reconnection;
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.debug("连接信令通道", address);
|
||||
console.debug("连接信令通道", address);
|
||||
self.channel = new WebSocket(address);
|
||||
self.channel.onopen = async function (e) {
|
||||
logger.debug("打开信令通道", e);
|
||||
console.debug("打开信令通道", e);
|
||||
// 注册终端
|
||||
const battery = await navigator.getBattery();
|
||||
self.push(
|
||||
protocol.buildMessage("client::register", {
|
||||
ip: "localhost",
|
||||
clientId: config.clientId,
|
||||
clientId: self.taoyao.clientId,
|
||||
signal: 100,
|
||||
battery: battery.level * 100,
|
||||
charging: battery.charging,
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
username: self.taoyao.username,
|
||||
password: self.taoyao.password,
|
||||
})
|
||||
);
|
||||
// 重置时间
|
||||
@@ -114,14 +107,14 @@ const signalChannel = {
|
||||
resolve(e);
|
||||
};
|
||||
self.channel.onclose = function (e) {
|
||||
logger.error("信令通道关闭", self.channel, e);
|
||||
console.error("信令通道关闭", self.channel, e);
|
||||
if (self.reconnection) {
|
||||
self.reconnect();
|
||||
}
|
||||
reject(e);
|
||||
};
|
||||
self.channel.onerror = function (e) {
|
||||
logger.error("信令通道异常", self.channel, e);
|
||||
console.error("信令通道异常", self.channel, e);
|
||||
if (self.reconnection) {
|
||||
self.reconnect();
|
||||
}
|
||||
@@ -134,24 +127,24 @@ const signalChannel = {
|
||||
* 3. 如果前面所有回调没有返回true执行默认回调。
|
||||
*/
|
||||
self.channel.onmessage = function (e) {
|
||||
logger.debug("信令通道消息", e.data);
|
||||
console.debug("信令通道消息", e.data);
|
||||
let done = false;
|
||||
let data = JSON.parse(e.data);
|
||||
const message = JSON.parse(e.data);
|
||||
// 请求回调
|
||||
if (self.callbackMapping.has(data.header.id)) {
|
||||
if (self.callbackMapping.has(message.header.id)) {
|
||||
try {
|
||||
done = self.callbackMapping.get(data.header.id)(data);
|
||||
done = self.callbackMapping.get(message.header.id)(message);
|
||||
} finally {
|
||||
self.callbackMapping.delete(data.header.id);
|
||||
self.callbackMapping.delete(message.header.id);
|
||||
}
|
||||
}
|
||||
// 全局回调
|
||||
if (!done && self.callback) {
|
||||
done = self.callback(data);
|
||||
done = self.callback(message);
|
||||
}
|
||||
// 默认回调
|
||||
if (!done) {
|
||||
self.defaultCallback(data);
|
||||
self.defaultCallback(message);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -159,8 +152,8 @@ const signalChannel = {
|
||||
/**
|
||||
* 重连
|
||||
*/
|
||||
reconnect: function () {
|
||||
let self = this;
|
||||
reconnect() {
|
||||
const self = this;
|
||||
if (self.lockReconnect) {
|
||||
return;
|
||||
}
|
||||
@@ -175,55 +168,53 @@ const signalChannel = {
|
||||
}
|
||||
// 打开定时重连
|
||||
self.reconnectTimer = setTimeout(function () {
|
||||
logger.info("信令通道重连", self.address);
|
||||
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;
|
||||
}
|
||||
self.connectionTimeout = Math.min(
|
||||
self.connectionTimeout + self.minReconnectionDelay,
|
||||
self.maxReconnectionDelay
|
||||
);
|
||||
},
|
||||
/**
|
||||
* 异步请求
|
||||
*
|
||||
* @param {*} data 消息内容
|
||||
* @param {*} callback 注册回调
|
||||
* @param {*} message 消息
|
||||
* @param {*} callback 回调
|
||||
*/
|
||||
push: function (data, callback) {
|
||||
push(message, callback) {
|
||||
const self = this;
|
||||
// 注册回调
|
||||
let self = this;
|
||||
if (callback) {
|
||||
self.callbackMapping.set(data.header.id, callback);
|
||||
self.callbackMapping.set(message.header.id, callback);
|
||||
}
|
||||
// 发送消息
|
||||
self.channel.send(JSON.stringify(data));
|
||||
self.channel.send(JSON.stringify(message));
|
||||
},
|
||||
/**
|
||||
* 同步请求
|
||||
*
|
||||
* @param {*} data 消息内容
|
||||
* @param {*} message 消息
|
||||
*
|
||||
* @returns Promise
|
||||
*/
|
||||
request: async function (data) {
|
||||
let self = this;
|
||||
async request(message) {
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
let callback = false;
|
||||
let done = false;
|
||||
// 设置回调
|
||||
self.callbackMapping.set(data.header.id, (response) => {
|
||||
callback = true;
|
||||
self.callbackMapping.set(message.header.id, (response) => {
|
||||
resolve(response);
|
||||
done = true;
|
||||
return true;
|
||||
});
|
||||
// 发送请求
|
||||
self.channel.send(JSON.stringify(data));
|
||||
self.channel.send(JSON.stringify(message));
|
||||
// 设置超时
|
||||
setTimeout(() => {
|
||||
if (!callback) {
|
||||
reject("请求超时", data);
|
||||
if (!done) {
|
||||
reject("请求超时", message);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
@@ -231,54 +222,62 @@ const signalChannel = {
|
||||
/**
|
||||
* 关闭通道
|
||||
*/
|
||||
close: function () {
|
||||
close() {
|
||||
let self = this;
|
||||
self.reconnection = false;
|
||||
self.channel.close();
|
||||
clearTimeout(self.heartbeatTimer);
|
||||
clearTimeout(self.reconnectTimer);
|
||||
},
|
||||
/**
|
||||
* 默认回调
|
||||
*
|
||||
* @param {*} data 消息内容
|
||||
* @param {*} message 消息
|
||||
*/
|
||||
defaultCallback: function (data) {
|
||||
defaultCallback(message) {
|
||||
let self = this;
|
||||
logger.debug("没有适配信令消息默认处理", data);
|
||||
switch (data.header.signal) {
|
||||
console.debug("没有适配信令消息执行默认处理", message);
|
||||
switch (message.header.signal) {
|
||||
case "client::config":
|
||||
self.defaultClientConfig(data);
|
||||
self.defaultClientConfig(message);
|
||||
break;
|
||||
case "client::reboot":
|
||||
self.defaultClientReboot(data);
|
||||
self.defaultClientReboot(message);
|
||||
break;
|
||||
case "client::register":
|
||||
logger.info("桃夭终端注册成功");
|
||||
console.info("终端注册成功");
|
||||
break;
|
||||
case "platform::error":
|
||||
logger.error("信令发生错误", data);
|
||||
console.error("信令异常", message);
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 默认配置回调
|
||||
* 配置默认回调
|
||||
*
|
||||
* @param {*} data 消息内容
|
||||
* @param {*} message 消息
|
||||
*/
|
||||
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);
|
||||
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;
|
||||
self.taoyao.mediaServerList = message.body.media.mediaServerList;
|
||||
console.debug(
|
||||
"终端配置",
|
||||
self.taoyao.audio,
|
||||
self.taoyao.video,
|
||||
self.taoyao.webrtc,
|
||||
self.taoyao.mediaServerList
|
||||
);
|
||||
},
|
||||
/**
|
||||
* 默认终端重启回调
|
||||
* 终端重启默认回调
|
||||
*
|
||||
* @param {*} data 消息内容
|
||||
* @param {*} message 消息
|
||||
*/
|
||||
defaultClientReboot: function (data) {
|
||||
logger.info("重启终端");
|
||||
defaultClientReboot(message) {
|
||||
console.info("重启终端");
|
||||
location.reload();
|
||||
},
|
||||
};
|
||||
@@ -287,66 +286,91 @@ const signalChannel = {
|
||||
* 桃夭
|
||||
*/
|
||||
class Taoyao {
|
||||
// 房间标识
|
||||
roomId;
|
||||
// 终端标识
|
||||
clientId;
|
||||
// 信令地址
|
||||
host;
|
||||
// 信令端口
|
||||
port;
|
||||
// 信令帐号
|
||||
username;
|
||||
// 信令密码
|
||||
password;
|
||||
// 音频媒体配置
|
||||
audio;
|
||||
// 视频媒体配置
|
||||
video;
|
||||
// WebRTC配置
|
||||
webrtc;
|
||||
// 媒体服务配置
|
||||
mediaServerList;
|
||||
// 发送信令
|
||||
push;
|
||||
// 房间ID
|
||||
roomId;
|
||||
// 请求信令
|
||||
request;
|
||||
// 本地视频
|
||||
localVideo;
|
||||
// 本地终端
|
||||
localClient;
|
||||
// 远程终端
|
||||
remoteClients = new Map();
|
||||
// 媒体通道
|
||||
sendTransport;
|
||||
recvTransport;
|
||||
// 信令通道
|
||||
signalChannel;
|
||||
// 发送媒体通道
|
||||
sendTransport;
|
||||
// 接收媒体通道
|
||||
recvTransport;
|
||||
// 媒体设备
|
||||
mediasoupDevice;
|
||||
// 是否消费
|
||||
consume = true;
|
||||
consume;
|
||||
// 是否生产
|
||||
produce = true;
|
||||
// 强制使用TCP
|
||||
forceTcp = false;
|
||||
// 使用数据通道
|
||||
useDataChannel = true;
|
||||
produce;
|
||||
// 是否生产音频
|
||||
audioProduce = true && this.produce;
|
||||
audioProduce;
|
||||
// 是否生成视频
|
||||
videoProduce = true && this.produce;
|
||||
videoProduce;
|
||||
// 强制使用TCP
|
||||
forceTcp;
|
||||
// 使用数据通道
|
||||
useDataChannel;
|
||||
// 音频生产者
|
||||
audioProducer;
|
||||
// 视频生产者
|
||||
videoProducer;
|
||||
// 消费者
|
||||
// 数据生产者
|
||||
dataChannnelProducer;
|
||||
// 媒体消费者
|
||||
consumers = new Map();
|
||||
// 数据消费者
|
||||
dataConsumers = new Map();
|
||||
|
||||
constructor({
|
||||
roomId,
|
||||
peerId,
|
||||
displayName,
|
||||
device,
|
||||
handlerName,
|
||||
useSimulcast,
|
||||
useSharingSimulcast,
|
||||
forceTcp,
|
||||
produce,
|
||||
consume,
|
||||
forceH264,
|
||||
forceVP9,
|
||||
svc,
|
||||
datachannel,
|
||||
externalVideo,
|
||||
e2eKey,
|
||||
consumerReplicas
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,161 +380,366 @@ class Taoyao {
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
buildSignal = async function (callback) {
|
||||
signalChannel.taoyao = this;
|
||||
this.signalChannel = signalChannel;
|
||||
async buildSignal(callback) {
|
||||
const self = this;
|
||||
signalChannel.taoyao = self;
|
||||
self.signalChannel = signalChannel;
|
||||
// 不能直接this.push = this.signalChannel.push这样导致this对象错误
|
||||
this.push = function (data, pushCallback) {
|
||||
this.signalChannel.push(data, pushCallback);
|
||||
self.push = function (data, pushCallback) {
|
||||
self.signalChannel.push(data, pushCallback);
|
||||
};
|
||||
this.request = async function (data) {
|
||||
return await this.signalChannel.request(data);
|
||||
self.request = async function (data) {
|
||||
return await self.signalChannel.request(data);
|
||||
};
|
||||
return this.signalChannel.connect(config.signal(), callback);
|
||||
};
|
||||
return self.signalChannel.connect(
|
||||
`wss://${self.host}:${self.port}/websocket.signal`,
|
||||
callback
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 打开媒体通道
|
||||
* TODO:共享 navigator.mediaDevices.getDisplayMedia();
|
||||
*/
|
||||
buildMedia = async function (roomId) {
|
||||
async buildMedia(roomId) {
|
||||
let self = this;
|
||||
// 释放资源
|
||||
self.closeMedia();
|
||||
if (roomId) {
|
||||
self.roomId = roomId;
|
||||
}
|
||||
self.mediasoupDevice = new mediasoupClient.Device();
|
||||
const response = await self.request(protocol.buildMessage(
|
||||
"router::rtp::capabilities",
|
||||
{ roomId : roomId || self.roomId }
|
||||
));
|
||||
const response = await self.request(
|
||||
protocol.buildMessage("media::router::rtp::capabilities", {
|
||||
roomId: roomId,
|
||||
})
|
||||
);
|
||||
const routerRtpCapabilities = response.body;
|
||||
self.mediasoupDevice.load({ routerRtpCapabilities });
|
||||
self.produce();
|
||||
};
|
||||
self.produceMedia();
|
||||
}
|
||||
/**
|
||||
* 生产媒体
|
||||
* TODO:验证API试试修改媒体
|
||||
* audioTrack.getSettings
|
||||
* audioTrack.getCapabilities
|
||||
* audioTrack.applyCapabilities
|
||||
*/
|
||||
produceMedia = async function() {
|
||||
// 打开媒体:TODO:参数
|
||||
async produceMedia() {
|
||||
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("transport::webrtc::create", {
|
||||
roomId : self.roomId,
|
||||
forceTcp : self.forceTcp,
|
||||
producing : true,
|
||||
consuming : false,
|
||||
sctpCapabilities : self.useDataChannel ? self.mediasoupDevice.sctpCapabilities : undefined
|
||||
});
|
||||
self.checkDevice();
|
||||
// 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 {
|
||||
id,
|
||||
transportId,
|
||||
iceParameters,
|
||||
iceCandidates,
|
||||
dtlsParameters,
|
||||
sctpParameters
|
||||
} = transportInfo;
|
||||
|
||||
self.sendTransport = self.mediasoupDevice.createSendTransport(
|
||||
{
|
||||
id,
|
||||
iceParameters,
|
||||
iceCandidates,
|
||||
dtlsParameters :
|
||||
{
|
||||
...dtlsParameters,
|
||||
role : 'auto'
|
||||
},
|
||||
sctpParameters,
|
||||
iceServers : [],
|
||||
proprietaryConstraints : PC_PROPRIETARY_CONSTRAINTS,
|
||||
additionalSettings :
|
||||
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: true }
|
||||
});
|
||||
|
||||
{ encodedInsertableStreams: false },
|
||||
});
|
||||
self.sendTransport.on(
|
||||
'connect', ({ dtlsParameters }, callback, errback) =>
|
||||
{
|
||||
self.request(
|
||||
'transport::webrtc::connect',
|
||||
{
|
||||
transportId : self.sendTransport.id,
|
||||
dtlsParameters
|
||||
})
|
||||
"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, rtpParameters, appData }, callback, errback) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
"produce",
|
||||
async ({ kind, appData, rtpParameters }, callback, errback) => {
|
||||
try {
|
||||
const { id } = await self.request(
|
||||
'produce',
|
||||
{
|
||||
transportId : self.sendTransport.id,
|
||||
protocol.buildMessage("media::produce", {
|
||||
kind,
|
||||
appData,
|
||||
transportId: self.sendTransport.id,
|
||||
rtpParameters,
|
||||
appData
|
||||
});
|
||||
|
||||
})
|
||||
);
|
||||
callback({ id });
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
} 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();
|
||||
}
|
||||
/**
|
||||
* 生产音频
|
||||
*/
|
||||
async produceAudio() {
|
||||
const self = this;
|
||||
if (this.produceAudio && this.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')
|
||||
});
|
||||
|
||||
sef.sendTransport.on('producedata', async (
|
||||
{
|
||||
sctpStreamParameters,
|
||||
label,
|
||||
protocol,
|
||||
appData
|
||||
},
|
||||
callback,
|
||||
errback
|
||||
) =>
|
||||
{
|
||||
logger.debug(
|
||||
'"producedata" event: [sctpStreamParameters:%o, appData:%o]',
|
||||
sctpStreamParameters, appData);
|
||||
// TODO:加密解密
|
||||
// if (this._e2eKey && e2e.isSupported()) {
|
||||
// e2e.setupSenderTransform(this._micProducer.rtpSender);
|
||||
// }
|
||||
|
||||
try
|
||||
{
|
||||
const { id } = await self.request(
|
||||
'produceData',
|
||||
{
|
||||
transportId : self.sendTransport.id,
|
||||
sctpStreamParameters,
|
||||
label,
|
||||
protocol,
|
||||
appData
|
||||
});
|
||||
this.audioProducer.on("transportclose", () => {
|
||||
this.audioProducer = null;
|
||||
});
|
||||
|
||||
callback({ id });
|
||||
this.audioProducer.on("trackended", () => {
|
||||
console.warn("audio producer trackended", this.audioProducer);
|
||||
this.closeAudioProducer().catch(() => {});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("打开麦克风异常", error);
|
||||
if (track) {
|
||||
track.stop();
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
errback(error);
|
||||
}
|
||||
} else {
|
||||
console.warn("音频打开失败");
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生产视频
|
||||
*/
|
||||
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) {
|
||||
throw new Error("没有音频媒体设备");
|
||||
}
|
||||
if (!videoEnabled && self.videoProduce) {
|
||||
throw new Error("没有视频媒体设备");
|
||||
}
|
||||
} else {
|
||||
throw new Error("没有媒体设备");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 关闭媒体
|
||||
*/
|
||||
closeMedia = function() {
|
||||
closeMedia = function () {
|
||||
let self = this;
|
||||
if (self.sendTransport) {
|
||||
self.sendTransport.close();
|
||||
}
|
||||
if (self.recvTransport) {
|
||||
self.recvTransport.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 关闭
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user