var VisioUtils = { microphone: null, maxLevel: 0, remoteMaxLevel: 0, audioContext: null, meter: null, rafId: null, handleAudio: function () { if (VisioUtils.audioContext) { VisioUtils.audioContext.close(); VisioUtils.audioContext = null; } VisioUtils.audioContext = new AudioContext(); try { VisioUtils.microphone = VisioUtils.audioContext.createMediaStreamSource( MovimVisio.localAudio.srcObject ); } catch (error) { MovimUtils.logError(error); return; } let javascriptNode = VisioUtils.audioContext.createScriptProcessor(128 * 64, 1, 1); let icon = document.querySelector('#toggle_audio i'); let toggleAudio = document.getElementById('toggle_audio'); icon.innerText = 'mic'; let isMuteStep = 251; let noMicSound = document.querySelector('#no_mic_sound'); let defaultMicrophone = document.querySelector('#default_microphone'); if (defaultMicrophone) { defaultMicrophone.classList.add('muted'); } VisioUtils.meter = VisioUtils.audioContext.createAnalyser(); VisioUtils.meter.fftSize = 256; VisioUtils.microphone.connect(VisioUtils.meter); function startAnalyzer() { if (!VisioUtils.meter) return; const dataArray = new Uint8Array(VisioUtils.meter.frequencyBinCount); function update() { if (!VisioUtils.meter) return; VisioUtils.meter.getByteFrequencyData(dataArray); let sum = 0; for (let i = 0; i < dataArray.length; i++) { sum += dataArray[i]; } const avg = sum / dataArray.length; const level = Math.min(1, (avg / 255) ** .3); VisioUtils.rafId = requestAnimationFrame(update); let step = 0; if (level == 0) { isMuteStep++; } else { isMuteStep = 0; } if (isMuteStep > 32) { if (noMicSound) { noMicSound.classList.remove('disabled'); } if (defaultMicrophone) { defaultMicrophone.classList.add('muted'); } } else { if (noMicSound) { noMicSound.classList.add('disabled'); } if (defaultMicrophone) { defaultMicrophone.classList.remove('muted'); } // Lobby level if (isMuteStep <= 5) { document.querySelectorAll('.level span').forEach(span => { if (step < Math.floor(level * 10)) { span.classList.remove('disabled'); } else { span.classList.add('disabled'); } step++; }); toggleAudio.style.setProperty('--level', level.toFixed(2)); } } } update(); } startAnalyzer(); }, stopAudio: function () { if (VisioUtils.rafId) { cancelAnimationFrame(VisioUtils.rafId); VisioUtils.rafId = null; } if (VisioUtils.microphone) { VisioUtils.microphone.disconnect(); VisioUtils.microphone = null; } if (VisioUtils.audioContext) { VisioUtils.audioContext.close(); VisioUtils.audioContext = null; } }, toggleFullScreen: function () { let button = document.querySelector('#toggle_fullscreen i'); if (!document.fullscreenElement) { if (document.querySelector('#visio').requestFullscreen) { document.querySelector('#visio').requestFullscreen(); } button.innerText = 'fullscreen_exit'; } else { if (document.exitFullscreen) { document.exitFullscreen(); } button.innerText = 'fullscreen'; } }, toggleMode: function (active) { let button = document.querySelector('#toggle_mode i'); let participants = document.querySelector('#participants'); if (button.innerText == 'tile_small' || active) { participants.classList.add('active'); button.innerHTML = 'tile_large'; } else { participants.classList.remove('active'); button.innerHTML = 'tile_small'; } }, toggleAudio: function () { let button = document.querySelector('#toggle_audio i'); if (button.innerText == 'mic_off') { MovimJingles.enableAudio(true); button.innerText = 'mic'; } else { MovimJingles.enableAudio(false); button.innerText = 'mic_off'; } }, toggleVideo: async function () { let button = document.querySelector('#toggle_video i'); if (MovimVisio.localVideo.srcObject == null) { MovimVisio.getUserMedia(true).then(e => { MovimJingles.enableVideo(true); MovimVisio.localVideo.classList.remove('video_off'); button.innerText = 'videocam'; }); } else { if (button.innerText == 'videocam_off') { MovimJingles.enableVideo(true); MovimVisio.localVideo.classList.remove('video_off'); button.innerText = 'videocam'; } else { MovimJingles.enableVideo(false); MovimVisio.localVideo.classList.add('video_off'); button.innerText = 'videocam_off'; } } }, toggleScreenSharing: async function () { MovimVisio.switchCamera = document.querySelector("#visio #switch_camera"); let button = document.querySelector('#screen_sharing i'); if (MovimVisio.screenSharing.srcObject == null) { try { MovimVisio.screenSharing.srcObject = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: "always" }, audio: true }); MovimVisio.screenSharing.classList.add('sharing'); VisioUtils.disableSwitchCameraButton(); button.innerText = 'stop_screen_share'; if (MovimVisio.sfu) { MovimVisio.startScreenshareSFU(); } else { MovimJingles.enableScreenSharing(); MovimVisio.mujiPublish(); } } catch (err) { console.error("Error: " + err); } return; } else { if (MovimVisio.sfu) { MovimVisio.stopScreenshareSFU(); VisioUtils.disableScreenSharing(); } else { VisioUtils.disableScreenSharing(); MovimVisio.mujiPublish(); } } }, disableScreenSharing: function () { MovimJingles.disableScreenSharing(); if (MovimVisio.screenSharing && MovimVisio.screenSharing.srcObject) { MovimVisio.screenSharing.srcObject.getTracks().forEach(track => track.stop()); MovimVisio.screenSharing.srcObject = null; MovimVisio.screenSharing.classList.remove('sharing'); MovimVisio.switchCamera.classList.remove('disabled'); } if (button = document.querySelector('#screen_sharing i')) { button.innerText = 'screen_share'; } }, switchChat: function () { let visio = document.querySelector('#visio'); if (visio.dataset.jid) { Search.chat(visio.dataset.jid, (visio.dataset.muji == 'true')); } }, toggleDtmf: function () { document.querySelector('#visio #dtmf').classList.toggle('hide'); }, insertDtmf: function (s) { VisioDTMF.pressButton(s); setTimeout(() => VisioDTMF.stop(), 100); let insert = (s == '*') ? '🞳' : s; document.querySelector('#dtmf p.dtmf span').innerText += insert; MovimJingles.insertDtmf(s); }, clearDtMf: function () { document.querySelector('#dtmf p.dtmf span').innerText = ''; }, disableSwitchCameraButton: function () { MovimVisio.switchCamera.classList.add('disabled'); }, enableLobbyCallButton: function () { if (document.querySelector('#lobby_start')) { document.querySelector('#lobby_start').classList.remove('disabled'); } }, disableLobbyCallButton: function () { if (document.querySelector('#lobby_start')) { document.querySelector('#lobby_start').classList.add('disabled'); } }, getConstraints: function (withVideo) { const cpuCores = navigator.hardwareConcurrency || 2; let constraints = { audio: true, video: false, }; if (withVideo) { if (cpuCores <= 2) { constraints.video = { width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { max: 15, ideal: 15 } }; } else if (cpuCores <= 4) { constraints.video = { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { max: 30, ideal: 24 } }; } else { constraints.video = { width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { max: 30, ideal: 30 } }; } constraints.video.facingMode = 'user'; if (localStorage.defaultCamera) { constraints.video.deviceId = localStorage.defaultCamera } } if (localStorage.defaultMicrophone) { constraints.audio = { deviceId: localStorage.defaultMicrophone } } return constraints; }, setVideoCodecPreferences: function (transceiver) { let codecs = RTCRtpReceiver.getCapabilities('video').codecs; if (transceiver.setCodecPreferences != undefined) { let preferredOrder = [/*'video/AV1',*/ 'video/H264', 'video/VP8', 'video/VP9']; codecs.sort((a, b) => { const indexA = preferredOrder.indexOf(a.mimeType); const indexB = preferredOrder.indexOf(b.mimeType); const orderA = indexA >= 0 ? indexA : Number.MAX_VALUE; const orderB = indexB >= 0 ? indexB : Number.MAX_VALUE; return orderA - orderB; }); transceiver.setCodecPreferences(codecs); } }, adaptToNetworkCondition: async function (peerConnection) { const stats = await peerConnection.getStats(); let packetLossRate = 0; let rtt = 0; stats.forEach(stat => { if (stat.type === 'outbound-rtp' && stat.kind === 'video') { if (stat.packetsSent && stat.packetsLost) { packetLossRate = stat.packetsLost / stat.packetsSent; } } if (stat.type === 'remote-inbound-rtp') { rtt = stat.roundTripTime; } }); let networkIcon = document.querySelector('#network_condition'); networkIcon.classList.remove('excellent', 'good', 'bad'); peerConnection.getSenders().filter(s => s.track && s.track.kind === 'video' ).forEach(sender => { const parameters = sender.getParameters(); // Don't exceed original encoding if (!parameters.encodings || parameters.encodings.length === 0) { parameters.encodings = [{}]; } if (packetLossRate > 0.1 || rtt > 0.6) { networkIcon.classList.add('bad'); parameters.encodings[0].maxBitrate = 250000; // 250 kbps parameters.encodings[0].scaleResolutionDownBy = 4; // 1/4 resolution } else if (packetLossRate > 0.05 || rtt > 0.3) { networkIcon.classList.add('good'); parameters.encodings[0].maxBitrate = 500000; // 500 kbps parameters.encodings[0].scaleResolutionDownBy = 2; // 1/2 resolution } else { networkIcon.classList.add('excellent'); parameters.encodings[0].maxBitrate = 1500000; // 1.5 Mbps parameters.encodings[0].scaleResolutionDownBy = 1; // Full resolution } sender.setParameters(parameters); }); }, cancelLobby: function (fullJid, id) { if (fullJid && id) { Visio_ajaxReject(fullJid, id); } Visio_ajaxClear(); Dialog_ajaxClear(); Notif.snackbarClear(); }, updateStartedAtCounters: function (startedAt) { document.querySelectorAll('span[data-started-at]').forEach(span => { var dateStartedAt = Date.parse(span.dataset.startedAt); var dateNow = new Date(); var seconds = Math.floor((dateNow - (dateStartedAt)) / 1000); var minutes = Math.floor(seconds / 60); var hours = Math.floor(minutes / 60); var days = Math.floor(hours / 24); hours = hours - (days * 24); minutes = minutes - (days * 24 * 60) - (hours * 60); seconds = seconds - (days * 24 * 60 * 60) - (hours * 60 * 60) - (minutes * 60); span.innerHTML = hours > 0 ? hours + ':' + String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0') : String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0'); }); } }