[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 حذوفات

9
rtmpServers/README.md Normal file
عرض الملف

@@ -0,0 +1,9 @@
# MiroTalk RTMP Servers
![rtmp](./rtmpStreaming.jpeg)
### How to start the RTMP server?
[https://docs.mirotalk.com/mirotalk-sfu/rtmp/](https://docs.mirotalk.com/mirotalk-sfu/rtmp/)
---

عرض الملف

@@ -0,0 +1,256 @@
'use strict';
const videoElement = document.getElementById('video');
const startCameraButton = document.getElementById('startCamera');
const startScreenButton = document.getElementById('startScreen');
const stopButton = document.getElementById('stop');
const apiSecretInput = document.getElementById('apiSecret'); // Replace with your actual API secret
const rtmpInput = document.getElementById('rtmp');
const copyButton = document.getElementById('copy');
const popup = document.getElementById('popup');
const popupMessage = document.getElementById('popupMessage');
const closePopup = document.getElementById('closePopup');
/*
Low Latency: 1-2 seconds
Standard Use Case: 5 seconds
High Bandwidth/Stability: 10 seconds
*/
const chunkDuration = 4000; // ms
let mediaRecorder = null;
let rtmpKey = null; // To store the RTMP key
function toggleButtons(disabled = true) {
startCameraButton.disabled = disabled;
startScreenButton.disabled = disabled;
stopButton.disabled = disabled;
}
function showPopup(message, type) {
popup.classList.remove('success', 'error', 'warning', 'info');
popup.classList.add(type);
popupMessage.textContent = message;
popup.classList.remove('hidden');
setTimeout(() => {
hidePopup();
}, 5000); // Hide after 5 seconds
}
function hidePopup() {
popup.classList.add('hidden');
}
function showError(message) {
showPopup(message, 'error');
}
function checkBrowserSupport() {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('chrome') && !userAgent.includes('edge') && !userAgent.includes('opr')) {
console.log('Browser is Chrome-based. Proceed with functionality.');
} else {
showError(
'This application requires a Chrome-based browser (Chrome, Edge Chromium, etc.). Please switch to a supported browser.',
);
toggleButtons(true);
}
}
function checkRTMPEnabled() {
axios
.get('/rtmpEnabled')
.then((response) => {
const { enabled } = response.data;
if (!enabled) {
showPopup('The RTMP streaming feature has been disabled by the administrator', 'info');
toggleButtons(true);
}
})
.catch((error) => {
console.error('Error fetching RTMP status:', error);
showError(`Error fetching RTMP status: ${error.message}`);
});
}
window.onload = function () {
checkBrowserSupport();
checkRTMPEnabled();
};
async function startCapture(constraints) {
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
videoElement.srcObject = stream;
return stream;
} catch (err) {
console.error('Error accessing media devices.', err);
showError('Error accessing media devices. Please check your camera and microphone permissions.');
}
}
async function startScreenCapture(constraints) {
try {
const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
videoElement.srcObject = stream;
return stream;
} catch (err) {
console.error('Error accessing screen media.', err);
showError('Error accessing screen sharing. Please try again or check your screen sharing permissions.');
}
}
async function initRTMP(stream) {
const apiSecret = apiSecretInput.value;
try {
const response = await axios.post(`/initRTMP`, null, {
headers: {
authorization: apiSecret,
},
});
const { rtmp } = response.data;
console.log('initRTMP response:', { res: response, rtmp: rtmp });
rtmpInput.value = rtmp;
rtmpKey = new URL(rtmp).pathname.split('/').pop(); // Extract the RTMP key from the URL
toggleButtons(true);
stopButton.disabled = false; // Enable stopButton on successful initialization
return true;
} catch (error) {
if (error.response) {
const { status, data } = error.response;
showPopup(data, 'info');
console.log('Init RTMP', {
status,
data,
});
} else {
showError('Error initializing RTMP. Please try again.');
console.error('Error initializing RTMP:', error);
}
stopStreaming();
stopTracks(stream);
return false;
}
}
async function stopRTMP() {
if (mediaRecorder) {
mediaRecorder.stop();
const apiSecret = apiSecretInput.value;
videoElement.srcObject = null;
rtmpInput.value = '';
try {
await axios.post(`/stopRTMP?key=${rtmpKey}`, null, {
headers: {
authorization: apiSecret,
},
});
toggleButtons(false);
stopButton.disabled = true;
} catch (error) {
showError('Error stopping RTMP. Please try again.');
console.error('Error stopping RTMP:', error);
}
}
}
async function streamRTMPChunk(data) {
const apiSecret = apiSecretInput.value;
const arrayBuffer = await data.arrayBuffer();
const chunkSize = 1000000; // 1mb
const totalChunks = Math.ceil(arrayBuffer.byteLength / chunkSize);
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const chunk = arrayBuffer.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize);
try {
await axios.post(`/streamRTMP?key=${rtmpKey}`, chunk, {
headers: {
authorization: apiSecret,
'Content-Type': 'video/webm',
},
});
} catch (error) {
if (mediaRecorder) {
stopStreaming();
console.error('Error syncing chunk:', error.message);
showError(`Error syncing chunk: ${error.message}`);
}
}
}
}
function stopStreaming() {
if (mediaRecorder) {
mediaRecorder.stop();
}
videoElement.srcObject = null;
rtmpInput.value = '';
toggleButtons(false);
stopButton.disabled = true;
}
async function startStreaming(stream) {
if (!stream) return;
const initRTMPStream = await initRTMP(stream);
if (!initRTMPStream) {
return;
}
mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm; codecs=vp8,opus' });
mediaRecorder.ondataavailable = async (event) => {
if (event.data.size > 0) {
await streamRTMPChunk(event.data);
}
};
mediaRecorder.onstop = (event) => {
console.log('Media recorder stopped');
stopTracks(stream);
mediaRecorder = null;
};
mediaRecorder.start(chunkDuration); // Record in chunks of the specified duration
}
function stopTracks(stream) {
stream.getTracks().forEach((track) => {
track.stop();
});
}
async function startCameraStreaming() {
const stream = await startCapture({ video: true, audio: true });
await startStreaming(stream);
}
async function startScreenStreaming() {
const stream = await startScreenCapture({ video: true, audio: true });
await startStreaming(stream);
}
function copyRTMP() {
const rtmpInput = document.getElementById('rtmp');
if (!rtmpInput.value) {
return showPopup('No RTMP URL detected', 'info');
}
rtmpInput.select();
document.execCommand('copy');
showPopup('Copied: ' + rtmpInput.value, 'success');
}
startCameraButton.addEventListener('click', startCameraStreaming);
startScreenButton.addEventListener('click', startScreenStreaming);
stopButton.addEventListener('click', stopRTMP);
copyButton.addEventListener('click', copyRTMP);
closePopup.addEventListener('click', hidePopup);
// Stop RTMP streaming when the browser tab is closed
window.addEventListener('beforeunload', async (event) => {
if (mediaRecorder) {
await stopRTMP();
}
});

