[mirotalksfu] - add RTMP server and multi-source streaming!, update dep
هذا الالتزام موجود في:
175
app/src/Room.js
175
app/src/Room.js
@@ -1,6 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const crypto = require('crypto-js');
|
||||
const RtmpFile = require('./RtmpFile');
|
||||
const RtmpUrl = require('./RtmpUrl');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const Logger = require('./Logger');
|
||||
const log = new Logger('Room');
|
||||
|
||||
@@ -50,6 +56,11 @@ module.exports = class Room {
|
||||
this.router = null;
|
||||
this.routerSettings = config.mediasoup.router;
|
||||
this.createTheRouter();
|
||||
|
||||
// RTMP configuration
|
||||
this.rtmpFileStreamer = null;
|
||||
this.rtmpUrlStreamer = null;
|
||||
this.rtmp = config.server.rtmp || false;
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
@@ -66,6 +77,12 @@ module.exports = class Room {
|
||||
isLobbyEnabled: this._isLobbyEnabled,
|
||||
hostOnlyRecording: this._hostOnlyRecording,
|
||||
},
|
||||
rtmp: {
|
||||
enabled: this.rtmp && this.rtmp.enabled,
|
||||
fromFile: this.rtmp && this.rtmp.fromFile,
|
||||
fromUrl: this.rtmp && this.rtmp.fromUrl,
|
||||
fromStream: this.rtmp && this.rtmp.fromStream,
|
||||
},
|
||||
moderator: this._moderator,
|
||||
survey: this.survey,
|
||||
redirect: this.redirect,
|
||||
@@ -74,6 +91,164 @@ module.exports = class Room {
|
||||
};
|
||||
}
|
||||
|
||||
// ##############################################
|
||||
// RTMP from FILE
|
||||
// ##############################################
|
||||
|
||||
isRtmpFileStreamerActive() {
|
||||
return this.rtmpFileStreamer;
|
||||
}
|
||||
|
||||
async getRTMP(dir = 'rtmp') {
|
||||
const folderPath = path.join(__dirname, '..', dir);
|
||||
|
||||
// Create dir if not exists
|
||||
if (!fs.existsSync(folderPath)) {
|
||||
fs.mkdirSync(folderPath, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(folderPath);
|
||||
log.info('RTMP files', files);
|
||||
return files;
|
||||
} catch (error) {
|
||||
log.error(`[getRTMP] Error reading directory: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async startRTMP(socket_id, room, host = 'localhost', port = 1935, file = '../rtmp/BigBuckBunny.mp4') {
|
||||
if (!this.rtmp || !this.rtmp.enabled) {
|
||||
log.debug('RTMP server is not enabled or missing the config');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.rtmpFileStreamer) {
|
||||
log.debug('RtmpFile is already in progress');
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputFilePath = path.join(__dirname, file);
|
||||
|
||||
if (!fs.existsSync(inputFilePath)) {
|
||||
log.error(`File not found: ${inputFilePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.rtmpFileStreamer = new RtmpFile(socket_id, room);
|
||||
|
||||
const inputStream = fs.createReadStream(inputFilePath);
|
||||
|
||||
const rtmpUrl = this.getRTMPUrl(host, port);
|
||||
|
||||
const rtmpRun = await this.rtmpFileStreamer.start(inputStream, rtmpUrl);
|
||||
|
||||
if (!rtmpRun) {
|
||||
this.rtmpFileStreamer = false;
|
||||
return this.rtmpFileStreamer;
|
||||
}
|
||||
return rtmpUrl;
|
||||
}
|
||||
|
||||
stopRTMP() {
|
||||
if (!this.rtmp || !this.rtmp.enabled) {
|
||||
log.debug('RTMP server is not enabled or missing the config');
|
||||
return false;
|
||||
}
|
||||
if (this.rtmpFileStreamer) {
|
||||
this.rtmpFileStreamer.stop();
|
||||
this.rtmpFileStreamer = null;
|
||||
log.debug('RTMP File Streamer Stopped successfully!');
|
||||
return true;
|
||||
} else {
|
||||
log.debug('No RtmpFile process to stop');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// RTMP from URL
|
||||
// ####################################################
|
||||
|
||||
isRtmpUrlStreamerActive() {
|
||||
return this.rtmpUrlStreamer;
|
||||
}
|
||||
|
||||
async startRTMPfromURL(
|
||||
socket_id,
|
||||
room,
|
||||
host = 'localhost',
|
||||
port = 1935,
|
||||
inputVideoURL = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
) {
|
||||
if (!this.rtmp || !this.rtmp.enabled) {
|
||||
log.debug('RTMP server is not enabled or missing the config');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.rtmpUrlStreamer) {
|
||||
log.debug('RtmpFile is already in progress');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.rtmpUrlStreamer = new RtmpUrl(socket_id, room);
|
||||
|
||||
const rtmpUrl = this.getRTMPUrl(host, port);
|
||||
|
||||
const rtmpRun = await this.rtmpUrlStreamer.start(inputVideoURL, rtmpUrl);
|
||||
|
||||
if (!rtmpRun) {
|
||||
this.rtmpUrlStreamer = false;
|
||||
return this.rtmpUrlStreamer;
|
||||
}
|
||||
return rtmpUrl;
|
||||
}
|
||||
|
||||
stopRTMPfromURL() {
|
||||
if (!this.rtmp || !this.rtmp.enabled) {
|
||||
log.debug('RTMP server is not enabled or missing the config');
|
||||
return false;
|
||||
}
|
||||
if (this.rtmpUrlStreamer) {
|
||||
this.rtmpUrlStreamer.stop();
|
||||
this.rtmpUrlStreamer = null;
|
||||
log.debug('RTMP Url Streamer Stopped successfully!');
|
||||
return true;
|
||||
} else {
|
||||
log.debug('No RtmpUrl process to stop');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// RTMP COMMON
|
||||
// ####################################################
|
||||
|
||||
getRTMPUrl(host, port) {
|
||||
const rtmpServer = this.rtmp.server != '' ? this.rtmp.server : false;
|
||||
const rtmpAppName = this.rtmp.appName != '' ? this.rtmp.appName : 'live';
|
||||
const rtmpStreamKey = this.rtmp.streamKey != '' ? this.rtmp.streamKey : uuidv4();
|
||||
const rtmpServerSecret = this.rtmp.secret != '' ? this.rtmp.secret : false;
|
||||
const expirationHours = this.rtmp.expirationHours || 4;
|
||||
const rtmpServerURL = rtmpServer ? rtmpServer : `rtmp://${host}:${port}`;
|
||||
const rtmpServerPath = '/' + rtmpAppName + '/' + rtmpStreamKey;
|
||||
|
||||
const rtmpUrl = rtmpServerSecret
|
||||
? this.generateRTMPUrl(rtmpServerURL, rtmpServerPath, rtmpServerSecret, expirationHours)
|
||||
: rtmpServerURL + rtmpServerPath;
|
||||
|
||||
log.info('RTMP Url generated', rtmpUrl);
|
||||
return rtmpUrl;
|
||||
}
|
||||
|
||||
generateRTMPUrl(baseURL, streamPath, secretKey, expirationHours = 8) {
|
||||
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}`;
|
||||
return rtmpUrl;
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ROUTER
|
||||
// ####################################################
|
||||
|
||||
97
app/src/RtmpFile.js
Normal file
97
app/src/RtmpFile.js
Normal file
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
|
||||
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
|
||||
|
||||
const Logger = require('./Logger');
|
||||
const log = new Logger('RtmpFile');
|
||||
|
||||
class RtmpFile {
|
||||
constructor(socket_id = false, room = false) {
|
||||
this.socketId = socket_id;
|
||||
this.room = room;
|
||||
this.rtmpUrl = '';
|
||||
this.ffmpegProcess = null;
|
||||
}
|
||||
|
||||
async start(inputStream, rtmpUrl) {
|
||||
if (this.ffmpegProcess) {
|
||||
log.debug('Streaming is already in progress');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.rtmpUrl = rtmpUrl;
|
||||
|
||||
try {
|
||||
this.ffmpegProcess = ffmpeg(inputStream)
|
||||
.inputOptions(['-re']) // Read input at native frame rate
|
||||
.outputOptions([
|
||||
'-c:v libx264', // Encode video to H.264
|
||||
'-preset veryfast', // Set preset to very fast
|
||||
'-maxrate 3000k', // Max bitrate for the video stream
|
||||
'-bufsize 6000k', // Buffer size
|
||||
'-g 50', // GOP size
|
||||
'-c:a aac', // Encode audio to AAC
|
||||
'-b:a 128k', // Bitrate for the audio stream
|
||||
'-f flv', // Output format
|
||||
])
|
||||
.output(rtmpUrl)
|
||||
.on('start', (commandLine) => log.info('ffmpeg process starting with command:', commandLine))
|
||||
.on('progress', (progress) => {
|
||||
/* log.debug('Processing', progress); */
|
||||
})
|
||||
.on('error', (err, stdout, stderr) => {
|
||||
log.debug('Error: ' + err.message);
|
||||
this.ffmpegProcess = null;
|
||||
if (!err.message.includes('Exiting normally')) {
|
||||
this.handleError(err.message);
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
log.info('FFmpeg processing finished');
|
||||
this.ffmpegProcess = null;
|
||||
this.handleEnd();
|
||||
})
|
||||
.run();
|
||||
|
||||
log.info('RtmpFile started', rtmpUrl);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error('Error starting RtmpFile', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.ffmpegProcess && !this.ffmpegProcess.killed) {
|
||||
try {
|
||||
this.ffmpegProcess.kill('SIGTERM');
|
||||
this.ffmpegProcess = null;
|
||||
log.info('RtmpFile stopped');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error('Error stopping RtmpFile', error.message);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log.debug('No RtmpFile process to stop');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
handleEnd() {
|
||||
if (!this.room) return;
|
||||
this.room.send(this.socketId, 'endRTMP', { rtmpUrl: this.rtmpUrl });
|
||||
this.room.rtmpFileStreamer = false;
|
||||
}
|
||||
|
||||
handleError(message) {
|
||||
if (!this.room) return;
|
||||
this.room.send(this.socketId, 'errorRTMP', { message });
|
||||
this.room.rtmpFileStreamer = false;
|
||||
log.error('Error: ' + message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RtmpFile;
|
||||
74
app/src/RtmpStreamer.js
Normal file
74
app/src/RtmpStreamer.js
Normal file
@@ -0,0 +1,74 @@
|
||||
'use strict';
|
||||
|
||||
const { PassThrough } = require('stream');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
|
||||
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
|
||||
|
||||
const Logger = require('./Logger');
|
||||
const log = new Logger('RtmpStreamer');
|
||||
|
||||
class RtmpStreamer {
|
||||
constructor(rtmpUrl, rtmpKey) {
|
||||
this.rtmpUrl = rtmpUrl;
|
||||
this.rtmpKey = rtmpKey;
|
||||
this.log = log;
|
||||
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) => this.log.info('ffmpeg command', { id: this.rtmpKey, cmd: commandLine }))
|
||||
.on('progress', (progress) => {
|
||||
/* log.debug('Processing', progress); */
|
||||
})
|
||||
.on('error', (err, stdout, stderr) => {
|
||||
if (!err.message.includes('Exiting normally')) {
|
||||
this.log.error('FFmpeg error:', { id: this.rtmpKey, error: err.message });
|
||||
}
|
||||
this.end();
|
||||
})
|
||||
.on('end', () => {
|
||||
this.log.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;
|
||||
this.log.info('RTMP streaming stopped', this.rtmpKey);
|
||||
}
|
||||
if (this.ffmpegStream && !this.ffmpegStream.killed) {
|
||||
this.ffmpegStream.kill('SIGTERM');
|
||||
this.ffmpegStream = null;
|
||||
this.log.info('FFMPEG closed successfully', this.rtmpKey);
|
||||
}
|
||||
this.run = false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RtmpStreamer;
|
||||
93
app/src/RtmpUrl.js
Normal file
93
app/src/RtmpUrl.js
Normal file
@@ -0,0 +1,93 @@
|
||||
'use strict';
|
||||
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
|
||||
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
|
||||
|
||||
const Logger = require('./Logger');
|
||||
const log = new Logger('RtmpUrl');
|
||||
|
||||
class RtmpUrl {
|
||||
constructor(socket_id = false, room = false) {
|
||||
this.room = room;
|
||||
this.socketId = socket_id;
|
||||
this.rtmpUrl = '';
|
||||
this.ffmpegProcess = null;
|
||||
}
|
||||
|
||||
async start(inputVideoURL, rtmpUrl) {
|
||||
if (this.ffmpegProcess) {
|
||||
log.debug('Streaming is already in progress');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.rtmpUrl = rtmpUrl;
|
||||
|
||||
try {
|
||||
this.ffmpegProcess = ffmpeg(inputVideoURL)
|
||||
.inputOptions('-re') // Read input in real-time
|
||||
.audioCodec('aac') // Set audio codec to AAC
|
||||
.audioBitrate('128k') // Set audio bitrate to 128 kbps
|
||||
.videoCodec('libx264') // Set video codec to H.264
|
||||
.videoBitrate('3000k') // Set video bitrate to 3000 kbps
|
||||
.size('1280x720') // Scale video to 1280x720 resolution
|
||||
.format('flv') // Set output format to FLV
|
||||
.output(rtmpUrl)
|
||||
.on('start', (commandLine) => log.info('ffmpeg process starting with command:', commandLine))
|
||||
.on('progress', (progress) => {
|
||||
/* log.debug('Processing', progress); */
|
||||
})
|
||||
.on('error', (err, stdout, stderr) => {
|
||||
log.debug('Error: ' + err.message);
|
||||
this.ffmpegProcess = null;
|
||||
if (!err.message.includes('Exiting normally')) {
|
||||
this.handleError(err.message);
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
log.info('FFmpeg processing finished');
|
||||
this.ffmpegProcess = null;
|
||||
this.handleEnd();
|
||||
})
|
||||
.run();
|
||||
|
||||
log.info('RtmpUrl started', rtmpUrl);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error('Error starting RtmpUrl', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.ffmpegProcess && !this.ffmpegProcess.killed) {
|
||||
try {
|
||||
this.ffmpegProcess.kill('SIGTERM');
|
||||
this.ffmpegProcess = null;
|
||||
log.info('RtmpUrl stopped');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error('Error stopping RtmpUrl', error.message);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log.debug('No RtmpUrl process to stop');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
handleEnd() {
|
||||
if (!this.room) return;
|
||||
this.room.send(this.socketId, 'endRTMPfromURL', { rtmpUrl: this.rtmpUrl });
|
||||
this.room.rtmpUrlStreamer = false;
|
||||
}
|
||||
|
||||
handleError(message) {
|
||||
if (!this.room) return;
|
||||
this.room.send(this.socketId, 'errorRTMPfromURL', { message });
|
||||
this.room.rtmpUrlStreamer = false;
|
||||
log.error('Error: ' + message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RtmpUrl;
|
||||
@@ -8,6 +8,7 @@
|
||||
███████ ███████ ██ ██ ████ ███████ ██ ██
|
||||
|
||||
dependencies: {
|
||||
@ffmpeg-installer/ffmpeg: https://www.npmjs.com/package/@ffmpeg-installer/ffmpeg
|
||||
@sentry/node : https://www.npmjs.com/package/@sentry/node
|
||||
@sentry/integrations : https://www.npmjs.com/package/@sentry/integrations
|
||||
axios : https://www.npmjs.com/package/axios
|
||||
@@ -18,6 +19,7 @@ dependencies: {
|
||||
crypto-js : https://www.npmjs.com/package/crypto-js
|
||||
express : https://www.npmjs.com/package/express
|
||||
express-openid-connect : https://www.npmjs.com/package/express-openid-connect
|
||||
fluent-ffmpeg : https://www.npmjs.com/package/fluent-ffmpeg
|
||||
httpolyglot : https://www.npmjs.com/package/httpolyglot
|
||||
jsonwebtoken : https://www.npmjs.com/package/jsonwebtoken
|
||||
js-yaml : https://www.npmjs.com/package/js-yaml
|
||||
@@ -42,7 +44,7 @@ dependencies: {
|
||||
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||
* @version 1.4.51
|
||||
* @version 1.4.70
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -50,6 +52,7 @@ const express = require('express');
|
||||
const { auth, requiresAuth } = require('express-openid-connect');
|
||||
const cors = require('cors');
|
||||
const compression = require('compression');
|
||||
const socketIo = require('socket.io');
|
||||
const https = require('httpolyglot');
|
||||
const mediasoup = require('mediasoup');
|
||||
const mediasoupClient = require('mediasoup-client');
|
||||
@@ -75,6 +78,17 @@ const { CaptureConsole } = require('@sentry/integrations');
|
||||
const restrictAccessByIP = require('./middleware/IpWhitelist.js');
|
||||
const packageJson = require('../../package.json');
|
||||
|
||||
// Incoming Stream to RTPM
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const crypto = require('crypto-js');
|
||||
const RtmpStreamer = require('./RtmpStreamer.js'); // Import the RtmpStreamer class
|
||||
const rtmpCfg = config.server.rtmp;
|
||||
const rtmpDir = rtmpCfg && rtmpCfg.dir ? rtmpCfg.dir : 'rtmp';
|
||||
|
||||
// File and Url Rtmp streams count
|
||||
let rtmpFileStreamsCount = 0;
|
||||
let rtmpUrlStreamsCount = 0;
|
||||
|
||||
// Email alerts and notifications
|
||||
const nodemailer = require('./lib/nodemailer');
|
||||
|
||||
@@ -98,7 +112,7 @@ const corsOptions = {
|
||||
};
|
||||
|
||||
const httpsServer = https.createServer(options, app);
|
||||
const io = require('socket.io')(httpsServer, {
|
||||
const io = socketIo(httpsServer, {
|
||||
maxHttpBufferSize: 1e7,
|
||||
transports: ['websocket'],
|
||||
cors: corsOptions,
|
||||
@@ -169,7 +183,7 @@ if (config.chatGPT.enabled) {
|
||||
};
|
||||
chatGPT = new OpenAI(configuration);
|
||||
} else {
|
||||
log.warning('ChatGPT seems enabled, but you missing the apiKey!');
|
||||
log.warn('ChatGPT seems enabled, but you missing the apiKey!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,13 +214,16 @@ const views = {
|
||||
permission: path.join(__dirname, '../../', 'public/views/permission.html'),
|
||||
privacy: path.join(__dirname, '../../', 'public/views/privacy.html'),
|
||||
room: path.join(__dirname, '../../', 'public/views/Room.html'),
|
||||
rtmpStreamer: path.join(__dirname, '../../', 'public/views/RtmpStreamer.html'),
|
||||
};
|
||||
|
||||
const authHost = new Host(); // Authenticated IP by Login
|
||||
|
||||
const roomList = new Map(); // All Rooms
|
||||
|
||||
const presenters = {}; // collect presenters grp by roomId
|
||||
const presenters = {}; // Collect presenters grp by roomId
|
||||
|
||||
const streams = {}; // Collect all rtmp streams
|
||||
|
||||
const webRtcServerActive = config.mediasoup.webRtcServerActive;
|
||||
|
||||
@@ -288,24 +305,28 @@ function startServer() {
|
||||
// Start the app
|
||||
app.use(cors(corsOptions));
|
||||
app.use(compression());
|
||||
app.use(express.json());
|
||||
app.use(express.json({ limit: '50mb' })); // Ensure the body parser can handle large files
|
||||
app.use(express.static(dir.public));
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(bodyParser.raw({ type: 'video/webm', limit: '50mb' })); // handle raw binary data
|
||||
app.use(bodyParser.raw({ type: 'application/octet-stream', limit: '50mb' })); // handle raw binary data
|
||||
app.use(restApi.basePath + '/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); // api docs
|
||||
|
||||
// IP Whitelist check ...
|
||||
app.use(restrictAccessByIP);
|
||||
|
||||
// Logs requests
|
||||
/*
|
||||
app.use((req, res, next) => {
|
||||
log.debug('New request:', {
|
||||
// headers: req.headers,
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
method: req.method,
|
||||
path: req.originalUrl,
|
||||
});
|
||||
next();
|
||||
});
|
||||
*/
|
||||
|
||||
// POST start from here...
|
||||
app.post('*', function (next) {
|
||||
@@ -407,6 +428,14 @@ function startServer() {
|
||||
}
|
||||
});
|
||||
|
||||
// Route to display rtmp streamer
|
||||
app.get('/rtmp', OIDCAuth, (req, res) => {
|
||||
if (!rtmpCfg || !rtmpCfg.fromStream) {
|
||||
return res.json({ message: 'The RTMP Streamer is currently disabled.' });
|
||||
}
|
||||
return res.sendFile(views.rtmpStreamer);
|
||||
});
|
||||
|
||||
// set new room name and join
|
||||
app.get(['/newroom'], OIDCAuth, (req, res) => {
|
||||
//log.info('/newroom - hostCfg ----->', hostCfg);
|
||||
@@ -655,6 +684,128 @@ function startServer() {
|
||||
}
|
||||
});
|
||||
|
||||
// ###############################################################
|
||||
// INCOMING STREAM (getUserMedia || getDisplayMedia) TO RTMP
|
||||
// ###############################################################
|
||||
|
||||
function checkRTMPApiSecret(req, res, next) {
|
||||
const expectedApiSecret = rtmpCfg && rtmpCfg.apiSecret;
|
||||
const apiSecret = req.headers.authorization;
|
||||
|
||||
if (!apiSecret || apiSecret !== expectedApiSecret) {
|
||||
log.warn('RTMP apiSecret Unauthorized', {
|
||||
apiSecret: apiSecret,
|
||||
expectedApiSecret: expectedApiSecret,
|
||||
});
|
||||
return res.status(401).send('Unauthorized');
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function checkMaxStreams(req, res, next) {
|
||||
const maxStreams = (rtmpCfg && rtmpCfg.maxStreams) || 1; // Set your maximum allowed streams here
|
||||
const activeStreams = Object.keys(streams).length;
|
||||
if (activeStreams >= maxStreams) {
|
||||
log.warn('Maximum number of streams reached', activeStreams);
|
||||
return res.status(429).send('Maximum number of streams reached, please try later!');
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
app.get('/activeStreams', checkRTMPApiSecret, (req, res) => {
|
||||
const activeStreams = Object.keys(streams).length;
|
||||
log.info('Active Streams', activeStreams);
|
||||
res.json(activeStreams);
|
||||
});
|
||||
|
||||
app.get('/rtmpEnabled', (req, res) => {
|
||||
const rtmpEnabled = rtmpCfg && rtmpCfg.enabled;
|
||||
log.debug('RTMP enabled', rtmpEnabled);
|
||||
res.json({ enabled: rtmpEnabled });
|
||||
});
|
||||
|
||||
app.post('/initRTMP', checkRTMPApiSecret, checkMaxStreams, async (req, res) => {
|
||||
if (!rtmpCfg || !rtmpCfg.enabled) {
|
||||
return res.status(400).send('RTMP server is not enabled or missing the config');
|
||||
}
|
||||
|
||||
const domainName = config.ngrok.enabled ? 'localhost' : 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;
|
||||
|
||||
log.info('initRTMP', {
|
||||
headers: req.headers,
|
||||
rtmpServer,
|
||||
rtmpServerSecret,
|
||||
rtmpServerURL,
|
||||
rtmpServerPath,
|
||||
expirationHours,
|
||||
rtmpStreamKey,
|
||||
rtmp,
|
||||
});
|
||||
|
||||
const stream = new RtmpStreamer(rtmp, rtmpStreamKey);
|
||||
streams[rtmpStreamKey] = stream;
|
||||
|
||||
log.info('Active RTMP Streams', Object.keys(streams).length);
|
||||
|
||||
return 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];
|
||||
log.debug('Stream not found', { rtmpStreamKey, streams: Object.keys(streams).length });
|
||||
return res.status(404).send('FFmpeg Stream not found');
|
||||
}
|
||||
|
||||
log.debug('Received video data', {
|
||||
// data: req.body.slice(0, 20).toString('hex'),
|
||||
key: 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];
|
||||
log.debug('Active RTMP Streams', Object.keys(streams).length);
|
||||
}
|
||||
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
// ####################################################
|
||||
// REST API
|
||||
// ####################################################
|
||||
@@ -1098,6 +1249,10 @@ function startServer() {
|
||||
|
||||
log.info('[Join] - current active rooms', activeRooms);
|
||||
|
||||
const activeStreams = getRTMPActiveStreams();
|
||||
|
||||
log.info('[Join] - current active RTMP streams', activeStreams);
|
||||
|
||||
if (!(socket.room_id in presenters)) presenters[socket.room_id] = {};
|
||||
|
||||
// Set the presenters
|
||||
@@ -2135,6 +2290,93 @@ function startServer() {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('getRTMP', async ({}, cb) => {
|
||||
if (!roomList.has(socket.room_id)) return;
|
||||
const room = roomList.get(socket.room_id);
|
||||
const rtmpFiles = await room.getRTMP(rtmpDir);
|
||||
cb(rtmpFiles);
|
||||
});
|
||||
|
||||
socket.on('startRTMP', async (dataObject, cb) => {
|
||||
if (!roomList.has(socket.room_id)) return;
|
||||
|
||||
if (rtmpCfg && rtmpFileStreamsCount >= rtmpCfg.maxStreams) {
|
||||
log.warn('RTMP max file streams reached', rtmpFileStreamsCount);
|
||||
return cb(false);
|
||||
}
|
||||
|
||||
const data = checkXSS(dataObject);
|
||||
const { peer_name, peer_uuid, file } = data;
|
||||
const isPresenter = await isPeerPresenter(socket.room_id, socket.id, peer_name, peer_uuid);
|
||||
if (!isPresenter) return cb(false);
|
||||
|
||||
const room = roomList.get(socket.room_id);
|
||||
const host = config.ngrok.enabled ? 'localhost' : socket.handshake.headers.host.split(':')[0];
|
||||
const rtmp = await room.startRTMP(socket.id, room, host, 1935, `../${rtmpDir}/${file}`);
|
||||
|
||||
if (rtmp !== false) rtmpFileStreamsCount++;
|
||||
log.debug('startRTMP - rtmpFileStreamsCount ---->', rtmpFileStreamsCount);
|
||||
|
||||
cb(rtmp);
|
||||
});
|
||||
|
||||
socket.on('stopRTMP', async () => {
|
||||
if (!roomList.has(socket.room_id)) return;
|
||||
|
||||
const room = roomList.get(socket.room_id);
|
||||
|
||||
rtmpFileStreamsCount--;
|
||||
log.debug('stopRTMP - rtmpFileStreamsCount ---->', rtmpFileStreamsCount);
|
||||
|
||||
await room.stopRTMP();
|
||||
});
|
||||
|
||||
socket.on('endOrErrorRTMP', async () => {
|
||||
if (!roomList.has(socket.room_id)) return;
|
||||
rtmpFileStreamsCount--;
|
||||
log.debug('endRTMP - rtmpFileStreamsCount ---->', rtmpFileStreamsCount);
|
||||
});
|
||||
|
||||
socket.on('startRTMPfromURL', async (dataObject, cb) => {
|
||||
if (!roomList.has(socket.room_id)) return;
|
||||
|
||||
if (rtmpCfg && rtmpUrlStreamsCount >= rtmpCfg.maxStreams) {
|
||||
log.warn('RTMP max Url streams reached', rtmpUrlStreamsCount);
|
||||
return cb(false);
|
||||
}
|
||||
|
||||
const data = checkXSS(dataObject);
|
||||
const { peer_name, peer_uuid, inputVideoURL } = data;
|
||||
const isPresenter = await isPeerPresenter(socket.room_id, socket.id, peer_name, peer_uuid);
|
||||
if (!isPresenter) return cb(false);
|
||||
|
||||
const room = roomList.get(socket.room_id);
|
||||
const host = config.ngrok.enabled ? 'localhost' : socket.handshake.headers.host.split(':')[0];
|
||||
const rtmp = await room.startRTMPfromURL(socket.id, room, host, 1935, inputVideoURL);
|
||||
|
||||
if (rtmp !== false) rtmpUrlStreamsCount++;
|
||||
log.debug('startRTMPfromURL - rtmpUrlStreamsCount ---->', rtmpUrlStreamsCount);
|
||||
|
||||
cb(rtmp);
|
||||
});
|
||||
|
||||
socket.on('stopRTMPfromURL', async () => {
|
||||
if (!roomList.has(socket.room_id)) return;
|
||||
|
||||
const room = roomList.get(socket.room_id);
|
||||
|
||||
rtmpUrlStreamsCount--;
|
||||
log.debug('stopRTMPfromURL - rtmpUrlStreamsCount ---->', rtmpUrlStreamsCount);
|
||||
|
||||
await room.stopRTMPfromURL();
|
||||
});
|
||||
|
||||
socket.on('endOrErrorRTMPfromURL', async () => {
|
||||
if (!roomList.has(socket.room_id)) return;
|
||||
rtmpUrlStreamsCount--;
|
||||
log.debug('endRTMPfromURL - rtmpUrlStreamsCount ---->', rtmpUrlStreamsCount);
|
||||
});
|
||||
|
||||
socket.on('disconnect', async () => {
|
||||
if (!roomList.has(socket.room_id)) return;
|
||||
|
||||
@@ -2152,6 +2394,8 @@ function startServer() {
|
||||
|
||||
if (room.getPeers().size === 0) {
|
||||
//
|
||||
stopRTMPActiveStreams(isPresenter, room);
|
||||
|
||||
roomList.delete(socket.room_id);
|
||||
|
||||
delete presenters[socket.room_id];
|
||||
@@ -2161,6 +2405,10 @@ function startServer() {
|
||||
const activeRooms = getActiveRooms();
|
||||
|
||||
log.info('[Disconnect] - Last peer - current active rooms', activeRooms);
|
||||
|
||||
const activeStreams = getRTMPActiveStreams();
|
||||
|
||||
log.info('[Disconnect] - Last peer - current active RTMP streams', activeStreams);
|
||||
}
|
||||
|
||||
room.broadCast(socket.id, 'removeMe', removeMeData(room, peer_name, isPresenter));
|
||||
@@ -2193,6 +2441,8 @@ function startServer() {
|
||||
|
||||
if (room.getPeers().size === 0) {
|
||||
//
|
||||
stopRTMPActiveStreams(isPresenter, room);
|
||||
|
||||
roomList.delete(socket.room_id);
|
||||
|
||||
delete presenters[socket.room_id];
|
||||
@@ -2202,6 +2452,10 @@ function startServer() {
|
||||
const activeRooms = getActiveRooms();
|
||||
|
||||
log.info('[REMOVE ME] - Last peer - current active rooms', activeRooms);
|
||||
|
||||
const activeStreams = getRTMPActiveStreams();
|
||||
|
||||
log.info('[REMOVE ME] - Last peer - current active RTMP streams', activeStreams);
|
||||
}
|
||||
|
||||
socket.room_id = null;
|
||||
@@ -2270,15 +2524,54 @@ function startServer() {
|
||||
log.debug('[REMOVE ME DATA]', data);
|
||||
return data;
|
||||
}
|
||||
|
||||
function bytesToSize(bytes) {
|
||||
let sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes == 0) return '0 Byte';
|
||||
let i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
|
||||
}
|
||||
});
|
||||
|
||||
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}`;
|
||||
|
||||
log.debug('generateRTMPUrl', {
|
||||
currentTime,
|
||||
expirationTime,
|
||||
hashValue,
|
||||
rtmpUrl,
|
||||
});
|
||||
|
||||
return rtmpUrl;
|
||||
}
|
||||
|
||||
function getRTMPActiveStreams() {
|
||||
return {
|
||||
rtmpStreams: Object.keys(streams).length,
|
||||
rtmpFileStreamsCount,
|
||||
rtmpUrlStreamsCount,
|
||||
};
|
||||
}
|
||||
|
||||
function stopRTMPActiveStreams(isPresenter, room) {
|
||||
if (isPresenter) {
|
||||
if (room.isRtmpFileStreamerActive()) {
|
||||
room.stopRTMP();
|
||||
rtmpFileStreamsCount--;
|
||||
log.info('[REMOVE ME] - Stop RTMP Stream From FIle', rtmpFileStreamsCount);
|
||||
}
|
||||
if (room.isRtmpUrlStreamerActive()) {
|
||||
room.stopRTMPfromURL();
|
||||
rtmpUrlStreamsCount--;
|
||||
log.info('[REMOVE ME] - Stop RTMP Stream From URL', rtmpUrlStreamsCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
function clone(value) {
|
||||
if (value === undefined) return undefined;
|
||||
if (Number.isNaN(value)) return NaN;
|
||||
|
||||
@@ -59,6 +59,42 @@ module.exports = {
|
||||
endpoint: '', // Change the URL if you want to save the recording to a different server or cloud service (http://localhost:8080), otherwise leave it as is (empty).
|
||||
dir: 'rec',
|
||||
},
|
||||
rtmp: {
|
||||
/*
|
||||
Real-Time Messaging Protocol (RTMP) is a communication protocol for streaming audio, video, and data over the Internet. (beta)
|
||||
|
||||
Configuration:
|
||||
- enabled: Enable or disable the RTMP streaming feature. Set to 'true' to enable, 'false' to disable.
|
||||
- fromFile: Enable or disable the RTMP streaming from File. Set to 'true' to enable, 'false' to disable.
|
||||
- fromUrl: Enable or disable the RTMP streaming from Url. Set to 'true' to enable, 'false' to disable.
|
||||
- fromStream: Enable or disable the RTMP Streamer. Set to 'true' to enable, 'false' to disable.
|
||||
- maxStreams: Specifies the maximum number of simultaneous streams permitted for File, URL, and Stream. The default value is 1.
|
||||
- server: The URL of the RTMP server. Leave empty to use the built-in MiroTalk RTMP server (rtmp://localhost:1935). Change the URL to connect to a different RTMP server.
|
||||
- appName: The application name for the RTMP stream. Default is 'mirotalk'.
|
||||
- streamKey: The stream key for the RTMP stream. Leave empty if not required.
|
||||
- secret: The secret key for RTMP streaming. Must match the secret in rtmpServers/node-media-server/src/config.js. Leave empty if no authentication is needed.
|
||||
- apiSecret: The API secret for streaming WebRTC to RTMP through the MiroTalk API.
|
||||
- expirationHours: The number of hours before the RTMP URL expires. Default is 4 hours.
|
||||
- dir: Directory where your video files are stored to be streamed via RTMP.
|
||||
|
||||
Important: Ensure your RTMP server is operational before proceeding. You can start the server by running the following command:
|
||||
- Start: npm run nms-start - Start the RTMP server.
|
||||
- Stop: npm run npm-stop - Stop the RTMP server.
|
||||
- Logs: npm run npm-logs - View the logs of the RTMP server.
|
||||
*/
|
||||
enabled: false,
|
||||
fromFile: true,
|
||||
fromUrl: true,
|
||||
fromStream: true,
|
||||
maxStreams: 1,
|
||||
server: 'rtmp://localhost:1935',
|
||||
appName: 'mirotalk',
|
||||
streamKey: '',
|
||||
secret: 'mirotalkRtmpSecret',
|
||||
apiSecret: 'mirotalkRtmpApiSecret',
|
||||
expirationHours: 4,
|
||||
dir: 'rtmp',
|
||||
},
|
||||
},
|
||||
middleware: {
|
||||
/*
|
||||
@@ -333,6 +369,7 @@ module.exports = {
|
||||
lobbyButton: true, // presenter
|
||||
sendEmailInvitation: true, // presenter
|
||||
micOptionsButton: true, // presenter
|
||||
tabRTMPStreamingBtn: true, // presenter
|
||||
tabModerator: true, // presenter
|
||||
tabRecording: true,
|
||||
host_only_recording: true, // presenter
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم