[+] media::transport::webrtc::connect

This commit is contained in:
acgist
2023-02-19 11:57:09 +08:00
parent 9023883c5b
commit fd505dfbd2
105 changed files with 1511 additions and 2116 deletions

View File

@@ -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,

View File

@@ -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>

View File

@@ -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 };

View File

@@ -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);
},
},
};

View File

@@ -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);
},
},
};

View File

@@ -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();
}
}
}
};
/**
* 关闭
*/