عرض الملف

@@ -0,0 +1,51 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MiroTalk RTMP Streamer</title>
<link id="icon" rel="shortcut icon" href="./logo.svg" />
<link id="appleTouchIcon" rel="apple-touch-icon" href="./logo.svg" />
<link rel="stylesheet" href="./style.css" />
<script defer src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script defer src="./client.js"></script>
</head>
<body>
<div id="popup" class="popup hidden">
<span id="popupMessage"></span>
<button id="closePopup">X</button>
</div>
<div class="container">
<h1>MiroTalk RTMP Streamer</h1>
<div class="input-group-inline">
<input
id="apiSecret"
type="password"
value="mirotalkRtmpApiSecret"
placeholder="API Secret"
title="Enter the API secret here"
/>
</div>
<div class="input-group-inline">
<input
id="rtmp"
type="text"
value=""
placeholder="rtmp://server:port/app/streamKey"
readonly
title="This is your RTMP live URL. It cannot be edited."
/>
<button id="copy" title="Click to copy the RTMP URL">Copy</button>
</div>
<video id="video" width="640" height="480" autoplay></video>
<div class="button-group">
<button id="startCamera" title="Click to start camera streaming">Start Camera Streaming</button>
<button id="startScreen" title="Click to start screen streaming">Start Screen Streaming</button>
<button id="stop" disabled title="Click to stop streaming">Stop Streaming</button>
</div>
</div>
<footer>
<p>&copy; 2024 MiroTalk SFU, all rights reserved</p>
</footer>
</body>
</html>

عرض الملف

@@ -0,0 +1 @@
<svg width="28" height="32" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a"><stop stop-color="#00BFFB" offset="0%"/><stop stop-color="#0270D7" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="b"><stop stop-color="#1F232A" stop-opacity=".48" offset="0%"/><stop stop-color="#1F2329" stop-opacity="0" offset="100%"/></linearGradient><linearGradient x1="87.665%" y1="103.739%" x2="-3.169%" y2="38.807%" id="c"><stop stop-color="#FFF" stop-opacity="0" offset="0%"/><stop stop-color="#FFF" stop-opacity=".64" offset="100%"/></linearGradient><linearGradient x1="-14.104%" y1="111.262%" x2="109.871%" y2="26.355%" id="d"><stop stop-color="#0270D7" offset="0%"/><stop stop-color="#0270D7" stop-opacity="0" offset="100%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" transform="rotate(90 14 16)" d="M6 2l-8 13.999L6 30h16l8-14.001L22 2z"/><path fill="url(#b)" d="M14 0v32L0 24V8z"/><path fill="url(#c)" d="M28 24L0 8l14.001-8L28 8z"/><path fill-opacity=".48" fill="url(#d)" style="mix-blend-mode:multiply" d="M28 8L0 23.978V8l14.001-8L28 8z"/></g></svg>

بعد

العرض:  |  الارتفاع:  |  الحجم: 1.1 KiB

عرض الملف

@@ -0,0 +1,205 @@
@import url('https://fonts.googleapis.com/css?family=Comfortaa:wght@500&display=swap');
body {
font-family: 'Comfortaa'; /*, Arial, sans-serif;*/
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background: radial-gradient(#393939, #000000);
color: #fff;
}
.container {
max-width: 800px;
margin: 0 auto;
text-align: center;
padding: 20px;
background: radial-gradient(#393939, #000000);
color: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
margin-top: 10px;
color: #ffffff;
}
video {
border: 0.1px solid #ccc;
margin: 10px 0;
border-radius: 8px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.input-group-inline {
display: flex;
align-items: center;
gap: 10px;
}
#apiSecret {
flex: 1;
}
#rtmp {
flex: 1;
}
#copyButton {
flex: 1;
max-width: 100px;
}
.input-group-inline > * {
margin-bottom: 20px;
}
input,
button {
padding: 10px;
font-size: 16px;
border: none;
border-radius: 4px;
outline: none;
box-sizing: border-box;
}
input[type='text'],
input[type='password'] {
flex: 1;
background: #2c2c2c;
color: #fff;
}
input[type='text'][readonly] {
background: #2c2c2c;
}
button {
cursor: pointer;
background-color: #007bff;
color: #fff;
transition: background-color 0.3s ease;
}
button:disabled {
background: #2c2c2c;
cursor: not-allowed;
}
button:hover {
background-color: #0056b3;
}
.button-group {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.button-group button {
width: 100%;
}
.popup {
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
background-color: indianred;
color: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
display: flex;
align-items: center;
justify-content: space-between;
min-width: 300px;
max-width: 600px;
width: 80%;
}
.popup.success {
background-color: mediumseagreen;
color: white;
}
.popup.error {
background-color: indianred;
color: white;
}
.popup.warning {
background-color: gold;
color: white;
}
.popup.info {
background-color: cornflowerblue;
color: white;
}
.popup.hidden {
display: none;
}
#closePopup {
background: none;
border: none;
color: white;
font-size: 16px;
cursor: pointer;
margin-left: 20px;
}
footer {
color: grey;
}
/* Media Queries for Responsiveness */
@media (max-width: 1024px) {
.container {
padding: 15px;
}
input,
button {
font-size: 14px;
}
}
@media (max-width: 768px) {
.input-group-inline {
flex-direction: column;
}
input,
button {
width: 100%;
font-size: 14px;
margin-bottom: 10px;
}
video {
width: 100%;
height: auto;
}
#copyButton {
max-width: 100%;
}
}
@media (max-width: 480px) {
.container {
padding: 10px;
}
h1 {
font-size: 24px;
}
input,
button {
font-size: 12px;
padding: 8px;
}
}

عرض الملف

@@ -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;

عرض الملف

@@ -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"
}
}

عرض الملف

@@ -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);
});

عرض الملف

@@ -0,0 +1,212 @@
'use strict';
const videoElement = document.getElementById('video');
const startCameraButton = document.getElementById('startCamera');
const startScreenButton = document.getElementById('startScreen');
const stopButton = document.getElementById('stop');
const apiSecretInput = document.getElementById('apiSecret');
const rtmpInput = document.getElementById('rtmp');
const copyButton = document.getElementById('copy');
const popup = document.getElementById('popup');
const popupMessage = document.getElementById('popupMessage');
const closePopup = document.getElementById('closePopup');
/*
Low Latency: 1-2 seconds
Standard Use Case: 5 seconds
High Bandwidth/Stability: 10 seconds
*/
const chunkDuration = 4000; // ms
let mediaRecorder = null;
let rtmpKey = null;
let socket = null;
function toggleButtons(disabled = true) {
startCameraButton.disabled = disabled;
startScreenButton.disabled = disabled;
stopButton.disabled = disabled;
}
function showPopup(message, type) {
popup.classList.remove('success', 'error', 'warning', 'info');
popup.classList.add(type);
popupMessage.textContent = message;
popup.classList.remove('hidden');
setTimeout(() => {
hidePopup();
}, 5000); // Hide after 5 seconds
}
function hidePopup() {
popup.classList.add('hidden');
}
function showError(message) {
showPopup(message, 'error');
}
function checkBrowserSupport() {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('chrome') && !userAgent.includes('edge') && !userAgent.includes('opr')) {
console.log('Browser is Chrome-based. Proceed with functionality.');
} else {
showError(
'This application requires a Chrome-based browser (Chrome, Edge Chromium, etc.). Please switch to a supported browser.',
);
toggleButtons(true);
}
}
window.onload = checkBrowserSupport;
async function startCapture(constraints) {
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
videoElement.srcObject = stream;
return stream;
} catch (err) {
console.error('Error accessing media devices.', err);
showError('Error accessing media devices. Please check your camera and microphone permissions.');
}
}
async function startScreenCapture(constraints) {
try {
const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
videoElement.srcObject = stream;
return stream;
} catch (err) {
console.error('Error accessing screen media.', err);
showError('Error accessing screen sharing. Please try again or check your screen sharing permissions.');
}
}
async function initRTMP() {
const apiSecret = apiSecretInput.value;
socket.emit('initRTMP', { apiSecret });
}
async function streamRTMP(data) {
const apiSecret = apiSecretInput.value;
const arrayBuffer = await data.arrayBuffer();
const chunkSize = 1000000; // 1mb
const totalChunks = Math.ceil(arrayBuffer.byteLength / chunkSize);
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const chunk = arrayBuffer.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize);
socket.emit('streamRTMP', { apiSecret: apiSecret, rtmpStreamKey: rtmpKey, data: chunk });
}
}
async function stopRTMP() {
if (mediaRecorder) {
const apiSecret = apiSecretInput.value;
mediaRecorder.stop();
socket.emit('stopRTMP', { apiSecret: apiSecret, rtmpStreamKey: rtmpKey });
}
videoElement.srcObject = null;
rtmpInput.value = '';
toggleButtons(false);
stopButton.disabled = true;
}
async function startStreaming(stream) {
if (!stream) return;
try {
socket = io({ transports: ['websocket'] });
socket.on('connect', () => {
console.log('Connected to server');
initRTMP();
});
socket.on('initRTMP', (data) => {
console.log('initRTMP', data);
const { rtmp, rtmpStreamKey } = data;
rtmpInput.value = rtmp;
rtmpKey = rtmpStreamKey;
toggleButtons(true);
stopButton.disabled = false;
startMediaRecorder(stream);
});
socket.on('stopRTMP', () => {
console.log('RTMP stopped successfully!');
});
socket.on('error', (error) => {
console.error('Error:', error);
showError(error);
stopRTMP();
});
socket.on('disconnect', () => {
console.log('Disconnected from server');
stopRTMP();
});
} catch (error) {
showPopup('Error start Streaming: ' + error.message, 'error');
console.error('Error start Streaming', error);
stopRTMP();
}
}
async function startMediaRecorder(stream) {
if (!stream) return;
mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm; codecs=vp8,opus' });
mediaRecorder.ondataavailable = async (event) => {
if (event.data.size > 0) {
await streamRTMP(event.data);
}
};
mediaRecorder.onstop = (event) => {
console.log('Media recorder stopped');
stopTracks(stream);
};
mediaRecorder.start(chunkDuration); // Record in chunks of the specified duration
}
function stopTracks(stream) {
stream.getTracks().forEach((track) => {
track.stop();
});
}
async function startCameraStreaming() {
const stream = await startCapture({ video: true, audio: true });
await startStreaming(stream);
}
async function startScreenStreaming() {
const stream = await startScreenCapture({ video: true, audio: true });
await startStreaming(stream);
}
function copyRTMP() {
const rtmpInput = document.getElementById('rtmp');
if (!rtmpInput.value) {
return showPopup('No RTMP URL detected', 'info');
}
rtmpInput.select();
document.execCommand('copy');
showPopup('Copied: ' + rtmpInput.value, 'success');
}
startCameraButton.addEventListener('click', startCameraStreaming);
startScreenButton.addEventListener('click', startScreenStreaming);
stopButton.addEventListener('click', stopRTMP);
copyButton.addEventListener('click', copyRTMP);
closePopup.addEventListener('click', hidePopup);
window.addEventListener('beforeunload', async () => {
if (mediaRecorder) {
await stopRTMP();
}
});

