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

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

عرض الملف

@@ -0,0 +1,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;
}
}