[mirotalk] - add geoLocation, update dep

This commit is contained in:
Miroslav Pejic
2025-06-10 17:54:36 +02:00
parent 5068378693
commit ffbaa6dd2d
10 changed files with 478 additions and 7 deletions
+1
View File
@@ -142,6 +142,7 @@ module.exports = {
showSnapShotBtn: true,
showFileShareBtn: true,
showShareVideoAudioBtn: true,
showGeoLocationBtn: true,
showPrivateMessageBtn: true,
showZoomInOutBtn: false,
showVideoFocusBtn: true,
+37 -1
View File
@@ -45,7 +45,7 @@ dependencies: {
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.5.18
* @version 1.5.19
*
*/
@@ -1512,6 +1512,42 @@ io.sockets.on('connect', async (socket) => {
await sendToRoom(data.room_id, socket.id, 'message', data);
});
/**
* Relay commands to peers or specific peer in the same room
* @param {Object} cfg - The configuration object containing command details.
* @param {string} cfg.action - The action to be performed (e.g., 'geoLocation').
* @param {boolean} cfg.send_to_all - Whether to send the command to all peers in the room.
* @param {Object} cfg.data - The data associated with the command.
*/
socket.on('cmd', async (cfg) => {
const config = checkXSS(cfg);
const { action, send_to_all, data } = config;
const { room_id, peer_id, peer_name, peer_uuid, to_peer_id } = data;
log.info('cmd', config);
// Only the presenter can do this actions
const presenterActions = ['geoLocation'];
if (presenterActions.some((v) => action === v)) {
// Check if peer is presenter
const isPresenter = isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
// if not presenter do nothing
if (!isPresenter) return;
}
if (send_to_all) {
log.debug('[' + socket.id + '] emit cmd to [room_id: ' + room_id + ']', config);
await sendToRoom(room_id, socket.id, 'cmd', config);
} else {
log.debug('[' + socket.id + '] emit cmd to [' + to_peer_id + '] from room_id [' + room_id + ']');
await sendToPeer(to_peer_id, sockets, 'cmd', config);
}
});
/**
* Relay Audio Video Hand ... Status to peers
*/
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "mirotalk",
"version": "1.5.18",
"version": "1.5.19",
"description": "A free WebRTC browser-based video call",
"main": "server.js",
"scripts": {
@@ -61,14 +61,14 @@
"jsonwebtoken": "^9.0.2",
"js-yaml": "^4.1.0",
"nodemailer": "^7.0.3",
"openai": "^5.1.1",
"openai": "^5.2.0",
"qs": "^6.14.0",
"socket.io": "^4.8.1",
"swagger-ui-express": "^5.0.1",
"uuid": "11.1.0"
},
"devDependencies": {
"mocha": "^11.5.0",
"mocha": "^11.6.0",
"node-fetch": "^3.3.2",
"nodemon": "^3.1.10",
"prettier": "3.5.3",
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+1 -1
View File
@@ -73,7 +73,7 @@ let brand = {
},
about: {
imageUrl: '../images/mirotalk-logo.gif',
title: 'WebRTC P2P v1.5.18',
title: 'WebRTC P2P v1.5.19',
html: `
<button
id="support-button"
+1
View File
@@ -62,6 +62,7 @@ let buttons = {
showSnapShotBtn: true,
showFileShareBtn: true,
showShareVideoAudioBtn: true,
showGeoLocationBtn: true,
showPrivateMessageBtn: true,
showZoomInOutBtn: false,
showVideoFocusBtn: true,
+74 -2
View File
@@ -15,7 +15,7 @@
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.5.18
* @version 1.5.19
*
*/
@@ -51,6 +51,7 @@ const images = {
avatar: '../images/mirotalk-logo.png',
recording: '../images/recording.png',
poster: '../images/loader.gif',
geoLocation: '../images/geolocation.png',
}; // nice free icon: https://www.iconfinder.com
const className = {
@@ -75,6 +76,7 @@ const className = {
fsOn: 'fas fa-compress-alt',
fsOff: 'fas fa-expand-alt',
msgPrivate: 'fas fa-paper-plane',
geoLocation: 'fas fa-location-dot',
shareFile: 'fas fa-upload',
shareVideoAudio: 'fab fa-youtube',
kickOut: 'fas fa-sign-out-alt',
@@ -687,6 +689,29 @@ let surveyURL = 'https://www.questionpro.com/t/AUs7VZq00L';
let redirectActive = false;
let redirectURL = '/newcall';
// GeoLocation
const notificationService = new NotificationService({ Swal, swBg, images, playSound });
const geoService = GeoService;
let geo;
/**
* Load GeoLocation service
* @returns {void}
*/
function loadGeo() {
geo = new PeerGeoLocation({
room_id: roomId,
peer_name: myPeerName,
peer_id: myPeerId,
peer_uuid: myPeerUUID,
sendToServer,
msgPopup,
notificationService,
geoService,
openURL,
});
}
/**
* Load all Html elements by Id
*/
@@ -1168,6 +1193,7 @@ function initClientPeer() {
signalingSocket.on('peerName', handlePeerName);
signalingSocket.on('peerStatus', handlePeerStatus);
signalingSocket.on('peerAction', handlePeerAction);
signalingSocket.on('cmd', handleCmd);
signalingSocket.on('message', handleMessage);
signalingSocket.on('wbCanvasToJson', handleJsonToWbCanvas);
signalingSocket.on('whiteboardAction', handleWhiteboardAction);
@@ -1339,6 +1365,7 @@ function handleRules(isPresenter) {
buttons.settings.showTabRoomSecurity = false;
buttons.settings.showTabEmailInvitation = false;
buttons.remote.showKickOutBtn = false;
buttons.remote.showGeoLocationBtn = false;
buttons.whiteboard.whiteboardLockBtn = false;
//...
} else {
@@ -1471,6 +1498,7 @@ async function whoAreYou() {
if (!myToken) return userNameAlreadyInRoom(); // #209 Hack...
}
loadGeo();
checkPeerAudioVideo();
whoAreYouJoin();
playSound('addPeer');
@@ -1567,6 +1595,7 @@ async function whoAreYou() {
usernameEmoji.classList.add('hidden');
}
window.localStorage.peer_name = myPeerName;
loadGeo();
whoAreYouJoin();
}
},
@@ -3354,6 +3383,7 @@ async function loadRemoteMediaStream(stream, peers, peer_id, kind) {
const remoteVideoAudioUrlBtn = document.createElement('button');
const remoteFileShareBtn = document.createElement('button');
const remotePrivateMsgBtn = document.createElement('button');
const remoteGeoLocationBtn = document.createElement('button');
const remotePeerKickOut = document.createElement('button');
const remoteVideoToImgBtn = document.createElement('button');
const remoteVideoFullScreenBtn = document.createElement('button');
@@ -3406,6 +3436,10 @@ async function loadRemoteMediaStream(stream, peers, peer_id, kind) {
remotePrivateMsgBtn.setAttribute('id', peer_id + '_privateMsg');
remotePrivateMsgBtn.className = className.msgPrivate;
// remote geo location
remoteGeoLocationBtn.setAttribute('id', peer_id + '_geoLocation');
remoteGeoLocationBtn.className = className.geoLocation;
// remote share file
remoteFileShareBtn.setAttribute('id', peer_id + '_shareFile');
remoteFileShareBtn.className = className.shareFile;
@@ -3457,6 +3491,7 @@ async function loadRemoteMediaStream(stream, peers, peer_id, kind) {
setTippy(remoteAudioVolume, '🔊 Volume', 'top');
setTippy(remoteVideoAudioUrlBtn, 'Send Video or Audio', 'bottom');
setTippy(remotePrivateMsgBtn, 'Send private message', 'bottom');
setTippy(remoteGeoLocationBtn, 'Get Geo Location', 'bottom');
setTippy(remoteFileShareBtn, 'Send file', 'bottom');
setTippy(remoteVideoToImgBtn, 'Take a snapshot', 'bottom');
setTippy(remotePeerKickOut, 'Kick out', 'bottom');
@@ -3506,6 +3541,7 @@ async function loadRemoteMediaStream(stream, peers, peer_id, kind) {
remoteExpandContainerDiv.appendChild(remoteVideoZoomOutBtn);
}
buttons.remote.showPrivateMessageBtn && remoteExpandContainerDiv.appendChild(remotePrivateMsgBtn);
buttons.remote.showGeoLocationBtn && remoteExpandContainerDiv.appendChild(remoteGeoLocationBtn);
buttons.remote.showFileShareBtn && remoteExpandContainerDiv.appendChild(remoteFileShareBtn);
buttons.remote.showShareVideoAudioBtn && remoteExpandContainerDiv.appendChild(remoteVideoAudioUrlBtn);
buttons.remote.showKickOutBtn && remoteExpandContainerDiv.appendChild(remotePeerKickOut);
@@ -3608,6 +3644,8 @@ async function loadRemoteMediaStream(stream, peers, peer_id, kind) {
// handle remote peers video on-off
handlePeerVideoBtn(peer_id);
// handle remote geo location
buttons.remote.showGeoLocationBtn && handlePeerGeoLocation(peer_id, peer_name);
// handle remote private messages
buttons.remote.showPrivateMessageBtn && handlePeerPrivateMsg(peer_id, peer_name);
// handle remote send file
@@ -9035,6 +9073,15 @@ function handlePeerVideoBtn(peer_id) {
};
}
function handlePeerGeoLocation(peer_id) {
const remoteGeoLocationBtn = getId(peer_id + '_geoLocation');
remoteGeoLocationBtn.onclick = () => {
isPresenter
? geo.askPeerGeoLocation(peer_id)
: msgPopup('warning', 'Only the presenter can ask geolocation to the participants', 'top-end', 4000);
};
}
/**
* Send Private Message to specific peer
* @param {string} peer_id socket.id
@@ -9202,6 +9249,31 @@ function handlePeerAction(config) {
}
}
/**
* Handle commands from the server
* @param {object} config data
*/
function handleCmd(config) {
console.log('Handle cmd: ', config);
const { action, data } = config;
switch (action) {
case 'geoLocation':
// Peer is requesting your location
geo.confirmPeerGeoLocation(data);
break;
case 'geoLocationOK':
case 'geoLocationKO':
// Peer responded with their location or an error/denial
geo.handleGeoPeerLocation(config);
break;
//....
default:
break;
}
}
/**
* Handle incoming message
* @param {object} message
@@ -11168,7 +11240,7 @@ function showAbout() {
Swal.fire({
background: swBg,
position: 'center',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.5.18',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.5.19',
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
customClass: { image: 'img-about' },
html: `
+360
View File
@@ -0,0 +1,360 @@
'use strict';
/**
* NotificationService
* Handles all user notifications and popups related to geolocation actions.
* @class
*/
class NotificationService {
/**
* @param {Object} options
* @param {Object} options.Swal - SweetAlert2 instance.
* @param {string} options.swBg - Background color for Swal.
* @param {Object} options.images - Images object containing geoLocation image.
* @param {Function} options.playSound - Function to play notification sounds.
*/
constructor({ Swal, swBg, images, playSound }) {
this.Swal = Swal;
this.swalBackground = swBg;
this.image = images.geoLocation;
this.playSound = playSound;
}
/**
* Show a progress popup with a timer.
* @param {string} title
* @param {string} message
* @param {number} timeout - Duration in ms.
* @param {string} [icon='success']
*/
showProgress(title, message, timeout, icon = 'success') {
this.Swal.fire({
allowOutsideClick: false,
background: this.swalBackground,
icon,
title,
html: message,
timer: timeout,
timerProgressBar: true,
didOpen: () => {
this.Swal.showLoading();
},
});
}
/**
* Show a confirmation popup asking the user to share their location.
* @param {string} from_peer_name - Name of the peer requesting location.
* @returns {Promise<Object>} Swal result promise.
*/
showConfirmLocation(from_peer_name) {
this.playSound('notify');
return this.Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: this.swalBackground,
imageUrl: this.image,
position: 'center',
title: 'Geo Location',
html: `Would you like to share your location to ${from_peer_name}?`,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
/**
* Show a popup asking the user if they want to open a peer's geolocation.
* @param {string} peer_name - Name of the peer whose location is shared.
* @returns {Promise<Object>} Swal result promise.
*/
showOpenLocation(peer_name) {
this.playSound('notify');
return this.Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: this.swalBackground,
imageUrl: this.image,
position: 'center',
title: 'Geo Location',
html: `Would you like to open ${peer_name} geolocation?`,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
}
/**
* GeoService
* Static utility for accessing browser geolocation.
* @class
*/
class GeoService {
/**
* Get the current position using the browser's geolocation API.
* @param {Function} success - Success callback.
* @param {Function} error - Error callback.
* @param {PositionOptions} [options] - Geolocation options.
*/
static getCurrentPosition(success, error, options = {}) {
if (typeof navigator !== 'undefined' && 'geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(success, error, {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
...options,
});
} else {
error({ code: 'NOT_SUPPORTED' });
}
}
}
/**
* PeerGeoLocation
* Handles all peer-to-peer geolocation request/response logic.
* @class
*/
class PeerGeoLocation {
/**
* @param {Object} options
* @param {string} options.room_id - Room identifier.
* @param {string} options.peer_name - Local peer name.
* @param {string} options.peer_id - Local peer ID.
* @param {string} options.peer_uuid - Local peer UUID.
* @param {Function} options.sendToServer - Function to send data to the server.
* @param {Function} options.msgPopup - Function to show message popups.
* @param {NotificationService} options.notificationService - Notification service instance.
* @param {GeoService} options.geoService - Geolocation service instance.
* @param {Function} options.openURL - Function to open URLs.
*/
constructor({
room_id,
peer_name,
peer_id,
peer_uuid,
sendToServer,
msgPopup,
notificationService,
geoService,
openURL,
}) {
this.room_id = room_id;
this.peer_name = peer_name;
this.peer_id = peer_id;
this.peer_uuid = peer_uuid;
this.sendToServer = sendToServer;
this.msgPopup = msgPopup;
this.notificationService = notificationService;
this.geoService = geoService;
this.openURL = openURL;
this._geoResponded = {}; // Guard for each request
}
/**
* Generate a unique request key for each geo request.
* @param {string} requester_peer_id
* @returns {string}
*/
_makeRequestKey(requester_peer_id) {
// You can use timestamp or a random string for uniqueness
return `${requester_peer_id}:${Date.now()}:${Math.random().toString(36).substr(2, 6)}`;
}
/**
* Show a progress popup for peer actions.
* @param {string} title
* @param {string} message
* @param {number} timeout
* @param {string} [action='na']
*/
peerActionProgress(title, message, timeout, action = 'na') {
const icon = action === 'eject' ? 'warning' : 'success';
this.notificationService.showProgress(title, message, timeout, icon);
}
/**
* Send a geolocation request to a peer.
* @param {string} to_peer_id - The peer to request location from.
*/
askPeerGeoLocation(to_peer_id) {
this.sendToServer('cmd', {
action: 'geoLocation',
send_to_all: false,
data: {
room_id: this.room_id,
peer_name: this.peer_name,
peer_id: this.peer_id,
peer_uuid: this.peer_uuid,
to_peer_id: to_peer_id,
request_id: this._makeRequestKey(this.peer_id), // Add unique request id
},
});
this.peerActionProgress(
'Geolocation',
'Geolocation requested. Please wait for confirmation...',
6000,
'geolocation'
);
}
/**
* Send a geolocation response (OK or KO) to the requester.
* @param {string} requester_peer_id - The peer who requested location.
* @param {string} action - 'geoLocationOK' or 'geoLocationKO'.
* @param {Object|null} geoLocation - Geolocation data or null.
* @param {string|boolean} [error=false] - Error message or false.
* @param {string} [request_id] - Unique request id.
*/
sendPeerGeoLocation(requester_peer_id, action, geoLocation = null, error = false, request_id = null) {
this.sendToServer('cmd', {
action: action,
send_to_all: false,
data: {
room_id: this.room_id,
peer_name: this.peer_name,
peer_id: this.peer_id,
peer_uuid: this.peer_uuid,
to_peer_id: requester_peer_id,
geoLocation,
request_id,
},
error,
});
}
/**
* Prompt the user to confirm sharing their location.
* @param {Object} data - Data from the geoLocation request.
*/
confirmPeerGeoLocation(data) {
const request_id = data.request_id || this._makeRequestKey(data.peer_id);
if (this._geoResponded[request_id]) return;
this._geoResponded[request_id] = false;
this.notificationService.showConfirmLocation(data.peer_name).then((result) => {
if (result.isConfirmed) {
this.getPeerGeoLocation(data.peer_id, request_id);
} else {
this.denyPeerGeoLocation(data.peer_id, request_id);
}
});
}
/**
* Request the user's geolocation and send the result.
* @param {string} requester_peer_id - The peer who requested location.
* @param {string} request_id - Unique request id.
*/
getPeerGeoLocation(requester_peer_id, request_id) {
this.geoService.getCurrentPosition(
(position) => this.handleGeoLocationSuccess(requester_peer_id, position, request_id),
(error) => this.handleGeoLocationError(requester_peer_id, error, request_id)
);
}
/**
* Handle successful geolocation retrieval.
* @param {string} requester_peer_id
* @param {Object} position
* @param {string} request_id
*/
handleGeoLocationSuccess(requester_peer_id, position, request_id) {
if (this._geoResponded[request_id]) return;
this._geoResponded[request_id] = true;
setTimeout(() => {
delete this._geoResponded[request_id];
}, 60000); // Cleanup after 1 min
const geoLocation = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
};
this.sendPeerGeoLocation(requester_peer_id, 'geoLocationOK', geoLocation, false, request_id);
}
/**
* Handle geolocation errors.
* @param {string} requester_peer_id
* @param {Object} error
* @param {string} request_id
*/
handleGeoLocationError(requester_peer_id, error, request_id) {
console.error('GeoLocation Error:', error);
if (this._geoResponded[request_id]) return;
this._geoResponded[request_id] = true;
setTimeout(() => {
delete this._geoResponded[request_id];
}, 60000); // Cleanup after 1 min
let geoError;
switch (error.code) {
case error.PERMISSION_DENIED:
geoError = 'User denied the request for Geolocation';
break;
case error.POSITION_UNAVAILABLE:
geoError = 'Location information is unavailable';
break;
case error.TIMEOUT:
geoError = 'The request to get user location timed out';
break;
case error.UNKNOWN_ERROR:
geoError = 'An unknown error occurred';
break;
case 'NOT_SUPPORTED':
geoError = 'Geolocation is not supported by this browser';
break;
default:
geoError = 'Geolocation error';
break;
}
this.sendPeerGeoLocation(requester_peer_id, 'geoLocationKO', null, geoError, request_id);
this.msgPopup('warning', geoError, 'top-end', 5000);
}
/**
* Send a denial response for geolocation sharing.
* @param {string} requester_peer_id
* @param {string} request_id
*/
denyPeerGeoLocation(requester_peer_id, request_id) {
if (this._geoResponded[request_id]) return;
this._geoResponded[request_id] = true;
setTimeout(() => {
delete this._geoResponded[request_id];
}, 60000); // Cleanup after 1 min
this.sendPeerGeoLocation(
requester_peer_id,
'geoLocationKO',
null,
`${this.peer_name}: Has declined permission for geolocation`,
request_id
);
}
/**
* Handle receiving a peer's geolocation response.
* @param {Object} config - The command config received from the server.
*/
handleGeoPeerLocation(config) {
if (config.error) {
this.msgPopup('warning', config.error, 'top-end', 5000);
return;
}
if (!config.data || !config.data.geoLocation) {
this.msgPopup('warning', 'Geolocation data is not available', 'top-end', 5000);
return;
}
const geoLocation = config.data.geoLocation;
this.notificationService.showOpenLocation(config.data.peer_name).then((result) => {
if (result.isConfirmed) {
this.openURL(
`https://www.google.com/maps/search/?api=1&query=${geoLocation.latitude},${geoLocation.longitude}`,
true
);
}
});
}
}
Binary file not shown.
+1
View File
@@ -1173,6 +1173,7 @@ access to use this app.
<script defer src="../js/common.js"></script>
<script defer src="../js/brand.js"></script>
<script defer src="../js/buttons.js"></script>
<script defer src="../js/geoLocation.js"></script>
<script defer src="../js/client.js"></script>
<script defer src="../js/detectSpeaking.js"></script>
<script defer src="../js/speechRecognition.js"></script>