عرض الملف

@@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MiroTalk RTMP Streamer</title>
<link id="icon" rel="shortcut icon" href="./logo.svg" />
<link id="appleTouchIcon" rel="apple-touch-icon" href="./logo.svg" />
<link rel="stylesheet" href="./style.css" />
<script defer src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script defer src="/socket.io/socket.io.js"></script>
<script defer src="./client.js"></script>
</head>
<body>
<div id="popup" class="popup hidden">
<span id="popupMessage"></span>
<button id="closePopup">X</button>
</div>
<div class="container">
<h1>MiroTalk RTMP Streamer</h1>
<div class="input-group-inline">
<input
id="apiSecret"
type="password"
value="mirotalkRtmpApiSecret"
placeholder="API Secret"
title="Enter the API secret here"
/>
</div>
<div class="input-group-inline">
<input
id="rtmp"
type="text"
value=""
placeholder="rtmp://server:port/app/streamKey"
readonly
title="This is your RTMP live URL. It cannot be edited."
/>
<button id="copy" title="Click to copy the RTMP URL">Copy</button>
</div>
<video id="video" width="640" height="480" autoplay></video>
<div class="button-group">
<button id="startCamera" title="Click to start camera streaming">Start Camera Streaming</button>
<button id="startScreen" title="Click to start screen streaming">Start Screen Streaming</button>
<button id="stop" disabled title="Click to stop streaming">Stop Streaming</button>
</div>
</div>
<footer>
<p>&copy; 2024 MiroTalk SFU, all rights reserved</p>
</footer>
</body>
</html>

عرض الملف

@@ -0,0 +1 @@
<svg width="28" height="32" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a"><stop stop-color="#00BFFB" offset="0%"/><stop stop-color="#0270D7" offset="100%"/></linearGradient><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="b"><stop stop-color="#1F232A" stop-opacity=".48" offset="0%"/><stop stop-color="#1F2329" stop-opacity="0" offset="100%"/></linearGradient><linearGradient x1="87.665%" y1="103.739%" x2="-3.169%" y2="38.807%" id="c"><stop stop-color="#FFF" stop-opacity="0" offset="0%"/><stop stop-color="#FFF" stop-opacity=".64" offset="100%"/></linearGradient><linearGradient x1="-14.104%" y1="111.262%" x2="109.871%" y2="26.355%" id="d"><stop stop-color="#0270D7" offset="0%"/><stop stop-color="#0270D7" stop-opacity="0" offset="100%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" transform="rotate(90 14 16)" d="M6 2l-8 13.999L6 30h16l8-14.001L22 2z"/><path fill="url(#b)" d="M14 0v32L0 24V8z"/><path fill="url(#c)" d="M28 24L0 8l14.001-8L28 8z"/><path fill-opacity=".48" fill="url(#d)" style="mix-blend-mode:multiply" d="M28 8L0 23.978V8l14.001-8L28 8z"/></g></svg>

بعد

العرض:  |  الارتفاع:  |  الحجم: 1.1 KiB

عرض الملف

@@ -0,0 +1,205 @@
@import url('https://fonts.googleapis.com/css?family=Comfortaa:wght@500&display=swap');
body {
font-family: 'Comfortaa'; /*, Arial, sans-serif;*/
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background: radial-gradient(#393939, #000000);
color: #fff;
}
.container {
max-width: 800px;
margin: 0 auto;
text-align: center;
padding: 20px;
background: radial-gradient(#393939, #000000);
color: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
margin-top: 10px;
color: #ffffff;
}
video {
border: 0.1px solid #ccc;
margin: 10px 0;
border-radius: 8px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.input-group-inline {
display: flex;
align-items: center;
gap: 10px;
}
#apiSecret {
flex: 1;
}
#rtmp {
flex: 1;
}
#copyButton {
flex: 1;
max-width: 100px;
}
.input-group-inline > * {
margin-bottom: 20px;
}
input,
button {
padding: 10px;
font-size: 16px;
border: none;
border-radius: 4px;
outline: none;
box-sizing: border-box;
}
input[type='text'],
input[type='password'] {
flex: 1;
background: #2c2c2c;
color: #fff;
}
input[type='text'][readonly] {
background: #2c2c2c;
}
button {
cursor: pointer;
background-color: #007bff;
color: #fff;
transition: background-color 0.3s ease;
}
button:disabled {
background: #2c2c2c;
cursor: not-allowed;
}
button:hover {
background-color: #0056b3;
}
.button-group {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.button-group button {
width: 100%;
}
.popup {
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
background-color: indianred;
color: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
display: flex;
align-items: center;
justify-content: space-between;
min-width: 300px;
max-width: 600px;
width: 80%;
}
.popup.success {
background-color: mediumseagreen;
color: white;
}
.popup.error {
background-color: indianred;
color: white;
}
.popup.warning {
background-color: gold;
color: white;
}
.popup.info {
background-color: cornflowerblue;
color: white;
}
.popup.hidden {
display: none;
}
#closePopup {
background: none;
border: none;
color: white;
font-size: 16px;
cursor: pointer;
margin-left: 20px;
}
footer {
color: grey;
}
/* Media Queries for Responsiveness */
@media (max-width: 1024px) {
.container {
padding: 15px;
}
input,
button {
font-size: 14px;
}
}
@media (max-width: 768px) {
.input-group-inline {
flex-direction: column;
}
input,
button {
width: 100%;
font-size: 14px;
margin-bottom: 10px;
}
video {
width: 100%;
height: auto;
}
#copyButton {
max-width: 100%;
}
}
@media (max-width: 480px) {
.container {
padding: 10px;
}
h1 {
font-size: 24px;
}
input,
button {
font-size: 12px;
padding: 8px;
}
}

عرض الملف

@@ -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);
});

عرض الملف

