[+] server
This commit is contained in:
@@ -41,6 +41,10 @@
|
||||
|
||||
## TODO
|
||||
|
||||
* 录制
|
||||
* 录制(Recorder)
|
||||
* 音频:降噪、混音、变声
|
||||
* 视频:水印、美颜、AI识别
|
||||
* 一个信令服务多个媒体服务
|
||||
* 信令服务集群
|
||||
* 信令直传
|
||||
* 媒体交互式启动
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
## 资料
|
||||
|
||||
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
33
docs/Learning.md
Normal 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
7
docs/Test.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 测试
|
||||
|
||||
## 推流测试
|
||||
|
||||
```
|
||||
ffmpeg
|
||||
```
|
||||
@@ -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:实时传输协议(音频视频)
|
||||
* RTCP:RTP传输控制协议(监控数据传输质量并给予数据发送方反馈)
|
||||
* SCTP:流控制传输协议(自定义的应用数据传输)
|
||||
* RTMP:实时消息传送协议
|
||||
* RTSP:可以控制媒体(点播)
|
||||
|
||||
### ICE/SDP/SIP
|
||||
|
||||
ICE信息的描述格式通常采用标准的SDP,其全称为Session Description Protocol,即会话描述协议。<br />
|
||||
SDP只是一种信息格式的描述标准,不属于传输协议,但是可以被其他传输协议用来交换必要的信息,例如:SIP、RTSP等等。
|
||||
|
||||
## 其他常见WebRTC媒体服务
|
||||
|
||||
* [Janus](https://github.com/meetecho/janus-gateway/)
|
||||
* [Jitsi](https://github.com/jitsi)
|
||||
* [Janus](https://github.com/meetecho/janus-gateway/)
|
||||
* [Licode](https://github.com/lynckia/licode)
|
||||
* [Kurento](https://github.com/Kurento/kurento-media-server)
|
||||
* [Medooze](https://github.com/medooze/media-server)
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
"private": true,
|
||||
"description": "taoyao media server",
|
||||
"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": {
|
||||
"mediasoup": "file:./mediasoup"
|
||||
"ws": "^8.12.0",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"releaseDependencies": {
|
||||
"mediasoup-local": "file:./mediasoup",
|
||||
"mediasoup-online": "github:versatica/mediasoup#v3"
|
||||
}
|
||||
}
|
||||
|
||||
37
taoyao-media-server/src/Command.js
Normal file
37
taoyao-media-server/src/Command.js
Normal 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);
|
||||
}
|
||||
};
|
||||
47
taoyao-media-server/src/Logger.js
Normal file
47
taoyao-media-server/src/Logger.js
Normal 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;
|
||||
28
taoyao-media-server/src/certs/privateKey.pem
Normal file
28
taoyao-media-server/src/certs/privateKey.pem
Normal 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-----
|
||||
22
taoyao-media-server/src/certs/publicKey.pem
Normal file
22
taoyao-media-server/src/certs/publicKey.pem
Normal 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-----
|
||||
@@ -1,149 +1,137 @@
|
||||
/**
|
||||
* 配置
|
||||
*/
|
||||
const os = require('os');
|
||||
const os = require("os");
|
||||
|
||||
module.exports =
|
||||
{
|
||||
domain : process.env.DOMAIN || 'localhost',
|
||||
// Signal
|
||||
https :
|
||||
{
|
||||
listenIp : '0.0.0.0',
|
||||
listenPort : process.env.PROTOO_LISTEN_PORT || 4443,
|
||||
tls :
|
||||
{
|
||||
cert : process.env.HTTPS_CERT_FULLCHAIN || `${__dirname}/certs/fullchain.pem`,
|
||||
key : process.env.HTTPS_CERT_PRIVKEY || `${__dirname}/certs/privkey.pem`
|
||||
}
|
||||
},
|
||||
// Mediasoup
|
||||
mediasoup :
|
||||
{
|
||||
// 按照CPU数量配置进程数量
|
||||
numWorkers : Object.keys(os.cpus()).length,
|
||||
// Worker:https://mediasoup.org/documentation/v3/mediasoup/api/#WorkerSettings
|
||||
workerSettings :
|
||||
{
|
||||
logLevel : 'warn',
|
||||
logTags :
|
||||
[
|
||||
'bwe',
|
||||
'ice',
|
||||
'rtp',
|
||||
'rtx',
|
||||
'svc',
|
||||
'dtls',
|
||||
'info',
|
||||
'sctp',
|
||||
'srtp',
|
||||
'rtcp',
|
||||
'score',
|
||||
'message',
|
||||
'simulcast'
|
||||
],
|
||||
rtcMinPort : process.env.MEDIASOUP_MIN_PORT || 40000,
|
||||
rtcMaxPort : process.env.MEDIASOUP_MAX_PORT || 49999
|
||||
},
|
||||
// Router:https://mediasoup.org/documentation/v3/mediasoup/api/#RouterOptions
|
||||
routerOptions :
|
||||
{
|
||||
mediaCodecs :
|
||||
[
|
||||
{
|
||||
kind : 'audio',
|
||||
mimeType : 'audio/opus',
|
||||
clockRate : 48000,
|
||||
channels : 2
|
||||
},
|
||||
{
|
||||
kind : 'video',
|
||||
mimeType : 'video/VP8',
|
||||
clockRate : 90000,
|
||||
parameters :
|
||||
{
|
||||
'x-google-start-bitrate' : 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
kind : 'video',
|
||||
mimeType : 'video/VP9',
|
||||
clockRate : 90000,
|
||||
parameters :
|
||||
{
|
||||
'profile-id' : 2,
|
||||
'x-google-start-bitrate' : 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
kind : 'video',
|
||||
mimeType : 'video/h264',
|
||||
clockRate : 90000,
|
||||
parameters :
|
||||
{
|
||||
'packetization-mode' : 1,
|
||||
'profile-level-id' : '4d0032',
|
||||
'level-asymmetry-allowed' : 1,
|
||||
'x-google-start-bitrate' : 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
kind : 'video',
|
||||
mimeType : 'video/h264',
|
||||
clockRate : 90000,
|
||||
parameters :
|
||||
{
|
||||
'packetization-mode' : 1,
|
||||
'profile-level-id' : '42e01f',
|
||||
'level-asymmetry-allowed' : 1,
|
||||
'x-google-start-bitrate' : 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// WebRtcServer:https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcServerOptions
|
||||
webRtcServerOptions :
|
||||
{
|
||||
listenInfos :
|
||||
[
|
||||
{
|
||||
protocol : 'udp',
|
||||
ip : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
|
||||
announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP,
|
||||
port : 44444
|
||||
},
|
||||
{
|
||||
protocol : 'tcp',
|
||||
ip : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
|
||||
announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP,
|
||||
port : 44444
|
||||
}
|
||||
],
|
||||
},
|
||||
// WebRtcTransport:https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
|
||||
webRtcTransportOptions :
|
||||
{
|
||||
listenIps :
|
||||
[
|
||||
{
|
||||
ip : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
|
||||
announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP
|
||||
}
|
||||
],
|
||||
initialAvailableOutgoingBitrate : 1000000,
|
||||
minimumAvailableOutgoingBitrate : 600000,
|
||||
maxSctpMessageSize : 262144,
|
||||
maxIncomingBitrate : 1500000
|
||||
},
|
||||
// PlainTransport:https://mediasoup.org/documentation/v3/mediasoup/api/#PlainTransportOptions
|
||||
plainTransportOptions :
|
||||
{
|
||||
listenIp :
|
||||
{
|
||||
ip : process.env.MEDIASOUP_LISTEN_IP || '0.0.0.0',
|
||||
announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP
|
||||
},
|
||||
maxSctpMessageSize : 262144
|
||||
}
|
||||
}
|
||||
};
|
||||
module.exports = {
|
||||
// 系统名称
|
||||
name: "taoyao-media-server",
|
||||
// 交互式命令行
|
||||
command: true,
|
||||
// 信令服务
|
||||
https: {
|
||||
listenIp: "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:https://mediasoup.org/documentation/v3/mediasoup/api/#WorkerSettings
|
||||
workerSettings: {
|
||||
logLevel: "warn",
|
||||
logTags: [
|
||||
"bwe",
|
||||
"ice",
|
||||
"rtp",
|
||||
"rtx",
|
||||
"svc",
|
||||
"dtls",
|
||||
"info",
|
||||
"sctp",
|
||||
"srtp",
|
||||
"rtcp",
|
||||
"score",
|
||||
"message",
|
||||
"simulcast",
|
||||
],
|
||||
rtcMinPort: process.env.MEDIASOUP_MIN_PORT || 40000,
|
||||
rtcMaxPort: process.env.MEDIASOUP_MAX_PORT || 49999,
|
||||
},
|
||||
// Router:https://mediasoup.org/documentation/v3/mediasoup/api/#RouterOptions
|
||||
routerOptions: {
|
||||
mediaCodecs: [
|
||||
{
|
||||
kind: "audio",
|
||||
mimeType: "audio/opus",
|
||||
clockRate: 48000,
|
||||
channels: 2,
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
mimeType: "video/VP8",
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
"x-google-start-bitrate": 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
mimeType: "video/VP9",
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
"profile-id": 2,
|
||||
"x-google-start-bitrate": 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
mimeType: "video/h264",
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "4d0032",
|
||||
"level-asymmetry-allowed": 1,
|
||||
"x-google-start-bitrate": 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "video",
|
||||
mimeType: "video/h264",
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "42e01f",
|
||||
"level-asymmetry-allowed": 1,
|
||||
"x-google-start-bitrate": 1000,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// WebRtcServer:https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcServerOptions
|
||||
webRtcServerOptions: {
|
||||
listenInfos: [
|
||||
{
|
||||
protocol: "udp",
|
||||
ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
|
||||
port: 44444,
|
||||
},
|
||||
{
|
||||
protocol: "tcp",
|
||||
ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
|
||||
port: 44444,
|
||||
},
|
||||
],
|
||||
},
|
||||
// WebRtcTransport:https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
|
||||
webRtcTransportOptions: {
|
||||
listenIps: [
|
||||
{
|
||||
ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
|
||||
},
|
||||
],
|
||||
initialAvailableOutgoingBitrate: 1000000,
|
||||
minimumAvailableOutgoingBitrate: 600000,
|
||||
maxSctpMessageSize: 262144,
|
||||
maxIncomingBitrate: 1500000,
|
||||
},
|
||||
// PlainTransport:https://mediasoup.org/documentation/v3/mediasoup/api/#PlainTransportOptions
|
||||
plainTransportOptions: {
|
||||
listenIp: {
|
||||
ip: process.env.MEDIASOUP_LISTEN_IP || "0.0.0.0",
|
||||
announcedIp: process.env.MEDIASOUP_ANNOUNCED_IP,
|
||||
},
|
||||
maxSctpMessageSize: 262144,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,557 +1,130 @@
|
||||
/**
|
||||
* 媒体服务
|
||||
*/
|
||||
#!/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();
|
||||
|
||||
// Async queue to manage rooms.
|
||||
// @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}
|
||||
const signal = new Signal();
|
||||
// HTTPS server
|
||||
let httpsServer;
|
||||
|
||||
// Express application.
|
||||
// @type {Function}
|
||||
let expressApp;
|
||||
|
||||
// Protoo WebSocket server.
|
||||
// @type {protoo.WebSocketServer}
|
||||
let protooWebSocketServer;
|
||||
|
||||
// mediasoup Workers.
|
||||
// @type {Array<mediasoup.Worker>}
|
||||
// WebSocket server
|
||||
let webSocketServer;
|
||||
// Mediasoup Worker列表
|
||||
const mediasoupWorkers = [];
|
||||
// Mediasoup Worker下个索引
|
||||
let nextMediasoupWorkerIndex = 0;
|
||||
|
||||
// Index of next mediasoup Worker to use.
|
||||
// @type {Number}
|
||||
let nextMediasoupWorkerIdx = 0;
|
||||
process.title = config.name;
|
||||
process.env.DEBUG = process.env.DEBUG || "*mediasoup* *INFO* *WARN* *ERROR*";
|
||||
logger.info("开始启动:%s", config.name);
|
||||
|
||||
run();
|
||||
|
||||
async function run()
|
||||
{
|
||||
// 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();
|
||||
|
||||
// Create Express app.
|
||||
await createExpressApp();
|
||||
|
||||
// Run HTTPS server.
|
||||
await runHttpsServer();
|
||||
|
||||
// Run a protoo WebSocketServer.
|
||||
await runProtooWebSocketServer();
|
||||
|
||||
// Log rooms status every X seconds.
|
||||
setInterval(() =>
|
||||
{
|
||||
for (const room of rooms.values())
|
||||
{
|
||||
room.logStatus();
|
||||
}
|
||||
}, 120000);
|
||||
async function run() {
|
||||
// 启动Mediasoup服务
|
||||
await runMediasoupWorkers();
|
||||
// 启动服务
|
||||
await runSignalServer();
|
||||
logger.info("启动完成:%s", config.name);
|
||||
// 交互式命令行
|
||||
if (config.command) {
|
||||
await command();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch as many mediasoup Workers as given in the configuration file.
|
||||
*/
|
||||
async function runMediasoupWorkers()
|
||||
{
|
||||
const { numWorkers } = config.mediasoup;
|
||||
|
||||
logger.info('running %d mediasoup Workers...', numWorkers);
|
||||
|
||||
for (let i = 0; i < numWorkers; ++i)
|
||||
{
|
||||
const worker = await mediasoup.createWorker(
|
||||
{
|
||||
logLevel : config.mediasoup.workerSettings.logLevel,
|
||||
logTags : config.mediasoup.workerSettings.logTags,
|
||||
rtcMinPort : Number(config.mediasoup.workerSettings.rtcMinPort),
|
||||
rtcMaxPort : Number(config.mediasoup.workerSettings.rtcMaxPort)
|
||||
});
|
||||
|
||||
worker.on('died', () =>
|
||||
{
|
||||
logger.error(
|
||||
'mediasoup Worker died, exiting in 2 seconds... [pid:%d]', worker.pid);
|
||||
|
||||
setTimeout(() => process.exit(1), 2000);
|
||||
});
|
||||
|
||||
mediasoupWorkers.push(worker);
|
||||
|
||||
// Create a WebRtcServer in this Worker.
|
||||
if (process.env.MEDIASOUP_USE_WEBRTC_SERVER !== 'false')
|
||||
{
|
||||
// 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;
|
||||
|
||||
for (const listenInfo of webRtcServerOptions.listenInfos)
|
||||
{
|
||||
listenInfo.port += portIncrement;
|
||||
}
|
||||
|
||||
const webRtcServer = await worker.createWebRtcServer(webRtcServerOptions);
|
||||
|
||||
worker.appData.webRtcServer = webRtcServer;
|
||||
}
|
||||
|
||||
// Log worker resource usage every X seconds.
|
||||
setInterval(async () =>
|
||||
{
|
||||
const usage = await worker.getResourceUsage();
|
||||
|
||||
logger.info('mediasoup Worker resource usage [pid:%d]: %o', worker.pid, usage);
|
||||
}, 120000);
|
||||
}
|
||||
async function runMediasoupWorkers() {
|
||||
const { numWorkers } = config.mediasoup;
|
||||
logger.info("启动Mediasoup服务(%d Worker)...", numWorkers);
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
const worker = await mediasoup.createWorker({
|
||||
logLevel: config.mediasoup.workerSettings.logLevel,
|
||||
logTags: config.mediasoup.workerSettings.logTags,
|
||||
rtcMinPort: Number(config.mediasoup.workerSettings.rtcMinPort),
|
||||
rtcMaxPort: Number(config.mediasoup.workerSettings.rtcMaxPort),
|
||||
});
|
||||
worker.on("died", () => {
|
||||
logger.error(
|
||||
"Mediasoup Worker停止服务(两秒之后自动退出)... [PID:%d]",
|
||||
worker.pid
|
||||
);
|
||||
setTimeout(() => process.exit(1), 2000);
|
||||
});
|
||||
mediasoupWorkers.push(worker);
|
||||
// 配置WebRTC服务
|
||||
if (process.env.MEDIASOUP_USE_WEBRTC_SERVER !== "false") {
|
||||
// 每个Worker端口不能相同
|
||||
const portIncrement = mediasoupWorkers.length - 1;
|
||||
const webRtcServerOptions = JSON.parse(JSON.stringify(config.mediasoup.webRtcServerOptions));
|
||||
for (const listenInfo of webRtcServerOptions.listenInfos) {
|
||||
listenInfo.port += portIncrement;
|
||||
}
|
||||
const webRtcServer = await worker.createWebRtcServer(webRtcServerOptions);
|
||||
worker.appData.webRtcServer = webRtcServer;
|
||||
}
|
||||
// 记录日志
|
||||
setInterval(async () => {
|
||||
const usage = await worker.getResourceUsage();
|
||||
logger.info(
|
||||
"Mediasoup Worker使用情况 [pid:%d]: %o",
|
||||
worker.pid,
|
||||
usage
|
||||
);
|
||||
}, 120 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
'protoo connection request [roomId:%s, peerId:%s, address:%s, origin:%s]',
|
||||
roomId, peerId, info.socket.remoteAddress, info.origin);
|
||||
|
||||
// Serialize this code into the queue to avoid that two peers connecting at
|
||||
// the same time with the same roomId create two separate rooms with same
|
||||
// roomId.
|
||||
queue.push(async () =>
|
||||
{
|
||||
const room = await getOrCreateRoom({ roomId, consumerReplicas });
|
||||
|
||||
// Accept the protoo WebSocket connection.
|
||||
const protooWebSocketTransport = accept();
|
||||
|
||||
room.handleProtooConnection({ peerId, protooWebSocketTransport });
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('room creation or room joining failed:%o', error);
|
||||
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next mediasoup Worker.
|
||||
*/
|
||||
function getMediasoupWorker()
|
||||
{
|
||||
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;
|
||||
async function runSignalServer() {
|
||||
const tls = {
|
||||
cert: fs.readFileSync(config.https.tls.cert),
|
||||
key: fs.readFileSync(config.https.tls.key),
|
||||
};
|
||||
logger.info("配置HTTPS服务...");
|
||||
httpsServer = https.createServer(tls, (request, response) => {
|
||||
response.writeHead(200);
|
||||
response.end("taoyao media server");
|
||||
});
|
||||
logger.info("配置WebSocket服务...");
|
||||
webSocketServer = new ws.Server({ server: httpsServer });
|
||||
webSocketServer.on("connection", (session) => {
|
||||
session.on("open", (message) => {
|
||||
logger.info("打开信令通道: %s", message);
|
||||
});
|
||||
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(
|
||||
Number(config.https.listenPort),
|
||||
config.https.listenIp,
|
||||
resolve
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
/**
|
||||
* 适配媒体信令
|
||||
* 信令
|
||||
* 1. 终端媒体流向
|
||||
* 2. 处理音频视频:降噪、水印等等
|
||||
*/
|
||||
|
||||
class Signal {
|
||||
|
||||
/**
|
||||
* 处理事件
|
||||
*
|
||||
* @param {*} message 消息
|
||||
* @param {*} session websocket
|
||||
*/
|
||||
on(message, session) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Signal;
|
||||
|
||||
@@ -32,27 +32,3 @@
|
||||
| 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
|
||||
|
||||
@@ -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:实时传输协议(音频视频)
|
||||
* RTCP:RTP传输控制协议(监控数据传输质量并给予数据发送方反馈)
|
||||
* SCTP:流控制传输协议(自定义的应用数据传输)
|
||||
* RTMP:实时消息传送协议
|
||||
* RTSP:可以控制媒体(点播)
|
||||
|
||||
### ICE/SDP/SIP
|
||||
|
||||
ICE信息的描述格式通常采用标准的SDP,其全称为Session Description Protocol,即会话描述协议。<br />
|
||||
SDP只是一种信息格式的描述标准,不属于传输协议,但是可以被其他传输协议用来交换必要的信息,例如:SIP、RTSP等等。
|
||||
|
||||
Reference in New Issue
Block a user