├── .dockerignore ├── .gitignore ├── README.md ├── Dockerfile ├── tsconfig.json ├── package.json ├── .github └── workflows │ └── main.yml └── src └── server.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rtmp-webrtc-adapter 2 | Feed rtmp stream to webrtc based on mediasoup 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | RUN apk add --no-cache linux-headers g++ make python2 ffmpeg 4 | 5 | WORKDIR /home/node/app 6 | COPY src /home/node/app/src 7 | COPY package.json /home/node/app/ 8 | COPY package-lock.json /home/node/app/ 9 | COPY tsconfig.json /home/node/app/ 10 | 11 | RUN npm ci 12 | RUN npm run build 13 | 14 | ENTRYPOINT [ "sh", "-c", "npm start" ] 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "outDir": "dist", 10 | "strict": true, 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "sourceMap": true 16 | }, 17 | "include": [ 18 | "src" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtmp-webrtc-adapter", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rm -rf dist/ && tsc", 8 | "start": "node dist/server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.17.1", 14 | "mediasoup": "^3.6.20", 15 | "socket.io": "^2.3.0" 16 | }, 17 | "devDependencies": { 18 | "@types/express": "^4.17.8", 19 | "@types/socket.io": "^2.1.11", 20 | "ts-node": "^9.0.0", 21 | "typescript": "^4.0.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: rtmp-webrtc-adapter-cicd 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Docker build & publish 15 | uses: docker/build-push-action@v1 16 | with: 17 | username: "${{ secrets.DOCKER_USERNAME }}" 18 | password: "${{ secrets.DOCKER_PASSWORD }}" 19 | registry: registry.cn-beijing.aliyuncs.com 20 | repository: mengli/rtmp-webrtc-adapter 21 | tags: latest 22 | 23 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as mediasoup from 'mediasoup'; 2 | import * as express from 'express'; 3 | import * as http from 'http'; 4 | import * as socketIO from 'socket.io'; 5 | import * as child_process from 'child_process'; 6 | 7 | interface Source { 8 | name: string; 9 | rtmpUrl: string; 10 | } 11 | 12 | const PORT = process.env.PORT || 3000; 13 | const RTC_MIN_PORT = Number(process.env.RTC_MIN_PORT || 30000); 14 | const RTC_MAX_PORT = Number(process.env.RTC_MAX_PORT || 31000); 15 | const RTC_LISTEN_IP = process.env.RTC_LISTEN_IP || '127.0.0.1'; 16 | const RTC_ANNOUNCED_IP = process.env.RTC_ANNOUNCED_IP || '0.0.0.0'; 17 | 18 | let expressApp: express.Express; 19 | let httpServer: http.Server; 20 | let socketServer: socketIO.Server; 21 | let webRtcWorker: mediasoup.types.Worker; 22 | let mediasoupRouter: mediasoup.types.Router; 23 | 24 | const consumerTransports = new Map(); 25 | const socketTransports = new Map(); 26 | const consumers = new Map(); 27 | const producerTransports = new Map(); 28 | const producers = new Map(); 29 | const sources: Source[] = []; 30 | 31 | function generateSSRC() { 32 | return Math.trunc(Math.random() * 10000); 33 | } 34 | 35 | (async () => { 36 | await runExpressApp(); 37 | await runSocketServer(); 38 | await runMediasoupWorker(); 39 | })(); 40 | 41 | async function runExpressApp() { 42 | expressApp = express(); 43 | expressApp.use(express.json()); 44 | 45 | httpServer = http.createServer(expressApp); 46 | return new Promise((resolve) => { 47 | httpServer.listen(PORT, () => { 48 | console.log(`server is running at https://localhost:${PORT}`); 49 | resolve(); 50 | }); 51 | }); 52 | } 53 | 54 | async function runSocketServer() { 55 | socketServer = socketIO(httpServer, { 56 | serveClient: false, 57 | path: '/server', 58 | }); 59 | socketServer.on('connection', (socket) => { 60 | console.log('client connected'); 61 | 62 | socket.on('disconnect', () => { 63 | console.log('client disconnected'); 64 | const transport = socketTransports.get(socket.id); 65 | if (transport) { 66 | console.log(`close transport ${transport.id}`); 67 | transport.close(); 68 | } 69 | }); 70 | 71 | socket.on('connect_error', (err) => { 72 | console.error('client connection error', err); 73 | }); 74 | 75 | socket.on('createSource', async (data, callback) => { 76 | console.log(`createSource ${JSON.stringify(data)}`); 77 | const source = data as Source; 78 | if (!sources.find(s => s.name === source.name)) { 79 | await createSource(source) 80 | sources.push(source); 81 | } 82 | callback(); 83 | }); 84 | 85 | socket.on('getRouterRtpCapabilities', (data, callback) => { 86 | console.log(`getRouterRtpCapabilities ${JSON.stringify(data)}`); 87 | callback(mediasoupRouter.rtpCapabilities); 88 | }); 89 | 90 | socket.on('createConsumerTransport', async (data, callback) => { 91 | console.log(`createConsumerTransport ${JSON.stringify(data)}`); 92 | const { transport, params } = await createWebRtcTransport(); 93 | consumerTransports.set(transport.id, transport); 94 | socketTransports.set(socket.id, transport); 95 | callback(params); 96 | }); 97 | 98 | socket.on('connectConsumerTransport', async (data, callback) => { 99 | console.log(`connectConsumerTransport ${JSON.stringify(data)}`); 100 | const { transportId } = data; 101 | const consumerTransport = consumerTransports.get(transportId); 102 | if (!consumerTransport) { 103 | throw new Error(`consumer transport ${transportId} not found`); 104 | } 105 | await consumerTransport.connect({ dtlsParameters: data.dtlsParameters }); 106 | callback(); 107 | }); 108 | 109 | socket.on('consume', async (data, callback) => { 110 | console.log(`consume ${JSON.stringify(data)}`); 111 | const { source, transportId } = data; 112 | const producer = producers.get(source) as mediasoup.types.Producer; 113 | const consumerTransport = consumerTransports.get(transportId); 114 | if (!consumerTransport) { 115 | throw new Error(`consumer transport ${transportId} not found`); 116 | } 117 | const consumer = await createConsumer(producer, consumerTransport, data.rtpCapabilities); 118 | consumers.set(consumer.id, consumer); 119 | callback({ 120 | producerId: producer.id, 121 | id: consumer.id, 122 | kind: consumer.kind, 123 | rtpParameters: consumer.rtpParameters, 124 | type: consumer.type, 125 | producerPaused: consumer.producerPaused 126 | }); 127 | }); 128 | 129 | socket.on('resume', async (data, callback) => { 130 | console.log(`resume ${JSON.stringify(data)}`); 131 | const { consumerId } = data; 132 | const consumer = consumers.get(consumerId); 133 | if (!consumer) { 134 | throw new Error(`consumer ${consumerId} not found`); 135 | } 136 | await consumer.resume(); 137 | callback(); 138 | }); 139 | }); 140 | } 141 | 142 | async function runMediasoupWorker() { 143 | webRtcWorker = await mediasoup.createWorker({ 144 | rtcMinPort: RTC_MIN_PORT, 145 | rtcMaxPort: RTC_MAX_PORT, 146 | }); 147 | mediasoupRouter = await webRtcWorker.createRouter({ 148 | mediaCodecs: [ 149 | { 150 | kind: 'video' as mediasoup.types.MediaKind, 151 | mimeType: 'video/H264', 152 | clockRate: 90000 153 | } 154 | ], 155 | }); 156 | } 157 | 158 | async function createWebRtcTransport() { 159 | const transport = await mediasoupRouter.createWebRtcTransport({ 160 | listenIps: [ { ip: RTC_LISTEN_IP, announcedIp: RTC_ANNOUNCED_IP } ], 161 | enableUdp: true, 162 | enableTcp: true, 163 | preferUdp: true, 164 | }); 165 | return { 166 | transport, 167 | params: { 168 | id: transport.id, 169 | iceParameters: transport.iceParameters, 170 | iceCandidates: transport.iceCandidates, 171 | dtlsParameters: transport.dtlsParameters 172 | }, 173 | }; 174 | } 175 | 176 | async function createConsumer( 177 | producer: mediasoup.types.Producer, 178 | consumerTransport: mediasoup.types.WebRtcTransport, 179 | rtpCapabilities: mediasoup.types.RtpCapabilities 180 | ) { 181 | if (!mediasoupRouter.canConsume( 182 | { 183 | producerId: producer.id, 184 | rtpCapabilities, 185 | }) 186 | ) { 187 | throw new Error('can not consume'); 188 | } 189 | return consumerTransport.consume({ 190 | producerId: producer.id, 191 | rtpCapabilities, 192 | paused: producer.kind === 'video', 193 | }); 194 | } 195 | 196 | async function createSource(source: Source) { 197 | const transport = await mediasoupRouter.createPlainTransport({ 198 | listenIp: '0.0.0.0', 199 | rtcpMux: false, 200 | comedia: true 201 | }); 202 | 203 | const ssrc = generateSSRC(); 204 | const payloadType = 102; 205 | const producer = await transport.produce({ 206 | kind: 'video', 207 | rtpParameters: { 208 | codecs: [ 209 | { 210 | mimeType: 'video/H264', 211 | clockRate: 90000, 212 | payloadType: payloadType, 213 | } 214 | ], 215 | encodings: [ 216 | { 217 | ssrc: ssrc, 218 | } 219 | ] 220 | } 221 | }); 222 | producerTransports.set(source.name, transport); 223 | producers.set(source.name, producer); 224 | 225 | const args = [ 226 | '-analyzeduration', '20M', 227 | '-i', `${source.rtmpUrl}`, 228 | '-map', '0:v:0', 229 | '-pix_fmt', 'yuv420p', 230 | '-c:v', 'libx264', 231 | '-tune', 'zerolatency', 232 | '-preset', 'ultrafast', 233 | '-f', 'tee', 234 | `"[select=v:f=rtp:ssrc=${ssrc}:payload_type=${payloadType}]rtp://127.0.0.1:${transport.tuple.localPort}?rtcpport=${transport.rtcpTuple?.localPort}"`, 235 | ]; 236 | 237 | console.log(`Executing ffmpeg ${args.join(' ')}`); 238 | const process = child_process.spawn('ffmpeg', args, { shell: true }); 239 | 240 | process.stderr.on('data', data => { 241 | console.warn(`[${source.name}] ${data.toString()}`); 242 | }); 243 | 244 | process.on('exit', () => { 245 | console.log(`process exit: ${source}`); 246 | }); 247 | } 248 | --------------------------------------------------------------------------------