[mirotalksfu] - improve nodemailer, update dep, speaker focus WIP

هذا الالتزام موجود في:
Miroslav Pejic
2025-06-08 15:14:08 +02:00
الأصل 561dfb474d
التزام 7869df811e
12 ملفات معدلة مع 190 إضافات و62 حذوفات

عرض الملف

@@ -160,6 +160,7 @@ EMAIL_HOST=smtp.gmail.com # SMTP server host
EMAIL_PORT=587 # SMTP port
EMAIL_USERNAME=your_username # SMTP username
EMAIL_PASSWORD=your_password # SMTP password
EMAIL_FROM= # Sender email address
EMAIL_SEND_TO=sfu.mirotalk@gmail.com # Notification recipient
# Slack Integration
@@ -358,4 +359,6 @@ STATS_ID=41d26670-f275-45bb-af82-3ce91fe57756 # Stats tracking ID
# 10. Mediasoup Configuration
# ----------------------------------------------------
MEDIASOUP_LOG_LEVEL=error # Mediasoup log level (debug, warn, error)
MEDIASOUP_ROUTER_AUDIO_LEVEL_OBSERVER_ENABLED=true # Enable audio level observer (true|false)
MEDIASOUP_ROUTER_ACTIVE_SPEAKER_OBSERVER_ENABLED=false # Enable active speaker observer (true|false)
MEDIASOUP_LOG_LEVEL=error # Mediasoup log level (debug, warn, error)

عرض الملف

