[mirotalksfu] - #194 Refactoring
هذا الالتزام موجود في:
@@ -58,7 +58,7 @@ dev dependencies: {
|
|||||||
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
* @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
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.7.40
|
* @version 1.7.42
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mirotalksfu",
|
"name": "mirotalksfu",
|
||||||
"version": "1.7.40",
|
"version": "1.7.42",
|
||||||
"description": "WebRTC SFU browser-based video calls",
|
"description": "WebRTC SFU browser-based video calls",
|
||||||
"main": "Server.js",
|
"main": "Server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ let BRAND = {
|
|||||||
},
|
},
|
||||||
about: {
|
about: {
|
||||||
imageUrl: '../images/mirotalk-logo.gif',
|
imageUrl: '../images/mirotalk-logo.gif',
|
||||||
title: '<strong>WebRTC SFU v1.7.40</strong>',
|
title: '<strong>WebRTC SFU v1.7.42</strong>',
|
||||||
html: `
|
html: `
|
||||||
<button
|
<button
|
||||||
id="support-button"
|
id="support-button"
|
||||||
|
|||||||
@@ -189,13 +189,18 @@ const initMicrophoneSelect = getId('initMicrophoneSelect');
|
|||||||
const speakerSelect = getId('speakerSelect');
|
const speakerSelect = getId('speakerSelect');
|
||||||
const initSpeakerSelect = getId('initSpeakerSelect');
|
const initSpeakerSelect = getId('initSpeakerSelect');
|
||||||
|
|
||||||
|
// ####################################################
|
||||||
|
// VIRTUAL BACKGROUND DEFAULT IMAGES
|
||||||
|
// ####################################################
|
||||||
|
|
||||||
|
const virtualBackgrounds = Object.values(image.virtualBackground);
|
||||||
|
|
||||||
// ####################################################
|
// ####################################################
|
||||||
// DYNAMIC SETTINGS
|
// DYNAMIC SETTINGS
|
||||||
// ####################################################
|
// ####################################################
|
||||||
|
|
||||||
let virtualBackgroundBlurLevel;
|
let virtualBackgroundBlurLevel;
|
||||||
let virtualBackgroundSelectedImage;
|
let virtualBackgroundSelectedImage;
|
||||||
let imageCounter = 0; // Virtual Background upload custom images
|
|
||||||
|
|
||||||
let swalBackground = 'radial-gradient(#393939, #000000)'; //'rgba(0, 0, 0, 0.7)';
|
let swalBackground = 'radial-gradient(#393939, #000000)'; //'rgba(0, 0, 0, 0.7)';
|
||||||
|
|
||||||
@@ -4943,270 +4948,160 @@ function adaptAspectRatio(participantsCount) {
|
|||||||
|
|
||||||
function showImageSelector() {
|
function showImageSelector() {
|
||||||
elemDisplay('imageGrid', true, 'grid');
|
elemDisplay('imageGrid', true, 'grid');
|
||||||
|
if (imageGrid.innerHTML !== '') return;
|
||||||
|
|
||||||
if (imageGrid.innerHTML != '') return;
|
imageGrid.innerHTML = ''; // Clear previous images
|
||||||
|
|
||||||
// Clear previous images
|
function createImage(id, src, tooltip, index, clickHandler) {
|
||||||
imageGrid.innerHTML = '';
|
const img = document.createElement('img');
|
||||||
|
img.id = id;
|
||||||
|
img.src = src;
|
||||||
|
img.dataset.index = index;
|
||||||
|
img.addEventListener('click', clickHandler);
|
||||||
|
imageGrid.appendChild(img);
|
||||||
|
if (tooltip) {
|
||||||
|
setTippy(img.id, tooltip, 'top');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get virtual background images from the `image` object
|
// Common function to handle virtual background changes
|
||||||
const virtualBackgrounds = Object.values(image.virtualBackground);
|
async function handleVirtualBackground(blurLevel = null, imgSrc = null) {
|
||||||
|
if (!blurLevel && !imgSrc) {
|
||||||
|
virtualBackgroundBlurLevel = null;
|
||||||
|
virtualBackgroundSelectedImage = null;
|
||||||
|
elemDisplay('imageGrid', false);
|
||||||
|
}
|
||||||
|
await applyVirtualBackground(initVideo, initStream, blurLevel, imgSrc);
|
||||||
|
}
|
||||||
|
|
||||||
// Create clean virtual bg Image
|
// Create clean virtual bg Image
|
||||||
const cleanVbImg = document.createElement('img');
|
createImage('initCleanVbImg', image.user, 'Remove virtual background', 'cleanVb', () =>
|
||||||
cleanVbImg.id = 'initCleanVbImg';
|
handleVirtualBackground(null, null),
|
||||||
cleanVbImg.src = image.user;
|
);
|
||||||
cleanVbImg.alt = 'Clean virtual background';
|
|
||||||
cleanVbImg.dataset.index = 'cleanVb';
|
|
||||||
cleanVbImg.addEventListener('click', async function () {
|
|
||||||
elemDisplay('imageGrid', false);
|
|
||||||
|
|
||||||
if (!virtualBackgroundBlurLevel && !virtualBackgroundSelectedImage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
virtualBackgroundBlurLevel = null;
|
|
||||||
virtualBackgroundSelectedImage = null;
|
|
||||||
initVideoSelect.onchange();
|
|
||||||
saveVirtualBackgroundSettings(virtualBackgroundBlurLevel, virtualBackgroundSelectedImage);
|
|
||||||
});
|
|
||||||
imageGrid.appendChild(cleanVbImg);
|
|
||||||
setTippy(cleanVbImg.id, 'Remove virtual background', 'top');
|
|
||||||
|
|
||||||
// Create High Blur Image
|
// Create High Blur Image
|
||||||
const highBlurImg = document.createElement('img');
|
createImage('initHighBlurImg', image.blurHigh, 'High Blur', 'high', () => handleVirtualBackground(20));
|
||||||
highBlurImg.id = 'initHighBlurImg';
|
|
||||||
highBlurImg.src = image.blurHigh;
|
|
||||||
highBlurImg.alt = 'High Blur';
|
|
||||||
highBlurImg.dataset.index = 'high';
|
|
||||||
highBlurImg.addEventListener('click', async function () {
|
|
||||||
await applyVirtualBackground(initVideo, initStream, 20);
|
|
||||||
});
|
|
||||||
imageGrid.appendChild(highBlurImg);
|
|
||||||
setTippy(highBlurImg.id, 'High Blur', 'top');
|
|
||||||
|
|
||||||
// Create Low Blur Image
|
// Create Low Blur Image
|
||||||
const lowBlurImg = document.createElement('img');
|
createImage('initLowBlurImg', image.blurLow, 'Low Blur', 'low', () => handleVirtualBackground(10));
|
||||||
lowBlurImg.id = 'initLowBlurImg';
|
|
||||||
lowBlurImg.src = image.blurLow;
|
|
||||||
lowBlurImg.alt = 'Low Blur';
|
|
||||||
lowBlurImg.dataset.index = 'low';
|
|
||||||
lowBlurImg.addEventListener('click', async function () {
|
|
||||||
await applyVirtualBackground(initVideo, initStream, 10);
|
|
||||||
});
|
|
||||||
imageGrid.appendChild(lowBlurImg);
|
|
||||||
setTippy(lowBlurImg.id, 'Low Blur', 'top');
|
|
||||||
|
|
||||||
// Create a button for uploading custom images
|
// Handle file upload (common logic for file selection)
|
||||||
const uploadImg = document.createElement('img');
|
function setupFileUploadButton(buttonId, sourceImg, tooltip, handler) {
|
||||||
uploadImg.id = 'initUploadImg';
|
const imgButton = document.createElement('img');
|
||||||
uploadImg.src = image.upload;
|
imgButton.id = buttonId;
|
||||||
|
imgButton.src = sourceImg;
|
||||||
|
imgButton.addEventListener('click', handler);
|
||||||
|
imageGrid.appendChild(imgButton);
|
||||||
|
setTippy(imgButton.id, tooltip, 'top');
|
||||||
|
}
|
||||||
|
|
||||||
// Create an input element for custom image upload, hidden initially
|
function handleFileUpload(event) {
|
||||||
const fileInput = document.createElement('input');
|
const file = event.target.files[0];
|
||||||
fileInput.type = 'file';
|
|
||||||
fileInput.accept = 'image/*'; // Only image files
|
|
||||||
fileInput.style.display = 'none'; // Hide the file input element
|
|
||||||
|
|
||||||
// Trigger file input when button is clicked
|
|
||||||
uploadImg.addEventListener('click', function () {
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle image selection
|
|
||||||
fileInput.addEventListener('change', function () {
|
|
||||||
const file = this.files[0];
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function (e) {
|
reader.onload = async (e) => {
|
||||||
const imgData = e.target.result;
|
const imgData = e.target.result;
|
||||||
saveImageToIndexedDB(imgData);
|
await indexedDBHelper.saveImage(imgData);
|
||||||
addImageToUI(imgData);
|
addImageToUI(imgData);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Load images from IndexedDB
|
|
||||||
async function loadStoredImages() {
|
|
||||||
const storedImages = await getImagesFromIndexedDB();
|
|
||||||
storedImages.forEach((imgData) => addImageToUI(imgData));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save image to IndexedDB
|
function createUploadImageButton() {
|
||||||
async function saveImageToIndexedDB(imgData) {
|
const fileInput = document.createElement('input');
|
||||||
const db = await openDB();
|
fileInput.type = 'file';
|
||||||
const transaction = db.transaction('images', 'readwrite');
|
fileInput.accept = 'image/*';
|
||||||
const store = transaction.objectStore('images');
|
fileInput.style.display = 'none';
|
||||||
store.add({ imgData });
|
fileInput.addEventListener('change', handleFileUpload);
|
||||||
|
|
||||||
|
setupFileUploadButton('initUploadImg', image.upload, 'Upload your custom image', () => fileInput.click());
|
||||||
|
|
||||||
|
return fileInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open IndexedDB
|
// Function to add an image to UI
|
||||||
function openDB() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open('customImageDB', 1);
|
|
||||||
request.onupgradeneeded = function (event) {
|
|
||||||
const db = event.target.result;
|
|
||||||
if (!db.objectStoreNames.contains('images')) {
|
|
||||||
db.createObjectStore('images', { keyPath: 'id', autoIncrement: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get images from IndexedDB
|
|
||||||
function getImagesFromIndexedDB() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const dbRequest = openDB();
|
|
||||||
dbRequest.then((db) => {
|
|
||||||
const transaction = db.transaction('images', 'readonly');
|
|
||||||
const store = transaction.objectStore('images');
|
|
||||||
const request = store.getAll();
|
|
||||||
|
|
||||||
request.onsuccess = () => resolve(request.result.map((item) => item.imgData));
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add image to UI
|
|
||||||
function addImageToUI(imgData) {
|
function addImageToUI(imgData) {
|
||||||
const imageContainer = document.createElement('div');
|
const imageContainer = document.createElement('div');
|
||||||
imageContainer.className = 'image-wrapper';
|
imageContainer.className = 'image-wrapper';
|
||||||
const customImg = document.createElement('img');
|
|
||||||
|
|
||||||
customImg.id = `customImageGrid${imageCounter}`;
|
const customImg = document.createElement('img');
|
||||||
customImg.src = imgData;
|
customImg.src = imgData;
|
||||||
customImg.alt = 'Custom Background';
|
customImg.addEventListener('click', () => handleVirtualBackground(null, imgData));
|
||||||
customImg.addEventListener('click', async function () {
|
|
||||||
await applyVirtualBackground(initVideo, initStream, null, imgData);
|
const deleteBtn = document.createElement('span');
|
||||||
|
deleteBtn.className = 'delete-icon fas fa-times';
|
||||||
|
deleteBtn.addEventListener('click', async (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
await indexedDBHelper.removeImage(imgData);
|
||||||
|
imageContainer.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create delete button
|
|
||||||
const deleteBtn = createDeleteIcon(imgData, imageContainer);
|
|
||||||
imageContainer.appendChild(customImg);
|
imageContainer.appendChild(customImg);
|
||||||
imageContainer.appendChild(deleteBtn);
|
imageContainer.appendChild(deleteBtn);
|
||||||
imageGrid.appendChild(imageContainer);
|
imageGrid.appendChild(imageContainer);
|
||||||
setTippy(customImg.id, 'Custom background', 'top');
|
|
||||||
|
|
||||||
imageCounter++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create delete icon
|
// Function to fetch and store an image from URL
|
||||||
function createDeleteIcon(imgData, container) {
|
|
||||||
const deleteIcon = document.createElement('span');
|
|
||||||
deleteIcon.className = 'delete-icon fas fa-times';
|
|
||||||
deleteIcon.addEventListener('click', function (event) {
|
|
||||||
event.stopPropagation(); // Prevent triggering background change
|
|
||||||
removeImageFromIndexedDB(imgData);
|
|
||||||
container.remove();
|
|
||||||
});
|
|
||||||
return deleteIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove image from IndexedDB
|
|
||||||
async function removeImageFromIndexedDB(imgData) {
|
|
||||||
const db = await openDB();
|
|
||||||
const transaction = db.transaction('images', 'readwrite');
|
|
||||||
const store = transaction.objectStore('images');
|
|
||||||
|
|
||||||
const request = store.getAll();
|
|
||||||
request.onsuccess = () => {
|
|
||||||
const items = request.result;
|
|
||||||
const itemToDelete = items.find((item) => item.imgData === imgData);
|
|
||||||
if (itemToDelete) store.delete(itemToDelete.id);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the button to the imageGrid
|
|
||||||
imageGrid.appendChild(uploadImg);
|
|
||||||
setTippy(uploadImg.id, 'Upload your custom image', 'top');
|
|
||||||
|
|
||||||
// Function to fetch image from URL and store it in IndexedDB
|
|
||||||
async function fetchAndStoreImage(url) {
|
async function fetchAndStoreImage(url) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
reader.onload = function (e) {
|
|
||||||
const imgData = e.target.result;
|
const imgData = e.target.result;
|
||||||
saveImageToIndexedDB(imgData);
|
await indexedDBHelper.saveImage(imgData);
|
||||||
addImageToUI(imgData);
|
addImageToUI(imgData);
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching image:', error);
|
console.error('Error fetching image:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveImageUrlBtn.addEventListener('click', async () => {
|
// Paste image from URL
|
||||||
elemDisplay(imageUrlModal.id, false);
|
|
||||||
if (isValidURL(imageUrlInput.value)) {
|
|
||||||
await fetchAndStoreImage(imageUrlInput.value);
|
|
||||||
imageUrlInput.value = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cancelImageUrlBtn.addEventListener('click', () => {
|
|
||||||
elemDisplay(imageUrlModal.id, false);
|
|
||||||
imageUrlInput.value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
function askForImageURL() {
|
function askForImageURL() {
|
||||||
elemDisplay(imageUrlModal.id, true);
|
elemDisplay(initImageUrlModal.id, true);
|
||||||
|
|
||||||
// Take URL from clipboard ex:
|
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
.readText()
|
.readText()
|
||||||
.then((clipboardText) => {
|
.then((clipboardText) => {
|
||||||
if (!clipboardText) return false;
|
if (isValidImageURL(filterXSS(clipboardText))) {
|
||||||
const sanitizedText = filterXSS(clipboardText);
|
initImageUrlInput.value = clipboardText;
|
||||||
if (isValidURL(sanitizedText) && imageUrlInput) {
|
|
||||||
imageUrlInput.value = sanitizedText;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {});
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to validate URL format
|
initSaveImageUrlBtn.addEventListener('click', async () => {
|
||||||
function isValidURL(url) {
|
elemDisplay(initImageUrlModal.id, false);
|
||||||
return (
|
if (isValidImageURL(initImageUrlInput.value)) {
|
||||||
url.match(/\.(jpeg|jpg|png|gif|webp|bmp|svg|apng|avif|heif|heic|tiff?|ico|cur|jfif|pjpeg|pjp|raw)$/i) !==
|
await fetchAndStoreImage(initImageUrlInput.value);
|
||||||
null
|
initImageUrlInput.value = '';
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Create link to upload image
|
|
||||||
const linkImg = document.createElement('img');
|
|
||||||
linkImg.id = 'linkImage';
|
|
||||||
linkImg.src = image.link;
|
|
||||||
linkImg.alt = 'Link image';
|
|
||||||
linkImg.addEventListener('click', askForImageURL);
|
|
||||||
imageGrid.appendChild(linkImg);
|
|
||||||
setTippy(linkImg.id, 'Upload Image from Url', 'top');
|
|
||||||
|
|
||||||
// Loop through virtual background images dynamically
|
|
||||||
virtualBackgrounds.forEach((imageUrl, index) => {
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = imageUrl;
|
|
||||||
img.dataset.index = index + 1;
|
|
||||||
img.addEventListener('click', async function () {
|
|
||||||
console.log('Selected Image Index:', this.dataset.index);
|
|
||||||
await applyVirtualBackground(initVideo, initStream, null, imageUrl);
|
|
||||||
});
|
|
||||||
imageGrid.appendChild(img);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load images from IndexedDB on page load
|
initCancelImageUrlBtn.addEventListener('click', () => {
|
||||||
loadStoredImages();
|
elemDisplay(initImageUrlModal.id, false);
|
||||||
|
initImageUrlInput.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload from file button
|
||||||
|
createUploadImageButton();
|
||||||
|
|
||||||
|
// Upload from URL button
|
||||||
|
setupFileUploadButton('initLinkImage', image.link, 'Upload Image from URL', askForImageURL);
|
||||||
|
|
||||||
|
// Load default virtual backgrounds
|
||||||
|
virtualBackgrounds.forEach((imageUrl, index) => {
|
||||||
|
createImage(`initVirtualBg${index}`, imageUrl, null, index + 1, () => handleVirtualBackground(null, imageUrl));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load stored images and add to image grid UI
|
||||||
|
indexedDBHelper.getAllImages().then((images) => images.forEach(addImageToUI));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ####################################################
|
||||||
|
// VIRTUAL BACKGROUND HELPER
|
||||||
|
// ####################################################
|
||||||
|
|
||||||
async function applyVirtualBackground(videoElement, stream, blurLevel, backgroundImage) {
|
async function applyVirtualBackground(videoElement, stream, blurLevel, backgroundImage) {
|
||||||
const videoTrack = stream.getVideoTracks()[0];
|
const videoTrack = stream.getVideoTracks()[0];
|
||||||
const processor = new WebRTCStreamProcessor();
|
const processor = new WebRTCStreamProcessor();
|
||||||
@@ -5228,6 +5123,58 @@ async function applyVirtualBackground(videoElement, stream, blurLevel, backgroun
|
|||||||
saveVirtualBackgroundSettings(blurLevel, backgroundImage);
|
saveVirtualBackgroundSettings(blurLevel, backgroundImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidImageURL(url) {
|
||||||
|
return (
|
||||||
|
url.match(/\.(jpeg|jpg|png|gif|webp|bmp|svg|apng|avif|heif|heic|tiff?|ico|cur|jfif|pjpeg|pjp|raw)$/i) !== null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ####################################################
|
||||||
|
// VIRTUAL BACKGROUND INDEXDB HELPER
|
||||||
|
// ####################################################
|
||||||
|
|
||||||
|
const indexedDBHelper = {
|
||||||
|
async openDB() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open('customImageDB', 1);
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
if (!db.objectStoreNames.contains('images')) {
|
||||||
|
db.createObjectStore('images', { keyPath: 'id', autoIncrement: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async saveImage(imgData) {
|
||||||
|
const db = await this.openDB();
|
||||||
|
const transaction = db.transaction('images', 'readwrite');
|
||||||
|
transaction.objectStore('images').add({ imgData });
|
||||||
|
},
|
||||||
|
async getAllImages() {
|
||||||
|
const db = await this.openDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction('images', 'readonly');
|
||||||
|
const store = transaction.objectStore('images');
|
||||||
|
const request = store.getAll();
|
||||||
|
request.onsuccess = () => resolve(request.result.map((item) => item.imgData));
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async removeImage(imgData) {
|
||||||
|
const db = await this.openDB();
|
||||||
|
const transaction = db.transaction('images', 'readwrite');
|
||||||
|
const store = transaction.objectStore('images');
|
||||||
|
|
||||||
|
const request = store.getAll();
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const item = request.result.find((item) => item.imgData === imgData);
|
||||||
|
if (item) store.delete(item.id);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// ####################################################
|
// ####################################################
|
||||||
// VIRTUAL BACKGROUND LOCAL STORAGE SETTINGS
|
// VIRTUAL BACKGROUND LOCAL STORAGE SETTINGS
|
||||||
// ####################################################
|
// ####################################################
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
* @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
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.7.40
|
* @version 1.7.42
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -1675,206 +1675,131 @@ class RoomClient {
|
|||||||
|
|
||||||
showVideoImageSelector() {
|
showVideoImageSelector() {
|
||||||
elemDisplay('imageGridVideo', true, 'grid');
|
elemDisplay('imageGridVideo', true, 'grid');
|
||||||
|
|
||||||
if (imageGridVideo.innerHTML != '') return;
|
if (imageGridVideo.innerHTML != '') return;
|
||||||
|
|
||||||
// Clear previous images
|
imageGrid.innerHTML = ''; // Clear previous init images
|
||||||
imageGridVideo.innerHTML = '';
|
imageGridVideo.innerHTML = ''; // Clear previous images
|
||||||
|
|
||||||
// Get virtual background images from the `image` object
|
function createImage(id, src, tooltip, index, clickHandler) {
|
||||||
const virtualBackgrounds = Object.values(image.virtualBackground);
|
const img = document.createElement('img');
|
||||||
|
img.id = id;
|
||||||
// Create clean virtual bg Image
|
img.src = src;
|
||||||
const cleanVbImg = document.createElement('img');
|
img.dataset.index = index;
|
||||||
cleanVbImg.id = 'cleanVbImg';
|
img.addEventListener('click', clickHandler);
|
||||||
cleanVbImg.src = image.user;
|
imageGridVideo.appendChild(img);
|
||||||
cleanVbImg.alt = 'Clean virtual background';
|
if (tooltip) {
|
||||||
cleanVbImg.dataset.index = 'cleanVb';
|
setTippy(img.id, tooltip, 'top');
|
||||||
cleanVbImg.addEventListener('click', async function () {
|
|
||||||
if (!virtualBackgroundBlurLevel && !virtualBackgroundSelectedImage) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
virtualBackgroundBlurLevel = null;
|
|
||||||
virtualBackgroundSelectedImage = null;
|
|
||||||
videoSelect.onchange();
|
|
||||||
saveVirtualBackgroundSettings(virtualBackgroundBlurLevel, virtualBackgroundSelectedImage);
|
|
||||||
});
|
|
||||||
imageGridVideo.appendChild(cleanVbImg);
|
|
||||||
setTippy(cleanVbImg.id, 'Remove virtual background', 'top');
|
|
||||||
|
|
||||||
// Create High Blur Image
|
|
||||||
const highBlurImg = document.createElement('img');
|
|
||||||
highBlurImg.id = 'highBlurImg';
|
|
||||||
highBlurImg.src = image.blurHigh;
|
|
||||||
highBlurImg.alt = 'High Blur';
|
|
||||||
highBlurImg.dataset.index = 'high';
|
|
||||||
highBlurImg.addEventListener('click', async function () {
|
|
||||||
await rc.applyVirtualBackground(20);
|
|
||||||
});
|
|
||||||
imageGridVideo.appendChild(highBlurImg);
|
|
||||||
setTippy(highBlurImg.id, 'High Blur', 'top');
|
|
||||||
|
|
||||||
// Create Low Blur Image
|
|
||||||
const lowBlurImg = document.createElement('img');
|
|
||||||
lowBlurImg.id = 'lowBlurImg';
|
|
||||||
lowBlurImg.src = image.blurLow;
|
|
||||||
lowBlurImg.alt = 'Low Blur';
|
|
||||||
lowBlurImg.dataset.index = 'low';
|
|
||||||
lowBlurImg.addEventListener('click', async function () {
|
|
||||||
await rc.applyVirtualBackground(10);
|
|
||||||
});
|
|
||||||
imageGridVideo.appendChild(lowBlurImg);
|
|
||||||
setTippy(lowBlurImg.id, 'Low Blur', 'top');
|
|
||||||
|
|
||||||
// Create a button for uploading custom images
|
|
||||||
const uploadImg = document.createElement('img');
|
|
||||||
uploadImg.id = 'uploadImg';
|
|
||||||
uploadImg.src = image.upload;
|
|
||||||
|
|
||||||
// Create an input element for custom image upload, hidden initially
|
|
||||||
const fileInput = document.createElement('input');
|
|
||||||
fileInput.type = 'file';
|
|
||||||
fileInput.accept = 'image/*'; // Only image files
|
|
||||||
fileInput.style.display = 'none'; // Hide the file input element
|
|
||||||
|
|
||||||
// Trigger file input when button is clicked
|
|
||||||
uploadImg.addEventListener('click', function () {
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load images from IndexedDB
|
|
||||||
async function loadStoredImages() {
|
|
||||||
const storedImages = await getImagesFromIndexedDB();
|
|
||||||
storedImages.forEach((imgData) => addImageToUI(imgData));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle image selection
|
// Common function to handle virtual background changes
|
||||||
fileInput.addEventListener('change', function () {
|
async function handleVirtualBackground(blurLevel = null, imgSrc = null) {
|
||||||
const file = this.files[0];
|
if (!blurLevel && !imgSrc) {
|
||||||
|
virtualBackgroundBlurLevel = null;
|
||||||
|
virtualBackgroundSelectedImage = null;
|
||||||
|
}
|
||||||
|
await rc.applyVirtualBackground(blurLevel, imgSrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create clean virtual bg Image
|
||||||
|
createImage('cleanVbImg', image.user, 'Remove virtual background', 'cleanVb', () =>
|
||||||
|
handleVirtualBackground(null, null),
|
||||||
|
);
|
||||||
|
// Create High Blur Image
|
||||||
|
createImage('highBlurImg', image.blurHigh, 'High Blur', 'high', () => handleVirtualBackground(20));
|
||||||
|
// Create Low Blur Image
|
||||||
|
createImage('lowBlurImg', image.blurLow, 'Low Blur', 'low', () => handleVirtualBackground(10));
|
||||||
|
|
||||||
|
// Handle file upload (common logic for file selection)
|
||||||
|
function setupFileUploadButton(buttonId, sourceImg, tooltip, handler) {
|
||||||
|
const imgButton = document.createElement('img');
|
||||||
|
imgButton.id = buttonId;
|
||||||
|
imgButton.src = sourceImg;
|
||||||
|
imgButton.addEventListener('click', handler);
|
||||||
|
imageGridVideo.appendChild(imgButton);
|
||||||
|
setTippy(imgButton.id, tooltip, 'top');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileUpload(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function (e) {
|
reader.onload = async (e) => {
|
||||||
const imgData = e.target.result;
|
const imgData = e.target.result;
|
||||||
saveImageToIndexedDB(imgData);
|
await indexedDBHelper.saveImage(imgData);
|
||||||
addImageToUI(imgData);
|
addImageToUI(imgData);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Add image with delete button to UI
|
function createUploadImageButton() {
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = 'image/*';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
fileInput.addEventListener('change', handleFileUpload);
|
||||||
|
|
||||||
|
setupFileUploadButton('uploadImg', image.upload, 'Upload your custom image', () => fileInput.click());
|
||||||
|
|
||||||
|
return fileInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to add an image to UI
|
||||||
function addImageToUI(imgData) {
|
function addImageToUI(imgData) {
|
||||||
const imageContainer = document.createElement('div');
|
const imageContainer = document.createElement('div');
|
||||||
imageContainer.className = 'image-wrapper';
|
imageContainer.className = 'image-wrapper';
|
||||||
const customImg = document.createElement('img');
|
|
||||||
|
|
||||||
customImg.id = `customImageGrid${imageCounter}`;
|
const customImg = document.createElement('img');
|
||||||
customImg.src = imgData;
|
customImg.src = imgData;
|
||||||
customImg.alt = 'Custom Background';
|
customImg.addEventListener('click', () => handleVirtualBackground(null, imgData));
|
||||||
customImg.addEventListener('click', async function () {
|
|
||||||
await rc.applyVirtualBackground(false, imgData);
|
const deleteBtn = document.createElement('span');
|
||||||
|
deleteBtn.className = 'delete-icon fas fa-times';
|
||||||
|
deleteBtn.addEventListener('click', async (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
await indexedDBHelper.removeImage(imgData);
|
||||||
|
imageContainer.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create delete button
|
|
||||||
const deleteBtn = createDeleteIcon(imgData, imageContainer);
|
|
||||||
imageContainer.appendChild(customImg);
|
imageContainer.appendChild(customImg);
|
||||||
imageContainer.appendChild(deleteBtn);
|
imageContainer.appendChild(deleteBtn);
|
||||||
imageGridVideo.appendChild(imageContainer);
|
imageGridVideo.appendChild(imageContainer);
|
||||||
setTippy(customImg.id, 'Custom background', 'top');
|
|
||||||
|
|
||||||
imageCounter++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create delete icon
|
// Function to fetch and store an image from URL
|
||||||
function createDeleteIcon(imgData, container) {
|
|
||||||
const deleteIcon = document.createElement('span');
|
|
||||||
deleteIcon.className = 'delete-icon fas fa-times';
|
|
||||||
deleteIcon.addEventListener('click', function (event) {
|
|
||||||
event.stopPropagation(); // Prevent triggering background change
|
|
||||||
removeImageFromIndexedDB(imgData);
|
|
||||||
container.remove();
|
|
||||||
});
|
|
||||||
return deleteIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save image to IndexedDB
|
|
||||||
async function saveImageToIndexedDB(imgData) {
|
|
||||||
const db = await openDB();
|
|
||||||
const transaction = db.transaction('images', 'readwrite');
|
|
||||||
const store = transaction.objectStore('images');
|
|
||||||
store.add({ imgData });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open IndexedDB
|
|
||||||
function openDB() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open('customImageDB', 1);
|
|
||||||
request.onupgradeneeded = function (event) {
|
|
||||||
const db = event.target.result;
|
|
||||||
if (!db.objectStoreNames.contains('images')) {
|
|
||||||
db.createObjectStore('images', { keyPath: 'id', autoIncrement: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get images from IndexedDB
|
|
||||||
function getImagesFromIndexedDB() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const dbRequest = openDB();
|
|
||||||
dbRequest.then((db) => {
|
|
||||||
const transaction = db.transaction('images', 'readonly');
|
|
||||||
const store = transaction.objectStore('images');
|
|
||||||
const request = store.getAll();
|
|
||||||
|
|
||||||
request.onsuccess = () => resolve(request.result.map((item) => item.imgData));
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove image from IndexedDB
|
|
||||||
async function removeImageFromIndexedDB(imgData) {
|
|
||||||
const db = await openDB();
|
|
||||||
const transaction = db.transaction('images', 'readwrite');
|
|
||||||
const store = transaction.objectStore('images');
|
|
||||||
|
|
||||||
const request = store.getAll();
|
|
||||||
request.onsuccess = () => {
|
|
||||||
const items = request.result;
|
|
||||||
const itemToDelete = items.find((item) => item.imgData === imgData);
|
|
||||||
if (itemToDelete) store.delete(itemToDelete.id);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the button to the imageGrid
|
|
||||||
imageGridVideo.appendChild(uploadImg);
|
|
||||||
setTippy(uploadImg.id, 'Upload your custom image', 'top');
|
|
||||||
|
|
||||||
// Function to fetch image from URL and store it in IndexedDB
|
|
||||||
async function fetchAndStoreImage(url) {
|
async function fetchAndStoreImage(url) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
reader.onload = function (e) {
|
|
||||||
const imgData = e.target.result;
|
const imgData = e.target.result;
|
||||||
saveImageToIndexedDB(imgData);
|
await indexedDBHelper.saveImage(imgData);
|
||||||
addImageToUI(imgData);
|
addImageToUI(imgData);
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching image:', error);
|
console.error('Error fetching image:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Paste image from URL
|
||||||
|
function askForImageURL() {
|
||||||
|
elemDisplay(imageUrlModal.id, true);
|
||||||
|
navigator.clipboard
|
||||||
|
.readText()
|
||||||
|
.then((clipboardText) => {
|
||||||
|
if (isValidImageURL(filterXSS(clipboardText))) {
|
||||||
|
imageUrlInput.value = clipboardText;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
saveImageUrlBtn.addEventListener('click', async () => {
|
saveImageUrlBtn.addEventListener('click', async () => {
|
||||||
elemDisplay(imageUrlModal.id, false);
|
elemDisplay(imageUrlModal.id, false);
|
||||||
if (isValidURL(imageUrlInput.value)) {
|
if (isValidImageURL(imageUrlInput.value)) {
|
||||||
await fetchAndStoreImage(imageUrlInput.value);
|
await fetchAndStoreImage(imageUrlInput.value);
|
||||||
imageUrlInput.value = '';
|
imageUrlInput.value = '';
|
||||||
}
|
}
|
||||||
@@ -1885,59 +1810,25 @@ class RoomClient {
|
|||||||
imageUrlInput.value = '';
|
imageUrlInput.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
function askForImageURL() {
|
// Upload from file button
|
||||||
elemDisplay(imageUrlModal.id, true);
|
createUploadImageButton();
|
||||||
|
|
||||||
// Take URL from clipboard ex:
|
// Upload from URL button
|
||||||
navigator.clipboard
|
setupFileUploadButton('linkImage', image.link, 'Upload Image from URL', askForImageURL);
|
||||||
.readText()
|
|
||||||
.then((clipboardText) => {
|
|
||||||
if (!clipboardText) return false;
|
|
||||||
const sanitizedText = filterXSS(clipboardText);
|
|
||||||
if (isValidURL(sanitizedText) && imageUrlInput) {
|
|
||||||
imageUrlInput.value = sanitizedText;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to validate URL format
|
// Load default virtual backgrounds
|
||||||
function isValidURL(url) {
|
|
||||||
return (
|
|
||||||
url.match(
|
|
||||||
/\.(jpeg|jpg|png|gif|webp|bmp|svg|apng|avif|heif|heic|tiff?|ico|cur|jfif|pjpeg|pjp|raw)$/i,
|
|
||||||
) !== null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create link to upload image
|
|
||||||
const linkImg = document.createElement('img');
|
|
||||||
linkImg.id = 'linkImage';
|
|
||||||
linkImg.src = image.link;
|
|
||||||
linkImg.alt = 'Link image';
|
|
||||||
linkImg.addEventListener('click', askForImageURL);
|
|
||||||
imageGridVideo.appendChild(linkImg);
|
|
||||||
setTippy(linkImg.id, 'Upload Image from Url', 'top');
|
|
||||||
|
|
||||||
// Loop through virtual background images dynamically
|
|
||||||
virtualBackgrounds.forEach((imageUrl, index) => {
|
virtualBackgrounds.forEach((imageUrl, index) => {
|
||||||
const img = document.createElement('img');
|
createImage(`virtualBg${index}`, imageUrl, null, index + 1, () => handleVirtualBackground(null, imageUrl));
|
||||||
img.src = imageUrl;
|
|
||||||
img.dataset.index = index + 1;
|
|
||||||
img.addEventListener('click', async function () {
|
|
||||||
console.log('Selected Image Index:', this.dataset.index);
|
|
||||||
await rc.applyVirtualBackground(false, imageUrl);
|
|
||||||
});
|
|
||||||
imageGridVideo.appendChild(img);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load images from IndexedDB on page load
|
// Load stored images and add to image grid UI
|
||||||
loadStoredImages();
|
indexedDBHelper.getAllImages().then((images) => images.forEach(addImageToUI));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ####################################################
|
||||||
|
// VIRTUAL BACKGROUND HELPER
|
||||||
|
// ####################################################
|
||||||
|
|
||||||
async applyVirtualBackground(blurLevel, backgroundImage) {
|
async applyVirtualBackground(blurLevel, backgroundImage) {
|
||||||
if (blurLevel) {
|
if (blurLevel) {
|
||||||
virtualBackgroundBlurLevel = blurLevel;
|
virtualBackgroundBlurLevel = blurLevel;
|
||||||
@@ -1949,8 +1840,8 @@ class RoomClient {
|
|||||||
virtualBackgroundSelectedImage = null;
|
virtualBackgroundSelectedImage = null;
|
||||||
virtualBackgroundBlurLevel = null;
|
virtualBackgroundBlurLevel = null;
|
||||||
}
|
}
|
||||||
videoSelect.onchange();
|
|
||||||
|
|
||||||
|
videoSelect.onchange();
|
||||||
saveVirtualBackgroundSettings(blurLevel, backgroundImage);
|
saveVirtualBackgroundSettings(blurLevel, backgroundImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,16 +175,25 @@ access to use this app.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="usernameEmoji" class="usernameEmoji fadein center hidden"></div>
|
<div id="usernameEmoji" class="usernameEmoji fadein center hidden"></div>
|
||||||
<div id="imageUrlModal" class="imageUrlModal">
|
<div id="initImageUrlModal" class="imageUrlModal">
|
||||||
<div class="imageUrlModal-content fadein">
|
<div class="imageUrlModal-content fadein">
|
||||||
<h2>Paste Image URL</h2>
|
<h2>Paste Image URL</h2>
|
||||||
<input type="text" id="imageUrlInput" placeholder="Enter the image URL here..." />
|
<input type="text" id="initImageUrlInput" placeholder="Enter the image URL here..." />
|
||||||
<button id="saveImageUrlBtn">Save</button>
|
<button id="initSaveImageUrlBtn">Save</button>
|
||||||
<button id="cancelImageUrlBtn">Cancel</button>
|
<button id="initCancelImageUrlBtn">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div id="imageUrlModal" class="imageUrlModal">
|
||||||
|
<div class="imageUrlModal-content fadein">
|
||||||
|
<h2>Paste Image URL</h2>
|
||||||
|
<input type="text" id="imageUrlInput" placeholder="Enter the image URL here..." />
|
||||||
|
<button id="saveImageUrlBtn">Save</button>
|
||||||
|
<button id="cancelImageUrlBtn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="control" class="fadein">
|
<div id="control" class="fadein">
|
||||||
<button id="shareButton" class="hidden"><i class="fas fa-share-alt"></i></button>
|
<button id="shareButton" class="hidden"><i class="fas fa-share-alt"></i></button>
|
||||||
<button id="hideMeButton" class="hidden"><i id="hideMeIcon" class="fas fa-user"></i></button>
|
<button id="hideMeButton" class="hidden"><i id="hideMeIcon" class="fas fa-user"></i></button>
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم