[mirotalksfu] - add RTMP server and multi-source streaming!, update dep

هذا الالتزام موجود في:
Miroslav Pejic
2024-06-29 18:49:10 +02:00
الأصل aaf5fe44ed
التزام 3929212631
52 ملفات معدلة مع 3986 إضافات و132 حذوفات

عرض الملف

@@ -0,0 +1,68 @@
'use strict';
const { PassThrough } = require('stream');
const ffmpeg = require('fluent-ffmpeg');
const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
class RtmpStreamer {
constructor(rtmpUrl, rtmpKey, socket) {
(this.socket = socket), (this.rtmpUrl = rtmpUrl);
this.rtmpKey = rtmpKey;
this.stream = new PassThrough();
this.ffmpegStream = null;
this.initFFmpeg();
this.run = true;
}
initFFmpeg() {
this.ffmpegStream = ffmpeg()
.input(this.stream)
.inputOptions('-re')
.inputFormat('webm')
.videoCodec('libx264')
.videoBitrate('3000k')
.size('1280x720')
.audioCodec('aac')
.audioBitrate('128k')
.outputOptions(['-f flv'])
.output(this.rtmpUrl)
.on('start', (commandLine) => console.info('ffmpeg command', { id: this.rtmpKey, cmd: commandLine }))
.on('error', (err, stdout, stderr) => {
if (!err.message.includes('Exiting normally')) {
console.error('FFmpeg error:', { id: this.rtmpKey, error: err.message });
this.socket.emit('error', err.message);
}
this.end();
})
.on('end', () => {
console.info('FFmpeg process ended', this.rtmpKey);
this.end();
})
.run();
}
write(data) {
if (this.stream) this.stream.write(data);
}
isRunning() {
return this.run;
}
end() {
if (this.stream) {
this.stream.end();
this.stream = null;
console.info('RTMP streaming stopped', this.rtmpKey);
}
if (this.ffmpegStream && !this.ffmpegStream.killed) {
this.ffmpegStream.kill('SIGTERM');
this.ffmpegStream = null;
console.info('FFMPEG closed successfully', this.rtmpKey);
}
this.run = false;
}
}
module.exports = RtmpStreamer;

عرض الملف

@@ -0,0 +1,30 @@
{
"name": "mirotalk-rtmp-server-streamer",
"version": "1.0.0",
"description": "MiroTalk RTMP Server Streamer",
"main": "server.js",
"scripts": {
"start": "node server.js",
"start-dev": "nodemon server.js",
"nms-start": "docker-compose -f ../../../node-media-server/docker-compose.yml up -d",
"nms-stop": "docker-compose -f ../../../node-media-server/docker-compose.yml down",
"nms-restart": "docker-compose -f ../../../node-media-server/docker-compose.yml down && docker-compose -f ../../node-media-server/docker-compose.yml up -d",
"nms-logs": "docker logs -f mirotalk-nms"
},
"keywords": [
"rtmp",
"server",
"streaming"
],
"author": "Miroslav Pejic",
"license": "AGPLv3",
"dependencies": {
"body-parser": "1.20.2",
"cors": "2.8.5",
"socket.io": "4.7.5"
},
"devDependencies": {
"uuid": "10.0.0",
"nodemon": "^3.1.3"
}
}

عرض الملف

