[+] server

This commit is contained in:
acgist
2023-02-04 16:33:39 +08:00
parent 5e1a25aa21
commit 531229b8d7
15 changed files with 489 additions and 773 deletions

View File

@@ -41,6 +41,10 @@
## TODO ## TODO
* 录制 * 录制Recorder
* 音频:降噪、混音、变声 * 音频:降噪、混音、变声
* 视频水印、美颜、AI识别 * 视频水印、美颜、AI识别
* 一个信令服务多个媒体服务
* 信令服务集群
* 信令直传
* 媒体交互式启动

View File

@@ -349,24 +349,3 @@ firewall-cmd --list-ports
``` ```
keytool -genkeypair -keyalg RSA -dname "CN=localhost, OU=acgist, O=taoyao, L=GZ, ST=GD, C=CN" -alias taoyao -validity 3650 -ext ku:c=dig,keyE -ext eku=serverAuth -ext SAN=dns:localhost,ip:127.0.0.1 -keystore taoyao.jks -keypass 123456 -storepass 123456 keytool -genkeypair -keyalg RSA -dname "CN=localhost, OU=acgist, O=taoyao, L=GZ, ST=GD, C=CN" -alias taoyao -validity 3650 -ext ku:c=dig,keyE -ext eku=serverAuth -ext SAN=dns:localhost,ip:127.0.0.1 -keystore taoyao.jks -keypass 123456 -storepass 123456
``` ```
## 资料
https://www.jianshu.com/p/fa047d7054eb
https://www.jianshu.com/p/59da3d350488
https://www.jianshu.com/p/fa047d7054eb
http://koca.szkingdom.com/forum/t/topic/218
https://segmentfault.com/a/1190000039782685
https://www.cnblogs.com/bolingcavalry/p/15473808.html
http://www.manoner.com/post/音视频基础/WebRTC核心组件和协议栈/
https://blog.csdn.net/eguid_1/article/details/117277841
https://blog.csdn.net/xiang_6119/article/details/108779678
https://blog.csdn.net/qq_40321119/article/details/108336324
https://blog.csdn.net/ababab12345/article/details/115585378
https://blog.csdn.net/m0_64867003/article/details/121901782
https://blog.csdn.net/jisuanji111111/article/details/121634199
https://blog.csdn.net/weixin_48638578/article/details/120191152
https://blog.csdn.net/weixin_45565568/article/details/108929438
https://blog.csdn.net/weixin_40425640/article/details/125444018
http://t.zoukankan.com/yjmyzz-p-webrtc-groupcall-using-kurento.html
https://lequ7.com/guan-yu-webrtc-yi-wen-xiang-jie-webrtc-ji-chu.html

33
docs/Learning.md Normal file
View File

@@ -0,0 +1,33 @@
# 资料
## WebRTC资料
https://www.cnblogs.com/ssyfj/p/14828185.html
https://www.cnblogs.com/ssyfj/p/14826516.html
https://www.cnblogs.com/ssyfj/p/14823861.html
https://www.cnblogs.com/ssyfj/p/14815266.html
https://www.cnblogs.com/ssyfj/p/14811253.html
https://www.cnblogs.com/ssyfj/p/14806678.html
https://www.cnblogs.com/ssyfj/p/14805040.html
https://www.cnblogs.com/ssyfj/p/14788663.html
https://www.cnblogs.com/ssyfj/p/14787012.html
https://www.cnblogs.com/ssyfj/p/14783168.html
https://www.cnblogs.com/ssyfj/p/14781982.html
https://www.cnblogs.com/ssyfj/p/14778839.html
## Mediasoup资料
https://www.cnblogs.com/ssyfj/p/14855454.html
https://www.cnblogs.com/ssyfj/p/14851442.html
https://www.cnblogs.com/ssyfj/p/14850041.html
https://www.cnblogs.com/ssyfj/p/14847097.html
https://www.cnblogs.com/ssyfj/p/14843182.html
https://www.cnblogs.com/ssyfj/p/14843082.html
## 更多资料
http://koca.szkingdom.com/forum/t/topic/218
http://www.manoner.com/post/音视频基础/WebRTC核心组件和协议栈/
https://blog.csdn.net/ababab12345/article/details/115585378
https://blog.csdn.net/jisuanji111111/article/details/121634199
https://lequ7.com/guan-yu-webrtc-yi-wen-xiang-jie-webrtc-ji-chu.html

7
docs/Test.md Normal file
View File

@@ -0,0 +1,7 @@
# 测试
## 推流测试
```
ffmpeg
```

View File

