[*]
This commit is contained in:
@@ -1,31 +1,33 @@
|
||||
/**
|
||||
* 服务配置
|
||||
*/
|
||||
const os = require("os");
|
||||
|
||||
/**
|
||||
* 配置
|
||||
*/
|
||||
module.exports = {
|
||||
// 服务名称
|
||||
name: "taoyao-media-server",
|
||||
// 交互式命令行
|
||||
command: true,
|
||||
// 日志级别
|
||||
logLevel: 'DEBUG',
|
||||
logLevel: "DEBUG",
|
||||
// 信令服务
|
||||
https: {
|
||||
// 信令服务地址端口
|
||||
listenIp: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
|
||||
listenPort: process.env.HTTPS_LISTEN_PORT || 4443,
|
||||
// WebSocket连接密码
|
||||
// 信令服务安全配置
|
||||
username: "taoyao",
|
||||
password: "taoyao",
|
||||
// 信令服务证书配置
|
||||
tls: {
|
||||
cert: process.env.HTTPS_CERT_PUBLIC_KEY || `${__dirname}/certs/publicKey.pem`,
|
||||
key: process.env.HTTPS_CERT_PRIVATE_KEY || `${__dirname}/certs/privateKey.pem`,
|
||||
}
|
||||
},
|
||||
},
|
||||
// Mediasoup
|
||||
mediasoup: {
|
||||
// 按照CPU数量配置进程数量
|
||||
numWorkers: Object.keys(os.cpus()).length,
|
||||
// 配置Worker进程数量
|
||||
workerSize: Object.keys(os.cpus()).length,
|
||||
// Worker:https://mediasoup.org/documentation/v3/mediasoup/api/#WorkerSettings
|
||||
workerSettings: {
|
||||
logLevel: "info",
|
||||
@@ -42,10 +44,10 @@ module.exports = {
|
||||
"srtp",
|
||||
"score",
|
||||
"message",
|
||||
"simulcast"
|
||||
"simulcast",
|
||||
],
|
||||
rtcMinPort: process.env.MEDIASOUP_MIN_PORT || 40000,
|
||||
rtcMaxPort: process.env.MEDIASOUP_MAX_PORT || 49999
|
||||
rtcMaxPort: process.env.MEDIASOUP_MAX_PORT || 49999,
|
||||
},
|
||||
// Router:https://mediasoup.org/documentation/v3/mediasoup/api/#RouterOptions
|
||||
routerOptions: {
|
||||
@@ -54,7 +56,7 @@ module.exports = {
|
||||
kind: "audio",
|
||||
mimeType: "audio/opus",
|
||||
clockRate: 48000,
|
||||
channels: 2
|
||||
channels: 2,
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
@@ -62,7 +64,7 @@ module.exports = {
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
"x-google-start-bitrate": 1000,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
@@ -70,8 +72,8 @@ module.exports = {
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
"profile-id": 2,
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
"x-google-start-bitrate": 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
@@ -81,8 +83,8 @@ module.exports = {
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "4d0032",
|
||||
"level-asymmetry-allowed": 1,
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
"x-google-start-bitrate": 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
@@ -92,10 +94,10 @@ module.exports = {
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "42e01f",
|
||||
"level-asymmetry-allowed": 1,
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
"x-google-start-bitrate": 1000,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// WebRtcServer:https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcServerOptions
|
||||
webRtcServerOptions: {
|
||||
@@ -105,16 +107,16 @@ module.exports = {
|
||||
ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
|
||||
port: 44444,
|
||||
// 公网地址
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
|
||||
},
|
||||
{
|
||||
protocol: "tcp",
|
||||
ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
|
||||
port: 44444,
|
||||
// 公网地址
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP
|
||||
}
|
||||
]
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
|
||||
},
|
||||
],
|
||||
},
|
||||
// WebRtcTransport:https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
|
||||
webRtcTransportOptions: {
|
||||
@@ -122,13 +124,13 @@ module.exports = {
|
||||
{
|
||||
ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
|
||||
// 公网地址
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP
|
||||
}
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
|
||||
},
|
||||
],
|
||||
initialAvailableOutgoingBitrate: 1000000,
|
||||
minimumAvailableOutgoingBitrate: 600000,
|
||||
maxSctpMessageSize: 262144,
|
||||
maxIncomingBitrate: 1500000
|
||||
maxIncomingBitrate: 1500000,
|
||||
},
|
||||
// PlainTransport:https://mediasoup.org/documentation/v3/mediasoup/api/#PlainTransportOptions
|
||||
plainTransportOptions: {
|
||||
@@ -137,8 +139,8 @@ module.exports = {
|
||||
// 公网地址
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
|
||||
},
|
||||
maxSctpMessageSize: 262144
|
||||
}
|
||||
maxSctpMessageSize: 262144,
|
||||
},
|
||||
},
|
||||
wellcome: `<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -154,5 +156,5 @@ module.exports = {
|
||||
<p><a href="https://gitee.com/acgist/taoyao">taoyao-media-server</a></p>
|
||||
<p><a href="https://www.acgist.com">acgist</a></p>
|
||||
</body>
|
||||
</html>`
|
||||
</html>`,
|
||||
};
|
||||
|
||||
@@ -1,93 +1,93 @@
|
||||
const moment = require("moment");
|
||||
const config = require("./Config");
|
||||
|
||||
/**
|
||||
* 日志
|
||||
*/
|
||||
const moment = require('moment')
|
||||
const config = require("./Config");
|
||||
|
||||
class Logger {
|
||||
|
||||
// 名称
|
||||
name = config.name;
|
||||
// 级别
|
||||
level = [ "DEBUG", "INFO", "WARN", "ERROR", "OFF" ];
|
||||
level = ["DEBUG", "INFO", "WARN", "ERROR", "OFF"];
|
||||
// 级别索引
|
||||
levelIndex = this.level.indexOf(config.logLevel.toUpperCase());
|
||||
|
||||
constructor(prefix) {
|
||||
if (prefix) {
|
||||
this.name = this.name + ':' + prefix;
|
||||
this.name = this.name + " : " + prefix;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* debug
|
||||
*
|
||||
*
|
||||
* @param {...any} args 参数
|
||||
*
|
||||
*
|
||||
* @returns this
|
||||
*/
|
||||
debug(...args) {
|
||||
return this.log(console.debug, '37m', '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);
|
||||
return this.log(console.info, "32m", "INFO", args);
|
||||
}
|
||||
|
||||
/**
|
||||
* warn
|
||||
*
|
||||
*
|
||||
* @param {...any} args 参数
|
||||
*
|
||||
*
|
||||
* @returns this
|
||||
*/
|
||||
warn(...args) {
|
||||
return this.log(console.warn, '33m', '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);
|
||||
return this.log(console.error, "31m", "ERROR", args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志
|
||||
*
|
||||
* @param {*} out 输出位置
|
||||
*
|
||||
* @param {*} out 输出
|
||||
* @param {*} color 颜色
|
||||
* @param {*} level 级别
|
||||
* @param {*} args 参数
|
||||
*
|
||||
*
|
||||
* @returns this
|
||||
*/
|
||||
log(out, color, level, args) {
|
||||
if(!args || this.level.indexOf(level) < this.levelIndex) {
|
||||
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, ' ')}] : ${args[0]}\x1B[0m`, ...args.slice(1));
|
||||
} 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, ' ')}] : ${args[0]}\x1B[0m`);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 服务
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const ws = require("ws");
|
||||
const https = require("https");
|
||||
const mediasoup = require("mediasoup");
|
||||
// const mediasoup = require("mediasoup");
|
||||
const config = require("./Config");
|
||||
const Logger = require("./Logger");
|
||||
const Signal = require("./Signal");
|
||||
@@ -22,16 +20,16 @@ const signal = new Signal();
|
||||
const client = [];
|
||||
// Mediasoup Worker列表
|
||||
const mediasoupWorker = [];
|
||||
|
||||
// 配置名称
|
||||
process.title = config.name;
|
||||
|
||||
/**
|
||||
* 启动Mediasoup Worker
|
||||
*/
|
||||
async function buildMediasoupWorker() {
|
||||
const { numWorkers } = config.mediasoup;
|
||||
logger.info("启动Mediasoup Worker(%d)...", numWorkers);
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
const { workerSize } = config.mediasoup;
|
||||
logger.info("启动Mediasoup Worker", workerSize);
|
||||
for (let i = 0; i < workerSize; i++) {
|
||||
// 新建Worker
|
||||
const worker = await mediasoup.createWorker({
|
||||
logLevel: config.mediasoup.workerSettings.logLevel,
|
||||
@@ -41,10 +39,7 @@ async function buildMediasoupWorker() {
|
||||
});
|
||||
// 监听Worker事件
|
||||
worker.on("died", () => {
|
||||
logger.error(
|
||||
"Mediasoup Worker停止服务(两秒之后自动退出)... [PID:%d]",
|
||||
worker.pid
|
||||
);
|
||||
logger.warn("Mediasoup Worker停止服务", worker.pid);
|
||||
setTimeout(() => process.exit(1), 2000);
|
||||
});
|
||||
// 加入Worker队列
|
||||
@@ -53,7 +48,9 @@ async function buildMediasoupWorker() {
|
||||
if (process.env.MEDIASOUP_USE_WEBRTC_SERVER !== "false") {
|
||||
// 配置Worker端口
|
||||
const portIncrement = mediasoupWorker.length - 1;
|
||||
const webRtcServerOptions = JSON.parse(JSON.stringify(config.mediasoup.webRtcServerOptions));
|
||||
const webRtcServerOptions = JSON.parse(
|
||||
JSON.stringify(config.mediasoup.webRtcServerOptions)
|
||||
);
|
||||
for (const listenInfo of webRtcServerOptions.listenInfos) {
|
||||
listenInfo.port += portIncrement;
|
||||
}
|
||||
@@ -64,7 +61,7 @@ async function buildMediasoupWorker() {
|
||||
// 定时记录使用日志
|
||||
setInterval(async () => {
|
||||
const usage = await worker.getResourceUsage();
|
||||
logger.info("Mediasoup Worker使用情况 [pid:%d]: %o", worker.pid, usage);
|
||||
logger.info("Mediasoup Worker使用情况", worker.pid, usage);
|
||||
}, 120 * 1000);
|
||||
}
|
||||
}
|
||||
@@ -75,28 +72,33 @@ async function buildMediasoupWorker() {
|
||||
async function buildSignalServer() {
|
||||
const tls = {
|
||||
cert: fs.readFileSync(config.https.tls.cert),
|
||||
key: fs.readFileSync(config.https.tls.key),
|
||||
key: fs.readFileSync(config.https.tls.key),
|
||||
};
|
||||
logger.info("配置HTTPS服务...");
|
||||
// 配置HTTPS
|
||||
httpsServer = https.createServer(tls, (request, response) => {
|
||||
response.writeHead(200);
|
||||
response.end(config.wellcome);
|
||||
});
|
||||
logger.info("配置WebSocket服务...");
|
||||
// 配置WebSocket
|
||||
webSocketServer = new ws.Server({ server: httpsServer });
|
||||
webSocketServer.on("connection", (session) => {
|
||||
logger.info("打开信令通道: %s", session._socket.remoteAddress);
|
||||
session.datetime = new Date().getTime();
|
||||
logger.info("打开信令通道", session._socket.remoteAddress);
|
||||
session.datetime = Date.now();
|
||||
session.authorize = false;
|
||||
client.push(session);
|
||||
session.on("close", (code) => {
|
||||
logger.info("关闭信令通道: %o", code);
|
||||
logger.info("关闭信令通道", session._socket.remoteAddress, code);
|
||||
});
|
||||
session.on("error", (error) => {
|
||||
logger.error("信令通道异常: %o", error);
|
||||
logger.error("信令通道异常", session._socket.remoteAddress, error);
|
||||
});
|
||||
session.on("message", (message) => {
|
||||
onmessage(message, session);
|
||||
logger.debug("处理信令消息", message);
|
||||
try {
|
||||
signal.on(JSON.parse(message), session);
|
||||
} catch (error) {
|
||||
logger.error("处理信令消息异常", message, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
// 打开监听
|
||||
@@ -109,11 +111,32 @@ async function buildSignalServer() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时清理无效信令终端
|
||||
*/
|
||||
async function buildClientInterval() {
|
||||
setInterval(() => {
|
||||
const datetime = Date.now();
|
||||
const oldLength = client.length;
|
||||
for (let i = 0; i < client.length; i++) {
|
||||
const session = client[i];
|
||||
// 超过五秒自动关闭
|
||||
if (datetime - session.datetime >= 5000) {
|
||||
client.splice(i, 1);
|
||||
session.close();
|
||||
i--;
|
||||
}
|
||||
}
|
||||
const newLength = client.length;
|
||||
logger.info("定时清理无效信令终端", oldLength - newLength);
|
||||
}, 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 交互式控制台
|
||||
*/
|
||||
async function buildCommandConsole() {
|
||||
if(!config.command) {
|
||||
async function buildCommandConsole() {
|
||||
if (!config.command) {
|
||||
return;
|
||||
}
|
||||
process.stdin.setEncoding("UTF-8");
|
||||
@@ -132,7 +155,7 @@ async function buildSignalServer() {
|
||||
case "":
|
||||
default: {
|
||||
logger.warn(`未知命令:'${command}'`);
|
||||
logger.info("查询命令:`h` | `help`");
|
||||
logger.info("查询命令:'h' | 'help'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -141,82 +164,17 @@ async function buildSignalServer() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时清理无效信令终端
|
||||
*/
|
||||
async function buildClientInterval() {
|
||||
setInterval(() => {
|
||||
const datetime = new Date().getTime();
|
||||
const oldLength = client.length;
|
||||
for (let i = 0; i < client.length; i++) {
|
||||
const session = client[i];
|
||||
// 超过五秒自动关闭
|
||||
if (datetime - session.datetime >= 5000) {
|
||||
client.splice(i, 1);
|
||||
session.close();
|
||||
i--;
|
||||
}
|
||||
}
|
||||
const newLength = client.length;
|
||||
logger.info("定时清理无效信令终端:%d", oldLength - newLength);
|
||||
}, 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理信令消息
|
||||
*
|
||||
* @param {*} message 消息
|
||||
* @param {*} session websocket
|
||||
*/
|
||||
async function onmessage(message, session) {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
// 授权验证
|
||||
if (!session.authorize) {
|
||||
if (
|
||||
data.body.username === config.https.username &&
|
||||
data.body.password === config.https.password
|
||||
) {
|
||||
logger.debug("授权成功:%s", session._socket.remoteAddress);
|
||||
session.authorize = true;
|
||||
data.code = "0000";
|
||||
data.message = "授权成功";
|
||||
data.body.username = null;
|
||||
data.body.password = null;
|
||||
session.send(JSON.stringify(data));
|
||||
} else {
|
||||
logger.warn("授权失败:%s", session._socket.remoteAddress);
|
||||
data.code = "3401";
|
||||
data.message = "授权失败";
|
||||
session.send(JSON.stringify(data));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 处理信令
|
||||
logger.debug("处理信令消息: %s", message);
|
||||
signal.on(data, session);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`处理信令消息异常:
|
||||
%s
|
||||
%o`,
|
||||
message,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动方法
|
||||
*/
|
||||
async function main() {
|
||||
logger.debug("DEBUG").info("INFO").warn("WARN").error("ERROR");
|
||||
logger.info("开始启动:%s", config.name);
|
||||
await buildMediasoupWorker();
|
||||
logger.info("开始启动", config.name);
|
||||
// await buildMediasoupWorker();
|
||||
await buildSignalServer();
|
||||
await buildCommandConsole();
|
||||
await buildClientInterval();
|
||||
logger.info("启动完成:%s", config.name);
|
||||
await buildCommandConsole();
|
||||
logger.info("启动完成", config.name);
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
const Logger = require("./Logger");
|
||||
|
||||
/**
|
||||
* 信令
|
||||
* 1. 终端媒体流向
|
||||
* 2. 处理音频视频:降噪、水印等等
|
||||
*/
|
||||
class Signal {
|
||||
|
||||
// 信令终端列表
|
||||
client = [];
|
||||
// 日志
|
||||
logger = new Logger();
|
||||
// Mediasoup Worker列表
|
||||
mediasoupWorker = [];
|
||||
// Mediasoup Worker索引
|
||||
@@ -21,7 +25,59 @@ class Signal {
|
||||
* @param {*} session websocket
|
||||
*/
|
||||
on(message, session) {
|
||||
// 授权验证
|
||||
if (!session.authorize) {
|
||||
if (
|
||||
message?.header?.signal === "media::register" &&
|
||||
message?.body?.username === config.https.username &&
|
||||
message?.body?.password === config.https.password
|
||||
) {
|
||||
this.logger.debug("授权成功", session._socket.remoteAddress);
|
||||
client.push(session);
|
||||
session.authorize = true;
|
||||
message.code = "0000";
|
||||
message.message = "授权成功";
|
||||
message.body.username = null;
|
||||
message.body.password = null;
|
||||
} else {
|
||||
this.logger.warn("授权失败", session._socket.remoteAddress);
|
||||
message.code = "3401";
|
||||
message.message = "授权失败";
|
||||
}
|
||||
this.push(message, session);
|
||||
return;
|
||||
}
|
||||
// 处理信令
|
||||
switch (message.header.signal) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知信令
|
||||
*
|
||||
* @param {*} message 消息
|
||||
* @param {*} session websocket
|
||||
*/
|
||||
push(message, session) {
|
||||
try {
|
||||
session.send(JSON.stringify(message));
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"通知信令失败",
|
||||
session._socket.remoteAddress,
|
||||
message,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知信令
|
||||
*
|
||||
* @param {*} message 消息
|
||||
*/
|
||||
push(message) {
|
||||
this.client.forEach((session) => this.push(message, session));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user