[+] server
This commit is contained in:
@@ -41,6 +41,10 @@
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
* 录制
|
* 录制(Recorder)
|
||||||
* 音频:降噪、混音、变声
|
* 音频:降噪、混音、变声
|
||||||
* 视频:水印、美颜、AI识别
|
* 视频:水印、美颜、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
|
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媒体服务
|
## 其他常见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)
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 =
|
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,
|
||||||
// Worker:https://mediasoup.org/documentation/v3/mediasoup/api/#WorkerSettings
|
// Worker:https://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,
|
||||||
},
|
},
|
||||||
// Router:https://mediasoup.org/documentation/v3/mediasoup/api/#RouterOptions
|
// Router:https://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
|
},
|
||||||
}
|
},
|
||||||
}
|
],
|
||||||
]
|
|
||||||
},
|
},
|
||||||
// WebRtcServer:https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcServerOptions
|
// WebRtcServer:https://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,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// WebRtcTransport:https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
|
// WebRtcTransport:https://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,
|
||||||
},
|
},
|
||||||
// PlainTransport:https://mediasoup.org/documentation/v3/mediasoup/api/#PlainTransportOptions
|
// PlainTransport:https://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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* 适配媒体信令
|
* 信令
|
||||||
* 1. 终端媒体流向
|
* 1. 终端媒体流向
|
||||||
* 2. 处理音频视频:降噪、水印等等
|
* 2. 处理音频视频:降噪、水印等等
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
class Signal {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理事件
|
||||||
|
*
|
||||||
|
* @param {*} message 消息
|
||||||
|
* @param {*} session websocket
|
||||||
|
*/
|
||||||
|
on(message, session) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Signal;
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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