@@ -1,6 +1,6 @@
# 媒体 # 媒体
只要负责媒体处理,不要添加任何业务逻辑,所有业务逻辑都由[taoyao-signal](../taoyao-signal)处理。 只要负责媒体处理,不要添加任何业务逻辑,所有业务逻辑都由[taoyao-signal-server](../taoyao-signal-server)处理。
## 使用 ## 使用
@@ -17,10 +17,45 @@ sudo npm install
``` ```
## 安全
默认媒体服务只要暴露媒体`UDP`端口,信令接口不用暴露,所以使用简单鉴权。
## WebRTC协议栈
```
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| HTTPS / WSS | | SCTP | SRTP / SRTCP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ICE / SDP / SIP +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| TLS | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ DTLS +-+-+-+-+-+-+-+-+-+
| HTTP / WS | NAT / STUN / TURN | | RTP / RTCP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| TCP | UDP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| IPv4 / IPv6 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### 协议简介
* 会话通道ICE/SIP/SDP
* 媒体通道RTP/RTCP/SRTP/SRTCP
* RTP实时传输协议音频视频
* RTCPRTP传输控制协议监控数据传输质量并给予数据发送方反馈
* SCTP流控制传输协议自定义的应用数据传输
* RTMP实时消息传送协议
* RTSP可以控制媒体点播
### ICE/SDP/SIP
ICE信息的描述格式通常采用标准的SDP其全称为Session Description Protocol即会话描述协议。<br />
SDP只是一种信息格式的描述标准不属于传输协议但是可以被其他传输协议用来交换必要的信息例如SIP、RTSP等等。
## 其他常见WebRTC媒体服务 ## 其他常见WebRTC媒体服务
* [Janus](https://github.com/meetecho/janus-gateway/)
* [Jitsi](https://github.com/jitsi) * [Jitsi](https://github.com/jitsi)
* [Janus](https://github.com/meetecho/janus-gateway/)
* [Licode](https://github.com/lynckia/licode) * [Licode](https://github.com/lynckia/licode)
* [Kurento](https://github.com/Kurento/kurento-media-server) * [Kurento](https://github.com/Kurento/kurento-media-server)
* [Medooze](https://github.com/medooze/media-server) * [Medooze](https://github.com/medooze/media-server)

View File

@@ -5,11 +5,15 @@
"private": true, "private": true,
"description": "taoyao media server", "description": "taoyao media server",
"scripts": { "scripts": {
"start": "DEBUG=${DEBUG:='*mediasoup* *INFO* *WARN* *ERROR*'} INTERACTIVE=${INTERACTIVE:='true'} node src/server.js" "dev": "node src/Server.js",
"release": "node src/Server.js"
}, },
"dependencies": { "dependencies": {
"mediasoup": "file:./mediasoup" "ws": "^8.12.0",
"debug": "^4.3.1"
}, },
"devDependencies": { "releaseDependencies": {
"mediasoup-local": "file:./mediasoup",
"mediasoup-online": "github:versatica/mediasoup#v3"
} }
} }

View File

@@ -0,0 +1,37 @@
const Logger = require("./Logger");
const logger = new Logger();
function openCommandConsole() {
logger.info("打开交互式控制台...");
process.stdin.resume();
process.stdin.setEncoding("utf-8");
process.stdin.on("data", (data) => {
process.stdin.pause();
const command = data.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
logger.info("");
switch (command) {
case "h":
case "help": {
logger.info("- h, help 帮助信息");
logger.info("- os 系统信息");
break;
}
case "":
default: {
logger.warn(`未知命令:'${command}'`);
logger.info("查询命令:`h` | `help`");
}
}
logger.info("");
process.stdin.resume();
});
}
module.exports = async function () {
try {
openCommandConsole();
} catch (error) {
logger.error("执行命令异常:%o", error);
}
};

View File

@@ -0,0 +1,47 @@
/**
* 日志
*/
const debug = require("debug");
const config = require("./Config");
const APP_NAME = config.name;
class Logger {
constructor(prefix) {
if (prefix) {
this._debug = debug(`${APP_NAME}:${prefix}`);
this._info = debug(`${APP_NAME}:INFO:${prefix}`);
this._warn = debug(`${APP_NAME}:WARN:${prefix}`);
this._error = debug(`${APP_NAME}:ERROR:${prefix}`);
} else {
this._debug = debug(APP_NAME);
this._info = debug(`${APP_NAME}:INFO`);
this._warn = debug(`${APP_NAME}:WARN`);
this._error = debug(`${APP_NAME}:ERROR`);
}
this._debug.log = console.debug.bind(console);
this._info.log = console.info.bind(console);
this._warn.log = console.warn.bind(console);
this._error.log = console.error.bind(console);
}
get debug() {
return this._debug.log;
}
get info() {
return this._info.log;
}
get warn() {
return this._warn.log;
}
get error() {
return this._error.log;
}
}
module.exports = Logger;

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCfmqQa2T/dGQIk
cXYiRE47csmhtOEmDL9e1uncyFOYaDLopqma+z0OWV71HrT9m5lEAbK3HZy56CWL
HNlFmnIztT84jTc11b3996cd6G1/5nffRj8D3FcSps9qVsRwAaEnKE6tGrCJhjWB
+/qKIZDP3MmGOkf1guQMTPeZBOEzo+JmL2MOwaLk8dXbb4KHbrye31UnvoXoANs3
1IbdAE3gRMDc40yBoVHeWOQ0h4c/wg1+rMhYPaUtgFkyqFr7zX1YvTa7BwZE4oVf
l7f5k+FRWO8OKn++kHIlE9H3pjdaGKWIuYFRYCxNC9IfGazhPSfYHVgg0q/8UUKg
jFqgbjIJAgMBAAECggEANs6ateGOjbUtyCfyQjgkiUOUu+PqQO+1s7KnYjqkgjyd
5sh8i4zk3Y2RDyl5S3FoQzM2FK2liS2P3uKMNdugheMij5/mqqT4dkLZ72pGV9pj
pZdwwjmi6PPBXCnpkPDuTw0HX2g/4SnmK/nEgjSejtKpnV9cIJHPD+5KRBCp6No+
JLgVFdhCCMEmyzTOU9ASxgRuw0sxGmPsdg3ZewUmoDb94mGDDot500rCB+wnYHue
ACegbaalrTWhY2DzYyNCfnR+F/mshIeqDjMVLsdPoj8MahdQbIoFcyNF+ts3pFVl
MXUUuO3bhZtrSVIT+4r07u82XoXaBufyPzK6Mo1vNQKBgQC9uebhZ1jCyYgGro5o
xuPuW+B5tHKGtlElJ0e7D/XjDvPWj3eGEkVCD5BH2L3qG9oqpKxX+hpmjc5ey21Q
zcnAGG2gC9o+uGQxxjix8sbZ0HEr2J6EdRlKzIZ/N2EnTmU+K8WWWI4g2PEqsId9
yt39EwU/D81bc0b5pcMGJTMk1wKBgQDXWxhywNFtPVreCeerXvvHLIQEtiZUMdhm
8zWtqHNhdojYoeaFPifVcajBo41Jl9qSVCot8cmdhIzmPwbRk1Ob7MtBC8t7OtzT
UmwT9nuxC5iSaGLJNBMtP3M0TzV7+qvvg+Az13kYtje9475LMEYdBLnCb4Mz1Y/R
QOR0mhWkHwKBgBxoepajl9nKvVBq0K4Fodlt7mWqzD85i1rpz8bFtAaklYQ6BSaR
E8e5dtwbKwyj0P3znE6sB0n1z8HH6f1gYuYdgkSloa8kgvQk/xY+COJSYK+1Br9E
nV3i0/y2eRiel3BAs5w4dEec1DeVKSR/vM+JCo8PuasIzsbQuCvyY/8PAoGBALWH
kT0xsZcej9j4inMXNq62pHYAQKDZ/2sQed/vTYsLSuEo39LTCOrPywum3LL7MQAF
uCRQWr3PfKGc4ReJ04FtAgvLcHNos7niET5ml+8uMia/nP2zSrLqeCbQ2emu7H2S
MUwhxm8BMk17iu2APKm7UQZHz1XDIF6oD6sGM1XLAoGALFpdCO486AbHYts2NHey
vQ39u3WDlrgzIM6hs8BI0FEZIdtuNWa/wpZYPaiXUvwTPsfQA1eCqXXuQGzH0fFn
g1M8RxsZ8XNjfQVpqSceZp+qFkTrR8zrbbQiZwBUm9WBdKfryMZHLhFraGLShFdM
ONu5qg3tXgMfFpNcMyiGgeA=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDnzCCAoegAwIBAgIJAIKYVI9RPbj+MA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV
BAYTAkNOMQswCQYDVQQIEwJHRDELMAkGA1UEBxMCR1oxDzANBgNVBAoTBnRhb3lh
bzEPMA0GA1UECxMGYWNnaXN0MRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMjIxMTA5
MDEyMTM3WhcNMzIxMTA2MDEyMTM3WjBdMQswCQYDVQQGEwJDTjELMAkGA1UECBMC
R0QxCzAJBgNVBAcTAkdaMQ8wDQYDVQQKEwZ0YW95YW8xDzANBgNVBAsTBmFjZ2lz
dDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAn5qkGtk/3RkCJHF2IkROO3LJobThJgy/Xtbp3MhTmGgy6Kapmvs9Dlle
9R60/ZuZRAGytx2cueglixzZRZpyM7U/OI03NdW9/fenHehtf+Z330Y/A9xXEqbP
albEcAGhJyhOrRqwiYY1gfv6iiGQz9zJhjpH9YLkDEz3mQThM6PiZi9jDsGi5PHV
22+Ch268nt9VJ76F6ADbN9SG3QBN4ETA3ONMgaFR3ljkNIeHP8INfqzIWD2lLYBZ
Mqha+819WL02uwcGROKFX5e3+ZPhUVjvDip/vpByJRPR96Y3WhiliLmBUWAsTQvS
Hxms4T0n2B1YINKv/FFCoIxaoG4yCQIDAQABo2IwYDAdBgNVHQ4EFgQUPpT59FzS
UUzHsxrKeGOQ/YeaqpswDgYDVR0PAQH/BAQDAgWgMBoGA1UdEQQTMBGCCWxvY2Fs
aG9zdIcEfwAAATATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOC
AQEAKsoYmBr9EmYev6sWVet8x+/YDZgXGZolZff3agPT+uFL/6mtyAqnFD/ncPOh
n206l4RimSChVlEVx3pE4r5sBiLzPaX/dcCRoZNxEQMtjfYCk+4iRfkxhIvpqLzf
ZsEGbJCh9JodG2xkYNViPF2AqR8OchEMRttYQa2dDkk2oDVMg0bmqgZgSD7vEjdk
ovBhEIQ1Rhgv0yi9IT+kXYa+nTpc+/9m/GmmejtaFaVdpj+WuNqPf/WyzR+3JQWZ
Y/O7ESF7tUcw0HSxNv/pk6Z13RQClUo6bPHzPF4JJw1tAIbkuyKZ6ZpErQePUEpk
dVZPy7rDD3N/BI//0vBZmy0v1w==
-----END CERTIFICATE-----

View File

@@ -1,149 +1,137 @@
/** /**
* 配置 * 配置
*/ */
const os = require('os'); const os = require("os");
module.exports = module.exports = {
{ // 系统名称
domain : process.env.DOMAIN || 'localhost', name: "taoyao-media-server",
// Signal // 交互式命令行
https : command: true,
{ // 信令服务
listenIp : '0.0.0.0', https: {
listenPort : process.env.PROTOO_LISTEN_PORT || 4443, listenIp: "0.0.0.0",
tls : listenPort: process.env.HTTPS_LISTEN_PORT || 4443,
{ // WebSocket连接密码
cert : process.env.HTTPS_CERT_FULLCHAIN || `${__dirname}/certs/fullchain.pem`, username: 'taoyao',
key : process.env.HTTPS_CERT_PRIVKEY || `${__dirname}/certs/privkey.pem` 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
mediasoup : mediasoup: {
{
// 按照CPU数量配置进程数量 // 按照CPU数量配置进程数量
numWorkers: Object.keys(os.cpus()).length, numWorkers: Object.keys(os.cpus()).length,
// Workerhttps://mediasoup.org/documentation/v3/mediasoup/api/#WorkerSettings // Workerhttps://mediasoup.org/documentation/v3/mediasoup/api/#WorkerSettings
workerSettings : workerSettings: {
{ logLevel: "warn",
logLevel : 'warn', logTags: [
logTags : "bwe",
[ "ice",
'bwe', "rtp",
'ice', "rtx",
'rtp', "svc",
'rtx', "dtls",
'svc', "info",
'dtls', "sctp",
'info', "srtp",
'sctp', "rtcp",
'srtp', "score",
'rtcp', "message",
'score', "simulcast",
'message',
'simulcast'
], ],
rtcMinPort: process.env.MEDIASOUP_MIN_PORT || 40000, rtcMinPort: process.env.MEDIASOUP_MIN_PORT || 40000,
rtcMaxPort : process.env.MEDIASOUP_MAX_PORT || 49999 rtcMaxPort: process.env.MEDIASOUP_MAX_PORT || 49999,
}, },
// Routerhttps://mediasoup.org/documentation/v3/mediasoup/api/#RouterOptions // Routerhttps://mediasoup.org/documentation/v3/mediasoup/api/#RouterOptions
routerOptions : routerOptions: {
mediaCodecs: [
{ {
mediaCodecs : kind: "audio",
[ mimeType: "audio/opus",
{
kind : 'audio',
mimeType : 'audio/opus',
clockRate: 48000, clockRate: 48000,
channels : 2 channels: 2,
}, },
{ {
kind : 'video', kind: "video",
mimeType : 'video/VP8', mimeType: "video/VP8",
clockRate: 90000, clockRate: 90000,
parameters : parameters: {
{ "x-google-start-bitrate": 1000,
'x-google-start-bitrate' : 1000 },
}
}, },
{ {
kind : 'video', kind: "video",
mimeType : 'video/VP9', mimeType: "video/VP9",
clockRate: 90000, clockRate: 90000,
parameters : parameters: {
{ "profile-id": 2,
'profile-id' : 2, "x-google-start-bitrate": 1000,
'x-google-start-bitrate' : 1000 },
}
}, },
{ {
kind : 'video', kind: "video",
mimeType : 'video/h264', mimeType: "video/h264",
clockRate: 90000, clockRate: 90000,
parameters : parameters: {
{ "packetization-mode": 1,
'packetization-mode' : 1, "profile-level-id": "4d0032",
'profile-level-id' : '4d0032', "level-asymmetry-allowed": 1,
'level-asymmetry-allowed' : 1, "x-google-start-bitrate": 1000,
'x-google-start-bitrate' : 1000 },
}
}, },
{ {
kind : 'video', kind: "video",
mimeType : 'video/h264', mimeType: "video/h264",
clockRate: 90000, clockRate: 90000,
parameters : parameters: {
{ "packetization-mode": 1,
'packetization-mode' : 1, "profile-level-id": "42e01f",
'profile-level-id' : '42e01f', "level-asymmetry-allowed": 1,
'level-asymmetry-allowed' : 1, "x-google-start-bitrate": 1000,
'x-google-start-bitrate' : 1000 },
} },
} ],
]
}, },
// WebRtcServerhttps://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcServerOptions // WebRtcServerhttps://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcServerOptions
webRtcServerOptions : webRtcServerOptions: {
listenInfos: [
{ {
listenInfos : protocol: "udp",
[ ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
{
protocol : 'udp',
ip : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP, announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
port : 44444 port: 44444,
}, },
{ {
protocol : 'tcp', protocol: "tcp",
ip : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0', ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP, announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
port : 44444 port: 44444,
} },
], ],
}, },
// WebRtcTransporthttps://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions // WebRtcTransporthttps://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
webRtcTransportOptions : webRtcTransportOptions: {
listenIps: [
{ {
listenIps : ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
[ announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
{ },
ip : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP
}
], ],
initialAvailableOutgoingBitrate: 1000000, initialAvailableOutgoingBitrate: 1000000,
minimumAvailableOutgoingBitrate: 600000, minimumAvailableOutgoingBitrate: 600000,
maxSctpMessageSize: 262144, maxSctpMessageSize: 262144,
maxIncomingBitrate : 1500000 maxIncomingBitrate: 1500000,
}, },
// PlainTransporthttps://mediasoup.org/documentation/v3/mediasoup/api/#PlainTransportOptions // PlainTransporthttps://mediasoup.org/documentation/v3/mediasoup/api/#PlainTransportOptions
plainTransportOptions : plainTransportOptions: {
{ listenIp: {
listenIp : ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
{ announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
ip : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0', },
announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP maxSctpMessageSize: 262144,
},
}, },
maxSctpMessageSize : 262144
}
}
}; };

View File

@@ -1,557 +1,130 @@
/**
* 媒体服务
*/
#!/usr/bin/env node #!/usr/bin/env node
/**
* 服务
*/
const fs = require("fs");
const ws = require("ws");
const https = require("https");
const mediasoup = require("mediasoup");
const config = require("./Config");
const Logger = require("./Logger");
const Signal = require("./Signal");
const command = require("./Command");
process.title = 'taoyao-client'; // 日志
process.env.DEBUG = process.env.DEBUG || '*INFO* *WARN* *ERROR*';
const config = require('./config');
/* eslint-disable no-console */
console.log('process.env.DEBUG:', process.env.DEBUG);
console.log('config.js:\n%s', JSON.stringify(config, null, ' '));
/* eslint-enable no-console */
const fs = require('fs');
const url = require('url');
const https = require('https');
const protoo = require('protoo-server');
const express = require('express');
const mediasoup = require('mediasoup');
const logger = new Logger(); const logger = new Logger();
const signal = new Signal();
// Async queue to manage rooms. // HTTPS server
// @type {AwaitQueue}
const queue = new AwaitQueue();
// Map of Room instances indexed by roomId.
// @type {Map<Number, Room>}
const rooms = new Map();
// HTTPS server.
// @type {https.Server}
let httpsServer; let httpsServer;
// WebSocket server
// Express application. let webSocketServer;
// @type {Function} // Mediasoup Worker列表
let expressApp;
// Protoo WebSocket server.
// @type {protoo.WebSocketServer}
let protooWebSocketServer;
// mediasoup Workers.
// @type {Array<mediasoup.Worker>}
const mediasoupWorkers = []; const mediasoupWorkers = [];
// Mediasoup Worker下个索引
let nextMediasoupWorkerIndex = 0;
// Index of next mediasoup Worker to use. process.title = config.name;
// @type {Number} process.env.DEBUG = process.env.DEBUG || "*mediasoup* *INFO* *WARN* *ERROR*";
let nextMediasoupWorkerIdx = 0; logger.info("开始启动:%s", config.name);
run(); run();
async function run() async function run() {
{ // 启动Mediasoup服务
// Open the interactive server.
await interactiveServer();
// Open the interactive client.
if (process.env.INTERACTIVE === 'true' || process.env.INTERACTIVE === '1')
await interactiveClient();
// Run a mediasoup Worker.
await runMediasoupWorkers(); await runMediasoupWorkers();
// 启动服务
// Create Express app. await runSignalServer();
await createExpressApp(); logger.info("启动完成:%s", config.name);
// 交互式命令行
// Run HTTPS server. if (config.command) {
await runHttpsServer(); await command();
// Run a protoo WebSocketServer.
await runProtooWebSocketServer();
// Log rooms status every X seconds.
setInterval(() =>
{
for (const room of rooms.values())
{
room.logStatus();
} }
}, 120000);
} }
/** async function runMediasoupWorkers() {
* Launch as many mediasoup Workers as given in the configuration file.
*/
async function runMediasoupWorkers()
{
const { numWorkers } = config.mediasoup; const { numWorkers } = config.mediasoup;
logger.info("启动Mediasoup服务%d Worker...", numWorkers);
logger.info('running %d mediasoup Workers...', numWorkers); for (let i = 0; i < numWorkers; i++) {
const worker = await mediasoup.createWorker({
for (let i = 0; i < numWorkers; ++i)
{
const worker = await mediasoup.createWorker(
{
logLevel: config.mediasoup.workerSettings.logLevel, logLevel: config.mediasoup.workerSettings.logLevel,
logTags: config.mediasoup.workerSettings.logTags, logTags: config.mediasoup.workerSettings.logTags,
rtcMinPort: Number(config.mediasoup.workerSettings.rtcMinPort), rtcMinPort: Number(config.mediasoup.workerSettings.rtcMinPort),
rtcMaxPort : Number(config.mediasoup.workerSettings.rtcMaxPort) rtcMaxPort: Number(config.mediasoup.workerSettings.rtcMaxPort),
}); });
worker.on("died", () => {
worker.on('died', () =>
{
logger.error( logger.error(
'mediasoup Worker died, exiting in 2 seconds... [pid:%d]', worker.pid); "Mediasoup Worker停止服务(两秒之后自动退出)... [PID%d]",
worker.pid
);
setTimeout(() => process.exit(1), 2000); setTimeout(() => process.exit(1), 2000);
}); });
mediasoupWorkers.push(worker); mediasoupWorkers.push(worker);
// 配置WebRTC服务
// Create a WebRtcServer in this Worker. if (process.env.MEDIASOUP_USE_WEBRTC_SERVER !== "false") {
if (process.env.MEDIASOUP_USE_WEBRTC_SERVER !== 'false') // 每个Worker端口不能相同
{
// Each mediasoup Worker will run its own WebRtcServer, so those cannot
// share the same listening ports. Hence we increase the value in config.js
// for each Worker.
const webRtcServerOptions = utils.clone(config.mediasoup.webRtcServerOptions);
const portIncrement = mediasoupWorkers.length - 1; const portIncrement = mediasoupWorkers.length - 1;
const webRtcServerOptions = JSON.parse(JSON.stringify(config.mediasoup.webRtcServerOptions));
for (const listenInfo of webRtcServerOptions.listenInfos) for (const listenInfo of webRtcServerOptions.listenInfos) {
{
listenInfo.port += portIncrement; listenInfo.port += portIncrement;
} }
const webRtcServer = await worker.createWebRtcServer(webRtcServerOptions); const webRtcServer = await worker.createWebRtcServer(webRtcServerOptions);
worker.appData.webRtcServer = webRtcServer; worker.appData.webRtcServer = webRtcServer;
} }
// 记录日志
// Log worker resource usage every X seconds. setInterval(async () => {
setInterval(async () =>
{
const usage = await worker.getResourceUsage(); const usage = await worker.getResourceUsage();
logger.info('mediasoup Worker resource usage [pid:%d]: %o', worker.pid, usage);
}, 120000);
}
}
/**
* Create an Express based API server to manage Broadcaster requests.
*/
async function createExpressApp()
{
logger.info('creating Express app...');
expressApp = express();
expressApp.use(bodyParser.json());
/**
* For every API request, verify that the roomId in the path matches and
* existing room.
*/
expressApp.param(
'roomId', (req, res, next, roomId) =>
{
// The room must exist for all API requests.
if (!rooms.has(roomId))
{
const error = new Error(`room with id "${roomId}" not found`);
error.status = 404;
throw error;
}
req.room = rooms.get(roomId);
next();
});
/**
* API GET resource that returns the mediasoup Router RTP capabilities of
* the room.
*/
expressApp.get(
'/rooms/:roomId', (req, res) =>
{
const data = req.room.getRouterRtpCapabilities();
res.status(200).json(data);
});
/**
* POST API to create a Broadcaster.
*/
expressApp.post(
'/rooms/:roomId/broadcasters', async (req, res, next) =>
{
const {
id,
displayName,
device,
rtpCapabilities
} = req.body;
try
{
const data = await req.room.createBroadcaster(
{
id,
displayName,
device,
rtpCapabilities
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* DELETE API to delete a Broadcaster.
*/
expressApp.delete(
'/rooms/:roomId/broadcasters/:broadcasterId', (req, res) =>
{
const { broadcasterId } = req.params;
req.room.deleteBroadcaster({ broadcasterId });
res.status(200).send('broadcaster deleted');
});
/**
* POST API to create a mediasoup Transport associated to a Broadcaster.
* It can be a PlainTransport or a WebRtcTransport depending on the
* type parameters in the body. There are also additional parameters for
* PlainTransport.
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports',
async (req, res, next) =>
{
const { broadcasterId } = req.params;
const { type, rtcpMux, comedia, sctpCapabilities } = req.body;
try
{
const data = await req.room.createBroadcasterTransport(
{
broadcasterId,
type,
rtcpMux,
comedia,
sctpCapabilities
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* POST API to connect a Transport belonging to a Broadcaster. Not needed
* for PlainTransport if it was created with comedia option set to true.
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/connect',
async (req, res, next) =>
{
const { broadcasterId, transportId } = req.params;
const { dtlsParameters } = req.body;
try
{
const data = await req.room.connectBroadcasterTransport(
{
broadcasterId,
transportId,
dtlsParameters
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* POST API to create a mediasoup Producer associated to a Broadcaster.
* The exact Transport in which the Producer must be created is signaled in
* the URL path. Body parameters include kind and rtpParameters of the
* Producer.
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/producers',
async (req, res, next) =>
{
const { broadcasterId, transportId } = req.params;
const { kind, rtpParameters } = req.body;
try
{
const data = await req.room.createBroadcasterProducer(
{
broadcasterId,
transportId,
kind,
rtpParameters
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* POST API to create a mediasoup Consumer associated to a Broadcaster.
* The exact Transport in which the Consumer must be created is signaled in
* the URL path. Query parameters must include the desired producerId to
* consume.
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/consume',
async (req, res, next) =>
{
const { broadcasterId, transportId } = req.params;
const { producerId } = req.query;
try
{
const data = await req.room.createBroadcasterConsumer(
{
broadcasterId,
transportId,
producerId
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* POST API to create a mediasoup DataConsumer associated to a Broadcaster.
* The exact Transport in which the DataConsumer must be created is signaled in
* the URL path. Query body must include the desired producerId to
* consume.
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/consume/data',
async (req, res, next) =>
{
const { broadcasterId, transportId } = req.params;
const { dataProducerId } = req.body;
try
{
const data = await req.room.createBroadcasterDataConsumer(
{
broadcasterId,
transportId,
dataProducerId
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* POST API to create a mediasoup DataProducer associated to a Broadcaster.
* The exact Transport in which the DataProducer must be created is signaled in
*/
expressApp.post(
'/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/produce/data',
async (req, res, next) =>
{
const { broadcasterId, transportId } = req.params;
const { label, protocol, sctpStreamParameters, appData } = req.body;
try
{
const data = await req.room.createBroadcasterDataProducer(
{
broadcasterId,
transportId,
label,
protocol,
sctpStreamParameters,
appData
});
res.status(200).json(data);
}
catch (error)
{
next(error);
}
});
/**
* Error handler.
*/
expressApp.use(
(error, req, res, next) =>
{
if (error)
{
logger.warn('Express app %s', String(error));
error.status = error.status || (error.name === 'TypeError' ? 400 : 500);
res.statusMessage = error.message;
res.status(error.status).send(String(error));
}
else
{
next();
}
});
}
/**
* Create a Node.js HTTPS server. It listens in the IP and port given in the
* configuration file and reuses the Express application as request listener.
*/
async function runHttpsServer()
{
logger.info('running an HTTPS server...');
// HTTPS server for the protoo WebSocket server.
const tls =
{
cert : fs.readFileSync(config.https.tls.cert),
key : fs.readFileSync(config.https.tls.key)
};
httpsServer = https.createServer(tls, expressApp);
await new Promise((resolve) =>
{
httpsServer.listen(
Number(config.https.listenPort), config.https.listenIp, resolve);
});
}
/**
* Create a protoo WebSocketServer to allow WebSocket connections from browsers.
*/
async function runProtooWebSocketServer()
{
logger.info('running protoo WebSocketServer...');
// Create the protoo WebSocket server.
protooWebSocketServer = new protoo.WebSocketServer(httpsServer,
{
maxReceivedFrameSize : 960000, // 960 KBytes.
maxReceivedMessageSize : 960000,
fragmentOutgoingMessages : true,
fragmentationThreshold : 960000
});
// Handle connections from clients.
protooWebSocketServer.on('connectionrequest', (info, accept, reject) =>
{
// The client indicates the roomId and peerId in the URL query.
const u = url.parse(info.request.url, true);
const roomId = u.query['roomId'];
const peerId = u.query['peerId'];
if (!roomId || !peerId)
{
reject(400, 'Connection request without roomId and/or peerId');
return;
}
let consumerReplicas = Number(u.query['consumerReplicas']);
if (isNaN(consumerReplicas))
{
consumerReplicas = 0;
}
logger.info( logger.info(
'protoo connection request [roomId:%s, peerId:%s, address:%s, origin:%s]', "Mediasoup Worker使用情况 [pid%d]: %o",
roomId, peerId, info.socket.remoteAddress, info.origin); worker.pid,
usage
);
}, 120 * 1000);
}
}
// Serialize this code into the queue to avoid that two peers connecting at async function runSignalServer() {
// the same time with the same roomId create two separate rooms with same const tls = {
// roomId. cert: fs.readFileSync(config.https.tls.cert),
queue.push(async () => key: fs.readFileSync(config.https.tls.key),
{ };
const room = await getOrCreateRoom({ roomId, consumerReplicas }); logger.info("配置HTTPS服务...");
httpsServer = https.createServer(tls, (request, response) => {
// Accept the protoo WebSocket connection. response.writeHead(200);
const protooWebSocketTransport = accept(); response.end("taoyao media server");
});
room.handleProtooConnection({ peerId, protooWebSocketTransport }); logger.info("配置WebSocket服务...");
}) webSocketServer = new ws.Server({ server: httpsServer });
.catch((error) => webSocketServer.on("connection", (session) => {
{ session.on("open", (message) => {
logger.error('room creation or room joining failed:%o', error); logger.info("打开信令通道: %s", message);
});
reject(error); session.on("close", (code) => {
logger.info("关闭信令通道: %o", code);
});
session.on("error", (e) => {
logger.error("信令通道异常: %o", e);
});
session.on("message", (message) => {
logger.debug("收到信令消息: %s", message);
try {
signal.on(JSON.parse(message), session);
} catch (error) {
logger.error(
`处理信令消息异常:
%s
%o`,
message,
error
);
}
}); });
}); });
} logger.info("开启服务监听...");
await new Promise((resolve) => {
/** httpsServer.listen(
* Get next mediasoup Worker. Number(config.https.listenPort),
*/ config.https.listenIp,
function getMediasoupWorker() resolve
{ );
const worker = mediasoupWorkers[nextMediasoupWorkerIdx]; });
if (++nextMediasoupWorkerIdx === mediasoupWorkers.length)
nextMediasoupWorkerIdx = 0;
return worker;
}
/**
* Get a Room instance (or create one if it does not exist).
*/
async function getOrCreateRoom({ roomId, consumerReplicas })
{
let room = rooms.get(roomId);
// If the Room does not exist create a new one.
if (!room)
{
logger.info('creating a new Room [roomId:%s]', roomId);
const mediasoupWorker = getMediasoupWorker();
room = await Room.create({ mediasoupWorker, roomId, consumerReplicas });
rooms.set(roomId, room);
room.on('close', () => rooms.delete(roomId));
}
return room;
} }

View File

@@ -1,5 +1,21 @@
/** /**
* 适配媒体信令 * 信令
* 1. 终端媒体流向 * 1. 终端媒体流向
* 2. 处理音频视频:降噪、水印等等 * 2. 处理音频视频:降噪、水印等等
*/ */
class Signal {
/**
* 处理事件
*
* @param {*} message 消息
* @param {*} session websocket
*/
on(message, session) {
}
}
module.exports = Signal;

View File

@@ -32,27 +32,3 @@
| taoyao-boot | | taoyao-boot |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
``` ```
## WebRTC资料
https://www.cnblogs.com/ssyfj/p/14828185.html
https://www.cnblogs.com/ssyfj/p/14826516.html
https://www.cnblogs.com/ssyfj/p/14823861.html
https://www.cnblogs.com/ssyfj/p/14815266.html
https://www.cnblogs.com/ssyfj/p/14811253.html
https://www.cnblogs.com/ssyfj/p/14806678.html
https://www.cnblogs.com/ssyfj/p/14805040.html
https://www.cnblogs.com/ssyfj/p/14788663.html
https://www.cnblogs.com/ssyfj/p/14787012.html
https://www.cnblogs.com/ssyfj/p/14783168.html
https://www.cnblogs.com/ssyfj/p/14781982.html
https://www.cnblogs.com/ssyfj/p/14778839.html
## Mediasoup资料
https://www.cnblogs.com/ssyfj/p/14855454.html
https://www.cnblogs.com/ssyfj/p/14851442.html
https://www.cnblogs.com/ssyfj/p/14850041.html
https://www.cnblogs.com/ssyfj/p/14847097.html
https://www.cnblogs.com/ssyfj/p/14843182.html
https://www.cnblogs.com/ssyfj/p/14843082.html

View File

@@ -1,34 +1 @@
# 媒体 # 媒体
# WebRTC
## WebRTC协议栈
```
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| HTTPS / WSS | | SCTP | SRTP / SRTCP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ICE / SDP / SIP +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| TLS | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ DTLS +-+-+-+-+-+-+-+-+-+
| HTTP / WS | NAT / STUN / TURN | | RTP / RTCP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| TCP | UDP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| IPv4 / IPv6 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
## 协议简介
* 会话通道ICE/SIP/SDP
* 媒体通道RTP/RTCP/SRTP/SRTCP
* RTP实时传输协议音频视频
* RTCPRTP传输控制协议监控数据传输质量并给予数据发送方反馈
* SCTP流控制传输协议自定义的应用数据传输
* RTMP实时消息传送协议
* RTSP可以控制媒体点播
### ICE/SDP/SIP
ICE信息的描述格式通常采用标准的SDP其全称为Session Description Protocol即会话描述协议。<br />
SDP只是一种信息格式的描述标准不属于传输协议但是可以被其他传输协议用来交换必要的信息例如SIP、RTSP等等。