@@ -205,6 +205,8 @@ module.exports = class Peer {
kind: producer_kind,
rtpParameters: producer_rtpParameters,
});
this.addProducer(producer.id, producer);
} catch (error) {
log.error(`Error creating producer for transport ID ${producerTransportId}`, {
error: error.message,
@@ -222,8 +224,6 @@ module.exports = class Peer {
appData.mediaType = producer_type;
this.addProducer(id, producer);
if (['simulcast', 'svc'].includes(type)) {
const { scalabilityMode } = rtpParameters.encodings[0];
const spatialLayer = parseInt(scalabilityMode.substring(1, 2)); // 1/2/3
@@ -316,6 +316,8 @@ module.exports = class Peer {
paused: true, // Start the consumer in a paused state
ignoreDtx: true, // Ignore DTX (Discontinuous Transmission)
});
this.addConsumer(consumer.id, consumer);
} catch (error) {
log.error(`Error creating consumer for transport ID ${consumer_transport_id}`, {
error: error.message,
@@ -330,8 +332,6 @@ module.exports = class Peer {
const { id, type, kind, rtpParameters, producerPaused } = consumer;
this.addConsumer(id, consumer);
if (['simulcast', 'svc'].includes(type)) {
// simulcast - L1T3/L2T3/L3T3 | svc - L3T3
const { scalabilityMode } = rtpParameters.encodings[0];

عرض الملف

@@ -102,6 +102,7 @@ module.exports = class Room {
videoAIEnabled: this.videoAIEnabled,
thereIsPolls: this.thereIsPolls(),
shareMediaData: this.shareMediaData,
dominantSpeaker: this.activeSpeakerObserverEnabled,
peers: JSON.stringify([...this.peers]),
};
}
@@ -311,10 +312,16 @@ module.exports = class Room {
.then((router) => {
this.router = router;
if (this.audioLevelObserverEnabled) {
this.startAudioLevelObservation();
log.info('Audio Level Observer enabled, starting observation...');
this.startAudioLevelObservation().catch((err) => {
log.error('Failed to start audio level observation', err);
});
}
if (this.activeSpeakerObserverEnabled) {
this.startActiveSpeakerObserver();
log.info('Active Speaker Observer enabled, starting observation...');
this.startActiveSpeakerObserver().catch((err) => {
log.error('Failed to start active speaker observer', err);
});
}
this.router.observer.on('close', () => {
log.info('---------------> Router is now closed as the last peer has left the room', {
@@ -385,9 +392,9 @@ module.exports = class Room {
peer_name: peer_name,
audioVolume: audioVolume,
};
// Uncomment the following line for debugging
// log.debug('Sending audio volume', data);
this.sendToAll('audioVolume', data);
return;
}
});
});
@@ -401,6 +408,7 @@ module.exports = class Room {
addProducerToAudioLevelObserver(producer) {
if (this.audioLevelObserverEnabled) {
this.audioLevelObserver.addProducer(producer);
log.info('Producer added to audio level observer', { producer });
}
}
@@ -417,25 +425,40 @@ module.exports = class Room {
// ####################################################
async startActiveSpeakerObserver() {
log.debug('Start activeSpeakerObserver for signaling dominant speaker...');
this.activeSpeakerObserver = await this.router.createActiveSpeakerObserver();
this.activeSpeakerObserver.on('dominantspeaker', (dominantSpeaker) => {
if (!dominantSpeaker.producer) {
return;
}
log.debug('activeSpeakerObserver "dominantspeaker" event', dominantSpeaker.producer.id);
this.peers.forEach((peer) => {
const { id, peer_audio, peer_name } = peer;
peer.producers.forEach((peerProducer) => {
if (
peerProducer.id === dominantSpeaker.producer.id &&
peerProducer.kind === 'audio' &&
peer_audio
) {
const data = {
peer_id: id,
peer_name: peer_name,
};
// log.debug('Sending dominant speaker', data);
this.sendToAll('dominantSpeaker', data);
if (peer.producers instanceof Map) {
for (const peerProducer of peer.producers.values()) {
if (
peerProducer.id === dominantSpeaker.producer.id &&
peerProducer.kind === 'audio' &&
peer_audio
) {
let videoProducerId = null;
for (const p of peer.producers.values()) {
if (p.kind === 'video') {
videoProducerId = p.id;
break;
}
}
const data = {
producer_id: videoProducerId,
peer_id: id,
peer_name: peer_name,
};
log.debug('Sending dominant speaker', data);
this.sendToAll('dominantSpeaker', data);
break;
}
}
});
}
});
});
}
@@ -443,6 +466,7 @@ module.exports = class Room {
addProducerToActiveSpeakerObserver(producer) {
if (this.activeSpeakerObserverEnabled) {
this.activeSpeakerObserver.addProducer(producer);
log.info('Producer added to active speaker observer', { producer });
}
}

عرض الملف

@@ -64,7 +64,7 @@ dev 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.8.61
* @version 1.8.62
*
*/
@@ -1976,7 +1976,7 @@ function startServer() {
peerInfo: peerInfo,
});
// add & monitor producer audio level and active speaker
// add & monitor producer audio level and dominant speaker
if (kind === 'audio') {
room.addProducerToAudioLevelObserver({ producerId: producer_id });
room.addProducerToActiveSpeakerObserver({ producerId: producer_id });

عرض الملف

@@ -647,6 +647,7 @@ module.exports = {
* - port : SMTP port (default: 587 for TLS)
* - username : SMTP auth username
* - password : SMTP auth password (store ONLY in .env)
* - from : Sender email address (default: same as username)
* - sendTo : Recipient email for alerts
*
* Common Providers:
@@ -669,6 +670,7 @@ module.exports = {
port: parseInt(process.env.EMAIL_PORT) || 587,
username: process.env.EMAIL_USERNAME || 'your_username',
password: process.env.EMAIL_PASSWORD || 'your_password',
from: process.env.EMAIL_FROM || process.env.EMAIL_USERNAME,
sendTo: process.env.EMAIL_SEND_TO || 'sfu.mirotalk@gmail.com',
},
@@ -1341,10 +1343,10 @@ module.exports = {
*/
router: {
// Enable audio level monitoring (for detecting who is speaking)
audioLevelObserverEnabled: true,
audioLevelObserverEnabled: process.env.MEDIASOUP_ROUTER_AUDIO_LEVEL_OBSERVER_ENABLED !== 'false',
// Disable active speaker detection (uses more CPU)
activeSpeakerObserverEnabled: false,
activeSpeakerObserverEnabled: process.env.MEDIASOUP_ROUTER_ACTIVE_SPEAKER_OBSERVER_ENABLED === 'true',
/**
* Supported Media Codecs

عرض الملف

@@ -15,6 +15,7 @@ const EMAIL_HOST = emailConfig.host || false;
const EMAIL_PORT = emailConfig.port || false;
const EMAIL_USERNAME = emailConfig.username || false;
const EMAIL_PASSWORD = emailConfig.password || false;
const EMAIL_FROM = emailConfig.from || emailConfig.username;
const EMAIL_SEND_TO = emailConfig.sendTo || false;
if (EMAIL_ALERT && EMAIL_HOST && EMAIL_PORT && EMAIL_USERNAME && EMAIL_PASSWORD && EMAIL_SEND_TO) {
@@ -24,12 +25,16 @@ if (EMAIL_ALERT && EMAIL_HOST && EMAIL_PORT && EMAIL_USERNAME && EMAIL_PASSWORD
port: EMAIL_PORT,
username: EMAIL_USERNAME,
password: EMAIL_PASSWORD,
from: EMAIL_FROM,
to: EMAIL_SEND_TO,
});
}
const IS_TLS_PORT = EMAIL_PORT === 465;
const transport = nodemailer.createTransport({
host: EMAIL_HOST,
port: EMAIL_PORT,
secure: IS_TLS_PORT,
auth: {
user: EMAIL_USERNAME,
pass: EMAIL_PASSWORD,
@@ -67,7 +72,7 @@ function sendEmailAlert(event, data) {
function sendEmail(subject, body) {
transport
.sendMail({
from: EMAIL_USERNAME,
from: EMAIL_FROM,
to: EMAIL_SEND_TO,
subject: subject,
html: body,

عرض الملف

@@ -1,6 +1,6 @@
{
"name": "mirotalksfu",
"version": "1.8.61",
"version": "1.8.62",
"description": "WebRTC SFU browser-based video calls",
"main": "Server.js",
"scripts": {
@@ -57,8 +57,8 @@
"node": ">=18"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.825.0",
"@aws-sdk/lib-storage": "^3.825.0",
"@aws-sdk/client-s3": "^3.826.0",
"@aws-sdk/lib-storage": "^3.826.0",
"@mattermost/client": "10.8.0",
"@ngrok/ngrok": "1.5.1",
"@sentry/node": "^9.27.0",

عرض الملف

@@ -64,7 +64,7 @@ let BRAND = {
},
about: {
imageUrl: '../images/mirotalk-logo.gif',
title: '<strong>WebRTC SFU v1.8.61</strong>',
title: '<strong>WebRTC SFU v1.8.62</strong>',
html: `
<button
id="support-button"

عرض الملف

@@ -31,6 +31,7 @@ class LocalStorage {
moderator_chat_cant_deep_seek: false, // Everyone can't chat with DeepSeek
moderator_media_cant_sharing: false, // Everyone can't share media
moderator_disconnect_all_on_leave: false, // Disconnect all participants on leave room
dominant_speaker_focus: false, // Focus on dominant speaker
mic_auto_gain_control: false, // Automatic gain control
mic_echo_cancellations: true, // Echo cancellation
mic_noise_suppression: true, // Noise suppression

عرض الملف

@@ -11,7 +11,7 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h
* @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.8.61
* @version 1.8.62
*
*/
@@ -313,6 +313,11 @@ function initClient() {
refreshMainButtonsToolTipPlacement();
setTippy('closeEmojiPickerContainer', 'Close', 'bottom');
setTippy('mySettingsCloseBtn', 'Close', 'bottom');
setTippy(
'switchDominantSpeakerFocus',
'If Active, When a participant speaks, their video will be focused and enlarged',
'right'
);
setTippy(
'switchPushToTalk',
'If Active, When SpaceBar keydown the microphone will be resumed, on keyup will be paused, like a walkie-talkie.',
@@ -2561,6 +2566,11 @@ function handleSelects() {
rc.changeAudioDestination();
refreshLsDevices();
};
switchDominantSpeakerFocus.onchange = async (e) => {
localStorageSettings.dominant_speaker_focus = e.currentTarget.checked;
lS.setSettings(localStorageSettings);
e.target.blur();
}
switchPushToTalk.onchange = async (e) => {
const producerExist = rc.producerExist(RoomClient.mediaType.audio);
if (!producerExist && !isPushToTalkActive) {
@@ -3286,6 +3296,8 @@ function loadSettingsFromLocalStorage() {
selectTheme.disabled = themeCustom.keep;
themeCustom.input.value = themeCustom.color;
switchDominantSpeakerFocus.checked = localStorageSettings.dominant_speaker_focus;
switchAutoGainControl.checked = localStorageSettings.mic_auto_gain_control;
switchEchoCancellation.checked = localStorageSettings.mic_echo_cancellations;
switchNoiseSuppression.checked = localStorageSettings.mic_noise_suppression;
@@ -5453,7 +5465,7 @@ function showAbout() {
position: 'center',
imageUrl: BRAND.about?.imageUrl && BRAND.about.imageUrl.trim() !== '' ? BRAND.about.imageUrl : image.about,
customClass: { image: 'img-about' },
title: BRAND.about?.title && BRAND.about.title.trim() !== '' ? BRAND.about.title : 'WebRTC SFU v1.8.61',
title: BRAND.about?.title && BRAND.about.title.trim() !== '' ? BRAND.about.title : 'WebRTC SFU v1.8.62',
html: `
<br />
<div id="about">

عرض الملف

@@ -9,7 +9,7 @@
* @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.8.61
* @version 1.8.62
*
*/
@@ -237,6 +237,7 @@ class RoomClient {
this.maxReconnectAttempts = 5;
this.reconnectInterval = 3000;
this.maxReconnectInterval = 15000;
this.silentReconnect = false;
// Handle ICE
this.iceRestarting = false;
@@ -291,6 +292,7 @@ class RoomClient {
this.renderAIToken = null;
this.peerConnection = null;
this.dominantSpeaker = false;
this.isAudioAllowed = isAudioAllowed;
this.isVideoAllowed = isVideoAllowed;
this.isScreenAllowed = isScreenAllowed;
@@ -683,6 +685,12 @@ class RoomClient {
) {
this.shareVideoAction(room.shareMediaData);
}
// Dominant Speaker
this.dominantSpeaker = room.dominantSpeaker || false;
if (!this.dominantSpeaker) {
elemDisplay('dominantSpeakerFocusDiv', false);
}
}
// PARTICIPANTS
@@ -1315,6 +1323,7 @@ class RoomClient {
// Helper functions
const showReconnectAlert = () => {
if (this.silentReconnect) return;
this.sound('reconnect');
reconnectAlert = Swal.fire({
background: swalBackground,
@@ -1331,6 +1340,7 @@ class RoomClient {
};
const showMaxAttemptsAlert = () => {
if (this.silentReconnect) return;
this.sound('alert');
Swal.fire({
allowOutsideClick: false,
@@ -8138,9 +8148,8 @@ class RoomClient {
// HANDLE DOMINANT SPEAKER
// ###################################################
handleDominantSpeaker(data) {
console.log('Dominant Speaker', data);
const { peer_id } = data;
handleDominantSpeakerHighlight(peer_id) {
// Highlight the peer name
const peerNameElement = this.getId(peer_id + '__name');
if (peerNameElement) {
peerNameElement.style.color = 'lime';
@@ -8148,7 +8157,66 @@ class RoomClient {
peerNameElement.style.color = '#FFFFFF';
}, 5000);
}
//...
}
handleDominantSpeakerFocus(producer_id, consumer_id = null, timeout = 10000) {
// Find the consumer id for this producer
const consumerId = consumer_id ? consumer_id : this.getConsumerIdByProducerId(producer_id);
console.log('handleDominantSpeakerFocus', { consumersList: this.consumers, consumerId, producer_id });
if (!consumerId) return;
// Track the currently focused video container
if (!this._dominantSpeakerState) {
this._dominantSpeakerState = { prevConsumerId: null, timeout: null };
}
// Remove focus mode from previous dominant speaker if any
if (this._dominantSpeakerState.prevConsumerId && this._dominantSpeakerState.prevConsumerId !== consumerId) {
const prevVideoContainer = this.getId(this._dominantSpeakerState.prevConsumerId + '__video');
const prevFocusBtn = this.getId(this._dominantSpeakerState.prevConsumerId + '__hideALL');
if (prevVideoContainer && prevVideoContainer.hasAttribute('focus-mode') && prevFocusBtn) {
prevFocusBtn.click();
}
}
// Set focus mode for the new dominant speaker
const videoContainer = this.getId(consumerId + '__video');
const focusBtn = this.getId(consumerId + '__hideALL');
if (videoContainer && focusBtn && !videoContainer.hasAttribute('focus-mode')) {
focusBtn.click();
}
// Update the state
this._dominantSpeakerState.prevConsumerId = consumerId;
// Clear any previous timeout
if (this._dominantSpeakerState.timeout) {
clearTimeout(this._dominantSpeakerState.timeout);
}
// Set a timeout to remove focus after 'timeout' seconds of inactivity
this._dominantSpeakerState.timeout = setTimeout(() => {
// Remove focus mode if still focused
if (this._dominantSpeakerState.prevConsumerId) {
const prevVideoContainer = this.getId(this._dominantSpeakerState.prevConsumerId + '__video');
const prevFocusBtn = this.getId(this._dominantSpeakerState.prevConsumerId + '__hideALL');
if (prevVideoContainer && prevVideoContainer.hasAttribute('focus-mode') && prevFocusBtn) {
prevFocusBtn.click();
}
this._dominantSpeakerState.prevConsumerId = null;
}
}, timeout); // 10 seconds
}
handleDominantSpeaker(data) {
console.log('Dominant Speaker', data);
const { peer_id, producer_id } = data;
this.handleDominantSpeakerHighlight(peer_id);
if (this.dominantSpeaker && switchDominantSpeakerFocus.checked) {
this.handleDominantSpeakerFocus(producer_id);
}
}
// ####################################################
@@ -8206,35 +8274,35 @@ class RoomClient {
// HANDLE VIDEO
// ###################################################
toggleFocusMode(videoContainerId, btnHa = null) {
if (isHideMeActive) {
this.userLog('warning', 'To use this feature, please toggle Hide self view before', 'top-end', 6000);
return;
}
const videoContainer = this.getId(videoContainerId);
isHideALLVideosActive = !isHideALLVideosActive;
if (btnHa) btnHa.style.color = isHideALLVideosActive ? 'lime' : 'white';
if (isHideALLVideosActive) {
videoContainer.style.width = '100%';
videoContainer.style.height = '100%';
videoContainer.setAttribute('focus-mode', 'true');
} else {
resizeVideoMedia();
videoContainer.removeAttribute('focus-mode');
}
const children = this.videoMediaContainer.children;
for (let child of children) {
if (child.id != videoContainerId) {
child.style.display = isHideALLVideosActive ? 'none' : 'block';
}
}
}
handleHA(uid, videoContainerId) {
let btnHa = this.getId(uid);
if (btnHa) {
btnHa.addEventListener('click', (e) => {
if (isHideMeActive) {
return this.userLog(
'warning',
'To use this feature, please toggle Hide self view before',
'top-end',
6000
);
}
const videoContainer = this.getId(videoContainerId);
isHideALLVideosActive = !isHideALLVideosActive;
e.target.style.color = isHideALLVideosActive ? 'lime' : 'white';
if (isHideALLVideosActive) {
videoContainer.style.width = '100%';
videoContainer.style.height = '100%';
videoContainer.setAttribute('focus-mode', 'true');
} else {
resizeVideoMedia();
videoContainer.removeAttribute('focus-mode');
}
const children = this.videoMediaContainer.children;
for (let child of children) {
if (child.id != videoContainerId) {
child.style.display = isHideALLVideosActive ? 'none' : 'block';
}
}
this.toggleFocusMode(videoContainerId, btnHa);
});
}
}

عرض الملف

@@ -721,6 +721,19 @@ access to use this app.
<div id="pushToTalkDiv" class="hidden">
<hr />
<table class="settingsTable">
<tr id="dominantSpeakerFocusDiv">
<td>
<div class="title">
<i class="fa-solid fa-eye"></i>
<p>Speaker Focus</p>
</div>
</td>
<td>
<div class="form-check form-switch form-switch-md title">
<input id="switchDominantSpeakerFocus" class="form-check-input" type="checkbox" />
</div>
</td>
</tr>
<tr>
<td>
<div class="title">