@@ -0,0 +1,179 @@
'use strict';
const express = require('express');
const cors = require('cors');
const { v4: uuidv4 } = require('uuid');
const crypto = require('crypto-js');
const RtmpStreamer = require('./RtmpStreamer.js');
const http = require('http');
const path = require('path');
const socketIo = require('socket.io');
const port = 9999;
const rtmpCfg = {
enabled: true,
maxStreams: 1,
server: 'rtmp://localhost:1935',
appName: 'mirotalk',
streamKey: '',
secret: 'mirotalkRtmpSecret', // Must match the key in node-media-server/src/config.js if play and publish are set to true, otherwise leave it ''
apiSecret: 'mirotalkRtmpApiSecret', // Must match the apiSecret specified in the Client side.
expirationHours: 4,
};
const dir = {
client: path.join(__dirname, '../', 'client'),
index: path.join(__dirname, '../', 'client/index.html'),
};
const streams = {}; // Collect all rtmp streams
const corsOptions = { origin: '*', methods: ['GET'] };
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
transports: ['websocket'],
cors: corsOptions,
});
app.use(express.static(dir.client));
app.use(cors(corsOptions));
// Logs requests
app.use((req, res, next) => {
console.debug('New request:', {
method: req.method,
path: req.originalUrl,
});
next();
});
function checkRTMPApiSecret(apiSecret) {
return apiSecret && apiSecret === rtmpCfg.apiSecret;
}
function checkMaxStreams() {
return Object.keys(streams).length < rtmpCfg.maxStreams;
}
app.get('/', (req, res) => {
res.sendFile(dir.index);
});
io.on('connection', (socket) => {
console.log(`Socket connected: ${socket.id}`);
socket.on('initRTMP', ({ apiSecret }) => {
//
if (!checkRTMPApiSecret(apiSecret)) {
return socket.emit('error', 'Unauthorized');
}
if (!checkMaxStreams()) {
return socket.emit('error', 'Maximum number of streams reached, please try later!');
}
const hostHeader = socket.handshake.headers.host;
const domainName = hostHeader.split(':')[0]; // Extract domain name
const rtmpServer = rtmpCfg.server !== '' ? rtmpCfg.server : false;
const rtmpServerAppName = rtmpCfg.appName !== '' ? rtmpCfg.appName : 'live';
const rtmpStreamKey = rtmpCfg.streamKey !== '' ? rtmpCfg.streamKey : uuidv4();
const rtmpServerSecret = rtmpCfg.secret !== '' ? rtmpCfg.secret : false;
const expirationHours = rtmpCfg.expirationHours || 4;
const rtmpServerURL = rtmpServer ? rtmpServer : `rtmp://${domainName}:1935`;
const rtmpServerPath = '/' + rtmpServerAppName + '/' + rtmpStreamKey;
const rtmp = rtmpServerSecret
? generateRTMPUrl(rtmpServerURL, rtmpServerPath, rtmpServerSecret, expirationHours)
: rtmpServerURL + rtmpServerPath;
console.info('initRTMP', {
rtmpServer,
rtmpServerSecret,
rtmpServerURL,
rtmpServerPath,
expirationHours,
rtmpStreamKey,
rtmp,
});
const stream = new RtmpStreamer(rtmp, rtmpStreamKey, socket);
streams[rtmpStreamKey] = stream;
console.log('Active RTMP Streams', Object.keys(streams).length);
return socket.emit('initRTMP', { rtmp, rtmpStreamKey });
});
socket.on('streamRTMP', async ({ apiSecret, rtmpStreamKey, data }) => {
if (!checkRTMPApiSecret(apiSecret)) {
return socket.emit('error', 'Unauthorized');
}
const stream = streams[rtmpStreamKey];
if (!stream || !stream.isRunning()) {
delete streams[rtmpStreamKey];
console.debug('Stream not found', { rtmpStreamKey, streams: Object.keys(streams).length });
return;
}
console.debug('Received video data via Socket.IO', {
// data: data.slice(0, 20).toString('hex'),
key: rtmpStreamKey,
size: bytesToSize(data.length),
});
stream.write(Buffer.from(data));
socket.emit('ack');
});
socket.on('stopRTMP', ({ apiSecret, rtmpStreamKey }) => {
if (!checkRTMPApiSecret(apiSecret)) {
return socket.emit('error', 'Unauthorized');
}
const stream = streams[rtmpStreamKey];
if (stream) {
stream.end();
delete streams[rtmpStreamKey];
console.debug('Streams', Object.keys(streams).length);
}
socket.emit('stopRTMP');
});
socket.on('disconnect', () => {
console.log(`Socket disconnected: ${socket.id}`);
});
});
function generateRTMPUrl(baseURL, streamPath, secretKey, expirationHours = 4) {
const currentTime = Math.floor(Date.now() / 1000);
const expirationTime = currentTime + expirationHours * 3600;
const hashValue = crypto.MD5(`${streamPath}-${expirationTime}-${secretKey}`).toString();
const rtmpUrl = `${baseURL}${streamPath}?sign=${expirationTime}-${hashValue}`;
console.debug('generateRTMPUrl', {
currentTime,
expirationTime,
hashValue,
rtmpUrl,
});
return rtmpUrl;
}
function bytesToSize(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 Byte';
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
}
server.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`, rtmpCfg);
});