@@ -0,0 +1,61 @@
FROM buildpack-deps:bullseye
# Versions of Nginx and nginx-rtmp-module to use
ENV NGINX_VERSION=1.24.0
ENV NGINX_RTMP_MODULE_VERSION=1.2.2
# Install dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
openssl \
libssl-dev \
wget \
&& rm -rf /var/lib/apt/lists/*
# Download and decompress Nginx and RTMP module
RUN mkdir -p /tmp/build && \
cd /tmp/build && \
wget https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz && \
tar -zxf nginx-${NGINX_VERSION}.tar.gz && \
wget https://github.com/arut/nginx-rtmp-module/archive/v${NGINX_RTMP_MODULE_VERSION}.tar.gz && \
tar -zxf v${NGINX_RTMP_MODULE_VERSION}.tar.gz
# Build and install Nginx with RTMP module
RUN cd /tmp/build/nginx-${NGINX_VERSION} && \
./configure \
--sbin-path=/usr/local/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx/nginx.lock \
--http-log-path=/var/log/nginx/access.log \
--http-client-body-temp-path=/tmp/nginx-client-body \
--with-http_ssl_module \
--with-threads \
--with-ipv6 \
--add-module=/tmp/build/nginx-rtmp-module-${NGINX_RTMP_MODULE_VERSION} \
--with-debug && \
make -j $(getconf _NPROCESSORS_ONLN) && \
make install && \
mkdir /var/lock/nginx && \
rm -rf /tmp/build
# Forward logs to Docker
RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
ln -sf /dev/stderr /var/log/nginx/error.log
# ------------------------
# FROM tiangolo/nginx-rtmp
# ------------------------
# Copy nginx.conf with RTMP configuration and stat.xsl
COPY nginx.conf /etc/nginx/nginx.conf
COPY stat.xsl /usr/share/nginx/html/stat.xsl
# Rtmp port
EXPOSE 1935
# Http port
EXPOSE 8081
CMD ["nginx", "-g", "daemon off;"]

عرض الملف

@@ -0,0 +1,30 @@
# RTMP Streaming
![rtmpStreaming](../rtmpStreaming.jpeg)
For running an `RTMP` (Real-Time Messaging Protocol) server in Docker, **Nginx with the RTMP module** is one of the best options. It is widely used for streaming video content due to its high performance and flexibility.
## Setting up Nginx with RTMP in Docker
```sh
# Copy the docker.compose.yml
$ cp docker-compose.template.yml docker-compose.yml
# Pull the official mirotalk rtmp image
$ docker pull mirotalk/rtmp:latest
# Create and start containers
$ docker-compose up -d
# Check the logs
$ docker logs -f mirotalk-rtmp
# To stop and remove resources
$ docker-compose down
```
## Custom Configuration
Modify the `nginx.conf` to suit your specific needs, such as enabling recording, adding authentication, or configuring HLS (HTTP Live Streaming).
By using Nginx with the RTMP module in Docker, you can quickly and easily set up a robust RTMP server for live video streaming.

عرض الملف

@@ -0,0 +1,14 @@
version: '3'
services:
mirotalk-rtmp:
container_name: mirotalk-rtmp
#image: tiangolo/nginx-rtmp
image: mirotalk/rtmp:latest
volumes:
- ./nginx.conf/:/etc/nginx/nginx.conf/:ro
- ./stat.xsl/:/usr/share/nginx/html/stat.xsl/:ro
ports:
- '1935:1935'
- '8081:8081'
restart: unless-stopped

عرض الملف

@@ -0,0 +1,34 @@
worker_processes auto;
rtmp_auto_push on;
events {
worker_connections 1024;
}
rtmp {
server {
listen 1935;
listen [::]:1935 ipv6only=on;
application live {
live on;
record off;
}
}
}
http {
server {
listen 8081;
location /stat {
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}
location /stat.xsl {
root /usr/share/nginx/html;
}
}
}

عرض الملف

@@ -0,0 +1,30 @@
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<body>
<h1>RTMP Statistics</h1>
<table border="none">
<tr bgcolor="#9acd32">
<th>Stream</th>
<th>Bitrate (kbps)</th>
<th>Bytes</th>
<th>Client</th>
<th>BW (kbps)</th>
<th>Time</th>
</tr>
<xsl:for-each select="rtmp/server/application/live/">
<tr>
<td><xsl:value-of select="@name"/></td>
<td><xsl:value-of select="bw_in_video"/></td>
<td><xsl:value-of select="bytes"/></td>
<td><xsl:value-of select="client_ip"/></td>
<td><xsl:value-of select="bw_out_video"/></td>
<td><xsl:value-of select="time"/></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

عرض الملف

@@ -0,0 +1,26 @@
# Use a lightweight Node.js image
FROM node:20-slim
# Set working directory
WORKDIR /app
# Copy package.json and install npm dependencies
COPY package.json .
RUN npm install
# Cleanup unnecessary packages and files
RUN npm cache clean --force \
&& rm -rf /tmp/* /var/tmp/* /usr/share/doc/*
# Copy the application code
COPY src src
# Rtmp port
EXPOSE 1935
# Http port
EXPOSE 8081
# Https port
EXPOSE 8043
# Set default command to start the application
CMD ["npm", "start"]

عرض الملف

@@ -0,0 +1,47 @@
# RTMP Streaming
![rtmpStreaming](../rtmpStreaming.jpeg)
For running an `RTMP` (Real-Time Messaging Protocol) server in Node, **[Node-Media-Server](https://github.com/illuspas/Node-Media-Server)** is one of the best options.
## Quick Start
```sh
# Create the config file for the server
$ cp config.template.js config.js
# Install the dependencies
$ npm install
# Start the RTMP Server
$ npm start
```
## Using Docker
```sh
# Create the config file for the server
$ cp config.template.js config.js
# Copy the docker.compose.yml
$ cp docker-compose.template.yml docker-compose.yml
# Pull the official mirotalk rtmp image
$ docker pull mirotalk/nms:latest
# Create and start containers
$ docker-compose up -d
# Check the logs
$ docker logs -f mirotalk-nms
# To stop and remove resources
$ docker-compose down
```
## Dashboard & API
[http://localhost:8081/admin](http://localhost:8081/admin)
[http://localhost:8081/api/server](http://localhost:8081/api/server)
## Custom Configuration
Modify the `config.js` to suit your specific needs.

عرض الملف

@@ -0,0 +1,13 @@
version: '3'
services:
mirotalk-nms:
container_name: mirotalk-nms
image: mirotalk/nms:latest
volumes:
- ./src/config.js/:/app/src/config.js/:ro
ports:
- '1935:1935'
- '8081:8081'
- '8043:8043'
restart: unless-stopped

عرض الملف

@@ -0,0 +1,26 @@
{
"name": "mirotalk-rtmp-nms",
"version": "1.0.0",
"description": "MiroTalk RTMP Node Media Server",
"main": "server.js",
"scripts": {
"start": "node src/server.js",
"start-dev": "nodemon src/server.js"
},
"keywords": [
"rtmp",
"node",
"media",
"server"
],
"author": "Miroslav Pejic",
"license": "AGPLv3",
"dependencies": {
"crypto": "^1.0.1",
"node-media-server": "^2.7.0"
},
"devDependencies": {
"uuid": "10.0.0",
"nodemon": "^3.1.4"
}
}

عرض الملف

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDfzCCAmcCFHLVDcza5/3VZ8U5Vd2LnWRvwME1MA0GCSqGSIb3DQEBCwUAMHsx
CzAJBgNVBAYTAklUMQ4wDAYDVQQIDAVJdGFseTERMA8GA1UECgwITWlyb1RhbGsx
HTAbBgNVBAMMFE1pcm9UYWxrIFJUTVAgU2VydmVyMSowKAYJKoZIhvcNAQkBFhtt
aXJvc2xhdi5wZWppYy44NUBnbWFpbC5jb20wIBcNMjQwNjIwMjE1MDQ4WhgPMjA1
MTExMDUyMTUwNDhaMHsxCzAJBgNVBAYTAklUMQ4wDAYDVQQIDAVJdGFseTERMA8G
A1UECgwITWlyb1RhbGsxHTAbBgNVBAMMFE1pcm9UYWxrIFJUTVAgU2VydmVyMSow
KAYJKoZIhvcNAQkBFhttaXJvc2xhdi5wZWppYy44NUBnbWFpbC5jb20wggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJfFke3Df+0tgei1djU1Af0i4OukeI
UHxWzr3mibVxJ8qtNwukXc0F9XnRXA9kF7WJa+vzuQXQgClH75mrrTNzwV1IWXSZ
nKokogyweHC+XZ/Frdv+yCWyJcC7YrIJVTJNCU+Z4wDBLira35Z2tBTd7UV9gURB
oQZhqeG0b653D1kvb9oEzTJFbmcst5YzEHUfs4CeF6RvOK4q0wD1CXImJYpXrJ02
YbpLyZ8hLZuGq3uDdPOjHXQApxmLhSUgJHnIcHxR/xVFdLyAqP+aTKnozLCS3PZC
yB1lJbuEmPO5WfeQDmL7W3COEtdAdlCNF8VZ09z1AlCKLnp75vi9M04hAgMBAAEw
DQYJKoZIhvcNAQELBQADggEBAHOVKxSt+BGtDFynltp0pfHGyFo1sr5ULUams67s
LQuiOm0Iuw1kXRA9Yf/hAcL12/taEBfNqYxveQXe8xbodwobkOpHmyYYLZ+50a8I
+hP15UkmlJb0iy7OkjoalDqVFFN2WQTJK3OqMg4RdJlTMpzDibNYzZWZ6Xaxl670
FDh3xJO9/MweHO/ScGS5RVIdYIdDbFGzzcYHiWpsbcYgYdvsNVofNsZpotWd37/x
CbYImc1RKhRnBQTcnnK0u+6ugD26Yho3eB5f0nbj2gkikDYueYYZG+7uV2w+9QKI
e+nipac/6/ACwo1ZMsEYR3arjdLN8Rxr39s5PStP63EkGv4=
-----END CERTIFICATE-----

عرض الملف

@@ -0,0 +1,30 @@
'use strict';
const config = {
rtmp: {
port: 1935,
chunk_size: 60000,
gop_cache: true,
ping: 60,
ping_timeout: 30,
},
http: {
port: 8081,
allow_origin: '*',
},
https: {
port: 8043,
key: __dirname + '/key.pem',
cert: __dirname + '/cert.pem',
},
auth: {
api: true,
api_user: 'mirotalk',
api_pass: 'mirotalkRtmpPassword', // http://localhost:8081/admin
play: false, // Require authentication for playing streams
publish: false, // Require authentication for publishing streams
secret: 'mirotalkRtmpSecret', // Check the sign.js file to generate a valid RTMP URL
},
};
module.exports = config;

عرض الملف

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAyXxZHtw3/tLYHotXY1NQH9IuDrpHiFB8Vs695om1cSfKrTcL
pF3NBfV50VwPZBe1iWvr87kF0IApR++Zq60zc8FdSFl0mZyqJKIMsHhwvl2fxa3b
/sglsiXAu2KyCVUyTQlPmeMAwS4q2t+WdrQU3e1FfYFEQaEGYanhtG+udw9ZL2/a
BM0yRW5nLLeWMxB1H7OAnhekbziuKtMA9QlyJiWKV6ydNmG6S8mfIS2bhqt7g3Tz
ox10AKcZi4UlICR5yHB8Uf8VRXS8gKj/mkyp6Mywktz2QsgdZSW7hJjzuVn3kA5i
+1twjhLXQHZQjRfFWdPc9QJQii56e+b4vTNOIQIDAQABAoIBAQC98pSyGzpO6ccF
RKfl460t0p/JEqRNRlNyIwW0SS7ctn7EPZikJCoc7Acj8H4yBogGPc/7vPpWTfyc
7K0aw/Y1sp2Wj371MlTUpFECLQlc7japzfYQg+/FuwGvpqPhWIhLR/PbR752YGfW
X+MhlTP25LEWWL9Yf83cVKOLz53Sbt9KH/baJhpFhXrrxl/1rcR6U7MhZOLlq1aa
XbYwglzd/l20cED1wbCzbxVwl/M73ZIOgn+2vgSxkNCxkZVpGYvE0Tx9S9b3rmtl
OD9WqTS5beoKABXbMPTMdbyekPAnK2pAUs+WmKD8iJ69djKeMj/lO1vQBZOBJzlL
pxteIS7hAoGBAPvbO27SPnMI76lfXDAECNHCq1YLiVpqhctFvcJlM1aQMbWKqYOX
XVho+drGlh/Mf9JpY3rtfd7VNZKbJQRT/6Wf7j7L7WOldClxIhBXxziqOJ/bemJP
ELRau321q5x2aNLGZbioaDgB9fzEm/aPyjRC8JnIvePyksAzXJNw2mtzAoGBAMzM
9w7nyfa16pG14hAdiYkCtB052jZ71sz+Y9XbA12D36EDLxkQ3l5I6FNrvWvu30N8
snG+SMmk8LSjy3b4bv5DPP1Bnh+HQG/5quoG61uODkRC7aIgLCgdbmnggWrI+gV7
E+YM6HMZFVk3Lvo1GobyxsBCLBRCPdfW15nQ0eMbAoGBAJiXtIOpeFLEOEibUUR6
PUmxs5N3e+m/Hn8RKy6LmDY7ORLwB1KGM/Ur7S3jIfP0OAGo/q/tElUfQs0nmJ7t
sbeMlZGQhqzYAvBU7jmOpVKst5ALLzQ/CTTswCojFu2+RDZoJBtkVXiRn5NdH82c
Qvu1Dwdtu7dPMiCnPdDLEFsHAoGBAJjHhr7N13J+f0C4CK6w+jsFk0wCLnFarQE7
/Uo6GiaXDCrXbzkpxllb1kT1KNft2QxFZ/FGXJJgw1heoJhd+J8hlcvwOX+XrFBc
Vk5DXyxrquTtcMzzZz19xzKg0qrQxwNzr4J8uqOyYKSvcBIjr2hgkDg4pR1v1SbB
FRGgIBNlAoGAeMJrhQy5RU6xCG9l8+jH42PhG4+F9pV5EQI0v421KQ4hklgY+pT6
KrTuZp6tjX7hErYNNd77ELDRLZ3p8VlqxuvF3UI6s+I7rRxpXpjSve3si8USYS4L
aKAp6qDc3Vt1e6uin9NwZS6jtDvH8VOIMOHTQYJwUTnjpSLuYIxOzU0=
-----END RSA PRIVATE KEY-----

عرض الملف

@@ -0,0 +1,81 @@
'use strict';
const NodeMediaServer = require('node-media-server');
const config = require('./config');
console.log('Rtmp Server config', {
config: config,
http: {
admin: 'http://localhost:8081/admin',
stats: 'http://localhost:8081/api/server',
streams: 'http://localhost:8081/api/streams',
},
https: {
admin: 'https://localhost:8043/admin',
stats: 'https://localhost:8043/api/server',
streams: 'http://localhost:8043/api/streams',
},
});
const nms = new NodeMediaServer(config);
nms.run();
nms.on('preConnect', (id, args) => {
console.log('[NodeEvent on preConnect]', `id=${id} args=${JSON.stringify(args)}`);
// let session = nms.getSession(id);
// session.reject();
});
nms.on('postConnect', (id, args) => {
console.log('[NodeEvent on postConnect]', `id=${id} args=${JSON.stringify(args)}`);
});
nms.on('doneConnect', (id, args) => {
console.log('[NodeEvent on doneConnect]', `id=${id} args=${JSON.stringify(args)}`);
});
nms.on('prePublish', (id, StreamPath, args) => {
console.log('[NodeEvent on prePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
// let session = nms.getSession(id);
// session.reject();
});
nms.on('postPublish', (id, StreamPath, args) => {
console.log('[NodeEvent on postPublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});
nms.on('donePublish', (id, StreamPath, args) => {
console.log('[NodeEvent on donePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});
nms.on('prePlay', (id, StreamPath, args) => {
console.log('[NodeEvent on prePlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});
nms.on('postPlay', (id, StreamPath, args) => {
console.log('[NodeEvent on postPlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
// let session = nms.getSession(id);
// session.reject();
});
nms.on('donePlay', (id, StreamPath, args) => {
console.log('[NodeEvent on donePlay]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
});
nms.on('logMessage', (...args) => {
// custom logger log message handler
});
nms.on('errorMessage', (...args) => {
// custom logger error message handler
});
nms.on('debugMessage', (...args) => {
// custom logger debug message handler
});
nms.on('ffDebugMessage', (...args) => {
// custom logger ffmpeg debug message handler
});

عرض الملف

@@ -0,0 +1,58 @@
'use strict';
const crypto = require('crypto-js');
const { v4: uuidv4 } = require('uuid');
/**
* Generates an RTMP URL with an expiration timestamp and a hash value.
*
* @param {string} baseURL - The base URL of the RTMP server.
* @param {string} streamPath - The path to the stream.
* @param {string} secretKey - The secret key used for generating the hash.
* @param {number} expirationHours - The number of hours until the URL expires.
* @returns {string} - The generated RTMP URL for Node Media Server.
*/
function generateRTMPUrl(baseURL, streamPath, secretKey, expirationHours = 8) {
// Current timestamp in seconds
const currentTime = Math.floor(Date.now() / 1000);
// Expiration time (current time + expirationHours in seconds)
const expirationTime = currentTime + expirationHours * 3600;
// Generate the hash value
const hashValue = crypto.MD5(`${streamPath}-${expirationTime}-${secretKey}`).toString();
// Construct the final request address
const rtmpUrl = `${baseURL}${streamPath}?sign=${expirationTime}-${hashValue}`;
// Print some log
log.debug('generateRTMPUrl', {
currentTime: currentTime,
expirationTime: expirationTime,
hashValue: hashValue,
rtmpUrl: rtmpUrl,
});
return rtmpUrl;
}
// Example usage
const baseURL = 'rtmp://localhost:1935';
const streamKey = uuidv4();
const streamPath = '/live/' + streamKey; // path/stream-key
const secretKey = 'mirotalkRtmpSecret';
const expirationHours = 8;
// Run: node sign.js
const rtmpUrl = generateRTMPUrl(baseURL, streamPath, secretKey, expirationHours);
console.log('Generated RTMP URL:', rtmpUrl);
/*
OBS:
- Server: rtmp://localhost:1935/live
- StreamKey: demo?sign=1719169535-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FFMPEG:
- ffmpeg -re -i input.mp4 -c:v libx264 -preset veryfast -maxrate 3000k -bufsize 6000k -vf "scale=-2:720" -g 50 -c:a aac -b:a 128k -f flv "rtmp://localhost:1935/live/demo?sign=1719169535-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
*/

ثنائية
rtmpServers/rtmpStreaming.jpeg Normal file

ملف ثنائي غير معروض.

بعد

العرض:  |  الارتفاع:  |  الحجم: 60 KiB