From a17857fcd960356668b480e52743abfc78ed0b49 Mon Sep 17 00:00:00 2001 From: renatospirito17 Date: Mon, 27 Apr 2026 13:24:56 +0200 Subject: [PATCH 1/7] Imp: local profile avatar. -Implemented the possibility to add a locally stored avatar quickly and easily during the voice chat, without resorting to editing the URL -The avatar can be changed seamlessly and with no delay or reload -It resets on reload REASONS: I implemented this feature because I felt the editing of the URL is a bit cluckly as a whole This feature makes for a more streamlined experience for the end-user --- app/src/server.js | 6 +- public/css/client.css | 9 +++ public/js/client.js | 120 +++++++++++++++++++++++++++++++++++++++ public/views/client.html | 9 +++ 4 files changed, 142 insertions(+), 2 deletions(-) diff --git a/app/src/server.js b/app/src/server.js index b4e71abc..bfd6a41a 100755 --- a/app/src/server.js +++ b/app/src/server.js @@ -1616,17 +1616,19 @@ io.sockets.on('connect', async (socket) => { let peer_id_to_update = null; for (let peer_id in peers[room_id]) { - if (peers[room_id][peer_id]['peer_name'] == peer_name_old && peer_id == socket.id) { + if (peer_id == socket.id) { peers[room_id][peer_id]['peer_name'] = peer_name_new; + peers[room_id][peer_id]['peer_avatar'] = peer_avatar; // presenter if (presenters && presenters[room_id] && presenters[room_id][peer_id]) { presenters[room_id][peer_id]['peer_name'] = peer_name_new; } peer_id_to_update = peer_id; - log.debug('[' + socket.id + '] Peer name changed', { + log.debug('[' + socket.id + '] Peer profile changed', { peer_name_old: peer_name_old, peer_name_new: peer_name_new, }); + break; } } diff --git a/public/css/client.css b/public/css/client.css index 82ba618b..f692e9cd 100755 --- a/public/css/client.css +++ b/public/css/client.css @@ -3161,6 +3161,8 @@ button { #activeRoomsBtn, #myPeerNameSetBtn, +#myProfileAvatarUploadBtn, +#myProfileAvatarResetBtn, #captionEveryoneBtn, #muteEveryoneBtn, #hideEveryoneBtn, @@ -3177,6 +3179,8 @@ button { } #myPeerNameSetBtn:hover, +#myProfileAvatarUploadBtn:hover, +#myProfileAvatarResetBtn:hover, #roomSendEmailBtn:hover, #lockRoomBtn:hover, #unlockRoomBtn:hover { @@ -3220,6 +3224,11 @@ button { transition: all 0.3s ease-in-out; } +#myProfileAvatarUploadBtn, +#myProfileAvatarResetBtn { + margin-top: 6px; +} + /*-------------------------------------------------------------- # Settings Table --------------------------------------------------------------*/ diff --git a/public/js/client.js b/public/js/client.js index 5770aee6..884a9b23 100644 --- a/public/js/client.js +++ b/public/js/client.js @@ -360,6 +360,9 @@ const tabLanguagesBtn = getId('tabLanguagesBtn'); const mySettingsCloseBtn = getId('mySettingsCloseBtn'); const myPeerNameSet = getId('myPeerNameSet'); const myPeerNameSetBtn = getId('myPeerNameSetBtn'); +const myProfileAvatarFile = getId('myProfileAvatarFile'); +const myProfileAvatarUploadBtn = getId('myProfileAvatarUploadBtn'); +const myProfileAvatarResetBtn = getId('myProfileAvatarResetBtn'); const switchSounds = getId('switchSounds'); const switchShare = getId('switchShare'); const switchKeepButtonsVisible = getId('switchKeepButtonsVisible'); @@ -563,6 +566,8 @@ let thisMaxRoomParticipants = 8; let swBg = 'rgba(0, 0, 0, 0.7)'; // swAlert background color let isDocumentOnFullScreen = false; let isToggleExtraBtnClicked = false; +const maxAvatarFileSizeBytes = 1 * 1024 * 1024; // 1MB in-memory avatar limit +let hasTemporaryAvatar = false; // peer let myPeerId; // This socket.id @@ -847,6 +852,8 @@ function setButtonsToolTip() { // Settings setTippy(mySettingsCloseBtn, 'Close', 'bottom'); setTippy(myPeerNameSetBtn, 'Change name', 'top'); + setTippy(myProfileAvatarUploadBtn, 'Upload temporary avatar', 'top'); + setTippy(myProfileAvatarResetBtn, 'Reset temporary avatar', 'top'); setTippy(myRoomId, 'Room name (click to copy/share)', 'right'); setTippy(mySessionTime, 'Session time', 'right'); setTippy( @@ -2695,6 +2702,17 @@ async function handleAddPeer(config) { screenReaderAccessibility.announceMessage(`${peer_name} joined the room`); } +/** + * Broadcast my current profile (name + avatar) to room peers + */ +function emitMyPeerProfile() { + sendToDataChannel({ + type: 'peerAvatar', + peer_name: myPeerName, + peer_avatar: myPeerAvatar, + }); +} + /** * Handle peers connection state * https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event @@ -2923,6 +2941,13 @@ async function handleRTCDataChannels(peer_id) { case 'micVolume': handlePeerVolume(dataMessage); break; + case 'peerAvatar': + handlePeerName({ + peer_id: peer_id, + peer_name: dataMessage.peer_name, + peer_avatar: dataMessage.peer_avatar, + }); + break; default: break; } @@ -7121,6 +7146,16 @@ function setMySettingsBtn() { myPeerNameSetBtn.addEventListener('click', (e) => { updateMyPeerName(); }); + myProfileAvatarUploadBtn.addEventListener('click', () => { + myProfileAvatarFile?.click(); + }); + myProfileAvatarFile.addEventListener('change', async (e) => { + await updateMyPeerAvatarInMemory(e); + }); + myProfileAvatarResetBtn.addEventListener('click', () => { + resetMyPeerAvatarInMemory(); + }); + updateMyAvatarResetButtonVisibility(); // Sounds switchSounds.addEventListener('change', (e) => { notifyBySound = e.currentTarget.checked; @@ -9651,6 +9686,11 @@ function createChatDataChannel(peer_id) { chatDataChannels[peer_id] = peerConnections[peer_id].createDataChannel('mirotalk_chat_channel'); chatDataChannels[peer_id].onopen = (event) => { console.log('chatDataChannels created', event); + if (hasTemporaryAvatar) { + chatDataChannels[peer_id].send( + JSON.stringify({ type: 'peerAvatar', peer_name: myPeerName, peer_avatar: myPeerAvatar }), + ); + } }; } @@ -11634,6 +11674,8 @@ function isValidHttpURL(input) { */ function isImageURL(input) { if (!input || typeof input !== 'string') return false; + // Allow in-memory avatars loaded from local file input. + if (input.startsWith('data:image/')) return true; try { const url = new URL(input); return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.svg'].some((ext) => @@ -12075,6 +12117,84 @@ async function updateMyPeerName() { userLog('toast', 'My name changed to ' + myPeerName); } +/** + * Update my avatar in-memory only (cleared on page refresh) + * @param {Event} event input file change event + */ +async function updateMyPeerAvatarInMemory(event) { + const file = event?.target?.files?.[0]; + if (!file) return; + + if (!file.type || !file.type.startsWith('image/')) { + myProfileAvatarFile.value = ''; + return userLog('warning', 'Please select a valid image file'); + } + + if (file.size > maxAvatarFileSizeBytes) { + myProfileAvatarFile.value = ''; + return userLog('warning', 'Avatar too large. Max allowed size is 1MB'); + } + + try { + const avatarDataUrl = await readFileAsDataUrl(file); + myPeerAvatar = avatarDataUrl; + hasTemporaryAvatar = true; + + setPeerAvatarImgName('myVideoAvatarImage', myPeerName, myPeerAvatar); + setPeerAvatarImgName('myProfileAvatar', myPeerName, myPeerAvatar); + setPeerChatAvatarImgName('right', myPeerName, myPeerAvatar); + updateMyAvatarResetButtonVisibility(); + + emitMyPeerProfile(); + + userLog('toast', 'Temporary avatar applied (will reset on refresh)'); + } catch (err) { + console.error('Failed to read avatar file', err); + userLog('error', 'Unable to load avatar file'); + } finally { + myProfileAvatarFile.value = ''; + } +} + +/** + * Reset in-memory avatar to default generated/fallback avatar + */ +function resetMyPeerAvatarInMemory() { + myPeerAvatar = false; + hasTemporaryAvatar = false; + setPeerAvatarImgName('myVideoAvatarImage', myPeerName, myPeerAvatar); + setPeerAvatarImgName('myProfileAvatar', myPeerName, myPeerAvatar); + setPeerChatAvatarImgName('right', myPeerName, myPeerAvatar); + updateMyAvatarResetButtonVisibility(); + + emitMyPeerProfile(); + + userLog('toast', 'Temporary avatar reset'); +} + +/** + * Show reset avatar button only for uploaded temporary avatars + */ +function updateMyAvatarResetButtonVisibility() { + if (!myProfileAvatarResetBtn) return; + myProfileAvatarResetBtn.classList.toggle('hidden', !hasTemporaryAvatar); + if (myProfileAvatarUploadBtn) myProfileAvatarUploadBtn.classList.toggle('hidden', hasTemporaryAvatar); +} + +/** + * Convert file to data URL + * @param {File} file + * @returns {Promise} + */ +function readFileAsDataUrl(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(file); + }); +} + /** * Append updated peer name to video player * @param {object} config data diff --git a/public/views/client.html b/public/views/client.html index e7159873..dc5f8cf8 100755 --- a/public/views/client.html +++ b/public/views/client.html @@ -1048,6 +1048,15 @@ access to use this app.
+
+ + + +

From ee3313785866d2362039a2ec887716ba122f38ba Mon Sep 17 00:00:00 2001 From: renatospirito17 Date: Mon, 27 Apr 2026 16:11:17 +0200 Subject: [PATCH 2/7] Updated: the avatar sharing system. Managing avatar exchange via the Signaling Server. --- public/js/client.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/public/js/client.js b/public/js/client.js index 884a9b23..b3c10208 100644 --- a/public/js/client.js +++ b/public/js/client.js @@ -2640,6 +2640,10 @@ async function handleAddPeer(config) { return; } + // Re-broadcast current profile to ensure late joiners receive latest avatar/name. + // This uses the existing peerName signaling path. + emitMyPeerProfile(); + console.log('iceServers', iceServers[0]); // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection @@ -2706,9 +2710,10 @@ async function handleAddPeer(config) { * Broadcast my current profile (name + avatar) to room peers */ function emitMyPeerProfile() { - sendToDataChannel({ - type: 'peerAvatar', - peer_name: myPeerName, + sendToServer('peerName', { + room_id: roomId, + peer_name_old: myPeerName, + peer_name_new: myPeerName, peer_avatar: myPeerAvatar, }); } @@ -2941,13 +2946,6 @@ async function handleRTCDataChannels(peer_id) { case 'micVolume': handlePeerVolume(dataMessage); break; - case 'peerAvatar': - handlePeerName({ - peer_id: peer_id, - peer_name: dataMessage.peer_name, - peer_avatar: dataMessage.peer_avatar, - }); - break; default: break; } @@ -9686,11 +9684,6 @@ function createChatDataChannel(peer_id) { chatDataChannels[peer_id] = peerConnections[peer_id].createDataChannel('mirotalk_chat_channel'); chatDataChannels[peer_id].onopen = (event) => { console.log('chatDataChannels created', event); - if (hasTemporaryAvatar) { - chatDataChannels[peer_id].send( - JSON.stringify({ type: 'peerAvatar', peer_name: myPeerName, peer_avatar: myPeerAvatar }), - ); - } }; } From c20adb3f5946d8e8e23680846cb05aa1cb7bb7d1 Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Mon, 27 Apr 2026 17:57:12 +0200 Subject: [PATCH 3/7] [mirotalk] - feat(avatar): switch to URL-based avatars and fix late-join avatar sync --- public/js/client.js | 81 ++++++++++++++++++---------------------- public/views/client.html | 3 +- 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/public/js/client.js b/public/js/client.js index b3c10208..075b17ee 100644 --- a/public/js/client.js +++ b/public/js/client.js @@ -360,7 +360,6 @@ const tabLanguagesBtn = getId('tabLanguagesBtn'); const mySettingsCloseBtn = getId('mySettingsCloseBtn'); const myPeerNameSet = getId('myPeerNameSet'); const myPeerNameSetBtn = getId('myPeerNameSetBtn'); -const myProfileAvatarFile = getId('myProfileAvatarFile'); const myProfileAvatarUploadBtn = getId('myProfileAvatarUploadBtn'); const myProfileAvatarResetBtn = getId('myProfileAvatarResetBtn'); const switchSounds = getId('switchSounds'); @@ -566,7 +565,6 @@ let thisMaxRoomParticipants = 8; let swBg = 'rgba(0, 0, 0, 0.7)'; // swAlert background color let isDocumentOnFullScreen = false; let isToggleExtraBtnClicked = false; -const maxAvatarFileSizeBytes = 1 * 1024 * 1024; // 1MB in-memory avatar limit let hasTemporaryAvatar = false; // peer @@ -852,7 +850,7 @@ function setButtonsToolTip() { // Settings setTippy(mySettingsCloseBtn, 'Close', 'bottom'); setTippy(myPeerNameSetBtn, 'Change name', 'top'); - setTippy(myProfileAvatarUploadBtn, 'Upload temporary avatar', 'top'); + setTippy(myProfileAvatarUploadBtn, 'Set temporary avatar URL', 'top'); setTippy(myProfileAvatarResetBtn, 'Reset temporary avatar', 'top'); setTippy(myRoomId, 'Room name (click to copy/share)', 'right'); setTippy(mySessionTime, 'Session time', 'right'); @@ -1244,10 +1242,11 @@ function generateRandomName() { function getPeerAvatar() { const avatar = getQueryParam('avatar'); const avatarDisabled = avatar === '0' || avatar === 'false'; + const isBase64Avatar = typeof avatar === 'string' && avatar.startsWith('data:image/'); console.log('Direct join', { avatar: avatar }); - if (avatarDisabled || !isImageURL(avatar)) { + if (avatarDisabled || isBase64Avatar || !isImageURL(avatar)) { return false; } return avatar; @@ -5282,6 +5281,7 @@ function genAvatarSvg(peerName, avatarImgSize) { */ function setPeerAvatarImgName(videoAvatarImageId, peerName, peerAvatar) { const videoAvatarImageElement = getId(videoAvatarImageId); + if (!videoAvatarImageElement) return; videoAvatarImageElement.style.pointerEvents = 'none'; // If a valid avatar image URL is provided @@ -7144,11 +7144,8 @@ function setMySettingsBtn() { myPeerNameSetBtn.addEventListener('click', (e) => { updateMyPeerName(); }); - myProfileAvatarUploadBtn.addEventListener('click', () => { - myProfileAvatarFile?.click(); - }); - myProfileAvatarFile.addEventListener('change', async (e) => { - await updateMyPeerAvatarInMemory(e); + myProfileAvatarUploadBtn.addEventListener('click', async () => { + await updateMyPeerAvatarByUrl(); }); myProfileAvatarResetBtn.addEventListener('click', () => { resetMyPeerAvatarInMemory(); @@ -11667,7 +11664,7 @@ function isValidHttpURL(input) { */ function isImageURL(input) { if (!input || typeof input !== 'string') return false; - // Allow in-memory avatars loaded from local file input. + // Data URLs can still be valid images for generic content handling. if (input.startsWith('data:image/')) return true; try { const url = new URL(input); @@ -12111,26 +12108,31 @@ async function updateMyPeerName() { } /** - * Update my avatar in-memory only (cleared on page refresh) - * @param {Event} event input file change event + * Update my avatar from URL in-memory only (cleared on page refresh) */ -async function updateMyPeerAvatarInMemory(event) { - const file = event?.target?.files?.[0]; - if (!file) return; +async function updateMyPeerAvatarByUrl() { + const result = await Swal.fire({ + background: swBg, + title: 'Set avatar URL', + input: 'url', + inputLabel: 'Public image URL', + inputPlaceholder: 'https://example.com/avatar.jpg', + confirmButtonText: 'Apply', + showCancelButton: true, + showClass: { popup: 'animate__animated animate__fadeInDown' }, + hideClass: { popup: 'animate__animated animate__fadeOutUp' }, + inputValidator: (value) => { + if (!value) return 'Please enter an image URL'; + if (value.startsWith('data:image/')) return 'Base64 avatars are not supported'; + if (!isImageURL(value)) return 'Please provide a valid image URL'; + return null; + }, + }); - if (!file.type || !file.type.startsWith('image/')) { - myProfileAvatarFile.value = ''; - return userLog('warning', 'Please select a valid image file'); - } - - if (file.size > maxAvatarFileSizeBytes) { - myProfileAvatarFile.value = ''; - return userLog('warning', 'Avatar too large. Max allowed size is 1MB'); - } + if (!result.isConfirmed || !result.value) return; try { - const avatarDataUrl = await readFileAsDataUrl(file); - myPeerAvatar = avatarDataUrl; + myPeerAvatar = result.value; hasTemporaryAvatar = true; setPeerAvatarImgName('myVideoAvatarImage', myPeerName, myPeerAvatar); @@ -12142,10 +12144,8 @@ async function updateMyPeerAvatarInMemory(event) { userLog('toast', 'Temporary avatar applied (will reset on refresh)'); } catch (err) { - console.error('Failed to read avatar file', err); - userLog('error', 'Unable to load avatar file'); - } finally { - myProfileAvatarFile.value = ''; + console.error('Failed to set avatar URL', err); + userLog('error', 'Unable to apply avatar URL'); } } @@ -12174,26 +12174,19 @@ function updateMyAvatarResetButtonVisibility() { if (myProfileAvatarUploadBtn) myProfileAvatarUploadBtn.classList.toggle('hidden', hasTemporaryAvatar); } -/** - * Convert file to data URL - * @param {File} file - * @returns {Promise} - */ -function readFileAsDataUrl(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = () => reject(reader.error); - reader.readAsDataURL(file); - }); -} - /** * Append updated peer name to video player * @param {object} config data */ function handlePeerName(config) { const { peer_id, peer_name, peer_avatar } = config; + + // Keep the latest profile in memory so late DOM creation still uses updated data. + if (allPeers && allPeers[peer_id]) { + allPeers[peer_id]['peer_name'] = peer_name; + allPeers[peer_id]['peer_avatar'] = peer_avatar; + } + const videoName = getId(peer_id + '_name'); const screenName = getId(peer_id + '_screen_name'); if (videoName) videoName.innerText = peer_name; diff --git a/public/views/client.html b/public/views/client.html index dc5f8cf8..11f13f29 100755 --- a/public/views/client.html +++ b/public/views/client.html @@ -1049,9 +1049,8 @@ access to use this app.
-