[mirotalksfu] - improve nodemailer, update dep, speaker focus WIP
هذا الالتزام موجود في:
@@ -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">
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم