diff --git a/README.md b/README.md
index f886be3..b01c9a3 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,10 @@
## TODO
-* 录制
+* 录制(Recorder)
* 音频:降噪、混音、变声
* 视频:水印、美颜、AI识别
+* 一个信令服务多个媒体服务
+* 信令服务集群
+* 信令直传
+* 媒体交互式启动
diff --git a/docs/Deploy.md b/docs/Deploy.md
index 2931cfa..09387c2 100644
--- a/docs/Deploy.md
+++ b/docs/Deploy.md
@@ -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
diff --git a/docs/Learning.md b/docs/Learning.md
new file mode 100644
index 0000000..17c129d
--- /dev/null
+++ b/docs/Learning.md
@@ -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
diff --git a/docs/Test.md b/docs/Test.md
new file mode 100644
index 0000000..70650e4
--- /dev/null
+++ b/docs/Test.md
@@ -0,0 +1,7 @@
+# 测试
+
+## 推流测试
+
+```
+ffmpeg
+```
diff --git a/taoyao-media-server/README.md b/taoyao-media-server/README.md
index 98ad436..a6607b2 100644
--- a/taoyao-media-server/README.md
+++ b/taoyao-media-server/README.md
@@ -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,即会话描述协议。
+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)
diff --git a/taoyao-media-server/package.json b/taoyao-media-server/package.json
index bac57ef..38907e4 100644
--- a/taoyao-media-server/package.json
+++ b/taoyao-media-server/package.json
@@ -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"
}
}
diff --git a/taoyao-media-server/src/Command.js b/taoyao-media-server/src/Command.js
new file mode 100644
index 0000000..e519265
--- /dev/null
+++ b/taoyao-media-server/src/Command.js
@@ -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);
+ }
+};
diff --git a/taoyao-media-server/src/Logger.js b/taoyao-media-server/src/Logger.js
new file mode 100644
index 0000000..512d821
--- /dev/null
+++ b/taoyao-media-server/src/Logger.js
@@ -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;
diff --git a/taoyao-media-server/src/certs/privateKey.pem b/taoyao-media-server/src/certs/privateKey.pem
new file mode 100644
index 0000000..b326ca4
--- /dev/null
+++ b/taoyao-media-server/src/certs/privateKey.pem
@@ -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-----
\ No newline at end of file
diff --git a/taoyao-media-server/src/certs/publicKey.pem b/taoyao-media-server/src/certs/publicKey.pem
new file mode 100644
index 0000000..1099efa
--- /dev/null
+++ b/taoyao-media-server/src/certs/publicKey.pem
@@ -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-----
\ No newline at end of file
diff --git a/taoyao-media-server/src/config.js b/taoyao-media-server/src/config.js
index 66c62f7..45bec43 100644
--- a/taoyao-media-server/src/config.js
+++ b/taoyao-media-server/src/config.js
@@ -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
- }
- }
-};
\ No newline at end of file
+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,
+ },
+ },
+};
diff --git a/taoyao-media-server/src/server.js b/taoyao-media-server/src/server.js
index ef7dd0d..1e96a8b 100644
--- a/taoyao-media-server/src/server.js
+++ b/taoyao-media-server/src/server.js
@@ -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}
-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}
+// 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
+ );
+ });
}
diff --git a/taoyao-media-server/src/signal.js b/taoyao-media-server/src/signal.js
index c6cd44b..ff65127 100644
--- a/taoyao-media-server/src/signal.js
+++ b/taoyao-media-server/src/signal.js
@@ -1,5 +1,21 @@
/**
- * 适配媒体信令
+ * 信令
* 1. 终端媒体流向
* 2. 处理音频视频:降噪、水印等等
*/
+
+class Signal {
+
+ /**
+ * 处理事件
+ *
+ * @param {*} message 消息
+ * @param {*} session websocket
+ */
+ on(message, session) {
+
+ }
+
+}
+
+module.exports = Signal;
diff --git a/taoyao-signal-server/README.md b/taoyao-signal-server/README.md
index b0d1c42..1e3b02a 100644
--- a/taoyao-signal-server/README.md
+++ b/taoyao-signal-server/README.md
@@ -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
diff --git a/taoyao-signal-server/taoyao-media/README.md b/taoyao-signal-server/taoyao-media/README.md
index 9212afd..0cece0c 100644
--- a/taoyao-signal-server/taoyao-media/README.md
+++ b/taoyao-signal-server/taoyao-media/README.md
@@ -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,即会话描述协议。
-SDP只是一种信息格式的描述标准,不属于传输协议,但是可以被其他传输协议用来交换必要的信息,例如:SIP、RTSP等等。