[mirotalksfu] - add RTMP server and multi-source streaming!, update dep
هذا الالتزام موجود في:
67
rtmpServers/demo/client-server-axios/server/RtmpStreamer.js
Normal file
67
rtmpServers/demo/client-server-axios/server/RtmpStreamer.js
Normal file
@@ -0,0 +1,67 @@
|
||||
'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) {
|
||||
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.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;
|
||||
28
rtmpServers/demo/client-server-axios/server/package.json
Normal file
28
rtmpServers/demo/client-server-axios/server/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "mirotalk-rtmp-streamer-server",
|
||||
"version": "1.0.0",
|
||||
"description": "MiroTalk RTMP Streamer Server",
|
||||
"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"
|
||||
],
|
||||
"author": "Miroslav Pejic",
|
||||
"license": "AGPLv3",
|
||||
"dependencies": {
|
||||
"body-parser": "1.20.2",
|
||||
"cors": "2.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"uuid": "10.0.0",
|
||||
"nodemon": "^3.1.3"
|
||||
}
|
||||
}
|
||||
182
rtmpServers/demo/client-server-axios/server/server.js
Normal file
182
rtmpServers/demo/client-server-axios/server/server.js
Normal file
@@ -0,0 +1,182 @@
|
||||
'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'); // Import the RtmpStreamer class
|
||||
const bodyParser = require('body-parser');
|
||||
const path = require('path');
|
||||
|
||||
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 app = express();
|
||||
|
||||
const corsOptions = { origin: '*', methods: ['GET', 'POST'] };
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
app.use(express.static(dir.client)); // Expose client
|
||||
app.use(express.json({ limit: '50mb' })); // Ensure the body parser can handle large files
|
||||
app.use(bodyParser.raw({ type: 'video/webm', limit: '50mb' })); // handle raw binary data
|
||||
|
||||
const streams = {}; // Collect all rtmp streams
|
||||
|
||||
function checkRTMPApiSecret(req, res, next) {
|
||||
const expectedApiSecret = rtmpCfg && rtmpCfg.apiSecret;
|
||||
const apiSecret = req.headers.authorization;
|
||||
|
||||
if (!apiSecret || apiSecret !== expectedApiSecret) {
|
||||
console.log('RTMP apiSecret Unauthorized', {
|
||||
apiSecret: apiSecret,
|
||||
expectedApiSecret: expectedApiSecret,
|
||||
});
|
||||
return res.status(401).send('Unauthorized');
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function checkMaxStreams(req, res, next) {
|
||||
const maxStreams = rtmpCfg.maxStreams || 10; // Set your maximum allowed streams here
|
||||
if (Object.keys(streams).length >= maxStreams) {
|
||||
console.log('Maximum number of streams reached', streams);
|
||||
return res.status(429).send('Maximum number of streams reached, please try later!');
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Define a route handler for the default home page
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(dir.index);
|
||||
});
|
||||
|
||||
app.get('/rtmpEnabled', (req, res) => {
|
||||
const rtmpEnabled = rtmpCfg && rtmpCfg.enabled;
|
||||
console.debug('RTMP enabled', rtmpEnabled);
|
||||
res.json({ enabled: rtmpEnabled });
|
||||
});
|
||||
|
||||
app.post('/initRTMP', checkRTMPApiSecret, checkMaxStreams, (req, res) => {
|
||||
if (!rtmpCfg || !rtmpCfg.enabled) {
|
||||
return res.status(400).send('RTMP server is not enabled or missing the config');
|
||||
}
|
||||
|
||||
const domainName = req.headers.host.split(':')[0];
|
||||
|
||||
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', {
|
||||
headers: req.headers,
|
||||
rtmpServer,
|
||||
rtmpServerSecret,
|
||||
rtmpServerURL,
|
||||
rtmpServerPath,
|
||||
expirationHours,
|
||||
rtmpStreamKey,
|
||||
rtmp,
|
||||
});
|
||||
|
||||
const stream = new RtmpStreamer(rtmp, rtmpStreamKey);
|
||||
streams[rtmpStreamKey] = stream;
|
||||
|
||||
console.log('Active RTMP Streams', Object.keys(streams).length);
|
||||
|
||||
res.json({ rtmp });
|
||||
});
|
||||
|
||||
app.post('/streamRTMP', checkRTMPApiSecret, (req, res) => {
|
||||
if (!rtmpCfg || !rtmpCfg.enabled) {
|
||||
return res.status(400).send('RTMP server is not enabled');
|
||||
}
|
||||
if (!req.body || req.body.length === 0) {
|
||||
return res.status(400).send('Invalid video data');
|
||||
}
|
||||
|
||||
const rtmpStreamKey = req.query.key;
|
||||
const stream = streams[rtmpStreamKey];
|
||||
|
||||
if (!stream || !stream.isRunning()) {
|
||||
delete streams[rtmpStreamKey];
|
||||
console.debug('Stream not found', { rtmpStreamKey, streams: Object.keys(streams).length });
|
||||
return res.status(404).send('FFmpeg Stream not found');
|
||||
}
|
||||
|
||||
console.debug('Received video data', {
|
||||
// data: req.body.slice(0, 20).toString('hex'),
|
||||
rtmpStreamKey: rtmpStreamKey,
|
||||
size: bytesToSize(req.headers['content-length']),
|
||||
});
|
||||
|
||||
stream.write(Buffer.from(req.body));
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.post('/stopRTMP', checkRTMPApiSecret, (req, res) => {
|
||||
if (!rtmpCfg || !rtmpCfg.enabled) {
|
||||
return res.status(400).send('RTMP server is not enabled');
|
||||
}
|
||||
|
||||
const rtmpStreamKey = req.query.key;
|
||||
const stream = streams[rtmpStreamKey];
|
||||
|
||||
if (stream) {
|
||||
stream.end();
|
||||
delete streams[rtmpStreamKey];
|
||||
console.debug('Active RTMP Streams', Object.keys(streams).length);
|
||||
}
|
||||
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
// Start the server and listen on port 3000
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on http://localhost:${port}`, rtmpCfg);
|
||||
});
|
||||
المرجع في مشكلة جديدة
حظر مستخدم