diff --git a/app/src/Server.js b/app/src/Server.js index 554bbd6a..eae2a19e 100644 --- a/app/src/Server.js +++ b/app/src/Server.js @@ -34,9 +34,10 @@ dependencies: { * @link GitHub: https://github.com/miroslavpejic85/mirotalksfu * @link Live demo: https://sfu.mirotalk.com * @license For open source use: AGPLv3 - * @license For commercial or closed source, contact us at info.mirotalk@gmail.com + * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or buy 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.0.0 + * @version 1.0.1 * */ diff --git a/package.json b/package.json index e7a29cf4..b628cd7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mirotalksfu", - "version": "1.0.0", + "version": "1.0.1", "description": "WebRTC SFU browser-based video calls", "main": "Server.js", "scripts": { diff --git a/public/css/Room.css b/public/css/Room.css index c7b4bfeb..e2d2941e 100644 --- a/public/css/Room.css +++ b/public/css/Room.css @@ -81,7 +81,8 @@ body { width: 100%; height: 100%; overflow: hidden; - background: url('../images/background.jpg'); + /* background: url('../images/background.jpg'); */ + background: var(--body-bg); } /*-------------------------------------------------------------- @@ -97,27 +98,22 @@ body { } /*-------------------------------------------------------------- -# Loading... +# Init User --------------------------------------------------------------*/ -#loadingDiv { - color: #fff; - padding: 30px; - border-radius: 10px; - background: transparent; +.init-user { + display: block; + padding: 5px; } -#loadingDiv h1 { + +.init-user select { + margin-top: 15px; padding: 10px; - font-size: 60px; - font-family: 'Comfortaa'; - background: rgba(0, 0, 0, 0.7); - border-radius: 10px; + cursor: pointer; } -#loadingDiv p { - padding: 10px; - font-family: 'Comfortaa'; - background: rgba(0, 0, 0, 0.7); - border-radius: 10px; + +.init-user button { + margin-top: 15px; } /*-------------------------------------------------------------- diff --git a/public/js/LocalStorage.js b/public/js/LocalStorage.js new file mode 100644 index 00000000..cc6fffcb --- /dev/null +++ b/public/js/LocalStorage.js @@ -0,0 +1,62 @@ +'use-strict'; + +class LocalStorage { + constructor() { + this.MEDIA_TYPE = { + audio: 'audio', + video: 'video', + speaker: 'speaker', + }; + + this.DEVICES_COUNT = { + audio: 0, + speaker: 0, + video: 0, + }; + + this.LOCAL_STORAGE_DEVICES = { + audio: { + count: 0, + index: 0, + select: null, + }, + speaker: { + count: 0, + index: 0, + select: null, + }, + video: { + count: 0, + index: 0, + select: null, + }, + }; + } + + setLocalStorageDevices(type, index, select) { + switch (type) { + case this.MEDIA_TYPE.audio: + this.LOCAL_STORAGE_DEVICES.audio.count = this.DEVICES_COUNT.audio; + this.LOCAL_STORAGE_DEVICES.audio.index = index; + this.LOCAL_STORAGE_DEVICES.audio.select = select; + break; + case this.MEDIA_TYPE.video: + this.LOCAL_STORAGE_DEVICES.video.count = this.DEVICES_COUNT.video; + this.LOCAL_STORAGE_DEVICES.video.index = index; + this.LOCAL_STORAGE_DEVICES.video.select = select; + break; + case this.MEDIA_TYPE.speaker: + this.LOCAL_STORAGE_DEVICES.speaker.count = this.DEVICES_COUNT.speaker; + this.LOCAL_STORAGE_DEVICES.speaker.index = index; + this.LOCAL_STORAGE_DEVICES.speaker.select = select; + break; + default: + break; + } + localStorage.setItem('LOCAL_STORAGE_DEVICES', JSON.stringify(this.LOCAL_STORAGE_DEVICES)); + } + + getLocalStorageDevices() { + return JSON.parse(localStorage.getItem('LOCAL_STORAGE_DEVICES')); + } +} diff --git a/public/js/Room.js b/public/js/Room.js index dea4afac..61578fda 100644 --- a/public/js/Room.js +++ b/public/js/Room.js @@ -8,9 +8,10 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h * @link GitHub: https://github.com/miroslavpejic85/mirotalksfu * @link Live demo: https://sfu.mirotalk.com * @license For open source use: AGPLv3 - * @license For commercial or closed source, contact us at info.mirotalk@gmail.com + * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or buy 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.0.0 + * @version 1.0.1 * */ @@ -53,6 +54,8 @@ const wbHeight = 600; const swalImageUrl = '../images/pricing-illustration.svg'; +const lS = new LocalStorage(); + // #################################################### // DYNAMIC SETTINGS // #################################################### @@ -109,6 +112,8 @@ let isButtonsBarOver = false; let isRoomLocked = false; +let initStream = null; + // #################################################### // INIT ROOM // #################################################### @@ -223,31 +228,27 @@ function makeId(length) { async function initEnumerateDevices() { console.log('01 ----> init Enumerate Devices'); - await initEnumerateAudioDevices(); + whoAreYou(); await initEnumerateVideoDevices(); + await initEnumerateAudioDevices(); + if (!isVideoAllowed) { + hide(initVideo); + hide(initVideoSelect); + } + if (!isAudioAllowed) { + hide(initMicrophoneSelect); + hide(initSpeakerSelect); + } if (!isAudioAllowed && !isVideoAllowed && !joinRoomWithoutAudioVideo) { openURL(`/permission?room_id=${room_id}&message=Not allowed both Audio and Video`); } else { - hide(loadingDiv); + setButtonsInit(); + setSelectsInit(); + handleSelectsInit(); getPeerGeoLocation(); - whoAreYou(); } } -async function initEnumerateAudioDevices() { - if (isEnumerateAudioDevices) return; - // allow the audio - await navigator.mediaDevices - .getUserMedia({ audio: true }) - .then((stream) => { - enumerateAudioDevices(stream); - isAudioAllowed = true; - }) - .catch(() => { - isAudioAllowed = false; - }); -} - async function initEnumerateVideoDevices() { if (isEnumerateVideoDevices) return; // allow the video @@ -262,31 +263,6 @@ async function initEnumerateVideoDevices() { }); } -function enumerateAudioDevices(stream) { - console.log('02 ----> Get Audio Devices'); - navigator.mediaDevices - .enumerateDevices() - .then((devices) => - devices.forEach((device) => { - let el = null; - if ('audioinput' === device.kind) { - el = microphoneSelect; - RoomClient.DEVICES_COUNT.audio++; - } else if ('audiooutput' === device.kind) { - el = speakerSelect; - RoomClient.DEVICES_COUNT.speaker++; - } - if (!el) return; - addChild(device, el); - }), - ) - .then(() => { - stopTracks(stream); - isEnumerateAudioDevices = true; - speakerSelect.disabled = !('sinkId' in HTMLMediaElement.prototype); - }); -} - function enumerateVideoDevices(stream) { console.log('03 ----> Get Video Devices'); navigator.mediaDevices @@ -294,12 +270,14 @@ function enumerateVideoDevices(stream) { .then((devices) => devices.forEach((device) => { let el = null; + let eli = null; if ('videoinput' === device.kind) { el = videoSelect; - RoomClient.DEVICES_COUNT.video++; + eli = initVideoSelect; + lS.DEVICES_COUNT.video++; } if (!el) return; - addChild(device, el); + addChild(device, [el, eli]); }), ) .then(() => { @@ -308,17 +286,76 @@ function enumerateVideoDevices(stream) { }); } +async function initEnumerateAudioDevices() { + if (isEnumerateAudioDevices) return; + // allow the audio + await navigator.mediaDevices + .getUserMedia({ audio: true }) + .then((stream) => { + enumerateAudioDevices(stream); + isAudioAllowed = true; + }) + .catch(() => { + isAudioAllowed = false; + }); +} + +function enumerateAudioDevices(stream) { + console.log('02 ----> Get Audio Devices'); + navigator.mediaDevices + .enumerateDevices() + .then((devices) => + devices.forEach((device) => { + let el = null; + let eli = null; + if ('audioinput' === device.kind) { + el = microphoneSelect; + eli = initMicrophoneSelect; + lS.DEVICES_COUNT.audio++; + } else if ('audiooutput' === device.kind) { + el = speakerSelect; + eli = initSpeakerSelect; + lS.DEVICES_COUNT.speaker++; + } + if (!el) return; + addChild(device, [el, eli]); + }), + ) + .then(() => { + stopTracks(stream); + isEnumerateAudioDevices = true; + const sinkId = 'sinkId' in HTMLMediaElement.prototype; + speakerSelect.disabled = !sinkId; + if (!sinkId) hide(initSpeakerSelect); + }); +} + function stopTracks(stream) { stream.getTracks().forEach((track) => { track.stop(); }); } -function addChild(device, el) { - let option = document.createElement('option'); - option.value = device.deviceId; - option.innerText = device.label; - el.appendChild(option); +function addChild(device, els) { + let kind = device.kind; + els.forEach((el) => { + let option = document.createElement('option'); + option.value = device.deviceId; + switch (kind) { + case 'videoinput': + option.innerHTML = `📹 ` + device.label || `📹 camera ${el.length + 1}`; + break; + case 'audioinput': + option.innerHTML = `🎤 ` + device.label || `🎤 microphone ${el.length + 1}`; + break; + case 'audiooutput': + option.innerHTML = `🔈 ` + device.label || `🔈 speaker ${el.length + 1}`; + break; + default: + break; + } + el.appendChild(option); + }); } // #################################################### @@ -407,6 +444,7 @@ function getPeerGeoLocation() { function whoAreYou() { console.log('04 ----> Who are you'); + sound('open'); if (peer_name) { checkMedia(); @@ -420,21 +458,18 @@ function whoAreYou() { default_name = getCookie(room_id + '_name'); } + const initUser = document.getElementById('initUser'); + initUser.classList.toggle('hidden'); + Swal.fire({ allowOutsideClick: false, allowEscapeKey: false, background: swalBackground, - imageAlt: 'mirotalksfu-username', - imageUrl: image.username, + title: 'MiroTalk SFU', input: 'text', inputPlaceholder: 'Enter your name', inputValue: default_name, - html: `
-
- - - -
`, + html: initUser, // Inject HTML confirmButtonText: `Join meeting`, showClass: { popup: 'animate__animated animate__fadeInDown', @@ -451,23 +486,13 @@ function whoAreYou() { peer_name = name; }, }).then(() => { + if (initStream) { + stopTracks(initStream); + hide(initVideo); + } getPeerInfo(); joinRoom(peer_name, room_id); }); - - if (!DetectRTC.isMobileDevice) { - setTippy('initAudioButton', 'Toggle the audio', 'left'); - setTippy('initVideoButton', 'Toggle the video', 'right'); - setTippy('initAudioVideoButton', 'Toggle the audio & video', 'right'); - } - - initAudioButton = document.getElementById('initAudioButton'); - initVideoButton = document.getElementById('initVideoButton'); - initAudioVideoButton = document.getElementById('initAudioVideoButton'); - if (!isAudioAllowed) hide(initAudioButton); - if (!isVideoAllowed) hide(initVideoButton); - if (!isAudioAllowed || !isVideoAllowed) hide(initAudioVideoButton); - isAudioVideoAllowed = isAudioAllowed && isVideoAllowed; } function handleAudio(e) { @@ -475,6 +500,7 @@ function handleAudio(e) { e.target.className = 'fas fa-microphone' + (isAudioAllowed ? '' : '-slash'); setColor(e.target, isAudioAllowed ? 'white' : 'red'); setColor(startAudioButton, isAudioAllowed ? 'white' : 'red'); + checkInitAudio(isAudioAllowed); } function handleVideo(e) { @@ -482,6 +508,7 @@ function handleVideo(e) { e.target.className = 'fas fa-video' + (isVideoAllowed ? '' : '-slash'); setColor(e.target, isVideoAllowed ? 'white' : 'red'); setColor(startVideoButton, isVideoAllowed ? 'white' : 'red'); + checkInitVideo(isVideoAllowed); } function handleAudioVideo(e) { @@ -500,6 +527,28 @@ function handleAudioVideo(e) { setColor(initVideoButton, isVideoAllowed ? 'white' : 'red'); setColor(startAudioButton, isAudioAllowed ? 'white' : 'red'); setColor(startVideoButton, isVideoAllowed ? 'white' : 'red'); + checkInitVideo(isVideoAllowed); + checkInitAudio(isAudioAllowed); +} + +function checkInitVideo(isVideoAllowed) { + if (isVideoAllowed) { + if (initVideoSelect.value) changeCamera(initVideoSelect.value); + sound('joined'); + } else { + if (initStream) { + stopTracks(initStream); + hide(initVideo); + sound('left'); + } + } + initVideoSelect.disabled = !isVideoAllowed; +} + +function checkInitAudio(isAudioAllowed) { + initMicrophoneSelect.disabled = !isAudioAllowed; + initSpeakerSelect.disabled = !isAudioAllowed; + isAudioAllowed ? sound('joined') : sound('left'); } function checkMedia() { @@ -557,7 +606,7 @@ async function shareRoom(useNavigator = false) { denyButtonText: `Email invite`, cancelButtonText: `Close`, showClass: { - popup: 'animate__animated animate__fadeInUp', + popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', @@ -1003,25 +1052,119 @@ function handleButtons() { } // #################################################### -// HTML SELECTS +// HANDLE INIT USER // #################################################### +function setButtonsInit() { + if (!DetectRTC.isMobileDevice) { + setTippy('initAudioButton', 'Toggle the audio', 'left'); + setTippy('initVideoButton', 'Toggle the video', 'right'); + setTippy('initAudioVideoButton', 'Toggle the audio & video', 'right'); + } + initAudioButton = document.getElementById('initAudioButton'); + initVideoButton = document.getElementById('initVideoButton'); + initAudioVideoButton = document.getElementById('initAudioVideoButton'); + if (!isAudioAllowed) hide(initAudioButton); + if (!isVideoAllowed) hide(initVideoButton); + if (!isAudioAllowed || !isVideoAllowed) hide(initAudioVideoButton); + isAudioVideoAllowed = isAudioAllowed && isVideoAllowed; +} + +function handleSelectsInit() { + // devices init options + initVideoSelect.onchange = () => { + changeCamera(initVideoSelect.value); + videoSelect.selectedIndex = initVideoSelect.selectedIndex; + lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value); + }; + initMicrophoneSelect.onchange = () => { + microphoneSelect.selectedIndex = initMicrophoneSelect.selectedIndex; + lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, microphoneSelect.selectedIndex, microphoneSelect.value); + }; + initSpeakerSelect.onchange = () => { + speakerSelect.selectedIndex = initSpeakerSelect.selectedIndex; + lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, initSpeakerSelect.selectedIndex, initSpeakerSelect.value); + }; +} + +function setSelectsInit() { + const localStorageDevices = lS.getLocalStorageDevices(); + console.log('04 ----> Get Local Storage Devices before', localStorageDevices); + if (localStorageDevices) { + initMicrophoneSelect.selectedIndex = localStorageDevices.audio.index; + initSpeakerSelect.selectedIndex = localStorageDevices.speaker.index; + initVideoSelect.selectedIndex = localStorageDevices.video.index; + // + microphoneSelect.selectedIndex = initMicrophoneSelect.selectedIndex; + speakerSelect.selectedIndex = initSpeakerSelect.selectedIndex; + videoSelect.selectedIndex = initVideoSelect.selectedIndex; + // + if (lS.DEVICES_COUNT.audio != localStorageDevices.audio.count) { + console.log('04.1 ----> Audio devices seems changed, use default index 0'); + initMicrophoneSelect.selectedIndex = 0; + microphoneSelect.selectedIndex = 0; + lS.setLocalStorageDevices( + lS.MEDIA_TYPE.audio, + initMicrophoneSelect.selectedIndex, + initMicrophoneSelect.value, + ); + } + if (lS.DEVICES_COUNT.speaker != localStorageDevices.speaker.count) { + console.log('04.2 ----> Speaker devices seems changed, use default index 0'); + initSpeakerSelect.selectedIndex = 0; + speakerSelect.selectedIndex = 0; + lS.setLocalStorageDevices( + lS.MEDIA_TYPE.speaker, + initSpeakerSelect.selectedIndexIndex, + initSpeakerSelect.value, + ); + } + if (lS.DEVICES_COUNT.video != localStorageDevices.video.count) { + console.log('04.3 ----> Video devices seems changed, use default index 0'); + initVideoSelect.selectedIndex = 0; + videoSelect.selectedIndex = 0; + lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, initVideoSelect.selectedIndex, initVideoSelect.value); + } + // + console.log('04.4 ----> Get Local Storage Devices after', lS.getLocalStorageDevices()); + } + if (initVideoSelect.value) changeCamera(initVideoSelect.value); +} + +function changeCamera(deviceId) { + if (initStream) { + stopTracks(initStream); + show(initVideo); + } + navigator.mediaDevices + .getUserMedia({ video: { deviceId: deviceId } }) + .then((camStream) => { + initVideo.srcObject = camStream; + initStream = camStream; + console.log('04.5 ----> Success attached init video stream'); + }) + .catch((err) => { + console.error('[Error] changeCamera', err); + userLog('error', 'Error while swapping camera' + err.tostring(), 'top-end'); + }); +} + function handleSelects() { // devices options videoSelect.onchange = () => { rc.closeThenProduce(RoomClient.mediaType.video, videoSelect.value); - rc.setLocalStorageDevices(RoomClient.mediaType.video, videoSelect.selectedIndex, videoSelect.value); + lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value); }; videoQuality.onchange = () => { rc.closeThenProduce(RoomClient.mediaType.video, videoSelect.value); }; microphoneSelect.onchange = () => { rc.closeThenProduce(RoomClient.mediaType.audio, microphoneSelect.value); - rc.setLocalStorageDevices(RoomClient.mediaType.audio, microphoneSelect.selectedIndex, microphoneSelect.value); + lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, microphoneSelect.selectedIndex, microphoneSelect.value); }; - speakerSelect.onchange = () => { - rc.attachSinkId(rc.myAudioEl, speakerSelect.value); - rc.setLocalStorageDevices(RoomClient.mediaType.speaker, speakerSelect.selectedIndex, speakerSelect.value); + initSpeakerSelect.onchange = () => { + rc.attachSinkId(rc.myAudioEl, initSpeakerSelect.value); + lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, initSpeakerSelect.selectedIndex, initSpeakerSelect.value); }; // room switchSounds.onchange = (e) => { @@ -2093,7 +2236,7 @@ function showAbout() { `, showClass: { - popup: 'animate__animated animate__fadeInUp', + popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', diff --git a/public/js/RoomClient.js b/public/js/RoomClient.js index 540f4545..a2c3da99 100644 --- a/public/js/RoomClient.js +++ b/public/js/RoomClient.js @@ -9,7 +9,7 @@ * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or buy 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.0.0 + * @version 1.0.1 * */ @@ -66,30 +66,6 @@ const mediaType = { speaker: 'speakerType', }; -const LOCAL_STORAGE_DEVICES = { - audio: { - count: 0, - index: 0, - select: null, - }, - speaker: { - count: 0, - index: 0, - select: null, - }, - video: { - count: 0, - index: 0, - select: null, - }, -}; - -const DEVICES_COUNT = { - audio: 0, - speaker: 0, - video: 0, -}; - const _EVENTS = { openRoom: 'openRoom', exitRoom: 'exitRoom', @@ -653,30 +629,7 @@ class RoomClient { // #################################################### startLocalMedia() { - let localStorageDevices = this.getLocalStorageDevices(); - console.log('08 ----> Get Local Storage Devices before', localStorageDevices); - if (localStorageDevices) { - microphoneSelect.selectedIndex = localStorageDevices.audio.index; - speakerSelect.selectedIndex = localStorageDevices.speaker.index; - videoSelect.selectedIndex = localStorageDevices.video.index; - // - if (DEVICES_COUNT.audio != localStorageDevices.audio.count) { - console.log('08.1 ----> Audio devices seems changed, use default index 0'); - microphoneSelect.selectedIndex = 0; - this.setLocalStorageDevices(mediaType.audio, microphoneSelect.selectedIndex, microphoneSelect.value); - } - if (DEVICES_COUNT.speaker != localStorageDevices.speaker.count) { - console.log('08.2 ----> Speaker devices seems changed, use default index 0'); - speakerSelect.selectedIndex = 0; - this.setLocalStorageDevices(mediaType.speaker, speakerSelect.selectedIndex, speakerSelect.value); - } - if (DEVICES_COUNT.video != localStorageDevices.video.count) { - console.log('08.3 ----> Video devices seems changed, use default index 0'); - videoSelect.selectedIndex = 0; - this.setLocalStorageDevices(mediaType.video, videoSelect.selectedIndex, videoSelect.value); - } - console.log('08.4 ----> Get Local Storage Devices after', this.getLocalStorageDevices()); - } + console.log('08 ----> Start local media'); if (this.isAudioAllowed) { console.log('09 ----> Start audio media'); this.produce(mediaType.audio, microphoneSelect.value); @@ -1804,7 +1757,7 @@ class RoomClient { console.error('Attach SinkId error: ', errorMessage); this.userLog('error', errorMessage, 'top-end'); speakerSelect.selectedIndex = 0; - this.setLocalStorageDevices(mediaType.speaker, 0, speakerSelect.value); + lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, 0, speakerSelect.value); }); } else { let error = `Browser seems doesn't support output device selection.`; @@ -4191,33 +4144,4 @@ class RoomClient { ); } } - - // #################################################### - // LOCAL STORAGE DEVICES - // #################################################### - - setLocalStorageDevices(type, index, select) { - switch (type) { - case RoomClient.mediaType.audio: - LOCAL_STORAGE_DEVICES.audio.count = DEVICES_COUNT.audio; - LOCAL_STORAGE_DEVICES.audio.index = index; - LOCAL_STORAGE_DEVICES.audio.select = select; - break; - case RoomClient.mediaType.video: - LOCAL_STORAGE_DEVICES.video.count = DEVICES_COUNT.video; - LOCAL_STORAGE_DEVICES.video.index = index; - LOCAL_STORAGE_DEVICES.video.select = select; - break; - case RoomClient.mediaType.speaker: - LOCAL_STORAGE_DEVICES.speaker.count = DEVICES_COUNT.speaker; - LOCAL_STORAGE_DEVICES.speaker.index = index; - LOCAL_STORAGE_DEVICES.speaker.select = select; - break; - } - localStorage.setItem('LOCAL_STORAGE_DEVICES', JSON.stringify(LOCAL_STORAGE_DEVICES)); - } - - getLocalStorageDevices() { - return JSON.parse(localStorage.getItem('LOCAL_STORAGE_DEVICES')); - } } diff --git a/public/views/Room.html b/public/views/Room.html index a425d1c6..5ff9ee33 100644 --- a/public/views/Room.html +++ b/public/views/Room.html @@ -73,6 +73,7 @@ + @@ -90,10 +91,25 @@ -
-

Loading...

-

Please allow the camera or microphone access to use this app.

-
+
+ +