From 531229b8d78daabfdc01330d149f374acf4b37f0 Mon Sep 17 00:00:00 2001 From: acgist <289547414@qq.com> Date: Sat, 4 Feb 2023 16:33:39 +0800 Subject: [PATCH] [+] server --- README.md | 6 +- docs/Deploy.md | 21 - docs/Learning.md | 33 + docs/Test.md | 7 + taoyao-media-server/README.md | 39 +- taoyao-media-server/package.json | 10 +- taoyao-media-server/src/Command.js | 37 ++ taoyao-media-server/src/Logger.js | 47 ++ taoyao-media-server/src/certs/privateKey.pem | 28 + taoyao-media-server/src/certs/publicKey.pem | 22 + taoyao-media-server/src/config.js | 278 ++++---- taoyao-media-server/src/server.js | 659 ++++--------------- taoyao-media-server/src/signal.js | 18 +- taoyao-signal-server/README.md | 24 - taoyao-signal-server/taoyao-media/README.md | 33 - 15 files changed, 489 insertions(+), 773 deletions(-) create mode 100644 docs/Learning.md create mode 100644 docs/Test.md create mode 100644 taoyao-media-server/src/Command.js create mode 100644 taoyao-media-server/src/Logger.js create mode 100644 taoyao-media-server/src/certs/privateKey.pem create mode 100644 taoyao-media-server/src/certs/publicKey.pem 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等等。